Skip to content

Commit 1e5814f

Browse files
Bryan JacobsEric Wong
authored andcommitted
git-svn: teach git-svn to populate svn:mergeinfo
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 <[email protected]> Signed-off-by: Bryan Jacobs <[email protected]>
1 parent 5738c9c commit 1e5814f

File tree

4 files changed

+742
-0
lines changed

4 files changed

+742
-0
lines changed

Documentation/git-svn.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,14 @@ discouraged.
225225
version 1.5 can make use of it. To specify merge information from multiple
226226
branches, use a single space character between the branches
227227
(`--mergeinfo="/branches/foo:1-10 /branches/bar:3,5-6,8"`)
228+
+
229+
[verse]
230+
config key: svn.pushmergeinfo
231+
+
232+
This option will cause git-svn to attempt to automatically populate the
233+
svn:mergeinfo property in the SVN repository when possible. Currently, this can
234+
only be done when dcommitting non-fast-forward merges where all parents but the
235+
first have already been pushed into SVN.
228236

229237
'branch'::
230238
Create a branch in the SVN repository.

git-svn.perl

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,195 @@ sub cmd_set_tree {
508508
unlink $gs->{index};
509509
}
510510

511+
sub split_merge_info_range {
512+
my ($range) = @_;
513+
if ($range =~ /(\d+)-(\d+)/) {
514+
return (int($1), int($2));
515+
} else {
516+
return (int($range), int($range));
517+
}
518+
}
519+
520+
sub combine_ranges {
521+
my ($in) = @_;
522+
523+
my @fnums = ();
524+
my @arr = split(/,/, $in);
525+
for my $element (@arr) {
526+
my ($start, $end) = split_merge_info_range($element);
527+
push @fnums, $start;
528+
}
529+
530+
my @sorted = @arr [ sort {
531+
$fnums[$a] <=> $fnums[$b]
532+
} 0..$#arr ];
533+
534+
my @return = ();
535+
my $last = -1;
536+
my $first = -1;
537+
for my $element (@sorted) {
538+
my ($start, $end) = split_merge_info_range($element);
539+
540+
if ($last == -1) {
541+
$first = $start;
542+
$last = $end;
543+
next;
544+
}
545+
if ($start <= $last+1) {
546+
if ($end > $last) {
547+
$last = $end;
548+
}
549+
next;
550+
}
551+
if ($first == $last) {
552+
push @return, "$first";
553+
} else {
554+
push @return, "$first-$last";
555+
}
556+
$first = $start;
557+
$last = $end;
558+
}
559+
560+
if ($first != -1) {
561+
if ($first == $last) {
562+
push @return, "$first";
563+
} else {
564+
push @return, "$first-$last";
565+
}
566+
}
567+
568+
return join(',', @return);
569+
}
570+
571+
sub merge_revs_into_hash {
572+
my ($hash, $minfo) = @_;
573+
my @lines = split(' ', $minfo);
574+
575+
for my $line (@lines) {
576+
my ($branchpath, $revs) = split(/:/, $line);
577+
578+
if (exists($hash->{$branchpath})) {
579+
# Merge the two revision sets
580+
my $combined = "$hash->{$branchpath},$revs";
581+
$hash->{$branchpath} = combine_ranges($combined);
582+
} else {
583+
# Just do range combining for consolidation
584+
$hash->{$branchpath} = combine_ranges($revs);
585+
}
586+
}
587+
}
588+
589+
sub merge_merge_info {
590+
my ($mergeinfo_one, $mergeinfo_two) = @_;
591+
my %result_hash = ();
592+
593+
merge_revs_into_hash(\%result_hash, $mergeinfo_one);
594+
merge_revs_into_hash(\%result_hash, $mergeinfo_two);
595+
596+
my $result = '';
597+
# Sort below is for consistency's sake
598+
for my $branchname (sort keys(%result_hash)) {
599+
my $revlist = $result_hash{$branchname};
600+
$result .= "$branchname:$revlist\n"
601+
}
602+
return $result;
603+
}
604+
605+
sub populate_merge_info {
606+
my ($d, $gs, $uuid, $linear_refs, $rewritten_parent) = @_;
607+
608+
my %parentshash;
609+
read_commit_parents(\%parentshash, $d);
610+
my @parents = @{$parentshash{$d}};
611+
if ($#parents > 0) {
612+
# Merge commit
613+
my $all_parents_ok = 1;
614+
my $aggregate_mergeinfo = '';
615+
my $rooturl = $gs->repos_root;
616+
617+
if (defined($rewritten_parent)) {
618+
# Replace first parent with newly-rewritten version
619+
shift @parents;
620+
unshift @parents, $rewritten_parent;
621+
}
622+
623+
foreach my $parent (@parents) {
624+
my ($branchurl, $svnrev, $paruuid) =
625+
cmt_metadata($parent);
626+
627+
unless (defined($svnrev)) {
628+
# Should have been caught be preflight check
629+
fatal "merge commit $d has ancestor $parent, but that change "
630+
."does not have git-svn metadata!";
631+
}
632+
unless ($branchurl =~ /^$rooturl(.*)/) {
633+
fatal "commit $parent git-svn metadata changed mid-run!";
634+
}
635+
my $branchpath = $1;
636+
637+
my $ra = Git::SVN::Ra->new($branchurl);
638+
my (undef, undef, $props) =
639+
$ra->get_dir(canonicalize_path("."), $svnrev);
640+
my $par_mergeinfo = $props->{'svn:mergeinfo'};
641+
unless (defined $par_mergeinfo) {
642+
$par_mergeinfo = '';
643+
}
644+
# Merge previous mergeinfo values
645+
$aggregate_mergeinfo =
646+
merge_merge_info($aggregate_mergeinfo,
647+
$par_mergeinfo, 0);
648+
649+
next if $parent eq $parents[0]; # Skip first parent
650+
# Add new changes being placed in tree by merge
651+
my @cmd = (qw/rev-list --reverse/,
652+
$parent, qw/--not/);
653+
foreach my $par (@parents) {
654+
unless ($par eq $parent) {
655+
push @cmd, $par;
656+
}
657+
}
658+
my @revsin = ();
659+
my ($revlist, $ctx) = command_output_pipe(@cmd);
660+
while (<$revlist>) {
661+
my $irev = $_;
662+
chomp $irev;
663+
my (undef, $csvnrev, undef) =
664+
cmt_metadata($irev);
665+
unless (defined $csvnrev) {
666+
# A child is missing SVN annotations...
667+
# this might be OK, or might not be.
668+
warn "W:child $irev is merged into revision "
669+
."$d but does not have git-svn metadata. "
670+
."This means git-svn cannot determine the "
671+
."svn revision numbers to place into the "
672+
."svn:mergeinfo property. You must ensure "
673+
."a branch is entirely committed to "
674+
."SVN before merging it in order for "
675+
."svn:mergeinfo population to function "
676+
."properly";
677+
}
678+
push @revsin, $csvnrev;
679+
}
680+
command_close_pipe($revlist, $ctx);
681+
682+
last unless $all_parents_ok;
683+
684+
# We now have a list of all SVN revnos which are
685+
# merged by this particular parent. Integrate them.
686+
next if $#revsin == -1;
687+
my $newmergeinfo = "$branchpath:" . join(',', @revsin);
688+
$aggregate_mergeinfo =
689+
merge_merge_info($aggregate_mergeinfo,
690+
$newmergeinfo, 1);
691+
}
692+
if ($all_parents_ok and $aggregate_mergeinfo) {
693+
return $aggregate_mergeinfo;
694+
}
695+
}
696+
697+
return undef;
698+
}
699+
511700
sub cmd_dcommit {
512701
my $head = shift;
513702
command_noisy(qw/update-index --refresh/);
@@ -558,6 +747,62 @@ sub cmd_dcommit {
558747
"without --no-rebase may be required."
559748
}
560749
my $expect_url = $url;
750+
751+
my $push_merge_info = eval {
752+
command_oneline(qw/config --get svn.pushmergeinfo/)
753+
};
754+
if (not defined($push_merge_info)
755+
or $push_merge_info eq "false"
756+
or $push_merge_info eq "no"
757+
or $push_merge_info eq "never") {
758+
$push_merge_info = 0;
759+
}
760+
761+
unless (defined($_merge_info) || ! $push_merge_info) {
762+
# Preflight check of changes to ensure no issues with mergeinfo
763+
# This includes check for uncommitted-to-SVN parents
764+
# (other than the first parent, which we will handle),
765+
# information from different SVN repos, and paths
766+
# which are not underneath this repository root.
767+
my $rooturl = $gs->repos_root;
768+
foreach my $d (@$linear_refs) {
769+
my %parentshash;
770+
read_commit_parents(\%parentshash, $d);
771+
my @realparents = @{$parentshash{$d}};
772+
if ($#realparents > 0) {
773+
# Merge commit
774+
shift @realparents; # Remove/ignore first parent
775+
foreach my $parent (@realparents) {
776+
my ($branchurl, $svnrev, $paruuid) = cmt_metadata($parent);
777+
unless (defined $paruuid) {
778+
# A parent is missing SVN annotations...
779+
# abort the whole operation.
780+
fatal "$parent is merged into revision $d, "
781+
."but does not have git-svn metadata. "
782+
."Either dcommit the branch or use a "
783+
."local cherry-pick, FF merge, or rebase "
784+
."instead of an explicit merge commit.";
785+
}
786+
787+
unless ($paruuid eq $uuid) {
788+
# Parent has SVN metadata from different repository
789+
fatal "merge parent $parent for change $d has "
790+
."git-svn uuid $paruuid, while current change "
791+
."has uuid $uuid!";
792+
}
793+
794+
unless ($branchurl =~ /^$rooturl(.*)/) {
795+
# This branch is very strange indeed.
796+
fatal "merge parent $parent for $d is on branch "
797+
."$branchurl, which is not under the "
798+
."git-svn root $rooturl!";
799+
}
800+
}
801+
}
802+
}
803+
}
804+
805+
my $rewritten_parent;
561806
Git::SVN::remove_username($expect_url);
562807
if (defined($_merge_info)) {
563808
$_merge_info =~ tr{ }{\n};
@@ -575,6 +820,14 @@ sub cmd_dcommit {
575820
print "diff-tree $d~1 $d\n";
576821
} else {
577822
my $cmt_rev;
823+
824+
unless (defined($_merge_info) || ! $push_merge_info) {
825+
$_merge_info = populate_merge_info($d, $gs,
826+
$uuid,
827+
$linear_refs,
828+
$rewritten_parent);
829+
}
830+
578831
my %ed_opts = ( r => $last_rev,
579832
log => get_commit_entry($d)->{log},
580833
ra => Git::SVN::Ra->new($url),
@@ -617,6 +870,9 @@ sub cmd_dcommit {
617870
@finish = qw/reset --mixed/;
618871
}
619872
command_noisy(@finish, $gs->refname);
873+
874+
$rewritten_parent = command_oneline(qw/rev-parse HEAD/);
875+
620876
if (@diff) {
621877
@refs = ();
622878
my ($url_, $rev_, $uuid_, $gs_) =

0 commit comments

Comments
 (0)