chiark / gitweb /
i18n: dgit: mark some messages (2)
[dgit.git] / git-debrebase
index 3d74d0ebc3876f1cb9ca677e56c9eaf88e80d3de..645b072aaf2a11eb34ca2943eb4b69961c49ed74 100755 (executable)
@@ -21,6 +21,7 @@
 END { $? = $Debian::Dgit::ExitStatus::desired // -1; };
 use Debian::Dgit::GDR;
 use Debian::Dgit::ExitStatus;
+use Debian::Dgit::I18n;
 
 use strict;
 
@@ -30,6 +31,7 @@ setup_sigwarn();
 use Memoize;
 use Carp;
 use POSIX;
+use Locale::gettext;
 use Data::Dumper;
 use Getopt::Long qw(:config posix_default gnu_compat bundling);
 use Dpkg::Version;
@@ -50,11 +52,14 @@ 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_noop_ok, $opt_merges, @opt_anchors);
 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 ($) {
@@ -107,8 +112,8 @@ sub get_commit ($) {
 sub D_UPS ()      { 0x02; } # upstream files
 sub D_PAT_ADD ()  { 0x04; } # debian/patches/ extra patches at end
 sub D_PAT_OTH ()  { 0x08; } # debian/patches other changes
-sub D_DEB_CLOG () { 0x10; } # debian/ (not patches/ or changelog)
-sub D_DEB_OTH ()  { 0x20; } # debian/changelog
+sub D_DEB_CLOG () { 0x10; } # debian/changelog
+sub D_DEB_OTH ()  { 0x20; } # debian/ (not patches/ or changelog)
 sub DS_DEB ()     { D_DEB_CLOG | D_DEB_OTH; } # debian/ (not patches/)
 
 our $playprefix = 'debrebase';
@@ -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,88 +175,166 @@ 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>)
+    # without $precheck, will crash if $x does not exist, so don't do that;
+    # instead pass '' to get ().
+    my ($x, $precheck, $recurse) = @_;
+
+    return () if !length $x;
+
+    if ($precheck) {
+       my ($type, $dummy) = git_cat_file $x, [qw(tree missing)];
+       return () if $type eq 'missing';
+    }
+
+    $recurse = !!$recurse;
+
+    confess "get_tree needs object not $x ?" unless $x =~ m{^[0-9a-f]+\:};
+
+    our (@get_tree_memo, %get_tree_memo);
+    my $memo = $get_tree_memo{$recurse,$x};
+    return @$memo if $memo;
+
+    local $Debian::Dgit::debugcmd_when_debuglevel = 3;
+    my @l;
+    my @cmd = (qw(git ls-tree -z --full-tree));
+    push @cmd, qw(-r) if $recurse;
+    push @cmd, qw(--), $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{$recurse,$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 [{..opts...},] $x, $y, sub {... }
+    # calls sub->($name, $ix, $iy) for each difference
+    # $x and $y are as for get_tree
+    # where $name, $ix, $iy are $name and $info from get_tree
+    # opts are   all       call even for names same in both
+    #            recurse   call even for names same in both
+    my $opts = shift @_ if @_>=4;
+    my ($x,$y,$call) = @_;
+    my $all = $opts->{all};
+    return if !$all and $x eq $y;
+    my @x = get_tree $x, 0, $opts->{recurse};
+    my @y = get_tree $y, 0, $opts->{recurse};
+    printdebug "trees_diff_walk(..$x,$y..) ".Dumper(\@x,\@y)
+       if $debuglevel >= 3;
+    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='';
+       printdebug "trees_diff_walk $cmp : @{ $x[0]//[] } | @{ $y[0]//[] }\n"
+           if $debuglevel >= 3;
+       ($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.
+    # This does a similar job to quiltify_trees_differ, in dgit, a bit.
     # But we don't care about modes, or dpkg-source-unrepresentable
     # changes, and we don't need the plethora of different modes.
     # Conversely we need to distinguish different kinds of changes to
     # debian/ and debian/patches/.
+    # Also, here we have, and want to use, trees_diff_walk, because
+    # we may be calling this an awful lot and we want it to be fast.
 
     my $differs = 0;
+    my @debian_info;
 
-    my $rundiff = sub {
-       my ($opts, $limits, $fn) = @_;
-       my @cmd = (@git, qw(diff-tree -z --no-renames));
-       push @cmd, @$opts;
-       push @cmd, "$_:" foreach $x, $y;
-       push @cmd, '--', @$limits;
-       my $diffs = cmdoutput @cmd;
-       foreach (split /\0/, $diffs) { $fn->(); }
-    };
+    no warnings qw(exiting);
 
-    $rundiff->([qw(--name-only)], [], sub {
-        $differs |= $_ eq 'debian' ? DS_DEB : D_UPS;
-    });
+    my $plain = sub { $_[0] =~ m{^(100|0*)644 blob }s; };
 
-    if ($differs & DS_DEB) {
-       $differs &= ~DS_DEB;
-       $rundiff->([qw(--name-only -r)], [qw(debian)], sub {
-            $differs |=
-               m{^debian/patches/}      ? D_PAT_OTH  :
-               $_ eq 'debian/changelog' ? D_DEB_CLOG :
-                                          D_DEB_OTH;
-       });
-       die "mysterious debian changes $x..$y"
-           unless $differs & (D_PAT_OTH|DS_DEB);
-    }
-
-    if ($differs & D_PAT_OTH) {
-       my $mode;
-       $differs &= ~D_PAT_OTH;
-       my $pat_oth = sub {
-           $differs |= D_PAT_OTH;
-           no warnings qw(exiting);  last;
-       };
-       $rundiff->([qw(--name-status -r)], [qw(debian/patches/)], sub {
-            no warnings qw(exiting);
-            if (!defined $mode) {
-               $mode = $_;  next;
+    trees_diff_walk "$x:", "$y:", sub {
+       my ($n,$ix,$iy) = @_;
+
+       # analyse difference at the toplevel
+
+       if ($n ne 'debian/') {
+           $differs |= D_UPS;
+           next;
+       }
+       if ($n eq 'debian') {
+           # one side has a non-tree for ./debian !
+           $differs |= D_DEB_OTH;
+           next;
+       }
+
+       my $xd = $ix && "$x:debian";
+       my $yd = $iy && "$y:debian";
+       trees_diff_walk $xd, $yd, sub {
+           my ($n,$ix,$iy) = @_;
+           
+           # analyse difference in debian/
+
+           if ($n eq 'changelog' && (!$ix || $plain->($ix))
+                                  &&          $plain->($iy) ) {
+               $differs |= D_DEB_CLOG;
+               next;
            }
-           die unless s{^debian/patches/}{};
-           my $ok;
-           if ($mode eq 'A' && !m/\.series$/s) {
-               $ok = 1;
-           } elsif ($mode eq 'M' && $_ eq 'series') {
-               my $x_s = (git_cat_file "$x:debian/patches/series", 'blob');
-               my $y_s = (git_cat_file "$y:debian/patches/series", 'blob');
-               chomp $x_s;  $x_s .= "\n";
-               $ok = $x_s eq substr($y_s, 0, length $x_s);
-           } else {
-               # nope
+           if ($n ne 'patches/') {
+               $differs |= D_DEB_OTH;
+               next;
            }
-           $mode = undef;
-           $differs |= $ok ? D_PAT_ADD : D_PAT_OTH;
-        });
-       die "mysterious debian/patches changes $x..$y"
-           unless $differs & (D_PAT_ADD|D_PAT_OTH);
-    }
+
+           my $xp = $ix && "$xd/patches";
+           my $yp = $iy && "$yd/patches";
+           trees_diff_walk { recurse=>1 }, $xp, $yp, sub {
+               my ($n,$ix,$iy) = @_;
+
+               # analyse difference in debian/patches
+
+               my $ok;
+               if ($n =~ m{/$}s) {
+                   # we are recursing; directories may appear and disappear
+                   $ok = 1;
+               } elsif ($n !~ m/\.series$/s && !$ix && $plain->($iy)) {
+                   $ok = 1;
+               } elsif ($n eq 'series' && $plain->($ix) && $plain->($iy)) {
+                   my $x_s = (git_cat_file "$xp/series", 'blob');
+                   my $y_s = (git_cat_file "$yp/series", 'blob');
+                   chomp $x_s;  $x_s .= "\n";
+                   $ok = $x_s eq substr($y_s, 0, length $x_s);
+               } else {
+                   # nope
+               }
+               $differs |= $ok ? D_PAT_ADD : D_PAT_OTH;
+           };    
+       };
+    };
 
     printdebug sprintf "get_differs %s %s = %#x\n", $x, $y, $differs;
 
@@ -330,11 +438,65 @@ sub any_snags () {
     return $snags_forced || $snags_tripped;
 }
 
+sub ffq_prev_branchinfo () {
+    my $current = git_get_symref();
+    return gdr_ffq_prev_branchinfo($current);
+}
+
+sub record_gdrlast ($$;$) {
+    my ($gdrlast, $newvalue, $oldvalue) = @_;
+    $oldvalue ||= $git_null_obj;
+    push @deferred_updates, "update $gdrlast $newvalue $oldvalue";
+}
+
+sub fail_unprocessable ($) {
+    my ($msg) = @_;
+    changedir $maindir;
+    my ($ffqs, $ffqm, $symref, $ffq_prev, $gdrlast) = ffq_prev_branchinfo();
+
+    my $mangled = <<END;
+Branch/history seems mangled - no longer in gdr format.
+See ILLEGAL OPERATIONS in git-debrebase(5).
+END
+    chomp $mangled;
+
+    if (defined $ffqm) {
+       fail <<END;
+$msg
+Is this meant to be a gdr branch?  $ffqm
+END
+    } elsif (git_get_ref $ffq_prev) {
+       fail <<END;
+$msg
+$mangled
+Consider git-debrebase scrap, to throw away your recent work.
+END
+    } elsif (!git_get_ref $gdrlast) {
+       fail <<END;
+$msg
+Branch does not seem to be meant to be a git-debrebase branch?
+Wrong branch, or maybe you needed git-debrebase convert-from-*.
+END
+    } elsif (is_fast_fwd $gdrlast, git_rev_parse 'HEAD') {
+       fail <<END;
+$msg
+$mangled
+END
+    } else {
+       fail <<END;
+$msg
+Branch/history mangled, and diverged since last git-debrebase.
+Maybe you reset to, or rebased from, somewhere inappropriate.
+END
+    }
+};
+
 sub gbp_pq_export ($$$) {
     my ($bname, $base, $tip) = @_;
     # 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.
+    # returns 1 if there were any patches
     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;
@@ -344,24 +506,68 @@ sub gbp_pq_export ($$$) {
        { local ($!,$?); copy('../gbp-pq-err', \*STDERR); }
        failedcmd @gbp_cmd;
     }
+    return 0 unless stat_exists 'debian/patches';
     runcmd @git, qw(add -f debian/patches);
+    return 1;
 }
 
 
-# xxx allow merge resolution separately from laundering, before git merge
-#
-# 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)
+# MERGE-TODO allow merge resolution separately from laundering, before git merge
 
 # 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,22 +592,23 @@ 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 {
-       my ($reftail, $commitish) = @_;
-       $wrecknotes->{$reftail} = $commitish;
-    };
+    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 $any = gbp_pq_export "p-$s", $q->{SeriesBase}, $q->{SeriesTip};
            my @earlier;
-           if (open S, $seriesfile) {
+           if ($any) {
+               open S, $seriesfile or die "$seriesfile $!";
                while (my $patch = <S>) {
                    chomp $patch or die $!;
                    $prereq{$patch} //= {};
@@ -413,8 +620,6 @@ sub merge_series ($$$;@) {
                }
                S->error and die "$seriesfile $!";
                close S;
-           } else {
-               die "$seriesfile $!" unless $!==ENOENT;
            }
            read_tree_upstream $newbase, 1;
            my $pec = make_commit [ grep { defined } $base_q->{MR}{PEC} ], [
@@ -423,7 +628,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:".
@@ -446,17 +651,14 @@ sub merge_series ($$$;@) {
            printdebug "pec' $pec\n";
             runcmd @git, qw(reset -q --hard), $pec;
            $q->{MR}{PEC} = $pec;
-           $mwrecknote->("$q->{LeftRight}-patchqueue");
+           $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
@@ -529,17 +731,32 @@ sub merge_series ($$$;@) {
 
        runcmd @git, qw(add), $seriesfile;
        runcmd @git, qw(commit --quiet -m), 'Merged patch queue form';
-       $mwrecknote->('merged-patchqueue', git_rev_parse 'HEAD');
+       $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));
+       # MERGE-TODO consider git-format-patch etc. instead,
+       # since gbp pq doesn't always round-trip :-/
 
        # 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";
@@ -553,7 +770,7 @@ sub merge_series ($$$;@) {
            $build = cmdoutput @git, qw(hash-object -w -t commit ../mcommit);
        }
        $result = $build;
-       $mwrecknote->('merged-result', $result);
+       mwrecknote($wrecknotes, 'merged-result', $result);
 
        runcmd @git, qw(update-ref refs/heads/result), $result;
 
@@ -563,7 +780,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->('merged-debug', $mdebug);
+       mwrecknote($wrecknotes, 'merged-debug', $mdebug);
     };
     printdebug "merge_series returns $result\n";
     return $result;
@@ -828,7 +1045,6 @@ sub classify ($) {
 
     if (@p == 2 and
        $r->{Msg} =~ m{^\[git-debrebase merged-breakwater.*\]$}m) {
-       # xxx ^ metadata tag needs adding to (5)
        return $classify->("MergedBreakwaters");
     }
     if ($r->{Msg} =~ m{^\[(git-debrebase|dgit)[: ].*\]$}m) {
@@ -838,7 +1054,7 @@ sub classify ($) {
        return $unknown->("octopus merge");
     }
 
-    if (!$ENV{GIT_DEBREBASE_EXPERIMENTAL_MERGE}) {
+    if (!$opt_merges) {
        return $unknown->("general two-parent merge");
     }
 
@@ -866,9 +1082,8 @@ sub keycommits ($;$$$$$) {
     my ($head, $furniture, $unclean, $trouble, $fatal, $claimed_bw) = @_;
     # => ($anchor, $breakwater)
 
-    # $unclean->("unclean-$tagsfx", $msg, $cl)
     # $furniture->("unclean-$tagsfx", $msg, $cl)
-    # $dgitimport->("unclean-$tagsfx", $msg, $cl))
+    # $unclean->("unclean-$tagsfx", $msg, $cl)
     #   is callled for each situation or commit that
     #   wouldn't be found in a laundered branch
     # $furniture is for furniture commits such as might be found on an
@@ -892,7 +1107,7 @@ sub keycommits ($;$$$$$) {
     my $clogonly;
     my $cl;
     my $found_pm;
-    $fatal //= sub { fail $_[1]; };
+    $fatal //= sub { fail_unprocessable $_[1]; };
     my $x = sub {
        my ($cb, $tagsfx, $mainwhy, $xwhy) = @_;
        my $why = $mainwhy.$xwhy;
@@ -989,8 +1204,8 @@ sub walk ($;$$$) {
        my ($prose, $info) = @_;
        my $ms = $cl->{Msg};
        chomp $ms;
-       $info //= '';
-       $ms .= "\n\n[git-debrebase$info: $prose]\n";
+       confess unless defined $info;
+       $ms .= "\n\n[git-debrebase $info: $prose]\n";
        return (Msg => $ms);
     };
     my $rewrite_from_here = sub {
@@ -1017,7 +1232,7 @@ sub walk ($;$$$) {
        if ($nogenerate) {
            return (undef,undef);
        }
-       fail "found unprocessable commit, cannot cope".
+       fail_unprocessable "found unprocessable commit, cannot cope".
            (defined $cl->{Why} ? "; $cl->{Why}:": ':').
            " (commit $cur) (d.".
            (join ' ', map { sprintf "%#x", $_->{Differs} }
@@ -1036,18 +1251,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}");
@@ -1072,7 +1286,7 @@ sub walk ($;$$$) {
        } elsif ($ty eq 'Mixed') {
            my $queue = sub {
                my ($q, $wh) = @_;
-               my $cls = { %$cl, $xmsg->("split mixed commit: $wh part") };
+               my $cls = { %$cl, $xmsg->("mixed commit: $wh part",'split') };
                push @$q, $cls;
            };
            $queue->(\@brw_cl, "debian");
@@ -1125,12 +1339,12 @@ sub walk ($;$$$) {
                push @brw_cl, {
                    %$cl,
                    SpecialMethod => 'DgitImportDebianUpdate',
-                    $xmsg->("convert dgit import: debian changes")
+                    $xmsg->("debian changes", 'convert dgit import')
                }, {
                    %$cl,
                    SpecialMethod => 'DgitImportUpstreamUpdate',
                     $xmsg->("convert dgit import: upstream update",
-                           " anchor")
+                           "anchor")
                };
                $prline->(" Import");
                $rewrite_from_here->(\@brw_cl);
@@ -1160,7 +1374,7 @@ sub walk ($;$$$) {
            # 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
+           # MERGE-TODO we should warn the user in the docs about this
 
            my $ok=1;
            my $best_anchor;
@@ -1292,8 +1506,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);
@@ -1322,7 +1537,7 @@ sub walk ($;$$$) {
                 %$cl,
                 SpecialMethod => 'MergeCreateMergedBreakwaters',
                 $xmsg->('constructed from vanilla merge',
-                       ' merged-breakwater'),
+                       'merged-breakwater'),
             };
            push @upp_cl, {
                 %$cl,
@@ -1347,11 +1562,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();
@@ -1363,7 +1573,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)) {
@@ -1377,7 +1592,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') {
@@ -1387,7 +1602,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);
@@ -1398,15 +1613,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:
@@ -1443,27 +1666,46 @@ 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 ?";
+               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};
+               trees_diff_walk "$want_upstream:", "$build:", sub {
+                   my ($n) = @_;
+                   no warnings qw(exiting);
+                   next if $n eq 'debian/';
+                   confess "mismatch @_ ?";
+               };
+               trees_diff_walk "$want_debian:debian", "$build:debian", sub {
+                   confess "mismatch @_ ?";
+               };
+               my @old_parents = map { $_->{CommitId} } @{ $cl->{Parents} };
+               confess "mismatch @parents != @old_parents ?"
+                   unless "@parents" eq "@old_parents";
            }
-           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;
             }
@@ -1498,7 +1740,7 @@ sub update_head_checkout ($$$) {
 
 sub update_head_postlaunder ($$$) {
     my ($old, $tip, $reflogmsg) = @_;
-    return if $tip eq $old;
+    return if $tip eq $old && !@deferred_updates;
     print "git-debrebase: laundered (head was $old)\n";
     update_head $old, $tip, $reflogmsg;
     # no tree changes except debian/patches
@@ -1557,11 +1799,6 @@ sub cmd_analyse () {
     STDOUT->error and die $!;
 }
 
-sub ffq_prev_branchinfo () {
-    my $current = git_get_symref();
-    return gdr_ffq_prev_branchinfo($current);
-}
-
 sub ffq_check ($;$$) {
     # calls $ff and/or $notff zero or more times
     # then returns either (status,message) where status is
@@ -1703,8 +1940,7 @@ sub stitch ($$$$$) {
            # ffq-prev is ahead of us, and the only tree changes it has
            # are possibly addition of things in debian/patches/.
            # Just wind forwards rather than making a pointless pseudomerge.
-           push @deferred_updates,
-               "update $gdrlast $ffq_prev_commitish $git_null_obj";
+           record_gdrlast $gdrlast, $ffq_prev_commitish;
            update_head_checkout $old_head, $ffq_prev_commitish,
                "stitch (fast forward)";
            return;
@@ -1717,7 +1953,7 @@ sub stitch ($$$$$) {
        'Declare fast forward / record previous work',
         "[git-debrebase pseudomerge: $prose]",
     ];
-    push @deferred_updates, "update $gdrlast $new_head $git_null_obj";
+    record_gdrlast $gdrlast, $new_head;
     update_head $old_head, $new_head, "stitch: $prose";
 }
 
@@ -1830,7 +2066,7 @@ sub cmd_new_upstream () {
     my $old_upstream;
     if (!$old_anchor_cl->{OrigParents}) {
        snag 'anchor-treated',
          'old anchor is recognised due to --anchor, cannot check upstream';
__ 'old anchor is recognised due to --anchor, cannot check upstream';
     } else {
        $old_upstream = parsecommit
            $old_anchor_cl->{OrigParents}[0]{CommitId};
@@ -1936,36 +2172,45 @@ sub cmd_new_upstream () {
  "[git-debrebase anchor: new upstream $new_upstream_version, merge]",
             ];
 
-       my $clogsignoff = cmdoutput qw(git show),
-           '--pretty=format:%an <%ae>  %aD',
-           $new_bw;
-
        # Now we have to add a changelog stanza so the Debian version
-       # is right.
-       die if unlink "debian";
-       die $! unless $!==ENOENT or $!==ENOTEMPTY;
-       unlink "debian/changelog" or $!==ENOENT or die $!;
-        mkdir "debian" or die $!;
-       open CN, ">", "debian/changelog" or die $!;
-       my $oldclog = git_cat_file ":debian/changelog";
-       $oldclog =~ m/^($package_re) \(\S+\) / or
-           fail "cannot parse old changelog to get package name";
-       my $p = $1;
-       print CN <<END, $oldclog or die $!;
-$p ($new_version) UNRELEASED; urgency=medium
-
-  * Update to new upstream version $new_upstream_version.
-
- -- $clogsignoff
-
-END
-       close CN or die $!;
+       # is right.  We use debchange to do this.  Invoking debchange
+       # here is a bit fiddly because it has a lot of optional
+       # exciting behaviours, some of which will break stuff, and
+       # some of which won't work in a playtree.
+
+       # Make debchange use git's idea of the user's identity.
+       # That way, if the user never uses debchange et al, configuring
+       # git is enough.
+       my $usetup = sub {
+           my ($e, $k) = @_;
+           my $v = cfg $k, 1;
+           defined $v or return;
+           $ENV{$e} = $v;
+       };
+       $usetup->('DEBEMAIL',    'user.email');
+       $usetup->('DEBFULLNAME', 'user.name');
+
+sleep 2;
+
+       my @dch = (qw(debchange
+                     --allow-lower-version .*
+                     --no-auto-nmu
+                     --preserve
+                     --vendor=Unknown-Vendor
+                     --changelog debian/changelog
+                     --check-dirname-level 0
+                     --release-heuristic=changelog
+                     -v), $new_version,
+                  "Update to new upstream version $new_upstream_version.");
+
+       runcmd @git, qw(checkout -q debian/changelog);
+       runcmd @dch;
        runcmd @git, qw(update-index --add --replace), 'debian/changelog';
 
        # Now we have the final new breakwater branch in the index
         $new_bw = make_commit [ $new_bw ],
             [ "Update changelog for new upstream $new_upstream_version",
-              "[git-debrebase: new upstream $new_upstream_version, changelog]",
+              "[git-debrebase changelog: new upstream $new_upstream_version]",
             ];
     };
 
@@ -2101,7 +2346,10 @@ sub cmd_stitch () {
     badusage "no arguments allowed" if @ARGV;
     do_stitch $prose, 0;
 }
-sub cmd_prepush () { cmd_stitch(); }
+sub cmd_prepush () {
+    $opt_noop_ok = 1;
+    cmd_stitch();
+}
 
 sub cmd_quick () {
     badusage "no arguments allowed" if @ARGV;
@@ -2125,19 +2373,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 ($) {
@@ -2146,23 +2407,30 @@ sub make_patches_staged ($) {
     # laundered.
     my ($secret_head, $secret_bw, $last_anchor) = walk $head;
     fresh_workarea();
+    my $any;
     in_workarea sub {
-       gbp_pq_export 'bw', $secret_bw, $secret_head;
+       $any = gbp_pq_export 'bw', $secret_bw, $secret_head;
     };
+    return $any;
 }
 
 sub make_patches ($) {
     my ($head) = @_;
     keycommits $head, 0, \&snag;
-    make_patches_staged $head;
+    my $any = make_patches_staged $head;
     my $out;
     in_workarea sub {
-       my $ptree = cmdoutput @git, qw(write-tree --prefix=debian/patches/);
+       my $ptree = !$any ? undef :
+           cmdoutput @git, qw(write-tree --prefix=debian/patches/);
        runcmd @git, qw(read-tree), $head;
-       read_tree_subdir 'debian/patches', $ptree;
+       if ($ptree) {
+           read_tree_subdir 'debian/patches', $ptree;
+       } else {
+           rm_subdir_cached 'debian/patches';
+       }
        $out = make_commit [$head], [
             'Commit patch queue (exported by git-debrebase)',
-            '[git-debrebase: export and commit patches]',
+            '[git-debrebase make-patches: export and commit patches]',
         ];
     };
     return $out;
@@ -2193,6 +2461,54 @@ sub cmd_make_patches () {
     }
 }
 
+sub check_series_has_all_patches ($) {
+    my ($head) = @_;
+    my $seriesfn = 'debian/patches/series';
+    my ($dummy, $series) = git_cat_file "$head:$seriesfn",
+       [qw(blob missing)];
+    $series //= '';
+    my %series;
+    our $comments_snagged;
+    foreach my $f (grep /\S/, grep {!m/^\s\#/} split /\n/, $series) {
+       if ($f =~ m/^\s*\#/) {
+           snag 'series-comments',
+               "$seriesfn contains comments, which will be discarded"
+               unless $comments_snagged++;
+           next;
+       }
+       fail "patch $f repeated in $seriesfn !" if $series{$f}++;
+    }
+    foreach my $patchfile (get_tree "$head:debian/patches", 1,1) {
+       my ($f,$i) = @$patchfile;
+       next if $series{$f};
+       next if $f eq 'series';
+       snag 'unused-patches', "Unused patch file $f will be discarded";
+    }
+}
+
+sub begin_convert_from () {
+    my $head = get_head();
+    my ($ffqs, $ffqm, $symref, $ffq_prev, $gdrlast) = ffq_prev_branchinfo();
+
+    fail "ffq-prev exists, this is already managed by git-debrebase!"
+       if $ffq_prev && git_get_ref $ffq_prev;
+
+    my $gdrlast_obj = $gdrlast && git_get_ref $gdrlast;
+    snag 'already-converted',
+       "ahead of debrebase-last, this is already managed by git-debrebase!"
+       if $gdrlast_obj && is_fast_fwd $gdrlast_obj, $head;
+    return ($head, { LastRef => $gdrlast, LastObj => $gdrlast_obj });
+}
+
+sub complete_convert_from ($$$$) {
+    my ($old_head, $new_head, $gi, $mrest) = @_;
+    ffq_check $new_head;
+    record_gdrlast $gi->{LastRef}, $new_head, $gi->{LastObj}
+       if $gi->{LastRef};
+    snags_maybe_bail();
+    update_head_checkout $old_head, $new_head, $mrest;
+}
+
 sub cmd_convert_from_gbp () {
     badusage "want only 1 optional argument, the upstream git commitish"
        unless @ARGV<=1;
@@ -2207,7 +2523,7 @@ sub cmd_convert_from_gbp () {
     my $upstream =
        resolve_upstream_version($upstream_spec, $upstream_version);
 
-    my $old_head = get_head();
+    my ($old_head, $gdrlastinfo) = begin_convert_from();
 
     my $upsdiff = get_differs $upstream, $old_head;
     if ($upsdiff & D_UPS) {
@@ -2240,6 +2556,8 @@ END
            "upstream ($upstream) contains debian/ directory";
     }
 
+    check_series_has_all_patches $old_head;
+
     my $previous_dgit_view = eval {
        my @clogcmd = qw(dpkg-parsechangelog --format rfc822 -n2);
        my ($lvsn, $suite);
@@ -2260,19 +2578,25 @@ END
        my $mtag = cmdoutput @git, qw(describe --always --abbrev=0 --match),
            $mtag_pat;
        die "could not find suitable maintainer view tag $mtag_pat\n"
-           unless $mtag_pat =~ m{/};
+           unless $mtag =~ m{/};
        is_fast_fwd $mtag, 'HEAD' or
            die "HEAD is not FF from maintainer tag $mtag!";
        my $dtag = "archive/$mtag";
+       git_get_ref "refs/tags/$dtag" or
+           die "dgit view tag $dtag not found\n";
        is_fast_fwd $mtag, $dtag or
-           die "dgit view tag $dtag is not FF from maintainer tag $mtag";
+           die "dgit view tag $dtag is not FF from maintainer tag $mtag\n";
        print "will stitch in dgit view, $dtag\n";
        git_rev_parse $dtag;
     };
     if (!$previous_dgit_view) {
        $@ =~ s/^\n+//;
        chomp $@;
-       print STDERR "cannot stitch in dgit view: $@\n";
+       print STDERR <<END;
+Cannot confirm dgit view: $@
+Failed to stitch in dgit view (see messages above).
+dgit --overwrite will be needed on the first dgit push after conversion.
+END
     }
 
     snags_maybe_bail_early();
@@ -2313,24 +2637,30 @@ END
        }
     };
 
-    ffq_check $work;
-    snags_maybe_bail();
-    update_head_checkout $old_head, $work, 'convert-from-gbp';
+    complete_convert_from $old_head, $work, $gdrlastinfo, 'convert-from-gbp';
+    print <<END or die $!;
+git-debrebase: converted from patched-unapplied (gbp) branch format, OK
+END
 }
 
 sub cmd_convert_to_gbp () {
     badusage "no arguments allowed" if @ARGV;
     my $head = get_head();
     my (undef, undef, undef, $ffq, $gdrlast) = ffq_prev_branchinfo();
-    keycommits $head, 0;
-    my $out;
-    make_patches_staged $head;
-    in_workarea sub {
-       $out = make_commit ['HEAD'], [
-            'Commit patch queue (converted from git-debrebase format)',
-            '[git-debrebase convert-to-gbp: commit patches]',
-        ];
-    };
+    my ($anchor, $breakwater) = keycommits $head, 0;
+    my $out = $breakwater;
+    my $any = make_patches_staged $head;
+    if ($any) {
+       in_workarea sub {
+           $out = make_commit [$out], [
+               'Commit patch queue (converted from git-debrebase format)',
+               '[git-debrebase convert-to-gbp: commit patches]',
+           ];
+       };
+    } else {
+       # in this case, it can be fast forward
+       $out = $head;
+    }
     if (defined $ffq) {
        push @deferred_updates, "delete $ffq";
        push @deferred_updates, "delete $gdrlast";
@@ -2372,7 +2702,7 @@ sub cmd_convert_from_dgit_view () {
                         };
     }
 
-    my $head = get_head();
+    my ($head, $gdrlastinfo) = begin_convert_from();
 
     if (!$always) {
        my $troubles = 0;
@@ -2390,6 +2720,8 @@ END
        }
     }
 
+    check_series_has_all_patches $head;
+
     snags_maybe_bail_early();
 
     my $version = upstreamversion $clogp->{Version};
@@ -2442,7 +2774,7 @@ Import effective orig tree for upstream version $version
 END
 This includes the contents of the .orig(s), minus any debian/ directory.
 
-[git-debrebase import-from-dgit-view upstream-import-convert: $version]
+[git-debrebase convert-from-dgit-view upstream-import-convert: $version]
 END
                                                    ];
                    push @upstreams, { Commit => $ups_synth,
@@ -2476,7 +2808,7 @@ END
  'git-debrebase convert-from-dgit-view: drop upstream changes from breakwater',
  "Drop upstream changes, and delete debian/patches, as part of converting\n".
  "to git-debrebase format.  Upstream changes will appear as commits.",
- '[git-debrebase convert-from-dgit-view: drop patches from tree]'
+ '[git-debrebase convert-from-dgit-view drop-patches]'
                                           ];
            }
            $work = make_commit [ $work, $u->{Commit} ], [
@@ -2526,12 +2858,62 @@ END
 
     printf STDERR "Yes, will base new branch on %s\n", $result->{Source};
 
-    ffq_check $result->{Result};
-    snags_maybe_bail();
-    update_head_checkout $head, $result->{Result},
+    complete_convert_from $head, $result->{Result}, $gdrlastinfo,
        'convert-from-dgit-view';
 }
 
+sub cmd_forget_was_ever_debrebase () {
+    badusage "forget-was-ever-debrebase takes no further arguments" if @ARGV;
+    my ($ffqstatus, $ffq_msg, $current, $ffq_prev, $gdrlast) =
+       ffq_prev_branchinfo();
+    fail "Not suitable for recording git-debrebaseness anyway: $ffq_msg"
+       if defined $ffq_msg;
+    push @deferred_updates, "delete $ffq_prev";
+    push @deferred_updates, "delete $gdrlast";
+    snags_maybe_bail();
+    run_deferred_updates "forget-was-ever-debrebase";
+}
+
+sub cmd_record_resolved_merge () {
+    badusage "record-resolved-merge takes no further arguments" if @ARGV;
+    # MERGE-TODO 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;
@@ -2581,6 +2963,9 @@ sub cmd_downstream_rebase_launder_v0 () {
     }
 }
 
+setlocale(LC_MESSAGES, "");
+textdomain("git-debrebase");
+
 getoptions_main
           ("bad options\n",
           "D+" => \$debuglevel,
@@ -2589,9 +2974,10 @@ getoptions_main
           'anchor=s' => \@opt_anchors,
           '--dgit=s' => \($dgit[0]),
           'force!',
+          'experimental-merge-resolution!', \$opt_merges,
           '-i:s' => sub {
               my ($opt,$val) = @_;
-              badusage "git-debrebase: no cuddling to -i for git-rebase"
+              badusage __ "git-debrebase: no cuddling to -i for git-rebase"
                   if length $val;
               die if $opt_defaultcmd_interactive; # should not happen
               $opt_defaultcmd_interactive = [ qw(-i) ];
@@ -2622,7 +3008,7 @@ if (!@ARGV || $opt_defaultcmd_interactive || $ARGV[0] =~ m{^-}) {
     $cmdfn =~ y/-/_/;
     $cmdfn = ${*::}{"cmd_$cmdfn"};
 
-    $cmdfn or badusage "unknown git-debrebase sub-operation $cmd";
+    $cmdfn or badusage f_ "unknown git-debrebase sub-operation %s", $cmd;
     $cmdfn->();
 }