chiark / gitweb /
git-debrebase: Provide new get_tree and trees_diff_walk
[dgit.git] / git-debrebase
index 62381e1de83c8fc1440e578593f0bf8422cdb632..d04f2a3af54882d2f256ea82341349525e54c5e9 100755 (executable)
@@ -50,11 +50,16 @@ usages:
 See git-debrebase(1), git-debrebase(5), dgit-maint-debrebase(7) (in dgit).
 END
 
-our ($opt_force, $opt_noop_ok, @opt_anchors);
+our ($opt_force, $opt_careful, $opt_noop_ok, @opt_anchors);
 our ($opt_defaultcmd_interactive);
 
+$opt_careful = 1;
+
 our $us = qw(git-debrebase);
 
+our $wrecknoteprefix = 'refs/debrebase/wreckage';
+our $merge_cache_ref = 'refs/debrebase/merge-resolutions';
+
 $|=1;
 
 sub badusage ($) {
@@ -126,17 +131,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,22 +175,70 @@ 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 = ();
     @deferred_update_messages = ();
 }
 
+sub get_tree ($) {
+    # tree object name => ([ $name, $info ], ...)
+    # where $name is the sort key, ie has / at end for subtrees
+    # $info is the LHS from git-ls-tree (<mode> <type> <hash>)
+    # will crash if $x does not exist, so don't do that
+    my ($x) = @_;
+    our (@get_tree_memo, %get_tree_memo);
+    my $memo = $get_tree_memo{$x};
+    return @$memo if $memo;
+
+    local $debugcmd_when_debuglevel = 3;
+    my @l;
+    my @cmd = (qw(git ls-tree -z --full-tree --), $x);
+    my $o = cmdoutput @cmd;
+    $o =~ s/\0$//s;
+    my $last = '';
+    foreach my $l (split /\0/, $o) {
+       my ($i, $n) = split /\t/, $l, 2;
+       $n .= '/' if $i =~ m/^\d+ tree /;
+       push @l, [ $n, $i ];
+       confess "$x need $last < $n ?" unless $last lt $n;
+    }
+    $get_tree_memo{$x} = \@l;
+    push @get_tree_memo, $x;
+    if (@get_tree_memo > 10) {
+       delete $get_tree_memo{ shift @get_tree_memo };
+    }
+    return @l;
+}
+
+sub trees_diff_walk ($$$;$) {
+    # trees_diff_walk [$all,] $x, $y, sub {... }
+    # calls sub->($name, $ix, $iy) for each difference (with $all, each name)
+    # $x and $y are as for get_tree
+    # where $name, $ix, $iy are $name and $info from get_tree
+    my $all = shift @_ if @_>=4;
+    my ($x,$y,$call) = @_;
+    return if !$all and $x eq $y;
+    my @x = get_tree $x;
+    my @y = get_tree $y;
+    while (@x || @y) {
+       my $cmp = !@x       <=> !@y          # eg @y empty? $cmp=-1, use x
+            ||    $x[0][0] cmp  $y[0][0];   # eg, x lt y ? $cmp=-1, use x
+       my ($n, $ix, $iy);                   # all same? $cmp=0, use both
+       $ix=$iy='';
+       ($n, $ix) = @{ shift @x } if $cmp <= 0;
+       ($n, $iy) = @{ shift @y } if $cmp >= 0;
+       next if !$all and $ix eq $iy;
+       printdebug sprintf
+           "trees_diff_walk(%d,'%s','%s') call('%s','%s','%s')\n",
+           !!$all,$x,$y, $n,$ix,$iy
+           if $debuglevel >= 2;
+       $call->($n, $ix, $iy);
+    }
+}
+
 sub get_differs ($$) {
     my ($x,$y) = @_;
     # This resembles quiltify_trees_differ, in dgit, a bit.
@@ -344,7 +422,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';
 }
 
 
@@ -353,15 +431,61 @@ sub gbp_pq_export ($$$) {
 # 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)
 
 # later/rework?
 #  use git-format-patch?
 #  our own patch identification algorithm?
 #  this is an alternative strategy
 
+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}
@@ -386,9 +510,14 @@ 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;
 
-    local $workarea = fresh_playground "$playprefix/merge";
+    my $mwrecknote = sub { &mwrecknote($wrecknotes, @_); };
+
+    my $attempt_cmd = sub { &merge_attempt_cmd($wrecknotes, @_); };
+
+    local $workarea;
+    fresh_workarea "merge";
     my $seriesfile = "debian/patches/series";
     in_workarea sub {
        playtree_setup();
@@ -418,7 +547,7 @@ 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:".
@@ -441,16 +570,14 @@ sub merge_series ($$$;@) {
            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
@@ -522,17 +649,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";
@@ -546,6 +687,8 @@ 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);
@@ -554,6 +697,7 @@ sub merge_series ($$$;@) {
        runcmd @git, qw(commit --allow-empty -q -m M-WORKTREE);
        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;
@@ -1026,18 +1170,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 {
-       my ($reftail, $commitish) = @_;
-       $cl->{MergeWreckNotes}{$reftail} = $commitish;
-    };
+    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}");
@@ -1282,8 +1425,9 @@ sub walk ($;$$$) {
            my ($btip, $bbw, $banchor) = eval {
                walk $ib, 0, $report, $report_lprefix.'  ';
            };
-           $nomerge->("walking interchange branch merge base ($ibleaf): ".
-                      $@) if length $@;
+           $nomerge->("walking interchange branch merge base ($ibleaf):\n".
+                      $@)
+               if length $@;
 
            $mwrecknote->("mergebase-laundered", $btip);
            $mwrecknote->("mergebase-breakwater", $bbw);
@@ -1291,7 +1435,9 @@ sub walk ($;$$$) {
 
            my $ibinfo = { SeriesTip => $btip,
                           SeriesBase => $bbw,
-                          Anchor => $banchor };
+                          Anchor => $banchor,
+                          LeftRight => 'mergebase' };
+
            $bbw eq $bwb
                or $nomerge->("interchange merge-base ($ib)'s".
                              " breakwater ($bbw)".
@@ -1335,11 +1481,6 @@ 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();
@@ -1351,7 +1492,12 @@ sub walk ($;$$$) {
     in_workarea sub {
        mkdir $rd or $!==EEXIST or die $!;
        my $current_method;
-       runcmd @git, qw(read-tree), $build;
+       my $want_debian = $build;
+       my $want_upstream = $build;
+
+       my $read_tree_upstream = sub { ($want_upstream) = @_; };
+       my $read_tree_debian = sub { ($want_debian) = @_; };
+
        foreach my $cl (qw(Debian), (reverse @brw_cl),
                        { SpecialMethod => 'RecordBreakwaterTip' },
                        qw(Upstream), (reverse @upp_cl)) {
@@ -1365,7 +1511,7 @@ sub walk ($;$$$) {
            printdebug "WALK BUILD ".($cltree//'undef').
                " $method (rewriting=$rewriting)\n";
            if ($method eq 'Debian') {
-               read_tree_debian($cltree);
+               $read_tree_debian->($cltree);
            } elsif ($method eq 'Upstream') {
                $read_tree_upstream->($cltree);
            } elsif ($method eq 'StartRewrite') {
@@ -1375,7 +1521,7 @@ sub walk ($;$$$) {
                $breakwater = $build;
                next;
            } elsif ($method eq 'DgitImportDebianUpdate') {
-               read_tree_debian($cltree);
+               $read_tree_debian->($cltree);
            } elsif ($method eq 'DgitImportUpstreamUpdate') {
                confess unless $rewriting;
                my $differs = (get_differs $build, $cltree);
@@ -1386,15 +1532,23 @@ sub walk ($;$$$) {
                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_debian($cltree);
+               $read_tree_debian->($cltree);
                @parents = map { $_->{Breakwater} } @{ $cl->{Parents} };
            } elsif ($method eq 'MergeMergeSeries') {
-               print "Running merge resolution for $cl->{CommitId}...\n";
-               $build = merge_series
-                   $build, $cl->{MergeWreckNotes},
-                   $cl->{MergeInterchangeBaseInfo},
-                   @{ $cl->{Parents} };
+               my $cachehit = reflog_cache_lookup
+                   $merge_cache_ref, "vanilla-merge $cl->{CommitId}";
+               if ($cachehit) {
+                   print "Using supplied resolution for $cl->{CommitId}...\n";
+                   $build = $cachehit;
+                   $mwrecknote->('cached-resolution', $build);
+               } else {
+                   print "Running merge resolution for $cl->{CommitId}...\n";
+                   $mwrecknote->('new-base', $build);
+                   $build = merge_series
+                       $build, $cl->{MergeWreckNotes},
+                       $cl->{MergeInterchangeBaseInfo},
+                       @{ $cl->{Parents} };
+               }
                $last_anchor = $cl->{MergeBestAnchor};
 
                # Check for mismerges:
@@ -1431,27 +1585,34 @@ sub walk ($;$$$) {
                    printdebug "WALK REWRITING NOW cl=$cl procd=$procd\n";
                }
            }
-           my $newtree = cmdoutput @git, qw(write-tree);
-           my $ch = $cl->{Hdr};
-           $ch =~ s{^tree .*}{tree $newtree}m or confess "$ch ?";
-           $ch =~ s{^parent .*\n}{}mg;
-           $ch =~ s{(?=^author)}{
-               join '', map { "parent $_\n" } @parents
-           }me or confess "$ch ?";
-           if ($rewriting) {
-               $ch =~ s{^committer .*$}{$committer_authline}m
-                   or confess "$ch ?";
+           if ($rewriting || $opt_careful) {
+               read_tree_upstream $want_upstream, 0, $want_debian;
+
+               my $newtree = cmdoutput @git, qw(write-tree);
+               my $ch = $cl->{Hdr};
+               $ch =~ s{^tree .*}{tree $newtree}m or confess "$ch ?";
+               $ch =~ s{^parent .*\n}{}mg;
+               $ch =~ s{(?=^author)}{
+                   join '', map { "parent $_\n" } @parents
+               }me or confess "$ch ?";
+               if ($rewriting) {
+                   $ch =~ s{^committer .*$}{$committer_authline}m
+                       or confess "$ch ?";
+               }
+               my $cf = "$rd/m$rewriting";
+               open CD, ">", $cf or die $!;
+               print CD $ch, "\n", $cl->{Msg} or die $!;
+               close CD or die $!;
+               my @cmd = (@git, qw(hash-object));
+               push @cmd, qw(-w) if $rewriting;
+               push @cmd, qw(-t commit), $cf;
+               my $newcommit = cmdoutput @cmd;
+               confess "$ch ?" unless $rewriting
+                   or $newcommit eq $cl->{CommitId};
+               $build = $newcommit;
+           } else {
+               $build = $cl->{CommitId};
            }
-           my $cf = "$rd/m$rewriting";
-           open CD, ">", $cf or die $!;
-           print CD $ch, "\n", $cl->{Msg} or die $!;
-           close CD or die $!;
-           my @cmd = (@git, qw(hash-object));
-           push @cmd, qw(-w) if $rewriting;
-           push @cmd, qw(-t commit), $cf;
-           my $newcommit = cmdoutput @cmd;
-           confess "$ch ?" unless $rewriting or $newcommit eq $cl->{CommitId};
-           $build = $newcommit;
             if (grep { $method eq $_ } qw(DgitImportUpstreamUpdate)) {
                 $last_anchor = $cur;
             }
@@ -2113,19 +2274,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 ($) {
@@ -2520,6 +2694,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;