chiark / gitweb /
git-debrebase: test suite: lib-gdr: move t-nmu-upload-[12]
[dgit.git] / git-debrebase
index 8f2e922355e440f7227a1f7afa3fd8bd8aa61753..cc51f775889c2e987e7c0f209721fc8d4b7f1fe4 100755 (executable)
@@ -3,7 +3,7 @@
 # Script helping make fast-forwarding histories while still rebasing
 # upstream deltas when working on Debian packaging
 #
-# Copyright (C)2017 Ian Jackson
+# Copyright (C)2017,2018 Ian Jackson
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+
 # usages:
-#    git-debrebase status
-#    git-debrebase start       # like ffqrebase start + debrebase launder
-#    git-debrebase new-upstream [stuff]  # see below
-#    git-debrebase <git-rebase options>  # does debrebase start if necessary
 #
-#    git-debrebase analyse
-#    git-debrebase launder     # prints breakwater tip
-#    git-debrebase create-new-upstream-breakwater [-f] <upstreaminfo>...
+#    git-debrebase [<options>] new-upstream-v0 \
+#             <new-version> <orig-commitish> \
+#            [<extra-orig-name> <extra-orig-commitish> ...] \
+#            [<git-rebase options>...]
+#
+#    git-debrebase [<options> --] [<git-rebase options...>]
+#    git-debrebase [<options>] analyse
+#    git-debrebase [<options>] breakwater      # prints breakwater tip only
+#    git-debrebase [<options>] launder         # prints breakwater tip etc.
+#    git-debrebase [<options>] stitch [--prose=<for commit message>]
+#    git-debrebase [<options>] downstream-rebase-launder-v0  # experimental
+#
+#    git-debrebase [<options>] convert-from-gbp [<upstream-git-rev>]
+#    git-debrebase [<options>] convert-to-gbp
+
+# problems / outstanding questions:
+#
+#  *  dgit push with a `3.0 (quilt)' package means doing quilt
+#     fixup.  Usually this involves recommitting the whole patch
+#     series, one at a time, with dpkg-source --commit.  This is
+#     terribly terribly slow.  (Maybe this should be fixed in dgit.)
+#
+#  * dgit push usually needs to (re)make a pseudomerge.  The "first"
+#    git-debrebase stripped out the previous pseudomerge and could
+#    have remembeed the HEAD.  But it's not quite clear what history
+#    ought to be preserved and what should be discarded.  For now
+#    the user will have to tell dgit --overwrite.
+#
+#    To fix this, do we need a new push hook for dgit ?
 #
-# <upstreaminfo> is
-#    [,][<subdir>:][+]<commitid>[,...]
+#  * Workflow is currently clumsy.  Lots of spurious runes to type.
+#    There's not even a guide.
 #
-# if initial comma is supplied, entries are not positional.  Unspecified
-# <subdir> means root (and there may be only one).
-# xxx want auto branch names
-# xxx too complicated
-# how about for now
-#    [+]<commit> [<subdir/> [+]<commit>...]
-# ?  plus options
-#     --new-upstream-different-subtrees
+#  * There are no tests.
 #
-#  automatic case
-#       git-debrebase new-upstream
-#             - previous breakwater merge must be gdr-generated
-#             - orig set is the same as before
-#             - implicitly uses upstream branches according to orig set
-#             - not all upstream branches need be updated
-#             - insists on fast-forward of each branch, unless
-#                  --force (or --force=<subdir>[/])
-#  branch set adjustments
-#       git-debrebase new-upstream --add <subdir>/
-#       git-debrebase new-upstream --rm <subdir>/
-#       git-debrebase new-upstream / [<subdir>/ ...]
-#             - orig set is adjusted
-#             - otherwise like auto (--add is not checked for ffness, obv)
-#             - multiple --add and --rm may be specified
-#             - --add makes new upstream the last contributor
-#  explicit
-#       git-debrebase / [<rootcommitid>] [<subdir>/ [<commitid>] ...]
-#             - orig set is precisely as specified now
-#             - previous breakwater merge is irrelevant
-#             - no fast forward checks
-#  for now only explicit with commitids
-
-#         implicitly uses `upstream'
-#                                     # (or multiple other branches)
-#       git-debrebase new-upstream \
-#             [<subdir>/]=<commitid>
-
-#    UPSTREAM[,[[SUBDIR:]SUBUPSTREAM]
-#    default for SUBDIR: is from previous upstream merge[xxx terminology]
-#    
+#  * new-upstream-v0 has a terrible UI.  You end up with giant
+#    runic command lines.
 #
-#xxx
-# when starting must record original start (for ff)
-# and new rebase basis
+#    One consequence of the lack of richness it can need --force in
+#    fairly sensible situations and there is no way to tell it what
+#    you are really trying to do, other than just --force.  There
+#    should be an interface with some default branch names.
 #
-#    git-ffqrebase start [BASE]
-#                # records previous HEAD so it can be overwritten
-#                # records base for future git-ffqrebase
-#    git-ffqrebase set-base BASE
-#    git-ffqrebase <git-rebase options>
-#    git-ffqrebase finish
-#    git-ffqrebase status [BRANCH]
+#  * There should be a standard convention for the version number,
+#    and unfinalised or not changelog, after new-upstream.
 #
-#  refs/ffqrebase-prev/BRANCH    BRANCH may be refs/...; if not it means
-#  refs/ffqrebase-base/BRANCH      refs/heads/BRANCH
-#                               zero, one, or both of these may exist
+#  * Handing of multi-orig dgit new-upstream .dsc imports is known to
+#    be broken.  They may be not recognised, improperly converted, or
+#    their conversion may be unrecognised.
 #
-# git-debrebase without start, if already started, is willing
-# to strip pseudomerges provided that they overwrite exactly
-# the previous HEAD
-#  xxxx is this right ?  what matters is have we pushed
-#    I think in fact the right answer is:
-#       git-debrebase always strips out pseudomerges from its branch
-#       a pseudomerge is put in at the time we want to push
-#       at that time, we make a pseudomerge of the remote tracking
-#           branch (if raw git) or the dgit view (if dgit)
-#       for raw git git-ffqrebase, do want preciseley to record
-#           value of remote tracking branch or our branch, on start, so we
-#           overwrite only things we intend to
-#  the previous pseudomerge    check for tags and remote branches ?
+#  * Docs need writing and updating.  Even README.git-debrebase
+#    describes a design but may not reflect the implementation.
+#
+#  * We need to develop a plausible model that works for derivatives,
+#    who probably want to maintain their stack on top of Debian's.
+#    downstream-rebase-launder-v0 may be a starting point?
 
 use strict;
 
+use Debian::Dgit qw(:DEFAULT :playground);
+setup_sigwarn();
+
 use Memoize;
 use Carp;
 use POSIX;
 use Data::Dumper;
 use Getopt::Long qw(:config posix_default gnu_compat bundling);
+use Dpkg::Version;
+use File::FnMatch qw(:fnmatch);
 
-use Debian::Dgit qw(:DEFAULT :playground);
+our ($opt_force, $opt_noop_ok);
+
+our $us = qw(git-debrebase);
 
 sub badusage ($) {
     my ($m) = @_;
     die "bad usage: $m\n";
 }
 
-sub cfg ($) {
-    my ($k) = @_;
-    $/ = "\0";
+sub cfg ($;$) {
+    my ($k, $optional) = @_;
+    local $/ = "\0";
     my @cmd = qw(git config -z);
     push @cmd, qw(--get-all) if wantarray;
     push @cmd, $k;
-    my $out = cmdoutput @cmd;
+    my $out = cmdoutput_errok @cmd;
+    if (!defined $out) {
+       fail "missing required git config $k" unless $optional;
+       return ();
+    }
     return split /\0/, $out;
 }
 
 memoize('cfg');
 
+sub dd ($) {
+    my ($v) = @_;
+    my $dd = new Data::Dumper [ $v ];
+    Terse $dd 1; Indent $dd 0; Useqq $dd 1;
+    return Dump $dd;
+}
+
 sub get_commit ($) {
     my ($objid) = @_;
-    my $data = git_cat_file $objid, 'commit';
+    my $data = (git_cat_file $objid, 'commit');
     $data =~ m/(?<=\n)\n/ or die "$objid ($data) ?";
     return ($`,$');
 }
 
-sub D_DEB ()     { return 0x1; } # debian/ (not including debian/patches/)
-sub D_UPS ()     { return 0x2; } # upstream files
-sub D_PAT_ADD () { return 0x4; } # debian/patches/ extra patches at end
-sub D_PAT_OTH () { return 0x8; } # debian/patches other changes
-
+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 DS_DEB ()     { D_DEB_CLOG | D_DEB_OTH; } # debian/ (not patches/)
 
 our $playprefix = 'debrebase';
 our $rd;
@@ -152,7 +147,8 @@ sub in_workarea ($) {
     my ($sub) = @_;
     changedir $workarea;
     my $r = eval { $sub->(); };
-    changedir $maindir;
+    { local $@; changedir $maindir; }
+    die $@ if $@;
 }
 
 sub fresh_workarea () {
@@ -175,22 +171,25 @@ sub get_differs ($$) {
        my @cmd = (@git, qw(diff-tree -z --no-renames));
        push @cmd, @$opts;
        push @cmd, "$_:" foreach $x, $y;
-       push @cmd, @$limits;
+       push @cmd, '--', @$limits;
        my $diffs = cmdoutput @cmd;
        foreach (split /\0/, $diffs) { $fn->(); }
     };
 
     $rundiff->([qw(--name-only)], [], sub {
-        $differs |= $_ eq 'debian' ? D_DEB : D_UPS;
+        $differs |= $_ eq 'debian' ? DS_DEB : D_UPS;
     });
 
-    if ($differs & D_DEB) {
-       $differs &= ~D_DEB;
+    if ($differs & DS_DEB) {
+       $differs &= ~DS_DEB;
        $rundiff->([qw(--name-only -r)], [qw(debian)], sub {
-            $differs |= $_ eq m{^debian/patches/} ? D_PAT_OTH : D_DEB;
+            $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|D_DEB);
+           unless $differs & (D_PAT_OTH|DS_DEB);
     }
 
     if ($differs & D_PAT_OTH) {
@@ -207,11 +206,11 @@ sub get_differs ($$) {
            }
            die unless s{^debian/patches/}{};
            my $ok;
-           if ($mode eq 'A' && !m/(?:^|\.)series$/s) {
+           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';
+               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 {
@@ -242,6 +241,62 @@ sub calculate_committer_authline () {
     return $&;
 }
 
+sub rm_subdir_cached ($) {
+    my ($subdir) = @_;
+    runcmd @git, qw(rm --quiet -rf --cached --ignore-unmatch), $subdir;
+}
+
+sub read_tree_subdir ($$) {
+    my ($subdir, $new_tree_object) = @_;
+    rm_subdir_cached $subdir;
+    runcmd @git, qw(read-tree), "--prefix=$subdir/", $new_tree_object;
+}
+
+sub make_commit ($$) {
+    my ($parents, $message_paras) = @_;
+    my $tree = cmdoutput @git, qw(write-tree);
+    my @cmd = (@git, qw(commit-tree), $tree);
+    push @cmd, qw(-p), $_ foreach @$parents;
+    push @cmd, qw(-m), $_ foreach @$message_paras;
+    return cmdoutput @cmd;
+}
+
+our @fproblem_force_opts;
+our $fproblems_forced;
+our $fproblems_tripped;
+sub fproblem ($$) {
+    my ($tag,$msg) = @_;
+    if (grep { $_ eq $tag } @fproblem_force_opts) {
+       $fproblems_forced++;
+       print STDERR "git-debrebase: safety catch overridden (-f$tag): $msg\n";
+    } else {
+       $fproblems_tripped++;
+       print STDERR "git-debrebase: safety catch tripped (-f$tag): $msg\n";
+    }
+}
+
+sub fproblems_maybe_bail () {
+    if ($fproblems_forced) {
+       printf STDERR
+           "%s: safety catch trips: %d overriden by individual -f options\n",
+           $us, $fproblems_forced;
+    }
+    if ($fproblems_tripped) {
+       if ($opt_force) {
+           printf STDERR
+               "%s: safety catch trips: %d overriden by global --force\n",
+               $us, $fproblems_tripped;
+       } else {
+           fail sprintf
+  "%s: safety catch trips: %d blockers (you could -f<tag>, or --force)",
+               $us, $fproblems_tripped;
+       }
+    }
+}
+sub any_fproblems () {
+    return $fproblems_forced || $fproblems_tripped;
+}
+
 # classify returns an info hash like this
 #   CommitId => $objid
 #   Hdr => # commit headers, including 1 final newline
@@ -260,10 +315,10 @@ sub calculate_committer_authline () {
 #
 # Types:
 #   Packaging
+#   Changelog
 #   Upstream
 #   AddPatches
 #   Mixed
-#   Unknown
 #
 #   Pseudomerge
 #     has additional entres in classification result
@@ -276,33 +331,55 @@ sub calculate_committer_authline () {
 #
 #   BreakwaterUpstreamMerge
 #     has additional entry in classification result
-#       OrigParents = [ subset of Parents ]
+#       OrigParents = [ subset of Parents ]  # singleton list
+#
+#   Unknown
+#     has additional entry in classification result
+#       Why => "prose"
 
-sub classify ($) {
-    my ($objid) = @_;
+sub parsecommit ($;$) {
+    my ($objid, $p_ref) = @_;
+    # => hash with                   CommitId Hdr Msg Tree Parents
+    #    Parents entries have only   Ix CommitId
+    #    $p_ref, if provided, must be [] and is used as a base for Parents
+
+    $p_ref //= [];
+    die if @$p_ref;
 
     my ($h,$m) = get_commit $objid;
 
     my ($t) = $h =~ m/^tree (\w+)$/m or die $objid;
     my (@ph) = $h =~ m/^parent (\w+)$/mg;
-    my @p;
 
     my $r = {
        CommitId => $objid,
        Hdr => $h,
        Msg => $m,
         Tree => $t,
-       Parents => \@p,
+       Parents => $p_ref,
     };
 
     foreach my $ph (@ph) {
-       push @p, {
-            Ix => $#p,
+       push @$p_ref, {
+            Ix => scalar @$p_ref,
             CommitId => $ph,
-            Differs => (get_differs $t, $ph),
         };
     }
 
+    return $r;
+}    
+
+sub classify ($) {
+    my ($objid) = @_;
+
+    my @p;
+    my $r = parsecommit($objid, \@p);
+    my $t = $r->{Tree};
+
+    foreach my $p (@p) {
+       $p->{Differs} = (get_differs $p->{CommitId}, $t),
+    }
+
     printdebug "classify $objid \$t=$t \@p",
        (map { sprintf " %s/%#x", $_->{CommitId}, $_->{Differs} } @p),
        "\n";
@@ -311,29 +388,37 @@ sub classify ($) {
        my ($type, @rest) = @_;
        $r = { %$r, Type => $type, @rest };
        if ($debuglevel) {
-           my $dd = new Data::Dumper [ $r ];
-           Terse $dd 1; Indent $dd 0; Useqq $dd 1;
-           printdebug " = $type ".(Dump $dd)."\n";
+           printdebug " = $type ".(dd $r)."\n";
        }
        return $r;
     };
     my $unknown = sub {
        my ($why) = @_;
-       $r = { %$r, Type => qw(Unknown) };
+       $r = { %$r, Type => qw(Unknown), Why => $why };
        printdebug " ** Unknown\n";
        return $r;
     };
 
+    my $claims_to_be_breakwater =
+       $r->{Msg} =~ m{^\[git-debrebase breakwater.*\]$}m;
+
     if (@p == 1) {
+       if ($claims_to_be_breakwater) {
+           return $unknown->("single-parent git-debrebase breakwater \`merge'");
+       }
        my $d = $r->{Parents}[0]{Differs};
        if ($d == D_PAT_ADD) {
            return $classify->(qw(AddPatches));
        } elsif ($d & (D_PAT_ADD|D_PAT_OTH)) {
            return $unknown->("edits debian/patches");
-       } elsif ($d == D_DEB) {
-           my ($ty,$dummy) = git_cat_file "$ph[0]:debian";
+       } elsif ($d & DS_DEB and !($d & ~DS_DEB)) {
+           my ($ty,$dummy) = git_cat_file "$p[0]{CommitId}:debian";
            if ($ty eq 'tree') {
-               return $classify->(qw(Packaging));
+               if ($d == D_DEB_CLOG) {
+                   return $classify->(qw(Changelog));
+               } else {
+                   return $classify->(qw(Packaging));
+               }
            } elsif ($ty eq 'missing') {
                return $classify->(qw(BreakwaterStart));
            } else {
@@ -341,7 +426,7 @@ sub classify ($) {
            }
        } elsif ($d == D_UPS) {
            return $classify->(qw(Upstream));
-       } elsif ($d == (D_DEB|D_UPS)) {
+       } elsif ($d & DS_DEB and $d & D_UPS and !($d & ~(DS_DEB|D_UPS))) {
            return $classify->(qw(Mixed));
        } elsif ($d == 0) {
            return $unknown->("no changes");
@@ -354,11 +439,14 @@ sub classify ($) {
     }
 
     my @identical = grep { !$_->{Differs} } @p;
-    if (@p == 2 && @identical == 1) {
+    if (@p == 2 && @identical == 1 && !$claims_to_be_breakwater
+       # breakwater merges can look like pseudomerges, if they are
+       # "declare" commits (ie, there are no upstream changes)
+       ) {
        my @overwritten = grep { $_->{Differs} } @p;
        confess "internal error $objid ?" unless @overwritten==1;
        return $classify->(qw(Pseudomerge),
-                          Overwritten => $overwritten[0],
+                          Overwritten => [ $overwritten[0] ],
                           Contributor => $identical[0]);
     }
     if (@p == 2 && @identical == 2) {
@@ -369,7 +457,7 @@ sub classify ($) {
        } @p;
        return $classify->(qw(Pseudomerge),
                           SubType => qw(Ambiguous),
-                          Overwritten => $bytime[0],
+                          Overwritten => [ $bytime[0] ],
                           Contributor => $bytime[1]);
     }
     foreach my $p (@p) {
@@ -378,7 +466,7 @@ sub classify ($) {
        ($p->{IsDgitImport},) = $p_m =~ m/^\[dgit import ([0-9a-z]+) .*\]$/m;
     }
     my @orig_ps = grep { ($_->{IsDgitImport}//'X') eq 'orig' } @p;
-    my $m2 = $m;
+    my $m2 = $r->{Msg};
     if (!(grep { !$_->{IsOrigin} } @p) and
        (@orig_ps >= @p - 1) and
        $m2 =~ s{^\[(dgit import unpatched .*)\]$}{[was: $1]}m) {
@@ -409,24 +497,64 @@ sub classify ($) {
        if (@p == 2 &&
            !$haspatches &&
            !$p[$prevbrw]{IsOrigin} && # breakwater never starts with an origin
-           !($p[$prevbrw]{Differs} & ~D_DEB) &&
-           !($p[!$prevbrw]{Differs} & ~D_UPS)) {
+           !($p[!$prevbrw]{Differs} & ~DS_DEB) && # no non-debian changess
+           !($p[$prevbrw]{Differs} & ~D_UPS)) { # no non-upstream changes
            return $classify->(qw(BreakwaterUpstreamMerge),
                               OrigParents => [ $p[!$prevbrw] ]);
        }
-       # xxx multi-.orig upstreams
     }
 
+    # multi-orig upstreams are represented with a breakwater merge
+    # from a single upstream commit which combines the orig tarballs
+
     return $unknown->("complex merge");
 }
 
+sub breakwater_of ($) {
+    my ($head) = @_; # must be laundered
+    my $breakwater;
+    my $unclean = sub {
+       my ($why) = @_;
+       fail "branch needs laundering (run git-debrebase): $why";
+    };
+    for (;;) {
+       my $cl = classify $head;
+       my $ty = $cl->{Type};
+       if ($ty eq 'Packaging' or
+           $ty eq 'Changelog') {
+           $breakwater //= $head;
+       } elsif ($ty eq 'BreakwaterUpstreamMerge' or
+                $ty eq 'BreakwaterStart') {
+           $breakwater //= $head;
+           last;
+       } elsif ($ty eq 'Upstream') {
+           $unclean->("packaging change ($breakwater)".
+                      " follows upstream change (eg $head)")
+               if defined $breakwater;
+       } elsif ($ty eq 'Mixed') {
+           $unclean->('found mixed upstream/packaging commit ($head)');
+       } elsif ($ty eq 'Pseudomerge' or
+                $ty eq 'AddPatches') {
+           $unclean->("found interchange conversion commit ($ty, $head)");
+       } elsif ($ty eq 'DgitImportUnpatched') {
+           $unclean->("found dgit dsc import ($head)");
+       } else {
+           fail "found unprocessable commit, cannot cope: $head; $cl->{Why}";
+       }
+       $head = $cl->{Parents}[0]{CommitId};
+    }
+    return $breakwater;
+}
+
 sub walk ($;$$);
 sub walk ($;$$) {
     my ($input,
        $nogenerate,$report) = @_;
-    # => ($tip, $breakwater_tip)
+    # => ($tip, $breakwater_tip, $last_upstream_merge_in_breakwater)
     # (or nothing, if $nogenerate)
 
+    printdebug "*** WALK $input ".($nogenerate//0)." ".($report//'-')."\n";
+
     # go through commits backwards
     # we generate two lists of commits to apply:
     # breakwater branch and upstream patches
@@ -437,10 +565,11 @@ sub walk ($;$$) {
 
     my $cl;
     my $xmsg = sub {
-       my ($appendinfo) = @_;
+       my ($prose, $info) = @_;
        my $ms = $cl->{Msg};
        chomp $ms;
-       $ms .= "\n\n[git-debrebase $appendinfo]\n";
+       $info //= '';
+       $ms .= "\n\n[git-debrebase$info: $prose]\n";
        return (Msg => $ms);
     };
     my $rewrite_from_here = sub {
@@ -465,7 +594,9 @@ sub walk ($;$$) {
        if ($nogenerate) {
            return (undef,undef);
        }
-       die "commit $cur: Cannot cope with this commit";
+       die "commit $cur: Cannot cope with this commit (d.".
+           (join ' ', map { sprintf "%#x", $_->{Differs} }
+            @{ $cl->{Parents} }). ")";
     };
 
     my $build;
@@ -478,6 +609,8 @@ sub walk ($;$$) {
        no warnings qw(exiting); last;
     };
 
+    my $last_upstream_update;
+
     for (;;) {
        $cl = classify $cur;
        my $ty = $cl->{Type};
@@ -490,11 +623,12 @@ sub walk ($;$$) {
            $cur = $p0;
            $rewrite_from_here->();
            next;
-       } elsif ($ty eq 'Packaging') {
+       } elsif ($ty eq 'Packaging' or $ty eq 'Changelog') {
            push @brw_cl, $cl;
            $cur = $p0;
            next;
        } elsif ($ty eq 'BreakwaterStart') {
+            $last_upstream_update = $cur;
            $build_start->('FirstPackaging', $cur);
        } elsif ($ty eq 'Upstream') {
            push @upp_cl, $cl;
@@ -503,7 +637,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->("split mixed commit: $wh part") };
                push @$q, $cls;
            };
            $queue->(\@brw_cl, "debian");
@@ -519,6 +653,7 @@ sub walk ($;$$) {
            $cur = $contrib;
            next;
        } elsif ($ty eq 'BreakwaterUpstreamMerge') {
+            $last_upstream_update = $cur;
            $build_start->("PreviousBreakwater", $cur);
        } elsif ($ty eq 'DgitImportUnpatched') {
            my $pm = $pseudomerges[-1];
@@ -532,9 +667,11 @@ sub walk ($;$$) {
                # suite intended by the non-dgit NMUer, and later
                # pseudomerges may represent in-archive copies.
                my $ovwrs = $pm->{Overwritten};
-               printf $report " PM=%s \@Overwr:%d", $pm, (scalar @$ovwrs)
+               printf $report " PM=%s \@Overwr:%d",
+                   $pm->{CommitId}, (scalar @$ovwrs)
                    if $report;
                if (@$ovwrs != 1) {
+                    printdebug "*** WALK BOMB DgitImportUnpatched\n";
                    return $bomb->();
                }
                my $ovwr = $ovwrs->[0]{CommitId};
@@ -554,21 +691,12 @@ sub walk ($;$$) {
                    %$cl,
                    SpecialMethod => 'DgitImportDebianUpdate',
                     $xmsg->("convert dgit import: debian changes")
+               }, {
+                   %$cl,
+                   SpecialMethod => 'DgitImportUpstreamUpdate',
+                    $xmsg->("convert dgit import: upstream update",
+                           " breakwater")
                };
-               my $differs = (get_differs $ovwr, $cl->{Tree});
-               printf $report " Differs=%#x", $differs if $report;
-               if ($differs & D_UPS) {
-                   printf $report " D_UPS" if $report;
-                   # This will also trigger if a non-dgit git-based NMU
-                   # deleted .gitignore (which is a thing that some of
-                   # the existing git tools do if the user doesn't
-                   # somehow tell them not to).  Ah well.
-                   push @brw_cl, {
-                       %$cl,
-                       SpecialMethod => 'DgitImportUpstreamUpdate',
-                       $xmsg->("convert dgit import: upstream changes")
-                   };
-               }
                $prline->(" Import");
                $rewrite_from_here->();
                $upp_limit //= $#upp_cl; # further, deeper, patches discarded
@@ -586,10 +714,15 @@ sub walk ($;$$) {
            }
            die "$ty ?";
         } else {
+            printdebug "*** WALK BOMB unrecognised\n";
            return $bomb->();
        }
     }
     $prprdelim->();
+
+    printdebug "*** WALK prep done cur=$cur".
+        " brw $#brw_cl upp $#upp_cl proc $#processed pm $#pseudomerges\n";
+
     return if $nogenerate;
 
     # Now we build it back up again
@@ -598,26 +731,29 @@ sub walk ($;$$) {
 
     my $rewriting = 0;
 
-    my $rm_tree_cached = sub {
-       my ($subdir) = @_;
-       runcmd @git, qw(rm --quiet -rf --cached --ignore-unmatch), $subdir;
-    };
     my $read_tree_debian = sub {
        my ($treeish) = @_;
-       $rm_tree_cached->(qw(debian));
-       runcmd @git, qw(read-tree --prefix=debian/), "$treeish:debian";
+       read_tree_subdir 'debian', "$treeish:debian";
+        rm_subdir_cached 'debian/patches';
     };
     my $read_tree_upstream = sub {
        my ($treeish) = @_;
        runcmd @git, qw(read-tree), $treeish;
        $read_tree_debian->($build);
     };
+
+    $#upp_cl = $upp_limit if defined $upp_limit;
  
     my $committer_authline = calculate_committer_authline();
 
+    printdebug "WALK REBUILD $build ".(scalar @processed)."\n";
+
+    confess "internal error" unless $build eq (pop @processed)->{CommitId};
+
     in_workarea sub {
        mkdir $rd or $!==EEXIST or die $!;
        my $current_method;
+       runcmd @git, qw(read-tree), $build;
        foreach my $cl (qw(Debian), (reverse @brw_cl),
                        { SpecialMethod => 'RecordBreakwaterTip' },
                        qw(Upstream), (reverse @upp_cl)) {
@@ -628,6 +764,8 @@ sub walk ($;$$) {
            my $method = $cl->{SpecialMethod} // $current_method;
            my @parents = ($build);
            my $cltree = $cl->{CommitId};
+           printdebug "WALK BUILD ".($cltree//'undef').
+               " $method (rewriting=$rewriting)\n";
            if ($method eq 'Debian') {
                $read_tree_debian->($cltree);
            } elsif ($method eq 'Upstream') {
@@ -640,20 +778,28 @@ sub walk ($;$$) {
                next;
            } elsif ($method eq 'DgitImportDebianUpdate') {
                $read_tree_debian->($cltree);
-               $rm_tree_cached->(qw(debian/patches));
            } elsif ($method eq 'DgitImportUpstreamUpdate') {
+               confess unless $rewriting;
+               my $differs = (get_differs $build, $cltree);
+               next unless $differs & D_UPS;
                $read_tree_upstream->($cltree);
                push @parents, map { $_->{CommitId} } @{ $cl->{OrigParents} };
            } else {
                confess "$method ?";
            }
-           $rewriting ||= $cl ne pop @processed;
+           if (!$rewriting) {
+               my $procd = (pop @processed) // 'UNDEF';
+               if ($cl ne $procd) {
+                   $rewriting = 1;
+                   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}{}m;
+           $ch =~ s{^parent .*\n}{}mg;
            $ch =~ s{(?=^author)}{
-               map { "parent $_\n" } @parents
+               join '', map { "parent $_\n" } @parents
            }me or confess "$ch ?";
            if ($rewriting) {
                $ch =~ s{^committer .*$}{$committer_authline}m
@@ -669,29 +815,67 @@ sub walk ($;$$) {
            my $newcommit = cmdoutput @cmd;
            confess "$ch ?" unless $rewriting or $newcommit eq $cl->{CommitId};
            $build = $newcommit;
+            if (grep { $method eq $_ } qw(DgitImportUpstreamUpdate)) {
+                $last_upstream_update = $cur;
+            }
        }
     };
 
-    runcmd @git, qw(diff-tree --quiet), $input, $build;
+    my $final_check = get_differs $build, $input;
+    die sprintf "internal error %#x %s %s", $final_check, $build, $input
+       if $final_check & ~D_PAT_ADD;
 
-    return ($build, $breakwater);
+    my @r = ($build, $breakwater, $last_upstream_update);
+    printdebug "*** WALK RETURN @r\n";
+    return @r
 }
 
-sub get_head () { return git_rev_parse qw(HEAD); }
+sub get_head () {
+    git_check_unmodified();
+    return git_rev_parse qw(HEAD);
+}
 
 sub update_head ($$$) {
     my ($old, $new, $mrest) = @_;
-    runcmd @git, qw(update-ref -m), "git-debrebase $mrest", $new, $old;
+    runcmd @git, qw(update-ref -m), "debrebase: $mrest", 'HEAD', $new, $old;
+}
+
+sub update_head_checkout ($$$) {
+    my ($old, $new, $mrest) = @_;
+    update_head $old, $new, $mrest;
+    runcmd @git, qw(reset --hard);
+}
+
+sub update_head_postlaunder ($$$) {
+    my ($old, $tip, $reflogmsg) = @_;
+    return if $tip eq $old;
+    print "git-debrebase: laundered (head was $old)\n";
+    update_head $old, $tip, $reflogmsg;
+    # no tree changes except debian/patches
+    runcmd @git, qw(rm --quiet --ignore-unmatch -rf debian/patches);
 }
 
 sub cmd_launder () {
     badusage "no arguments to launder allowed" if @ARGV;
     my $old = get_head();
-    my ($tip,$breakwater) = walk $old;
-    update_head $old, $tip, 'launder';
-    # no tree changes except debian/patches
-    runcmd @git, qw(rm --quiet -rf debian/patches);
+    my ($tip,$breakwater,$last_upstream_merge) = walk $old;
+    update_head_postlaunder $old, $tip, 'launder';
     printf "# breakwater tip\n%s\n", $breakwater;
+    printf "# working tip\n%s\n", $tip;
+    printf "# last upstream merge\n%s\n", $last_upstream_merge;
+}
+
+sub defaultcmd_rebase () {
+    my $old = get_head();
+    my ($status, $message) = record_ffq_prev();
+    if ($status eq 'written' || $status eq 'exists') {
+    } else {
+       fproblem $status, "could not record ffq-prev: $message";
+       fproblems_maybe_bail();
+    }
+    my ($tip,$breakwater) = walk $old;
+    update_head_postlaunder $old, $tip, 'launder for rebase';
+    runcmd @git, qw(rebase), @ARGV, $breakwater;
 }
 
 sub cmd_analyse () {
@@ -701,13 +885,482 @@ sub cmd_analyse () {
     if (defined $old) {
        $old = git_rev_parse $old;
     } else {
-       $old = get_head();
+       $old = git_rev_parse 'HEAD';
     }
     my ($dummy,$breakwater) = walk $old, 1,*STDOUT;
     STDOUT->error and die $!;
 }
 
-GetOptions("D+" => \$debuglevel) or die badusage "bad options\n";
+sub ffq_prev_branchinfo () {
+    # => ('status', "message", [$current, $ffq_prev])
+    # 'status' may be
+    #    branch         message is undef
+    #    weird-symref   } no $current,
+    #    notbranch      }  no $ffq_prev
+    my $current = git_get_symref();
+    return ('detached', 'detached HEAD') unless defined $current;
+    return ('weird-symref', 'HEAD symref is not to refs/')
+       unless $current =~ m{^refs/};
+    my $ffq_prev = "refs/$ffq_refprefix/$'";
+    return ('branch', undef, $current, $ffq_prev);
+}
+
+sub record_ffq_prev () {
+    # => ('status', "message")
+    # 'status' may be
+    #    written          message is undef
+    #    exists
+    #    detached
+    #    weird-symref
+    #    notbranch
+    # if not ff from some branch we should be ff from, is an fproblem
+    # if "written", will have printed something about that to stdout,
+    #   and also some messages about ff checks
+    my ($status, $message, $current, $ffq_prev) = ffq_prev_branchinfo();
+    return ($status, $message) unless $status eq 'branch';
+
+    my $currentval = get_head();
+
+    my $exists = git_get_ref $ffq_prev;
+    return ('exists',"$ffq_prev already exists") if $exists;
+
+    return ('not-branch', 'HEAD symref is not to refs/heads/')
+       unless $current =~ m{^refs/heads/};
+    my $branch = $';
+
+    my @check_specs = split /\;/, (cfg "branch.$branch.ffq-ffrefs",1) // '*';
+    my %checked;
+
+    my $check = sub {
+       my ($lrref, $desc) = @_;
+       my $invert;
+       for my $chk (@check_specs) {
+           my $glob = $chk;
+           $invert = $glob =~ s{^[^!]}{};
+           last if fnmatch $glob, $lrref;
+       }
+       return if $invert;
+       my $lrval = git_get_ref $lrref;
+       return unless defined $lrval;
+
+       if (is_fast_fwd $lrval, $currentval) {
+           print "OK, you are ahead of $lrref\n" or die $!;
+           $checked{$lrref} = 1;
+       } if (is_fast_fwd $currentval, $lrval) {
+           $checked{$lrref} = -1;
+           fproblem 'behind', "you are behind $lrref, divergence risk";
+       } else {
+           $checked{$lrref} = -1;
+           fproblem 'diverged', "you have diverged from $lrref";
+       }
+    };
+
+    my $merge = cfg "branch.$branch.merge",1;
+    if (defined $merge && $merge =~ m{^refs/heads/}) {
+       my $rhs = $';
+       my $check_remote = sub {
+           my ($remote, $desc) = (@_);
+           return unless defined $remote;
+           $check->("refs/remotes/$remote/$rhs", $desc);
+       };
+       $check_remote->((cfg "branch.$branch.remote",1),
+                       'remote fetch/merge branch');
+       $check_remote->((cfg "branch.$branch.pushRemote",1) //
+                       (cfg "branch.$branch.pushDefault",1),
+                       'remote push branch');
+    }
+    if ($branch =~ m{^dgit/}) {
+       $check->("remotes/dgit/$branch", 'remote dgit branch');
+    } elsif ($branch =~ m{^master$}) {
+       $check->("remotes/dgit/dgit/sid", 'remote dgit branch for sid');
+    }
+
+    fproblems_maybe_bail();
+    runcmd @git, qw(update-ref -m), "record current head for preservation",
+       $ffq_prev, $currentval, $git_null_obj;
+    print "Recorded current head for preservation\n" or die $!;
+    return ('written', undef);
+}
+
+sub cmd_new_upstream_v0 () {
+    # automatically and unconditionally launders before rebasing
+    # if rebase --abort is used, laundering has still been done
+
+    my %pieces;
+
+    badusage "need NEW-VERSION UPS-COMMITTISH" unless @ARGV >= 2;
+
+    # parse args - low commitment
+    my $new_version = (new Dpkg::Version scalar(shift @ARGV), check => 1);
+    my $new_upstream_version = $new_version->version();
+
+    my $new_upstream = git_rev_parse shift @ARGV;
+
+    my $piece = sub {
+        my ($n, @x) = @_; # may be ''
+        my $pc = $pieces{$n} //= {
+           Name => $n,
+           Desc => ($n ? "upstream piece \`$n'" : "upstream (main piece"),
+       };
+       while (my $k = shift @x) { $pc->{$k} = shift @x; }
+        $pc;
+    };
+
+    my @newpieces;
+    my $newpiece = sub {
+       my ($n, @x) = @_; # may be ''
+       my $pc = $piece->($n, @x, NewIx => (scalar @newpieces));
+       push @newpieces, $pc;
+    };
+
+    $newpiece->('',
+        OldIx => 0,
+        New => $new_upstream,
+    );
+    while (@ARGV && $ARGV[0] !~ m{^-}) {
+       my $n = shift @ARGV;
+
+        badusage "for each EXTRA-UPS-NAME need EXTRA-UPS-COMMITISH"
+            unless @ARGV && $ARGV[0] !~ m{^-};
+
+       my $c = git_rev_parse shift @ARGV;
+       die unless $n =~ m/^$extra_orig_namepart_re$/;
+       $newpiece->($n, New => $c);
+    }
+
+    # now we need to investigate the branch this generates the
+    # laundered version but we don't switch to it yet
+    my $old_head = get_head();
+    my ($old_laundered_tip,$old_bw,$old_upstream_update) = walk $old_head;
+
+    my $old_bw_cl = classify $old_bw;
+    my $old_upstream_update_cl = classify $old_upstream_update;
+    confess unless $old_upstream_update_cl->{OrigParents};
+    my $old_upstream = parsecommit
+        $old_upstream_update_cl->{OrigParents}[0]{CommitId};
+
+    $piece->('', Old => $old_upstream->{CommitId});
+
+    if ($old_upstream->{Msg} =~ m{^\[git-debrebase }m) {
+       if ($old_upstream->{Msg} =~
+ m{^\[git-debrebase upstream-combine \.((?: $extra_orig_namepart_re)+)\:.*\]$}m
+          ) {
+           my @oldpieces = ('', split / /, $1);
+           my $parentix = -1 + scalar @{ $old_upstream->{Parents} };
+           foreach my $i (0..$#oldpieces) {
+               my $n = $oldpieces[$i];
+               $piece->($n, Old => $old_upstream->{CommitId}.'^'.$parentix);
+           }
+       } else {
+           fproblem 'upstream-confusing',
+               "previous upstream $old_upstream->{CommitId} is from".
+               " git-debrebase but not an \`upstream-combine' commit";
+       }
+    }
+
+    foreach my $pc (values %pieces) {
+       if (!$pc->{Old}) {
+           fproblem 'upstream-new-piece',
+               "introducing upstream piece \`$pc->{Name}'";
+       } elsif (!$pc->{New}) {
+           fproblem 'upstream-rm-piece',
+               "dropping upstream piece \`$pc->{Name}'";
+       } elsif (!is_fast_fwd $pc->{Old}, $pc->{New}) {
+           fproblem 'upstream-not-ff',
+               "not fast forward: $pc->{Name} $pc->{Old}..$pc->{New}";
+       }
+    }
+
+    printdebug "%pieces = ", (dd \%pieces), "\n";
+    printdebug "\@newpieces = ", (dd \@newpieces), "\n";
+
+    fproblems_maybe_bail();
+
+    my $new_bw;
+
+    fresh_workarea();
+    in_workarea sub {
+       my @upstream_merge_parents;
+
+       if (!any_fproblems()) {
+           push @upstream_merge_parents, $old_upstream->{CommitId};
+       }
+
+       foreach my $pc (@newpieces) { # always has '' first
+           if ($pc->{Name}) {
+               read_tree_subdir $pc->{Name}, $pc->{New};
+           } else {
+               runcmd @git, qw(read-tree), $pc->{New};
+           }
+           push @upstream_merge_parents, $pc->{New};
+       }
+
+       # index now contains the new upstream
+
+       if (@newpieces > 1) {
+           # need to make the upstream subtree merge commit
+            $new_upstream = make_commit \@upstream_merge_parents,
+                [ "Combine upstreams for $new_upstream_version",
+ ("[git-debrebase upstream-combine . ".
+ (join " ", map { $_->{Name} } @newpieces[1..$#newpieces]).
+ ": new upstream]"),
+                ];
+       }
+
+       # $new_upstream is either the single upstream commit, or the
+       # combined commit we just made.  Either way it will be the
+       # "upstream" parent of the breakwater special merge.
+
+       read_tree_subdir 'debian', "$old_bw:debian";
+
+       # index now contains the breakwater merge contents
+        $new_bw = make_commit [ $old_bw, $new_upstream ],
+            [ "Update to upstream $new_upstream_version",
+ "[git-debrebase breakwater: new upstream $new_upstream_version, merge]",
+            ];
+
+       # 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.
+
+ -- 
+
+END
+       close CN or die $!;
+       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]",
+            ];
+    };
+
+    # we have constructed the new breakwater. we now need to commit to
+    # the laundering output, because git-rebase can't easily be made
+    # to make a replay list which is based on some other branch
+
+    update_head_postlaunder $old_head, $old_laundered_tip,
+        'launder for new upstream';
+
+    my @cmd = (@git, qw(rebase --onto), $new_bw, $old_bw, @ARGV);
+    runcmd @cmd;
+    # now it's for the user to sort out
+}
+
+sub cmd_record_ffq_prev () {
+    badusage "no arguments allowed" if @ARGV;
+    my ($status, $msg) = record_ffq_prev();
+    if ($status eq 'exists' && $opt_noop_ok) {
+       print "Previous head already recorded\n" or die $!;
+    } elsif ($status eq 'written') {
+    } else {
+       fail "Could not preserve: $msg";
+    }
+}
+
+sub cmd_breakwater () {
+    badusage "no arguments allowed" if @ARGV;
+    my $bw = breakwater_of git_rev_parse 'HEAD';
+    print "$bw\n" or die $!;
+}
+
+sub cmd_stitch () {
+    my $prose = '';
+    GetOptions('prose=s', \$prose) or die badusage("bad options to stitch");
+    badusage "no arguments allowed" if @ARGV;
+    my ($status, $message, $current, $ffq_prev) = ffq_prev_branchinfo();
+    if ($status ne 'branch') {
+       fproblem $status, "could not check ffq-prev: $message";
+       fproblems_maybe_bail();
+    }
+    my $prev = $ffq_prev && git_get_ref $ffq_prev;
+    if (!$prev) {
+       fail "No ffq-prev to stitch." unless $opt_noop_ok;
+    }
+    fresh_workarea();
+    my $old_head = get_head();
+    my $new_head = make_commit [ $old_head, $ffq_prev ], [
+       'Declare fast forward / record previous work',
+        "[git-debrebase pseudomerge: stitch$prose]",
+    ];
+    my @upd_cmd = (@git, qw(update-ref --stdin));
+    debugcmd '>|', @upd_cmd;
+    open U, "|-", @upd_cmd or die $!;
+    my $u = <<END;
+update HEAD $new_head $old_head
+delete $ffq_prev $prev
+END
+    printdebug ">= ", $_, "\n" foreach split /\n/, $u;
+    print U $u;
+    printdebug ">\$\n";
+    close U or failedcmd @upd_cmd;
+}
+
+sub cmd_convert_from_gbp () {
+    badusage "needs 1 optional argument, the upstream git rev"
+       unless @ARGV<=1;
+    my ($upstream_spec) = @ARGV;
+    $upstream_spec //= 'refs/heads/upstream';
+    my $upstream = git_rev_parse $upstream_spec;
+    my $old_head = get_head();
+
+    my $upsdiff = get_differs $upstream, $old_head;
+    if ($upsdiff & D_UPS) {
+       runcmd @git, qw(--no-pager diff),
+           $upstream, $old_head,
+           qw( -- :!/debian :/);
+ fail "upstream ($upstream_spec) and HEAD are not identical in upstream files";
+    }
+
+    if (!is_fast_fwd $upstream, $old_head) {
+       fproblem 'upstream-not-ancestor',
+           "upstream ($upstream) is not an ancestor of HEAD";
+    } else {
+       my $wrong = cmdoutput
+           (@git, qw(rev-list --ancestry-path), "$upstream..HEAD",
+            qw(-- :/ :!/debian));
+       if (length $wrong) {
+           fproblem 'unexpected-upstream-changes',
+               "history between upstream ($upstream) and HEAD contains direct changes to upstream files - are you sure this is a gbp (patches-unapplied) branch?";
+           print STDERR "list expected changes with:  git log --stat --ancestry-path $upstream_spec..HEAD -- :/ ':!/debian'\n";
+       }
+    }
+
+    if ((git_cat_file "$upstream:debian")[0] ne 'missing') {
+       fproblem 'upstream-has-debian',
+           "upstream ($upstream) contains debian/ directory";
+    }
+
+    fproblems_maybe_bail();
+
+    my $work;
+
+    fresh_workarea();
+    in_workarea sub {
+       runcmd @git, qw(checkout -q -b gdr-internal), $old_head;
+       # make a branch out of the patch queue - we'll want this in a mo
+       runcmd qw(gbp pq import);
+       # strip the patches out
+       runcmd @git, qw(checkout -q gdr-internal~0);
+       rm_subdir_cached 'debian/patches';
+       $work = make_commit ['HEAD'], [
+ 'git-debrebase convert-from-gbp: drop patches from tree',
+ 'Delete debian/patches, as part of converting to git-debrebase format.',
+ '[git-debrebase convert-from-gbp: drop patches from tree]'
+                             ];
+       # make the breakwater pseudomerge
+       # the tree is already exactly right
+       $work = make_commit [$work, $upstream], [
+ 'git-debrebase import: declare upstream',
+ 'First breakwater merge.',
+ '[git-debrebase breakwater: declare upstream]'
+                             ];
+
+       # rebase the patch queue onto the new breakwater
+       runcmd @git, qw(reset --quiet --hard patch-queue/gdr-internal);
+       runcmd @git, qw(rebase --quiet --onto), $work, qw(gdr-internal);
+       $work = git_rev_parse 'HEAD';
+    };
+
+    update_head_checkout $old_head, $work, 'convert-from-gbp';
+}
+
+sub cmd_convert_to_gbp () {
+    badusage "no arguments allowed" if @ARGV;
+    my $head = get_head();
+    my $ffq = (ffq_prev_branchinfo())[3];
+    my $bw = breakwater_of $head;
+    fresh_workarea();
+    my $out;
+    in_workarea sub {
+       runcmd @git, qw(checkout -q -b bw), $bw;
+       runcmd @git, qw(checkout -q -b patch-queue/bw), $head;
+       runcmd qw(gbp pq export);
+       runcmd @git, qw(add debian/patches);
+       $out = make_commit ['HEAD'], [
+            'Commit patch queue (converted from git-debrebase format)',
+            '[git-debrebase convert-to-gbp: commit patches]',
+        ];
+    };
+    if (defined $ffq) {
+       runcmd @git, qw(update-ref -m),
+           "debrebase: converting corresponding main branch to gbp format",
+           $ffq, $git_null_obj;
+    }
+    update_head_checkout $head, $out, "convert to gbp (v0)";
+    print <<END or die $!;
+git-debrebase: converted to git-buildpackage branch format
+git-debrebase: WARNING: do not now run "git-debrebase" any more
+git-debrebase: WARNING: doing so would drop all upstream patches!
+END
+}
+
+sub cmd_downstream_rebase_launder_v0 () {
+    badusage "needs 1 argument, the baseline" unless @ARGV==1;
+    my ($base) = @ARGV;
+    $base = git_rev_parse $base;
+    my $old_head = get_head();
+    my $current = $old_head;
+    my $topmost_keep;
+    for (;;) {
+       if ($current eq $base) {
+           $topmost_keep //= $current;
+           print " $current BASE stop\n";
+           last;
+       }
+       my $cl = classify $current;
+       print " $current $cl->{Type}";
+       my $keep = 0;
+       my $p0 = $cl->{Parents}[0]{CommitId};
+       my $next;
+       if ($cl->{Type} eq 'Pseudomerge') {
+           print " ^".($cl->{Contributor}{Ix}+1);
+           $next = $cl->{Contributor}{CommitId};
+       } elsif ($cl->{Type} eq 'AddPatches' or
+                $cl->{Type} eq 'Changelog') {
+           print " strip";
+           $next = $p0;
+       } else {
+           print " keep";
+           $next = $p0;
+           $keep = 1;
+       }
+       print "\n";
+       if ($keep) {
+           $topmost_keep //= $current;
+       } else {
+           die "to-be stripped changes not on top of the branch\n"
+               if $topmost_keep;
+       }
+       $current = $next;
+    }
+    if ($topmost_keep eq $old_head) {
+       print "unchanged\n";
+    } else {
+       print "updating to $topmost_keep\n";
+       update_head_checkout
+           $old_head, $topmost_keep,
+           'downstream-rebase-launder-v0';
+    }
+}
+
+GetOptions("D+" => \$debuglevel,
+          'noop-ok', => \$opt_noop_ok,
+          'f=s' => \@fproblem_force_opts,
+          'force!') or die badusage "bad options\n";
 initdebug('git-debrebase ');
 enabledebug if $debuglevel;
 
@@ -716,10 +1369,14 @@ chdir $toplevel or die "chdir $toplevel: $!";
 
 $rd = fresh_playground "$playprefix/misc";
 
-my $cmd = shift @ARGV;
-my $cmdfn = $cmd;
-$cmdfn =~ y/-/_/;
-$cmdfn = ${*::}{"cmd_$cmdfn"};
+if (!@ARGV || $ARGV[0] =~ m{^-}) {
+    defaultcmd_rebase();
+} else {
+    my $cmd = shift @ARGV;
+    my $cmdfn = $cmd;
+    $cmdfn =~ y/-/_/;
+    $cmdfn = ${*::}{"cmd_$cmdfn"};
 
-$cmdfn or badusage "unknown git-debrebase sub-operation $cmd";
-$cmdfn->();
+    $cmdfn or badusage "unknown git-debrebase sub-operation $cmd";
+    $cmdfn->();
+}