chiark / gitweb /
git-debrebase: merge: Actually record merge resolutions
[dgit.git] / git-debrebase
index 60ea976116fa1afad615a8c8be17e2171b55d7bf..96966adb3bb07b1c81a90da3427057d7ecc6c6f9 100755 (executable)
@@ -55,6 +55,9 @@ our ($opt_defaultcmd_interactive);
 
 our $us = qw(git-debrebase);
 
+our $wrecknoteprefix = 'refs/debrebase/wreckage';
+our $merge_cache_ref = 'refs/debrebase/merge-resolutions';
+
 $|=1;
 
 sub badusage ($) {
@@ -126,17 +129,42 @@ sub in_workarea ($) {
     die $@ if $@;
 }
 
-sub fresh_workarea () {
-    $workarea = fresh_playground "$playprefix/work";
+sub fresh_workarea (;$) {
+    my ($subdir) = @_;
+    $subdir //= 'work';
+    $workarea = fresh_playground "$playprefix/$subdir";
     in_workarea sub { playtree_setup };
 }
 
+sub run_ref_updates_now ($$) {
+    my ($mrest, $updates) = @_;
+    # @$updates is a list of lines for git-update-ref, without \ns
+
+    my @upd_cmd = (git_update_ref_cmd "debrebase: $mrest", qw(--stdin));
+    debugcmd '>|', @upd_cmd;
+    open U, "|-", @upd_cmd or die $!;
+    foreach (@$updates) {
+       printdebug ">= ", $_, "\n";
+       print U $_, "\n" or die $!;
+    }
+    printdebug ">\$\n";
+    close U or failedcmd @upd_cmd;
+}
+
 our $snags_forced = 0;
 our $snags_tripped = 0;
 our $snags_summarised = 0;
 our @deferred_updates;
 our @deferred_update_messages;
 
+sub merge_wreckage_cleaning ($) {
+    my ($updates) = @_;
+    git_for_each_ref("$wrecknoteprefix/*", sub {
+       my ($objid,$objtype,$fullrefname,$reftail) = @_;
+       push @$updates, "delete $fullrefname";
+    });
+}
+
 sub all_snags_summarised () {
     $snags_forced + $snags_tripped == $snags_summarised;
 }
@@ -145,16 +173,8 @@ sub run_deferred_updates ($) {
 
     confess 'dangerous internal error' unless all_snags_summarised();
 
-    my @upd_cmd = (git_update_ref_cmd "debrebase: $mrest", qw(--stdin));
-    debugcmd '>|', @upd_cmd;
-    open U, "|-", @upd_cmd or die $!;
-    foreach (@deferred_updates) {
-       printdebug ">= ", $_, "\n";
-       print U $_, "\n" or die $!;
-    }
-    printdebug ">\$\n";
-    close U or failedcmd @upd_cmd;
-
+    merge_wreckage_cleaning \@deferred_updates;
+    run_ref_updates_now $mrest, \@deferred_updates;
     print $_, "\n" foreach @deferred_update_messages;
 
     @deferred_updates = ();
@@ -344,7 +364,7 @@ sub gbp_pq_export ($$$) {
        { local ($!,$?); copy('../gbp-pq-err', \*STDERR); }
        failedcmd @gbp_cmd;
     }
-    runcmd @git, qw(add -f debian/patches);
+    runcmd @git, qw(add -f debian/patches) if stat_exists 'debian/patches';
 }
 
 
@@ -362,8 +382,57 @@ sub gbp_pq_export ($$$) {
 #  our own patch identification algorithm?
 #  this is an alternative strategy
 
-sub merge_series ($$;@) {
-    my ($newbase, $base_q, @input_qs) = @_;
+sub merge_failed ($$;@) {
+    my ($wrecknotes, $emsg, @xmsgs) = @_;
+    my @m;
+    push @m, "Merge resolution failed: $emsg";
+    push @m, @xmsgs;
+
+    changedir $maindir;
+
+    my @updates;
+    merge_wreckage_cleaning \@updates;
+    run_ref_updates_now "merge failed", \@updates;
+
+    @updates = ();
+    keys %$wrecknotes;
+    while (my ($k,$v) = each %$wrecknotes) {
+       push @updates, "create $wrecknoteprefix/$k $v";
+    }
+    run_ref_updates_now "merge failed", \@updates;
+    push @m, "Wreckage left in $wrecknoteprefix/*.";
+
+    push @m, "See git-debrebase(1) section FAILED MERGES for suggestions.";
+
+    # use finish rather than fail, in case we are within an eval
+    # (that can happen inside walk!)
+    print STDERR "\n";
+    print STDERR "$us: $_\n" foreach @m;
+    finish 15;
+}
+
+sub mwrecknote ($$$) {
+    my ($wrecknotes, $reftail, $commitish) = @_;
+    confess unless defined $commitish;
+    printdebug "mwrecknote $reftail $commitish\n";
+    $wrecknotes->{$reftail} = $commitish;
+}
+
+sub merge_attempt_cmd {
+    my $wrecknotes = shift @_;
+    debugcmd '+', @_;
+    $!=0; $?=-1;
+    if (system @_) {
+       merge_failed $wrecknotes,
+           failedcmd_waitstatus(),
+           "failed command: @_";
+    }
+}
+
+sub merge_series_patchqueue_convert ($$$);
+
+sub merge_series ($$$;@) {
+    my ($newbase, $wrecknotes, $base_q, @input_qs) = @_;
     # $base_q{SeriesBase}  $input_qs[]{SeriesBase}
     # $base_q{SeriesTip}   $input_qs[]{SeriesTip}
     # ^ specifies several patch series (currently we only support exactly 2)
@@ -386,17 +455,22 @@ sub merge_series ($$;@) {
     # $prereq{<patch filename>}{<possible prereq}  exists or not (later)
     # $prereq{<patch filename>}               exists or not (even later)
 
-    my $result;
+    my $merged_pq;
+
+    my $mwrecknote = sub { &mwrecknote($wrecknotes, @_); };
+
+    my $attempt_cmd = sub { &merge_attempt_cmd($wrecknotes, @_); };
 
-    local $workarea = fresh_playground "$playprefix/merge";
+    local $workarea;
+    fresh_workarea "merge";
     my $seriesfile = "debian/patches/series";
     in_workarea sub {
        playtree_setup();
        foreach my $q ($base_q, reverse @input_qs) {
            my $s = $q->{MR}{S};
            gbp_pq_export "p-$s", $q->{SeriesBase}, $q->{SeriesTip};
+           my @earlier;
            if (open S, $seriesfile) {
-               my @earlier;
                while (my $patch = <S>) {
                    chomp $patch or die $!;
                    $prereq{$patch} //= {};
@@ -418,25 +492,37 @@ sub merge_series ($$;@) {
                 " $q->{SeriesTip}]"
             ];
            printdebug "merge_series  pec $pec ";
-           runcmd @git, qw(rm -q --cached), $seriesfile;
+           runcmd @git, qw(rm -q --ignore-unmatch --cached), $seriesfile;
            $pec = make_commit [ $pec ], [
                 "Drop series file from $s to avoid merge trouble",
                 "[git-debrebase merge-innards patch-queue prep:".
                 " $q->{SeriesTip}]"
             ];
+
+           read_tree_debian $newbase;
+           if (@earlier) {
+               read_tree_subdir 'debian/patches', "$pec:debian/patches";
+           } else {
+               rm_subdir_cached 'debian/patches';
+           }
+           $pec = make_commit [ $pec ], [
+ "Update debian/ (excluding patches) to final to avoid re-merging",
+ "debian/ was already merged and we need to just take that.",
+                "[git-debrebase merge-innards patch-queue packaging:".
+                " $q->{SeriesTip}]"
+            ];
+
            printdebug "pec' $pec\n";
             runcmd @git, qw(reset -q --hard), $pec;
            $q->{MR}{PEC} = $pec;
+           $mwrecknote->("$q->{LeftRight}-patchqueue", $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;
-       if (system @mergecmd) {
-           failedcmd @mergecmd;
-       }
+
+       $attempt_cmd->(@mergecmd);
 
        printdebug "merge_series merge ok, series...\n";
        # We need to construct a new series file
@@ -508,17 +594,31 @@ sub merge_series ($$;@) {
        }
 
        runcmd @git, qw(add), $seriesfile;
-       runcmd @git, qw(commit --quiet -m), 'Merged series';
+       runcmd @git, qw(commit --quiet -m), 'Merged patch queue form';
+       $merged_pq = git_rev_parse 'HEAD';
+       $mwrecknote->('merged-patchqueue', $merged_pq);
+    };
+    return merge_series_patchqueue_convert
+           $wrecknotes, $newbase, $merged_pq;
+}
+
+sub merge_series_patchqueue_convert ($$$) {
+    my ($wrecknotes, $newbase, $merged_pq) = @_;
 
+    my $result;
+    in_workarea sub {
+       playtree_setup();
        printdebug "merge_series series gbp pq import\n";
-       runcmd qw(gbp pq import);
+       runcmd @git, qw(checkout -q -b mergec), $merged_pq;
+
+       merge_attempt_cmd($wrecknotes, 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));
+       my @lcmd = (@git, qw(rev-list --reverse mergec..patch-queue/mergec));
        foreach my $c (grep /./, split /\n/, cmdoutput @lcmd) {
            my $commit = git_cat_file $c, 'commit';
            printdebug "merge_series series ok, building $c\n";
@@ -532,14 +632,17 @@ sub merge_series ($$;@) {
            $build = cmdoutput @git, qw(hash-object -w -t commit ../mcommit);
        }
        $result = $build;
+       mwrecknote($wrecknotes, 'merged-result', $result);
+
        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';
+       my $mdebug = git_rev_parse 'HEAD';
+       printdebug sprintf "merge_series done debug=%s\n", $mdebug;
+       mwrecknote($wrecknotes, 'merged-debug', $mdebug);
     };
     printdebug "merge_series returns $result\n";
     return $result;
@@ -832,6 +935,8 @@ sub mergedbreakwaters_anchor ($) {
        $best_anchor = $panchor
            if !defined $best_anchor
            or is_fast_fwd $best_anchor, $panchor;
+       fail "inconsistent anchors in merged-breakwaters $p->{CommitId}"
+           unless is_fast_fwd $panchor, $best_anchor;
     }
     return $best_anchor;
 }
@@ -1010,13 +1115,17 @@ sub walk ($;$$$) {
     };
 
     my $nomerge = sub {
-       fail "something useful about failed merge attempt @_ xxx".Dumper($cl);
+       my ($emsg) = @_;
+       merge_failed $cl->{MergeWreckNotes}, $emsg;
     };
 
+    my $mwrecknote = sub { &mwrecknote($cl->{MergeWreckNotes}, @_); };
+
     my $last_anchor;
 
     for (;;) {
        $cl = classify $cur;
+       $cl->{MergeWreckNotes} //= {};
        my $ty = $cl->{Type};
        my $st = $cl->{SubType};
        $prline->("$cl->{CommitId} $cl->{Type}");
@@ -1116,7 +1225,11 @@ sub walk ($;$$$) {
                return $bomb->();
            }
            die "$ty ?";
-       } elsif ($ty eq 'VanillaMerge' or $ty eq 'MergedBreakwaters') {
+       } elsif ($ty eq 'MergedBreakwaters') {
+           $last_anchor = mergedbreakwaters_anchor $cl;
+           $build_start->(' MergedBreakwaters', $cur);
+           last;
+       } elsif ($ty eq 'VanillaMerge') {
            # 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
@@ -1152,15 +1265,14 @@ sub walk ($;$$$) {
 
            printdebug "*** MERGE\n";
 
-           # xxx avoid calling walk without nogenerate when
-           # we have a MergedBreakwater; instead call keycommits ?
-
            my @bwbcmd = (@git, qw(merge-base));
            my @ibcmd = (@git, qw(merge-base --all));
            my $might_be_in_bw = 1;
 
            my $ps = $cl->{Parents};
 
+           $mwrecknote->('vanilla-merge', $cl->{CommitId});
+
            foreach my $p (@$ps) {
                $prline->(" VanillaMerge ".$p->{Ix});
                $prprdelim->();
@@ -1171,6 +1283,17 @@ sub walk ($;$$$) {
                $p->{Breakwater} = $p->{SeriesBase} = $pbw;
                $p->{Anchor} = $panchor;
 
+               my $lr = $p->{LeftRight} = (qw(left right))[$p->{Ix}];
+               $mwrecknote->("$lr-input", $p->{CommitId});
+
+               my $mwrecknote_parent = sub {
+                   my ($which) = @_;
+                   $mwrecknote->("$lr-".(lc $which), $p->{$which});
+               };
+               $mwrecknote_parent->('Laundered');
+               $mwrecknote_parent->('Breakwater');
+               $mwrecknote_parent->('Anchor');
+
                $best_anchor = $panchor if
                    !defined $best_anchor or
                    is_fast_fwd $best_anchor, $panchor;
@@ -1179,10 +1302,12 @@ sub walk ($;$$$) {
                    " p=$panchor\n";
            }
 
+           $mwrecknote->('result-anchor', $best_anchor);
+
            foreach my $p (@$ps) {
                $prline->(" VanillaMerge ".$p->{Ix});
                if (!is_fast_fwd $p->{Anchor}, $best_anchor) {
-                   $nomerge->('DivergentAnchor');
+                   $nomerge->('divergent anchors');
                } elsif ($p->{Anchor} eq $best_anchor) {
                    print $report " SameAnchor" if $report;
                } else {
@@ -1200,14 +1325,14 @@ sub walk ($;$$$) {
            }
 
            if ($ok && $might_be_in_bw) {
+               # We could rewrite this to contaion the metadata
+               # declaring it to be MergedBreakwaters, but
+               # unnecessarily rewriting a merge seems unhelpful.
                $prline->(" VanillaMerge MergedBreakwaters");
                $last_anchor = $best_anchor;
                $build_start->('MergedBreakwaters', $cur);
            }
 
-           $nomerge->("alleged merged-breakwater is not a breakwater")
-               unless $ty eq 'VanillaMerge';
-
            my $bwb = cmdoutput @bwbcmd;
 
            # OK, now we have a breakwater base, but we need the merge
@@ -1218,25 +1343,51 @@ sub walk ($;$$$) {
            # breakwater merge base as an ancestor.
 
            my @ibs =
-               grep { is_fast_fwd $bwb, $_ }
                grep /./,
                split /\n/,
                cmdoutput @ibcmd;
-           my ($ib) = @ibs
-               or $nomerge->("no suitable interchange merge base");
+
+            @ibs or confess 'internal error, expected anchor at least ?';
+
+           my $ib;
+           my $ibleaf;
+           foreach my $tibix (0..$#ibs) {
+               my $tib = $ibs[$tibix];
+               my $ff = is_fast_fwd $bwb, $tib;
+               my $ok = !$ff ? 'rej' : $ib ? 'extra' : 'ok';
+               my $tibleaf = "interchange-mbcand-$ok-$tibix";
+               $mwrecknote->($tibleaf, $tib);
+               next unless $ff;
+               next if $ib;
+               $ib = $tib;
+               $ibleaf = $tibleaf;
+           }
+
+           $ib or $nomerge->("no suitable interchange merge base");
 
            $prline->("  VanillaMerge Base");
            $prprdelim->();
-           my ($btip, $bbw, $banchor) =
+           my ($btip, $bbw, $banchor) = eval {
                walk $ib, 0, $report, $report_lprefix.'  ';
+           };
+           $nomerge->("walking interchange branch merge base ($ibleaf):\n".
+                      $@)
+               if length $@;
+
+           $mwrecknote->("mergebase-laundered", $btip);
+           $mwrecknote->("mergebase-breakwater", $bbw);
+           $mwrecknote->("mergebase-anchor", $banchor);
 
            my $ibinfo = { SeriesTip => $btip,
                           SeriesBase => $bbw,
-                          Anchor => $banchor };
+                          Anchor => $banchor,
+                          LeftRight => 'mergebase' };
+
            $bbw eq $bwb
                or $nomerge->("interchange merge-base ($ib)'s".
                              " breakwater ($bbw)".
                              " != breakwaters' merge-base ($bwb)");
+
            grep { $_->{Anchor} eq $ibinfo->{Anchor} } @$ps
                 or $nomerge->("interchange merge-base ($ib)'s".
                               " anchor ($ibinfo->{SeriesBase})".
@@ -1244,7 +1395,6 @@ sub walk ($;$$$) {
                               (join ' ', map { $_->{Anchor} } @$ps).
                               ")");
 
-
            $cl->{MergeInterchangeBaseInfo} = $ibinfo;
            $cl->{MergeBestAnchor} = $best_anchor;
            push @brw_cl, {
@@ -1276,6 +1426,11 @@ sub walk ($;$$$) {
 
     my $rewriting = 0;
 
+    my $read_tree_upstream = sub {
+       my ($treeish) = @_;
+       read_tree_upstream $treeish, 0, $build;
+    };
+
     $#upp_cl = $upp_limit if defined $upp_limit;
  
     my $committer_authline = calculate_committer_authline();
@@ -1303,7 +1458,7 @@ sub walk ($;$$$) {
            if ($method eq 'Debian') {
                read_tree_debian($cltree);
            } elsif ($method eq 'Upstream') {
-               read_tree_upstream($cltree);
+               $read_tree_upstream->($cltree);
            } elsif ($method eq 'StartRewrite') {
                $rewriting = 1;
                next;
@@ -1316,19 +1471,20 @@ sub walk ($;$$$) {
                confess unless $rewriting;
                my $differs = (get_differs $build, $cltree);
                next unless $differs & D_UPS;
-               read_tree_upstream($cltree);
+               $read_tree_upstream->($cltree);
                push @parents, map { $_->{CommitId} } @{ $cl->{OrigParents} };
            } elsif ($method eq 'MergeCreateMergedBreakwaters') {
                print "Found a general merge, will try to tidy it up.\n";
                $rewriting = 1;
-               read_tree_upstream($cl->{MergeBestAnchor});
-               read_tree_upstream($cl->{MergeBestAnchor});
+               $read_tree_upstream->($cl->{MergeBestAnchor});
+               $read_tree_upstream->($cl->{MergeBestAnchor});
                read_tree_debian($cltree);
                @parents = map { $_->{Breakwater} } @{ $cl->{Parents} };
            } elsif ($method eq 'MergeMergeSeries') {
                print "Running merge resolution for $cl->{CommitId}...\n";
+               $mwrecknote->('new-base', $build);
                $build = merge_series
-                   $build,
+                   $build, $cl->{MergeWreckNotes},
                    $cl->{MergeInterchangeBaseInfo},
                    @{ $cl->{Parents} };
                $last_anchor = $cl->{MergeBestAnchor};
@@ -2049,19 +2205,32 @@ sub cmd_conclude () {
 sub cmd_scrap () {
     if (currently_rebasing()) {
        runcmd @git, qw(rebase --abort);
+       push @deferred_updates, 'verify HEAD HEAD';
+       # noop, but stops us complaining that scrap was a noop
     }
+    badusage "no arguments allowed" if @ARGV;
     my ($ffq_prev, $gdrlast, $ffq_prev_commitish) = ffq_prev_info();
-    if (!$ffq_prev_commitish) {
+    my $scrapping_head;
+    if ($ffq_prev_commitish) {
+       $scrapping_head = get_head();
+       push @deferred_updates,
+           "update $gdrlast $ffq_prev_commitish $git_null_obj",
+           "update $ffq_prev $git_null_obj $ffq_prev_commitish";
+    }
+    if (git_get_ref $merge_cache_ref) {
+       push @deferred_updates,
+           "delete $merge_cache_ref";
+    }
+    if (!@deferred_updates) {
        fail "No ongoing git-debrebase session." unless $opt_noop_ok;
        finish 0;
     }
-    my $scrapping_head = get_head();
-    badusage "no arguments allowed" if @ARGV;
-    push @deferred_updates,
-       "update $gdrlast $ffq_prev_commitish $git_null_obj",
-       "update $ffq_prev $git_null_obj $ffq_prev_commitish";
     snags_maybe_bail();
-    update_head_checkout $scrapping_head, $ffq_prev_commitish, "scrap";
+    if ($scrapping_head) {
+       update_head_checkout $scrapping_head, $ffq_prev_commitish, "scrap";
+    } else {
+       run_deferred_updates "scrap";
+    }
 }
 
 sub make_patches_staged ($) {
@@ -2456,6 +2625,46 @@ END
        'convert-from-dgit-view';
 }
 
+sub cmd_record_resolved_merge () {
+    badusage "record-resolved-merge takes no further arguments" if @ARGV;
+    # xxx needs documentation
+    my $new = get_head();
+    my $method;
+
+    print "Checking how you have resolved the merge problem\n";
+    my $nope = sub { print "Not $method: @_"; 0; };
+
+    my $maybe = sub { print "Seems to be $method.\n"; };
+    my $yes = sub {
+       my ($key, $ref) = @_;
+       reflog_cache_insert $merge_cache_ref, $key, $ref;
+       print "OK.  You can switch branches and try git-debrebase again.\n";
+       1;
+    };
+
+    fresh_workarea 'merge';
+    sub {
+       $method = 'vanilla-merge patchqueue';
+       my $vanilla = git_get_ref "$wrecknoteprefix/vanilla-merge";
+       $vanilla or return $nope->("wreckage was not of vanilla-merge");
+       foreach my $lr (qw(left right)) {
+           my $n = "$wrecknoteprefix/$lr-patchqueue";
+           my $lrpq = git_get_ref $n;
+           $lrpq or return $nope->("wreckage did not contain patchqueues");
+           is_fast_fwd $lrpq, $new or return $nope->("HEAD not ff of $n");
+       }
+       $maybe->();
+       my $newbase = git_get_ref "$wrecknoteprefix/new-base"
+           or die "wreckage element $wrecknoteprefix/new-base missing";
+       my $result = merge_series_patchqueue_convert
+           {}, $newbase, $new;
+       $yes->("vanilla-merge $vanilla", $result);
+       1;
+    }->() or sub {
+       fail "No resolved merge method seems applicable.\n";
+    }->();
+}
+
 sub cmd_downstream_rebase_launder_v0 () {
     badusage "needs 1 argument, the baseline" unless @ARGV==1;
     my ($base) = @ARGV;