# BEGIN BPS TAGGED BLOCK {{{ # COPYRIGHT: # # This software is Copyright (c) 2003-2006 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) # # # LICENSE: # # # This program is free software; you can redistribute it and/or # modify it under the terms of either: # # a) Version 2 of the GNU General Public License. You should have # received a copy of the GNU General Public License along with this # program. If not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 or visit # their web page on the internet at # http://www.gnu.org/copyleft/gpl.html. # # b) Version 1 of Perl's "Artistic License". You should have received # a copy of the Artistic License with this package, in the file # named "ARTISTIC". The license is also available at # http://opensource.org/licenses/artistic-license.php. # # This work is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # CONTRIBUTION SUBMISSION POLICY: # # (The following paragraph is not intended to limit the rights granted # to you to modify and distribute this software under the terms of the # GNU General Public License and is only of importance to you if you # choose to contribute your changes and enhancements to the community # by submitting them to Best Practical Solutions, LLC.) # # By intentionally submitting any modifications, corrections or # derivatives to this work, or any other work intended for use with SVK, # to Best Practical Solutions, LLC, you confirm that you are the # copyright holder for those contributions and you grant Best Practical # Solutions, LLC a nonexclusive, worldwide, irrevocable, royalty-free, # perpetual, license to use, copy, create derivative works based on # those contributions, and sublicense and distribute those contributions # and any derivatives thereof. # # END BPS TAGGED BLOCK }}} package SVK::Merge; use strict; use SVK::Util qw(traverse_history is_path_inside); use SVK::I18N; use SVK::Editor::Merge; use SVK::Editor::Rename; use SVK::Editor::Translate; use SVK::Editor::Delay; use SVK::Logger; use List::Util qw(min); =head1 NAME SVK::Merge - Merge context class =head1 SYNOPSIS use SVK::Merge; SVK::Merge->auto (repos => $repos, src => $src, dst => $dst)->run ($editor, %cb); =head1 DESCRIPTION The C class is for representing merge contexts, mainly including what delta is used for this merge, and what target the delta applies to. Given the 3 L objects: =over =item src =item dst =item base =back C will be applying I (C, C) to C. =head1 CONSTRUCTORS =head2 new Takes parameters the usual way. =head2 auto Like new, but the C object will be found automatically as the nearest ancestor of C and C. =head1 METHODS =over =cut sub new { my ($class, @arg) = @_; my $self = bless {}, $class; %$self = @arg; return $self; } sub auto { my $self = new (@_); @{$self}{qw/base fromrev/} = $self->find_merge_base(@{$self}{qw/src dst/}); $self->_rebase; return $self; } sub _rebase { my $self = shift; return unless $self->{base}->path eq $self->{dst}->path; $self->{src}->is_merged_from($self->{base}) or return; my $dst = $self->{src}->prev or return; $dst->root->check_path($dst->path) or return; # If the previous source hasn't been merged, use the original base # logic. Otherwise we are merging changes between the alleged # merge and actual revision. $self->{dst}->is_merged_from($dst) or return; require SVK::Path::Txn; $dst = $dst->clone; bless $dst, 'SVK::Path::Txn'; # XXX: need a saner api for this my $xmerge = SVK::Merge->auto(%$self, quiet => 1, src => $self->{base}, dst => $dst); my ($editor, $inspector, %cb) = $xmerge->{dst}->get_editor(); local $ENV{SVKRESOLVE} = 's'; unless ($xmerge->run( $editor, inspector => $inspector, %cb )) { # XXX why isn't the txnroot uptodate?? $self->{base} = $xmerge->{dst}; $self->{base}->inspector->root($self->{base}->txn->root($self->{base}->pool)); } } # DEPRECATED sub _is_merge_from { my ($self, $path, $target, $rev) = @_; my $fs = $self->{repos}->fs; my $u = $target->universal; my $resource = join (':', $u->{uuid}, $u->{path}); local $@; Carp::cluck unless defined $rev; my ($merge, $pmerge) = map {SVK::Merge::Info->new (eval { $fs->revision_root ($_)->node_prop ($path, 'svk:merge') })->{$resource}{rev} || 0} ($rev, $rev-1); return ($merge != $pmerge) ? $merge : 0; } sub _next_is_merge { my ($self, $repos, $path, $rev, $checkfrom) = @_; return if $rev == $checkfrom; my $fs = $repos->fs; my $nextrev; (traverse_history ( root => $fs->revision_root ($checkfrom), path => $path, cross => 0, callback => sub { return 0 if ($_[1] == $rev); # last $nextrev = $_[1]; return 1; } ) == 0) or return; return unless $nextrev; my ($merge, $pmerge) = map {$fs->revision_root ($_)->node_prop ($path, 'svk:merge') || ''} ($nextrev, $rev); return if $merge eq $pmerge; return ($nextrev, $merge); } sub find_merge_base { my ($self, $src, $dst) = @_; my $repos = $self->{repos}; my $fs = $repos->fs; my $yrev = $fs->youngest_rev; my ($srcinfo, $dstinfo) = map {$self->find_merge_sources ($_)} ($src, $dst); my ($basepath, $baserev, $baseentry); my ($merge_base, $merge_baserev) = $self->{merge_base} ? split(/:/, $self->{merge_base}) : ('', undef); ($merge_base, $merge_baserev) = (undef, $merge_base) if $merge_base =~ /^\d+$/; return ($src->as_depotpath->new (path => $merge_base, revision => $merge_baserev, targets => undef), $merge_baserev) if $merge_base && $merge_baserev; if ($merge_base) { my %allowed = map { ($_ =~ /:(.*)$/) => $_ } grep exists $srcinfo->{$_} && exists $dstinfo->{$_}, keys %{ { %$srcinfo, %$dstinfo } }; unless ($allowed{$merge_base}) { die loc("base '%1' is not allowed without revision specification.\nUse one of the next or provide revision:%2\n", $merge_base, (join '', map "\n $_", sort keys %allowed) ); } my $rev = min ($srcinfo->{$allowed{$merge_base}}, $dstinfo->{$allowed{$merge_base}}); return ($src->as_depotpath->new (path => $merge_base, revision => $rev, targets => undef), $rev); } for (grep {exists $srcinfo->{$_} && exists $dstinfo->{$_}} (sort keys %{ { %$srcinfo, %$dstinfo } })) { my ($path) = m/:(.*)$/; my $rev = min ($srcinfo->{$_}, $dstinfo->{$_}); # when the base is one of src or dst, make sure the base is # still the same node (not removed and replaced) if ($rev && $path eq $dst->path) { next unless $dst->related_to($dst->as_depotpath->seek_to($rev)); } if ($rev && $path eq $src->path) { next unless $src->related_to($src->as_depotpath->seek_to($rev)); } if ($path eq $dst->path && (my $src_base = $src->is_merged_from($src->mclone(path => $path, revision => $rev)))) { ($basepath, $baserev, $baseentry) = ($path, $rev, $_); last; } ($basepath, $baserev, $baseentry) = ($path, $rev, $_) if !$basepath || $fs->revision_prop($rev, 'svn:date') gt $fs->revision_prop($baserev, 'svn:date'); } return ($src->new (revision => $merge_baserev), $merge_baserev) if $merge_baserev; unless ($basepath) { return ($src->new (path => '/', revision => 0), 0) if $self->{baseless}; die loc("Can't find merge base for %1 and %2\n", $src->path, $dst->path); } # XXX: document this, cf t/07smerge-foreign.t if ($basepath ne $src->path && $basepath ne $dst->path) { my ($fromrev, $torev) = ($srcinfo->{$baseentry}, $dstinfo->{$baseentry}); ($fromrev, $torev) = ($torev, $fromrev) if $torev < $fromrev; if (my ($mrev, $merge) = $self->_next_is_merge ($repos, $basepath, $fromrev, $torev)) { my $minfo = SVK::Merge::Info->new ($merge); my $root = $fs->revision_root ($yrev); my ($srcinfo, $dstinfo) = map { SVK::Merge::Info->new ($root->node_prop ($_->path, 'svk:merge')) } ($src, $dst); $baserev = $mrev if $minfo->subset_of ($srcinfo) && $minfo->subset_of ($dstinfo); } } my $base = $src->as_depotpath->new (path => $basepath, revision => $baserev, targets => undef); $base->anchorify if exists $src->{targets}[0]; $base->{path} = '/' if $base->revision == 0; # When /A:1 is copied to /B:2, then removed, /B:2 copied to /A:5 # the fromrev shouldn't be /A:1, as it confuses the copy detection during merge. my $from = $dstinfo->{$fs->get_uuid.':'.$src->path}; if ($from) { my ($toroot, $fromroot) = $src->nearest_copy; $from = 0 if $toroot && $from < $toroot->revision_root_revision; } return ($base, $from || ($basepath eq $src->path ? $baserev : 0)); } sub merge_info { my ($self, $target) = @_; my $tgt = $target->path_target; return SVK::Merge::Info->new ( $target->inspector->localprop($tgt, 'svk:merge') ); } sub merge_info_with_copy { my ($self, $target) = @_; my $minfo = $self->merge_info($target); for ($self->copy_ancestors($target)) { my $srckey = join(':', $_->{uuid}, $_->{path}); $minfo->{$srckey} = $_ unless $minfo->{$srckey} && $minfo->{$srckey} > $_->{rev}; } return $minfo; } sub copy_ancestors { my ($self, $target) = @_; $target = $target->as_depotpath; return map { $target->new ( path => $_->[0], targets => undef, revision => $_->[1])->universal; } $target->copy_ancestors; } sub find_merge_sources { my ($self, $target, $verbatim, $noself) = @_; my $pool = SVN::Pool->new_default; my $info = $self->merge_info ($target->new); $target = $target->new->as_depotpath ($self->{xd}{checkout}->get ($target->copath. 1)->{revision}) if $target->isa('SVK::Path::Checkout'); $info->add_target ($target, $self->{xd}) unless $noself; return $info->verbatim if $verbatim || !$target->root->check_path($target->path); my $minfo = $info->resolve($target->depot); my $myuuid = $target->repos->fs->get_uuid (); for (reverse $target->copy_ancestors) { my ($path, $rev) = @$_; my $entry = "$myuuid:$path"; $minfo->{$entry} = $rev unless $minfo->{$entry} && $minfo->{$entry} > $rev; } return $minfo; } sub get_new_ticket { my ($self, $srcinfo) = @_; my $dstinfo = $self->merge_info ($self->{dst}); # We want the ticket representing src, but not dst. my $newinfo = $dstinfo->union ($srcinfo)->del_target ($self->{dst}); unless ($self->{quiet}) { for (sort keys %$newinfo) { $logger->info(loc("New merge ticket: %1:%2", $_, $newinfo->{$_}{rev})) if !$dstinfo->{$_} || $newinfo->{$_}{rev} > $dstinfo->{$_}{rev}; } } return $newinfo->as_string; } sub log { my ($self, $no_separator) = @_; open my $buf, '>', \ (my $tmp = ''); no warnings 'uninitialized'; require Sys::Hostname; my $get_remoterev = SVK::Command::Log::_log_remote_rev( $self->{src}, $self->{remoterev} ); my $host = $self->{host} || (split ('\.', Sys::Hostname::hostname(), 2))[0]; require SVK::Log::FilterPipeline; my $pipeline = SVK::Log::FilterPipeline->new( presentation => 'std', output => $buf, indent => 1, remote_only => $self->{remoterev}, host => $host, get_remoterev => $get_remoterev, no_sep => $no_separator, verbatim => $self->{verbatim} ? 1 : 0, quiet => 0, suppress => sub { $self->_is_merge_from ($self->{src}->path, $self->{dst}, $_[0]) }, ); SVK::Command::Log::do_log( repos => $self->{repos}, path => $self->{src}->path, fromrev => $self->{fromrev} + 1, torev => $self->{src}->revision, pipeline => $pipeline, ); return $tmp; } =item info Return a string about how the merge is done. =cut sub info { my $self = shift; return loc("Auto-merging (%1, %2) %3 to %4 (base %5%6:%7).\n", $self->{fromrev}, $self->{src}->revision, $self->{src}->path, $self->{dst}->path, $self->{base}->isa('SVK::Path::Txn') ? '*' : '', $self->{base}->path, $self->{base}->revision, ); } sub _collect_renamed { my ($renamed, $pathref, $reverse, $rev, $root, $props) = @_; my $entries; my $path = $$pathref; my $paths = $root->paths_changed(); for (keys %$paths) { my $entry = $paths->{$_}; require SVK::Command; my $action = $SVK::Command::Log::chg->[$entry->change_kind]; $entries->{$_} = [$action , $action eq 'D' ? (-1) : $root->copied_from ($_)]; # anchor is copied if ($action eq 'A' && $entries->{$_}[1] != -1 && (is_path_inside($path, $_))) { $path =~ s/^\Q$_\E/$entries->{$_}[2]/; $$pathref = $path; } } for (keys %$entries) { my $entry = $entries->{$_}; my $from = $entry->[2] or next; if (exists $entries->{$from} && $entries->{$from}[0] eq 'D') { s|^\Q$path\E/|| or next; $from =~ s|^\Q$path\E/|| or next; push @$renamed, $reverse ? [$from, $_] : [$_, $from]; } } } sub _collect_rename_for { my ($self, $renamed, $target, $base, $reverse) = @_; my $path = $target->path; SVK::Command::Log::do_log( repos => $target->repos, path => $path, torev => $base->revision + 1, fromrev => $target->revision, cb_log => sub { _collect_renamed( $renamed, \$path, $reverse, @_ ) } ); } sub track_rename { my ($self, $editor, $cb) = @_; my ($base) = $self->find_merge_base (@{$self}{qw/base dst/}); my ($renamed, $path) = ([]); print "Collecting renames, this might take a while.\n"; $self->_collect_rename_for($renamed, $self->{base}, $base, 0) unless $self->{track_rename} eq 'dst'; { # different base lookup logic for smerge if ($self->{track_rename} eq 'dst') { my $usrc = $self->{src}->universal; my $dstkey = $self->{dst}->universal->ukey; my $srcinfo = $self->merge_info_with_copy($self->{src}->new); use Data::Dumper; if ($srcinfo->{$dstkey}) { $base = $srcinfo->{$dstkey}->local($self->{src}->depot); } else { $base = $base->mclone(revision => 0); } } $self->_collect_rename_for($renamed, $self->{dst}, $base, 1); } return $editor unless @$renamed; my $rename_editor = SVK::Editor::Rename->new (editor => $editor, rename_map => $renamed); return $rename_editor; } =item run Given the storage editor and L callbacks, apply the merge to the storage editor. Returns the number of conflicts. =back =cut sub run { my ($self, $storage, %cb) = @_; my ($base, $src) = @{$self}{qw/base src/}; my $base_root = $self->{base_root} || $base->root; # XXX: for merge editor; this should really be in SVK::Path my ($report, $target) = ($self->{report}, $src->path_target); my $dsttarget = $self->{dst}->path_target; my $is_copath = $self->{dst}->isa('SVK::Path::Checkout'); my $notify_target = defined $self->{target} ? $self->{target} : $target; my $notify = $self->{notify} || SVK::Notify->new_with_report ($report, $notify_target, $is_copath); $notify->{quiet} = 1 if $self->{quiet}; my $translate_target; if ($target && $dsttarget && $target ne $dsttarget) { $translate_target = sub { $_[0] =~ s/^\Q$target\E/$dsttarget/ }; $storage = SVK::Editor::Translate->new (_editor => [$storage], translate => $translate_target); # if there's notify_target, the translation is done by svk::notify $notify->notify_translate ($translate_target) unless length $notify_target; } $storage = SVK::Editor::Delay->new ($storage) unless $self->{nodelay}; $storage = $self->track_rename ($storage, \%cb) if $self->{track_rename}; # XXX: this should be removed when cmerge is gone. also we should # use the inspector of the txn we are working on, rather than of # the (static) target # $cb{inspector} = $self->{dst}->inspector # unless ref($cb{inspector}) eq 'SVK::Inspector::Compat' ; my $meditor = SVK::Editor::Merge->new ( anchor => $src->path_anchor, repospath => $src->repospath, # for stupid copyfrom url base_anchor => $base->path_anchor, base_root => $base_root, target => $target, storage => $storage, notify => $notify, g_merge_no_a_change => ($src->path ne $base->path), # if storage editor is E::XD, applytext_delta returns undef # for failed operations, and merge editor should mark them as skipped storage_has_unwritable => $is_copath && !$self->{check_only}, allow_conflicts => $is_copath, resolve => $self->resolver, open_nonexist => $self->{track_rename}, # XXX: make the prop resolver more pluggable $self->{ticket} ? ( prop_resolver => { 'svk:merge' => sub { my ($path, $prop) = @_; return (undef, undef, 1) if $path eq $target; return ('G', SVK::Merge::Info->new ($prop->{new})->union (SVK::Merge::Info->new ($prop->{local}))->as_string); } }, ticket => sub { $self->get_new_ticket ($self->merge_info_with_copy ($src)->add_target ($src)) } ) : ( prop_resolver => { 'svk:merge' => sub { ('G', undef, 1)} # skip }), %cb, ); $meditor->inspector_translate($translate_target) if $translate_target; my $editor = $meditor; if ($self->{notice_copy}) { my $dstinfo = $self->merge_info_with_copy($self->{dst}->new); my $srcinfo = $self->merge_info_with_copy($self->{src}->new); my $boundry_rev; if ($self->{base}->path eq $self->{src}->path) { $boundry_rev = $self->{base}->revision; } else { my $usrc = $src->universal; my $srckey = join(':', $usrc->{uuid}, $usrc->{path}); if ($dstinfo->{$srckey}) { $boundry_rev = $src->merged_from ($self->{base}, $self, $self->{base}{path}); } else { # when did the branch first got created? $boundry_rev = $src->search_revision ( cmp => sub { my $rev = shift; my $root = $src->mclone(revision => $rev)->root(undef); return $root->node_history($src->path)->prev(0)->prev(0) ? 1 : 0; }) or die loc("Can't find the first revision of %1.\n", $src->path); } } $logger->debug("==> got $boundry_rev as copyboundry, add $self->{fromrev} as boundry as well"); if (defined $boundry_rev) { require SVK::Editor::Copy; $editor = SVK::Editor::Copy->new ( _editor => [$meditor], merge => $self, # XXX: just for merge_from, move it out copyboundry_rev => [$boundry_rev, $self->{fromrev}], copyboundry_root => $self->{repos}->fs->revision_root($boundry_rev ), src => $src, dst => $self->{dst}, cb_query_copy => sub { my ($from, $rev) = @_; return @{$meditor->{copy_info}{$from}{$rev}}; }, cb_resolve_copy => sub { my $path = shift; my $replace = shift; my ($src_from, $src_fromrev) = @_; # If the target exists, don't use copy unless it's a # replace, because merge editor can't handle it yet. return if !$replace && $self->{dst}->inspector->exist($path); my ($dst_from, $dst_fromrev) = $self->resolve_copy($srcinfo, $dstinfo, @_); return unless defined $dst_from; # ensure the dst from path exists my $dst_path = SVK::Path->real_new({depot => $self->{dst}->depot, path => $dst_from, revision => $dst_fromrev}); return unless $dst_path->root->check_path($dst_path->path); $dst_path->normalize; # Because the delta still need to carry the copy # information of the source, make merge editor note # the mapping so it can do the translation ($dst_from, $dst_fromrev) = ($dst_path->path, $dst_path->revision); $meditor->copy_info($src_from, $src_fromrev, $dst_from, $dst_fromrev); return ($dst_from, $dst_fromrev); } ); $editor = SVK::Editor::Delay->new ($editor); } } SVK::XD->depot_delta ( oldroot => $base_root, newroot => $src->root, oldpath => [$base->path_anchor, $base->path_target], newpath => $src->path, # pool => SVN::Pool->new, no_recurse => $self->{no_recurse}, editor => $editor, ); unless ($self->{quiet}) { $logger->warn(loc("%*(%1,conflict) found.", $meditor->{conflicts})) if $meditor->{conflicts}; $logger->warn(loc("%*(%1,file) skipped, you might want to rerun merge with --track-rename.", $meditor->{skipped})) if $meditor->{skipped} && !$self->{track_rename} && !$self->{auto}; } return $meditor->{conflicts}; } # translate to (path, rev) for dst sub resolve_copy { my ($self, $srcinfo, $dstinfo, $cp_path, $cp_rev) = @_; $logger->debug("==> to resolve $cp_path $cp_rev"); my $path = $cp_path; my $src = $self->{src}; my $srcpath = $src->path; my $dstpath = $self->{dst}->path; return ($cp_path, $cp_rev) if $path =~ m{^\Q$dstpath/}; my $cpsrc = $src->new( path => $path, revision => $cp_rev ); if ($path !~ m{^\Q$srcpath/}) { # if the copy source is not within the merge source path, only # allows using the copy if they are both not mirrored return !$src->is_mirrored && !$cpsrc->is_mirrored ? ($cp_path, $cp_rev) : (); } $path =~ s/^\Q$srcpath/$dstpath/; $cpsrc->normalize; $cp_rev = $cpsrc->revision; # now the hard part, reoslve the revision my $usrc = $src->universal; my $srckey = join(':', $usrc->{uuid}, $usrc->{path}); my $udst = $self->{dst}->universal; my $dstkey = join(':', $udst->{uuid}, $udst->{path}); unless ($dstinfo->{$srckey}) { return $srcinfo->{$dstkey}{rev} ? ($path, $srcinfo->{$dstkey}->local($self->{dst}->depot)->revision) : (); } if ($dstinfo->{$srckey}->local($self->{dst}->depot)->revision < $cp_rev) { # same as re-base in editor::copy my $rev = $self->{src}->merged_from ($self->{base}, $self, $self->{base}->path_anchor); return unless defined $rev; $rev = $self->merge_info_with_copy( $self->{src}->mclone(revision => $rev) )->{$dstkey} ->local($self->{dst}->depot) ->revision; return ($path, $rev); } # XXX: get rid of the merge context needed for # merged_from(); actually what the function needs is # just XD my $rev = $self->{dst}-> merged_from($src->new(revision => $cp_rev), $self, $cp_path); return ($path, $rev) if defined $rev; return; } sub resolver { return undef if $_[0]->{check_only}; require SVK::Resolve; return SVK::Resolve->new (action => $ENV{SVKRESOLVE}, external => $ENV{SVKMERGE}); } package SVK::Merge::Info; sub new { my ($class, $merge) = @_; my $minfo = { map { my ($uuid, $path, $rev) = m/(.*?):(.*):(\d+$)/; ("$uuid:$path" => SVK::Target::Universal->new ($uuid, $path, $rev)) } grep { length $_ } split (/\n/, $merge || '') }; bless $minfo, $class; return $minfo; } sub add_target { my ($self, $target) = @_; $target = $target->universal if $target->can('universal'); $self->{$target->ukey} = $target; return $self; } sub del_target { my ($self, $target) = @_; $target = $target->universal if $target->can('universal'); delete $self->{$target->ukey}; return $self; } sub remove_duplicated { my ($self, $other) = @_; for (keys %$other) { if ($self->{$_} && $self->{$_}{rev} <= $other->{$_}{rev}) { delete $self->{$_}; } } return $self; } sub subset_of { my ($self, $other) = @_; my $subset = 1; for (keys %$self) { return unless exists $other->{$_} && $self->{$_}{rev} <= $other->{$_}{rev}; } return 1; } sub union { my ($self, $other) = @_; # bring merge history up to date as from source my $new = SVK::Merge::Info->new; for (keys %{ { %$self, %$other } }) { if ($self->{$_} && $other->{$_}) { $new->{$_} = $self->{$_}{rev} > $other->{$_}{rev} ? $self->{$_} : $other->{$_}; } else { $new->{$_} = $self->{$_} ? $self->{$_} : $other->{$_}; } } return $new; } sub resolve { my ($self, $depot) = @_; my $uuid = $depot->repos->fs->get_uuid; return { map { my $local = $self->{$_}->local($depot); $local ? ("$uuid:".$local->path_anchor => $local->revision) : () } keys %$self }; } sub verbatim { my ($self) = @_; return { map { $_ => $self->{$_}{rev} } keys %$self }; } sub as_string { my $self = shift; return join ("\n", map {"$_:$self->{$_}{rev}"} sort keys %$self); } =head1 TODO Document the merge and ticket tracking mechanism. =head1 SEE ALSO L, L, Star-merge from GNU Arch =cut 1;