Code

git-svn: teach git-svn to populate svn:mergeinfo
authorBryan Jacobs <bjacobs@woti.com>
Wed, 7 Sep 2011 17:36:05 +0000 (13:36 -0400)
committerEric Wong <normalperson@yhbt.net>
Tue, 13 Sep 2011 08:12:13 +0000 (08:12 +0000)
Allow git-svn to populate the svn:mergeinfo property automatically in
a narrow range of circumstances. Specifically, when dcommitting a
revision with multiple parents, all but (potentially) the first of
which have been committed to SVN in the same repository as the target
of the dcommit.

In this case, the merge info is the union of that given by each of the
parents, plus all changes introduced to the first parent by the other
parents.

In all other cases where a revision to be committed has multiple
parents, cause "git svn dcommit" to raise an error rather than
completing the commit and potentially losing history information in
the upstream SVN repository.

This behavior is disabled by default, and can be enabled by setting
the svn.pushmergeinfo config option.

[ew: minor style changes and manpage merge fix]

Acked-by: Eric Wong <normalperson@yhbt.net>
Signed-off-by: Bryan Jacobs <bjacobs@woti.com>
Documentation/git-svn.txt
git-svn.perl
t/t9160-git-svn-mergeinfo-push.sh [new file with mode: 0755]
t/t9160/branches.dump [new file with mode: 0644]

index e75fc191d35fa98eac96dbc2e0cbf3d6069e5737..08cad6d2b676642b37b5a871cd52617c4308a748 100644 (file)
@@ -225,6 +225,14 @@ discouraged.
        version 1.5 can make use of it. To specify merge information from multiple
        branches, use a single space character between the branches
        (`--mergeinfo="/branches/foo:1-10 /branches/bar:3,5-6,8"`)
++
+[verse]
+config key: svn.pushmergeinfo
++
+This option will cause git-svn to attempt to automatically populate the
+svn:mergeinfo property in the SVN repository when possible. Currently, this can
+only be done when dcommitting non-fast-forward merges where all parents but the
+first have already been pushed into SVN.
 
 'branch'::
        Create a branch in the SVN repository.
index d0678372b9fd263d1ef896a04cb1fd0d8f1ba564..351e743a902568c8b6f68ffaf4030f087ba8568d 100755 (executable)
@@ -508,6 +508,195 @@ sub cmd_set_tree {
        unlink $gs->{index};
 }
 
+sub split_merge_info_range {
+       my ($range) = @_;
+       if ($range =~ /(\d+)-(\d+)/) {
+               return (int($1), int($2));
+       } else {
+               return (int($range), int($range));
+       }
+}
+
+sub combine_ranges {
+       my ($in) = @_;
+
+       my @fnums = ();
+       my @arr = split(/,/, $in);
+       for my $element (@arr) {
+               my ($start, $end) = split_merge_info_range($element);
+               push @fnums, $start;
+       }
+
+       my @sorted = @arr [ sort {
+               $fnums[$a] <=> $fnums[$b]
+       } 0..$#arr ];
+
+       my @return = ();
+       my $last = -1;
+       my $first = -1;
+       for my $element (@sorted) {
+               my ($start, $end) = split_merge_info_range($element);
+
+               if ($last == -1) {
+                       $first = $start;
+                       $last = $end;
+                       next;
+               }
+               if ($start <= $last+1) {
+                       if ($end > $last) {
+                               $last = $end;
+                       }
+                       next;
+               }
+               if ($first == $last) {
+                       push @return, "$first";
+               } else {
+                       push @return, "$first-$last";
+               }
+               $first = $start;
+               $last = $end;
+       }
+
+       if ($first != -1) {
+               if ($first == $last) {
+                       push @return, "$first";
+               } else {
+                       push @return, "$first-$last";
+               }
+       }
+
+       return join(',', @return);
+}
+
+sub merge_revs_into_hash {
+       my ($hash, $minfo) = @_;
+       my @lines = split(' ', $minfo);
+
+       for my $line (@lines) {
+               my ($branchpath, $revs) = split(/:/, $line);
+
+               if (exists($hash->{$branchpath})) {
+                       # Merge the two revision sets
+                       my $combined = "$hash->{$branchpath},$revs";
+                       $hash->{$branchpath} = combine_ranges($combined);
+               } else {
+                       # Just do range combining for consolidation
+                       $hash->{$branchpath} = combine_ranges($revs);
+               }
+       }
+}
+
+sub merge_merge_info {
+       my ($mergeinfo_one, $mergeinfo_two) = @_;
+       my %result_hash = ();
+
+       merge_revs_into_hash(\%result_hash, $mergeinfo_one);
+       merge_revs_into_hash(\%result_hash, $mergeinfo_two);
+
+       my $result = '';
+       # Sort below is for consistency's sake
+       for my $branchname (sort keys(%result_hash)) {
+               my $revlist = $result_hash{$branchname};
+               $result .= "$branchname:$revlist\n"
+       }
+       return $result;
+}
+
+sub populate_merge_info {
+       my ($d, $gs, $uuid, $linear_refs, $rewritten_parent) = @_;
+
+       my %parentshash;
+       read_commit_parents(\%parentshash, $d);
+       my @parents = @{$parentshash{$d}};
+       if ($#parents > 0) {
+               # Merge commit
+               my $all_parents_ok = 1;
+               my $aggregate_mergeinfo = '';
+               my $rooturl = $gs->repos_root;
+
+               if (defined($rewritten_parent)) {
+                       # Replace first parent with newly-rewritten version
+                       shift @parents;
+                       unshift @parents, $rewritten_parent;
+               }
+
+               foreach my $parent (@parents) {
+                       my ($branchurl, $svnrev, $paruuid) =
+                               cmt_metadata($parent);
+
+                       unless (defined($svnrev)) {
+                               # Should have been caught be preflight check
+                               fatal "merge commit $d has ancestor $parent, but that change "
+                     ."does not have git-svn metadata!";
+                       }
+                       unless ($branchurl =~ /^$rooturl(.*)/) {
+                               fatal "commit $parent git-svn metadata changed mid-run!";
+                       }
+                       my $branchpath = $1;
+
+                       my $ra = Git::SVN::Ra->new($branchurl);
+                       my (undef, undef, $props) =
+                               $ra->get_dir(canonicalize_path("."), $svnrev);
+                       my $par_mergeinfo = $props->{'svn:mergeinfo'};
+                       unless (defined $par_mergeinfo) {
+                               $par_mergeinfo = '';
+                       }
+                       # Merge previous mergeinfo values
+                       $aggregate_mergeinfo =
+                               merge_merge_info($aggregate_mergeinfo,
+                                                                $par_mergeinfo, 0);
+
+                       next if $parent eq $parents[0]; # Skip first parent
+                       # Add new changes being placed in tree by merge
+                       my @cmd = (qw/rev-list --reverse/,
+                                          $parent, qw/--not/);
+                       foreach my $par (@parents) {
+                               unless ($par eq $parent) {
+                                       push @cmd, $par;
+                               }
+                       }
+                       my @revsin = ();
+                       my ($revlist, $ctx) = command_output_pipe(@cmd);
+                       while (<$revlist>) {
+                               my $irev = $_;
+                               chomp $irev;
+                               my (undef, $csvnrev, undef) =
+                                       cmt_metadata($irev);
+                               unless (defined $csvnrev) {
+                                       # A child is missing SVN annotations...
+                                       # this might be OK, or might not be.
+                                       warn "W:child $irev is merged into revision "
+                                                ."$d but does not have git-svn metadata. "
+                                                ."This means git-svn cannot determine the "
+                                                ."svn revision numbers to place into the "
+                                                ."svn:mergeinfo property. You must ensure "
+                                                ."a branch is entirely committed to "
+                                                ."SVN before merging it in order for "
+                                                ."svn:mergeinfo population to function "
+                                                ."properly";
+                               }
+                               push @revsin, $csvnrev;
+                       }
+                       command_close_pipe($revlist, $ctx);
+
+                       last unless $all_parents_ok;
+
+                       # We now have a list of all SVN revnos which are
+                       # merged by this particular parent. Integrate them.
+                       next if $#revsin == -1;
+                       my $newmergeinfo = "$branchpath:" . join(',', @revsin);
+                       $aggregate_mergeinfo =
+                               merge_merge_info($aggregate_mergeinfo,
+                                                                $newmergeinfo, 1);
+               }
+               if ($all_parents_ok and $aggregate_mergeinfo) {
+                       return $aggregate_mergeinfo;
+               }
+       }
+
+       return undef;
+}
+
 sub cmd_dcommit {
        my $head = shift;
        command_noisy(qw/update-index --refresh/);
@@ -558,6 +747,62 @@ sub cmd_dcommit {
                     "without --no-rebase may be required."
        }
        my $expect_url = $url;
+
+       my $push_merge_info = eval {
+               command_oneline(qw/config --get svn.pushmergeinfo/)
+               };
+       if (not defined($push_merge_info)
+                       or $push_merge_info eq "false"
+                       or $push_merge_info eq "no"
+                       or $push_merge_info eq "never") {
+               $push_merge_info = 0;
+       }
+
+       unless (defined($_merge_info) || ! $push_merge_info) {
+               # Preflight check of changes to ensure no issues with mergeinfo
+               # This includes check for uncommitted-to-SVN parents
+               # (other than the first parent, which we will handle),
+               # information from different SVN repos, and paths
+               # which are not underneath this repository root.
+               my $rooturl = $gs->repos_root;
+               foreach my $d (@$linear_refs) {
+                       my %parentshash;
+                       read_commit_parents(\%parentshash, $d);
+                       my @realparents = @{$parentshash{$d}};
+                       if ($#realparents > 0) {
+                               # Merge commit
+                               shift @realparents; # Remove/ignore first parent
+                               foreach my $parent (@realparents) {
+                                       my ($branchurl, $svnrev, $paruuid) = cmt_metadata($parent);
+                                       unless (defined $paruuid) {
+                                               # A parent is missing SVN annotations...
+                                               # abort the whole operation.
+                                               fatal "$parent is merged into revision $d, "
+                                                        ."but does not have git-svn metadata. "
+                                                        ."Either dcommit the branch or use a "
+                                                        ."local cherry-pick, FF merge, or rebase "
+                                                        ."instead of an explicit merge commit.";
+                                       }
+
+                                       unless ($paruuid eq $uuid) {
+                                               # Parent has SVN metadata from different repository
+                                               fatal "merge parent $parent for change $d has "
+                                                        ."git-svn uuid $paruuid, while current change "
+                                                        ."has uuid $uuid!";
+                                       }
+
+                                       unless ($branchurl =~ /^$rooturl(.*)/) {
+                                               # This branch is very strange indeed.
+                                               fatal "merge parent $parent for $d is on branch "
+                                                        ."$branchurl, which is not under the "
+                                                        ."git-svn root $rooturl!";
+                                       }
+                               }
+                       }
+               }
+       }
+
+       my $rewritten_parent;
        Git::SVN::remove_username($expect_url);
        if (defined($_merge_info)) {
                $_merge_info =~ tr{ }{\n};
@@ -575,6 +820,14 @@ sub cmd_dcommit {
                        print "diff-tree $d~1 $d\n";
                } else {
                        my $cmt_rev;
+
+                       unless (defined($_merge_info) || ! $push_merge_info) {
+                               $_merge_info = populate_merge_info($d, $gs,
+                                                            $uuid,
+                                                            $linear_refs,
+                                                            $rewritten_parent);
+                       }
+
                        my %ed_opts = ( r => $last_rev,
                                        log => get_commit_entry($d)->{log},
                                        ra => Git::SVN::Ra->new($url),
@@ -617,6 +870,9 @@ sub cmd_dcommit {
                                @finish = qw/reset --mixed/;
                        }
                        command_noisy(@finish, $gs->refname);
+
+                       $rewritten_parent = command_oneline(qw/rev-parse HEAD/);
+
                        if (@diff) {
                                @refs = ();
                                my ($url_, $rev_, $uuid_, $gs_) =
diff --git a/t/t9160-git-svn-mergeinfo-push.sh b/t/t9160-git-svn-mergeinfo-push.sh
new file mode 100755 (executable)
index 0000000..216f3d7
--- /dev/null
@@ -0,0 +1,104 @@
+#!/bin/sh
+#
+# Portions copyright (c) 2007, 2009 Sam Vilain
+# Portions copyright (c) 2011 Bryan Jacobs
+#
+
+test_description='git-svn svn mergeinfo propagation'
+
+. ./lib-git-svn.sh
+
+test_expect_success 'load svn dump' "
+       svnadmin load -q '$rawsvnrepo' \
+         < '$TEST_DIRECTORY/t9160/branches.dump' &&
+       git svn init --minimize-url -R svnmerge \
+         -T trunk -b branches '$svnrepo' &&
+       git svn fetch --all
+       "
+
+test_expect_success 'propagate merge information' '
+       git config svn.pushmergeinfo yes &&
+       git checkout svnb1 &&
+       git merge --no-ff svnb2 &&
+       git svn dcommit
+       '
+
+test_expect_success 'check svn:mergeinfo' '
+       mergeinfo=$(svn_cmd propget svn:mergeinfo "$svnrepo"/branches/svnb1)
+       test "$mergeinfo" = "/branches/svnb2:3,8"
+       '
+
+test_expect_success 'merge another branch' '
+       git merge --no-ff svnb3 &&
+       git svn dcommit
+       '
+
+test_expect_success 'check primary parent mergeinfo respected' '
+       mergeinfo=$(svn_cmd propget svn:mergeinfo "$svnrepo"/branches/svnb1)
+       test "$mergeinfo" = "/branches/svnb2:3,8
+/branches/svnb3:4,9"
+       '
+
+test_expect_success 'merge existing merge' '
+       git merge --no-ff svnb4 &&
+       git svn dcommit
+       '
+
+test_expect_success "check both parents' mergeinfo respected" '
+       mergeinfo=$(svn_cmd propget svn:mergeinfo "$svnrepo"/branches/svnb1)
+       test "$mergeinfo" = "/branches/svnb2:3,8
+/branches/svnb3:4,9
+/branches/svnb4:5-6,10-12
+/branches/svnb5:6,11"
+       '
+
+test_expect_success 'make further commits to branch' '
+       git checkout svnb2 &&
+       touch newb2file &&
+       git add newb2file &&
+       git commit -m "later b2 commit" &&
+       touch newb2file-2 &&
+       git add newb2file-2 &&
+       git commit -m "later b2 commit 2" &&
+       git svn dcommit
+       '
+
+test_expect_success 'second forward merge' '
+       git checkout svnb1 &&
+       git merge --no-ff svnb2 &&
+       git svn dcommit
+       '
+
+test_expect_success 'check new mergeinfo added' '
+       mergeinfo=$(svn_cmd propget svn:mergeinfo "$svnrepo"/branches/svnb1)
+       test "$mergeinfo" = "/branches/svnb2:3,8,16-17
+/branches/svnb3:4,9
+/branches/svnb4:5-6,10-12
+/branches/svnb5:6,11"
+       '
+
+test_expect_success 'reintegration merge' '
+       git checkout svnb4 &&
+       git merge --no-ff svnb1 &&
+       git svn dcommit
+       '
+
+test_expect_success 'check reintegration mergeinfo' '
+       mergeinfo=$(svn_cmd propget svn:mergeinfo "$svnrepo"/branches/svnb4)
+       test "$mergeinfo" = "/branches/svnb1:2-4,7-9,13-18
+/branches/svnb2:3,8,16-17
+/branches/svnb3:4,9
+/branches/svnb4:5-6,10-12
+/branches/svnb5:6,11"
+       '
+
+test_expect_success 'dcommit a merge at the top of a stack' '
+       git checkout svnb1 &&
+       touch anotherfile &&
+       git add anotherfile &&
+       git commit -m "a commit" &&
+       git merge svnb4 &&
+       git svn dcommit
+       '
+
+test_done
diff --git a/t/t9160/branches.dump b/t/t9160/branches.dump
new file mode 100644 (file)
index 0000000..e61c3e7
--- /dev/null
@@ -0,0 +1,374 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 1ef08553-f2d1-45df-b38c-19af6b7c926d
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2011-09-02T16:08:02.941384Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 114
+Content-length: 114
+
+K 7
+svn:log
+V 12
+Base commit
+
+K 10
+svn:author
+V 7
+bjacobs
+K 8
+svn:date
+V 27
+2011-09-02T16:08:27.205062Z
+PROPS-END
+
+Node-path: branches
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 2
+Prop-content-length: 121
+Content-length: 121
+
+K 7
+svn:log
+V 19
+Create branch svnb1
+K 10
+svn:author
+V 7
+bjacobs
+K 8
+svn:date
+V 27
+2011-09-02T16:09:43.628137Z
+PROPS-END
+
+Node-path: branches/svnb1
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: trunk
+
+
+Revision-number: 3
+Prop-content-length: 121
+Content-length: 121
+
+K 7
+svn:log
+V 19
+Create branch svnb2
+K 10
+svn:author
+V 7
+bjacobs
+K 8
+svn:date
+V 27
+2011-09-02T16:09:46.339930Z
+PROPS-END
+
+Node-path: branches/svnb2
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: trunk
+
+
+Revision-number: 4
+Prop-content-length: 121
+Content-length: 121
+
+K 7
+svn:log
+V 19
+Create branch svnb3
+K 10
+svn:author
+V 7
+bjacobs
+K 8
+svn:date
+V 27
+2011-09-02T16:09:49.394515Z
+PROPS-END
+
+Node-path: branches/svnb3
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: trunk
+
+
+Revision-number: 5
+Prop-content-length: 121
+Content-length: 121
+
+K 7
+svn:log
+V 19
+Create branch svnb4
+K 10
+svn:author
+V 7
+bjacobs
+K 8
+svn:date
+V 27
+2011-09-02T16:09:54.114607Z
+PROPS-END
+
+Node-path: branches/svnb4
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: trunk
+
+
+Revision-number: 6
+Prop-content-length: 121
+Content-length: 121
+
+K 7
+svn:log
+V 19
+Create branch svnb5
+K 10
+svn:author
+V 7
+bjacobs
+K 8
+svn:date
+V 27
+2011-09-02T16:09:58.602623Z
+PROPS-END
+
+Node-path: branches/svnb5
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 1
+Node-copyfrom-path: trunk
+
+
+Revision-number: 7
+Prop-content-length: 110
+Content-length: 110
+
+K 7
+svn:log
+V 9
+b1 commit
+K 10
+svn:author
+V 7
+bjacobs
+K 8
+svn:date
+V 27
+2011-09-02T16:10:20.292369Z
+PROPS-END
+
+Node-path: branches/svnb1/b1file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 0
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 8
+Prop-content-length: 110
+Content-length: 110
+
+K 7
+svn:log
+V 9
+b2 commit
+K 10
+svn:author
+V 7
+bjacobs
+K 8
+svn:date
+V 27
+2011-09-02T16:10:38.429199Z
+PROPS-END
+
+Node-path: branches/svnb2/b2file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 0
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 9
+Prop-content-length: 110
+Content-length: 110
+
+K 7
+svn:log
+V 9
+b3 commit
+K 10
+svn:author
+V 7
+bjacobs
+K 8
+svn:date
+V 27
+2011-09-02T16:10:52.843023Z
+PROPS-END
+
+Node-path: branches/svnb3/b3file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 0
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 10
+Prop-content-length: 110
+Content-length: 110
+
+K 7
+svn:log
+V 9
+b4 commit
+K 10
+svn:author
+V 7
+bjacobs
+K 8
+svn:date
+V 27
+2011-09-02T16:11:17.489870Z
+PROPS-END
+
+Node-path: branches/svnb4/b4file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 0
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 11
+Prop-content-length: 110
+Content-length: 110
+
+K 7
+svn:log
+V 9
+b5 commit
+K 10
+svn:author
+V 7
+bjacobs
+K 8
+svn:date
+V 27
+2011-09-02T16:11:32.277404Z
+PROPS-END
+
+Node-path: branches/svnb5/b5file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 0
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 12
+Prop-content-length: 192
+Content-length: 192
+
+K 7
+svn:log
+V 90
+Merge remote-tracking branch 'svnb5' into HEAD
+
+* svnb5:
+  b5 commit
+  Create branch svnb5
+K 10
+svn:author
+V 7
+bjacobs
+K 8
+svn:date
+V 27
+2011-09-02T16:11:54.274722Z
+PROPS-END
+
+Node-path: branches/svnb4
+Node-kind: dir
+Node-action: change
+Prop-content-length: 56
+Content-length: 56
+
+K 13
+svn:mergeinfo
+V 21
+/branches/svnb5:6,11
+
+PROPS-END
+
+
+Node-path: branches/svnb4/b5file
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 0
+Text-content-md5: d41d8cd98f00b204e9800998ecf8427e
+Text-content-sha1: da39a3ee5e6b4b0d3255bfef95601890afd80709
+Content-length: 10
+
+PROPS-END
+
+