chiark / gitweb /
git-debrebase: merge: Get debian/ parts of new patch queue right
[dgit.git] / git-debrebase
index 6c1f5d1996d87337ee26f526f26d269d8daca2d9..60ea976116fa1afad615a8c8be17e2171b55d7bf 100755 (executable)
@@ -36,6 +36,8 @@ use Dpkg::Version;
 use File::FnMatch qw(:fnmatch);
 use File::Copy;
 
+$debugcmd_when_debuglevel = 2;
+
 our ($usage_message) = <<'END';
 usages:
   git-debrebase [<options>] [--|-i <git rebase options...>]
@@ -226,7 +228,7 @@ sub get_differs ($$) {
            unless $differs & (D_PAT_ADD|D_PAT_OTH);
     }
 
-    printdebug sprintf "get_differs %s, %s = %#x\n", $x, $y, $differs;
+    printdebug sprintf "get_differs %s %s = %#x\n", $x, $y, $differs;
 
     return $differs;
 }
@@ -261,11 +263,15 @@ sub read_tree_debian ($) {
     rm_subdir_cached 'debian/patches';
 }
 
-sub read_tree_upstream ($;$) {
-    my ($treeish, $keep_patches) = @_;
-    my $save = cmdoutput @git, qw(write-tree --prefix=debian/);
+sub read_tree_upstream ($;$$) {
+    my ($treeish, $keep_patches, $tree_with_debian) = @_;
+    # if $tree_with_debian is supplied, will use that for debian/
+    # otherwise will save and restore it.
+    my $debian =
+       $tree_with_debian ? "$tree_with_debian:debian"
+       : cmdoutput @git, qw(write-tree --prefix=debian/);
     runcmd @git, qw(read-tree), $treeish;
-    read_tree_subdir 'debian', $save;
+    read_tree_subdir 'debian', $debian;
     rm_subdir_cached 'debian/patches' unless $keep_patches;
 };
 
@@ -329,6 +335,7 @@ sub gbp_pq_export ($$$) {
     # must be run in a workarea.  $bname and patch-queue/$bname
     # ought not to exist.  Leaves you on patch-queue/$bname with
     # the patches staged but not committed.
+    printdebug "gbp_pq_export $bname $base $tip\n";
     runcmd @git, qw(checkout -q -b), $bname, $base;
     runcmd @git, qw(checkout -q -b), "patch-queue/$bname", $tip;
     my @gbp_cmd = (qw(gbp pq export));
@@ -341,11 +348,11 @@ sub gbp_pq_export ($$$) {
 }
 
 
-# xxx allow merge separately from laundering
+# xxx allow merge resolution separately from laundering, before git merge
 #
-# xxx docs highlight forbidden things
-# xxx docs list allowable things ?
-# xxx docs explicitly forbid some rebase
+# xxx general gdr docs highlight forbidden things
+# xxx general gdr docs list allowable things ?
+# xxx general gdr docs explicitly forbid some rebase
 #
 # xxx provide a way for the user to help
 # xxx (eg, provide wreckage provide way to continue)
@@ -369,6 +376,8 @@ sub merge_series ($$;@) {
 
     # Creates, in *_q, a key MR for its private use
 
+    printdebug "merge_series newbase=$newbase\n";
+
     $input_qs[$_]{MR}{S} = $_ foreach (0..$#input_qs);
     $base_q->{MR}{S} = 'base';
 
@@ -390,6 +399,7 @@ sub merge_series ($$;@) {
                my @earlier;
                while (my $patch = <S>) {
                    chomp $patch or die $!;
+                   $prereq{$patch} //= {};
                    foreach my $earlier (@earlier) {
                        $prereq{$patch}{$earlier}{$s}++ and die;
                    }
@@ -407,17 +417,20 @@ sub merge_series ($$;@) {
                 "[git-debrebase merge-innards patch-queue import:".
                 " $q->{SeriesTip}]"
             ];
+           printdebug "merge_series  pec $pec ";
            runcmd @git, qw(rm -q --cached), $seriesfile;
            $pec = make_commit [ $pec ], [
                 "Drop series file from $s to avoid merge trouble",
                 "[git-debrebase merge-innards patch-queue prep:".
                 " $q->{SeriesTip}]"
             ];
+           printdebug "pec' $pec\n";
             runcmd @git, qw(reset -q --hard), $pec;
            $q->{MR}{PEC} = $pec;
        }
        # now, because of reverse, we are on $input_q->{MR}{OQC}
        runcmd @git, qw(checkout -q -b merge);
+       printdebug "merge_series merging...\n";
        my @mergecmd = (@git, qw(merge --quiet --no-edit), "p-1");
        debugcmd '+', @mergecmd;
        $!=0; $?=-1;
@@ -425,23 +438,29 @@ sub merge_series ($$;@) {
            failedcmd @mergecmd;
        }
 
+       printdebug "merge_series merge ok, series...\n";
        # We need to construct a new series file
        # Firstly, resolve prereq
-       foreach my $f (keys %prereq) {
+       foreach my $f (sort keys %prereq) {
+           printdebug "merge_series  patch\t$f\t";
            if (!stat_exists "debian/patches/$f") {
+               print DEBUG " drop\n" if $debuglevel;
                # git merge deleted it; that's how we tell it's not wanted
                delete $prereq{$f};
                next;
            }
-           foreach my $g (keys %{ $prereq{$f} }) {
+           print DEBUG " keep\n" if $debuglevel;
+           foreach my $g (sort keys %{ $prereq{$f} }) {
                my $gfp = $prereq{$f}{$g};
-               next unless
-                   # want to keep it
-                   !!$gfp->{0} == !!$gfp->{1}
+               printdebug "merge_series  prereq\t$f\t-> $g\t";
+               if (!!$gfp->{0} == !!$gfp->{1}
                    ? $gfp->{0}
-                   : !$gfp->{base}
-                   ;
-               delete $prereq{$f}{$g};
+                   : !$gfp->{base}) {
+                   print DEBUG "\tkeep\n" if $debuglevel;
+               } else {
+                   print DEBUG "\tdrop\n" if $debuglevel;
+                   delete $prereq{$f}{$g};
+               }
            }
        }
 
@@ -480,6 +499,7 @@ sub merge_series ($$;@) {
                }
                $best = $try;
            }
+           printdebug "merge_series series next $best\n";
            print NS "$best\n" or die $!;
            delete $prereq{$best};
            foreach my $gp (values %prereq) {
@@ -490,16 +510,19 @@ sub merge_series ($$;@) {
        runcmd @git, qw(add), $seriesfile;
        runcmd @git, qw(commit --quiet -m), 'Merged series';
 
+       printdebug "merge_series series gbp pq import\n";
        runcmd qw(gbp pq import);
 
        # OK now we are on patch-queue/merge, and we need to rebase
        # onto the intended parent and drop the patches from each one
 
+       printdebug "merge_series series ok, building...\n";
        my $build = $newbase;
        my @lcmd = (@git, qw(rev-list --reverse merge..patch-queue/merge));
        foreach my $c (grep /./, split /\n/, cmdoutput @lcmd) {
            my $commit = git_cat_file $c, 'commit';
-           read_tree_upstream $c;
+           printdebug "merge_series series ok, building $c\n";
+           read_tree_upstream $c, 0, $newbase;
            my $tree = cmdoutput @git, qw(write-tree);
            $commit =~ s{^parent (\S+)$}{parent $build}m or confess;
            $commit =~ s{^tree (\S+)$}{tree $tree}m      or confess;
@@ -510,6 +533,13 @@ sub merge_series ($$;@) {
        }
        $result = $build;
        runcmd @git, qw(update-ref refs/heads/result), $result;
+
+       runcmd @git, qw(checkout -q -b debug);
+       runcmd @git, qw(commit --allow-empty -q -m M-INDEX);
+       runcmd @git, qw(add .);
+       runcmd @git, qw(commit --allow-empty -q -m M-WORKTREE);
+       printdebug sprintf "merge_series done debug=%s\n",
+           git_rev_parse 'HEAD';
     };
     printdebug "merge_series returns $result\n";
     return $result;
@@ -772,9 +802,6 @@ sub classify ($) {
                           OrigParents => \@orig_ps);
     }
 
-    if (@p > 2) {
-       return $unknown->("octopus merge");
-    }
     if (@p == 2 and
        $r->{Msg} =~ m{^\[git-debrebase merged-breakwater.*\]$}m) {
        # xxx ^ metadata tag needs adding to (5)
@@ -783,6 +810,9 @@ sub classify ($) {
     if ($r->{Msg} =~ m{^\[(git-debrebase|dgit)[: ].*\]$}m) {
        return $unknown->("unknown kind of merge from $1");
     }
+    if (@p > 2) {
+       return $unknown->("octopus merge");
+    }
 
     if (!$ENV{GIT_DEBREBASE_EXPERIMENTAL_MERGE}) {
        return $unknown->("general two-parent merge");
@@ -791,8 +821,23 @@ sub classify ($) {
     return $classify->("VanillaMerge");
 }
 
-sub keycommits ($;$$$$) {
-    my ($head, $furniture, $unclean, $trouble, $fatal) = @_;
+sub keycommits ($;$$$$$);
+
+sub mergedbreakwaters_anchor ($) {
+    my ($cl) = @_;
+    my $best_anchor;
+    foreach my $p (@{ $cl->{Parents} }) {
+       my ($panchor, $pbw) = keycommits $p->{CommitId},
+           undef,undef,undef,undef, 1;
+       $best_anchor = $panchor
+           if !defined $best_anchor
+           or is_fast_fwd $best_anchor, $panchor;
+    }
+    return $best_anchor;
+}
+
+sub keycommits ($;$$$$$) {
+    my ($head, $furniture, $unclean, $trouble, $fatal, $claimed_bw) = @_;
     # => ($anchor, $breakwater)
 
     # $unclean->("unclean-$tagsfx", $msg, $cl)
@@ -808,6 +853,8 @@ sub keycommits ($;$$$$) {
     # $fatal is for unprocessable commits, and should normally cause
     #    a failure.  If ignored, agaion, (undef, undef) is returned.
     #
+    # If $claimed_bw, this is supposed to be a breakwater commit.
+    #
     # If a callback is undef, fail is called instead.
     # If a callback is defined but false, the situation is ignored.
     # Callbacks may say:
@@ -815,6 +862,7 @@ sub keycommits ($;$$$$) {
     # if the answer is no longer wanted.
 
     my ($anchor, $breakwater);
+    $breakwater = $head if $claimed_bw;
     my $clogonly;
     my $cl;
     my $found_pm;
@@ -827,6 +875,13 @@ sub keycommits ($;$$$$) {
        return unless $cb;
        $cb->("unclean-$tagsfx", $why, $cl, $mainwhy);
     };
+    my $found_anchor = sub {
+       ($anchor) = @_;
+       $breakwater //= $clogonly;
+       $breakwater //= $head;
+       no warnings qw(exiting);
+       last;
+    };
     for (;;) {
        $cl = classify $head;
        my $ty = $cl->{Type};
@@ -840,10 +895,7 @@ sub keycommits ($;$$$$) {
        } elsif ($ty eq 'Anchor' or
                 $ty eq 'TreatAsAnchor' or
                 $ty eq 'BreakwaterStart') {
-           $anchor = $head;
-           $breakwater //= $clogonly;
-           $breakwater //= $head;
-           last;
+           $found_anchor->($head);
        } elsif ($ty eq 'Upstream') {
            $x->($unclean, 'ordering',
  "packaging change ($breakwater) follows upstream change"," (eg $head)")
@@ -875,6 +927,8 @@ sub keycommits ($;$$$$) {
            $x->($trouble, 'vanillamerge',
                 "found vanilla merge"," ($head)");
            return (undef,undef);
+       } elsif ($ty eq 'MergedBreakwaters') {
+           $found_anchor->(mergedbreakwaters_anchor $cl);
        } else {
            $x->($fatal, 'unprocessable',
                 "found unprocessable commit, cannot cope: $cl->{Why}",
@@ -1063,8 +1117,16 @@ sub walk ($;$$$) {
            }
            die "$ty ?";
        } elsif ($ty eq 'VanillaMerge' or $ty eq 'MergedBreakwaters') {
-           # xxx need to handle ffq if one side was unstitched
-           # wait both of them may be!
+           # User may have merged unstitched branch(es).  We will
+           # have now lost what ffq-prev was then (since the later
+           # pseudomerge may introduce further changes).  The effect
+           # of resolving such a merge is that we may have to go back
+           # further in history to find a merge base, since the one
+           # which was reachable via ffq-prev is no longer findable.
+           # This is suboptimal, but if it all works we'll have done
+           # the right thing.
+           # xxx we should warn the user in the docs about this
+
            my $ok=1;
            my $best_anchor;
            # We expect to find a dominating anchor amongst the
@@ -1188,7 +1250,8 @@ sub walk ($;$$$) {
            push @brw_cl, {
                 %$cl,
                 SpecialMethod => 'MergeCreateMergedBreakwaters',
-                $xmsg->('construct merged breakwater from vanilla merge'),
+                $xmsg->('constructed from vanilla merge',
+                       ' merged-breakwater'),
             };
            push @upp_cl, {
                 %$cl,
@@ -1269,7 +1332,29 @@ sub walk ($;$$$) {
                    $cl->{MergeInterchangeBaseInfo},
                    @{ $cl->{Parents} };
                $last_anchor = $cl->{MergeBestAnchor};
-               # xxx need to check the tree somehow
+
+               # Check for mismerges:
+               my $check = sub {
+                   my ($against, $allow, $what) = @_;
+                   my $differs = get_differs $build, $against;
+                   $nomerge->(sprintf
+       "merge misresolved: %s are not the same (%s %s d.%#x)",
+                              $what, $against, $build, $differs)
+                       if $differs & ~($allow | D_PAT_ADD);
+               };
+
+               # Breakwater changes which were in each side of the
+               # merge will have been incorporated into the
+               # MergeCreateMergedBreakwaters output.  Because the
+               # upstream series was rebased onto the new breakwater,
+               # so should all of the packaging changes which were in
+               # the input.
+               $check->($input, D_UPS, 'debian files');
+
+               # Upstream files are merge_series, which ought to
+               # have been identical to the original merge.
+               $check->($cl->{CommitId}, DS_DEB, 'upstream files');
+
                print "Merge resolution successful.\n";
                next;
            } else {
@@ -1310,7 +1395,7 @@ sub walk ($;$$$) {
     };
 
     my $final_check = get_differs $build, $input;
-    die sprintf "internal error %#x %s %s", $final_check, $build, $input
+    die sprintf "internal error %#x %s %s", $final_check, $input, $build
        if $final_check & ~D_PAT_ADD;
 
     my @r = ($build, $breakwater, $last_anchor);