chiark / gitweb /
Merge branch 'gdr-manpages' into wip.rebase
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Sun, 17 Jun 2018 22:02:06 +0000 (23:02 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Sun, 17 Jun 2018 22:02:06 +0000 (23:02 +0100)
30 files changed:
.gitignore
Debian/Dgit.pm
Debian/Dgit/ExitStatus.pm [new file with mode: 0644]
Debian/Dgit/GDR.pm [new file with mode: 0644]
Makefile
NOTES.git-debrebase [new file with mode: 0644]
debian/changelog
debian/control
debian/rules
debian/tests/control
debian/tests/control.in
dgit
dgit-badcommit-fixup
dgit.1
git-debrebase [new file with mode: 0755]
git-debrebase.1.pod [new file with mode: 0644]
git-debrebase.5.pod [new file with mode: 0644]
tests/enumerate-tests
tests/lib
tests/lib-core
tests/lib-gdr [new file with mode: 0644]
tests/setup/gdr-convert-gbp [new file with mode: 0755]
tests/setup/gdr-convert-gbp-noarchive [new file with mode: 0755]
tests/tests/gdr-diverge-nmu [new file with mode: 0755]
tests/tests/gdr-diverge-nmu-dgit [new file with mode: 0755]
tests/tests/gdr-edits [new file with mode: 0755]
tests/tests/gdr-import-dgit [new file with mode: 0755]
tests/tests/gdr-newupstream-v0 [new file with mode: 0755]
tests/tests/gdr-subcommands [new file with mode: 0755]
tests/tests/gdr-viagit [new file with mode: 0755]

index a804fa3cac5236228d3b5d737c3b60c4af06935e..ba7af7890f82c3a3d80d504b71755d5bdc8e9d6c 100644 (file)
@@ -1,6 +1,7 @@
 *~
 tests/tmp
 debian/dgit
+debian/git-debrebase
 debian/dgit-infrastructure
 debian/files
 debian/*.substvars
@@ -13,4 +14,6 @@ dgit-maint-merge.7
 dgit-maint-gbp.7
 dgit-maint-debrebase.7
 dgit-sponsorship.7
+git-debrebase.1
+git-debrebase.5
 substituted
index c4a61af1f081b62f21938ac86783a0deef000286..960f505aa1579036d27f3d7374b18d74490f6e37 100644 (file)
@@ -44,28 +44,33 @@ BEGIN {
                      server_branch server_ref
                       stat_exists link_ltarget
                      hashfile
-                      fail ensuredir must_getcwd executable_on_path
+                      fail failmsg ensuredir must_getcwd executable_on_path
                       waitstatusmsg failedcmd_waitstatus
                      failedcmd_report_cmd failedcmd
                       runcmd cmdoutput cmdoutput_errok
                       git_rev_parse git_cat_file
-                     git_get_ref git_for_each_ref
+                     git_get_ref git_get_symref git_for_each_ref
                       git_for_each_tag_referring is_fast_fwd
+                     git_check_unmodified
                       $package_re $component_re $deliberately_re
                      $distro_re $versiontag_re $series_filename_re
+                     $extra_orig_namepart_re
+                     $git_null_obj
                       $branchprefix
+                     $ffq_refprefix $gdrlast_refprefix
                       initdebug enabledebug enabledebuglevel
                       printdebug debugcmd
                       $debugprefix *debuglevel *DEBUG
                       shellquote printcmd messagequote
                       $negate_harmful_gitattrs
                      changedir git_slurp_config_src
+                     gdr_ffq_prev_branchinfo
                      playtree_setup);
     # implicitly uses $main::us
     %EXPORT_TAGS = ( policyflags => [qw(NOFFCHECK FRESHREPO NOCOMMITCHECK)],
                     playground => [qw(record_maindir $maindir $local_git_cfg
                                       $maindir_gitdir $maindir_gitcommon
-                                      fresh_playground $playground
+                                      fresh_playground
                                        ensure_a_playground)]);
     @EXPORT_OK   = ( @{ $EXPORT_TAGS{policyflags} },
                     @{ $EXPORT_TAGS{playground} } );
@@ -80,6 +85,10 @@ our $distro_re = $component_re;
 our $versiontag_re = qr{[-+.\%_0-9a-zA-Z/]+};
 our $branchprefix = 'dgit';
 our $series_filename_re = qr{(?:^|\.)series(?!\n)$}s;
+our $extra_orig_namepart_re = qr{[-0-9a-z]+};
+our $git_null_obj = '0' x 40;
+our $ffq_refprefix = 'ffq-prev';
+our $gdrlast_refprefix = 'debrebase-last';
 
 # policy hook exit status bits
 # see dgit-repos-server head comment for documentation
@@ -108,7 +117,7 @@ sub forkcheck_mainprocess () {
 sub setup_sigwarn () {
     forkcheck_setup();
     $SIG{__WARN__} = sub { 
-       die $_[0] if forkcheck_mainprocess;
+       confess $_[0] if forkcheck_mainprocess;
     };
 }
 
@@ -213,12 +222,16 @@ sub _us () {
     $::us // ($0 =~ m#[^/]*$#, $&);
 }
 
-sub fail { 
+sub failmsg {
     my $s = "@_\n";
     $s =~ s/\n\n$/\n/;
     my $prefix = _us().": ";
     $s =~ s/^/$prefix/gm;
-    die $s;
+    return $s;
+}
+
+sub fail {
+    die failmsg @_;
 }
 
 sub ensuredir ($) {
@@ -348,11 +361,21 @@ sub git_rev_parse ($) {
     return cmdoutput qw(git rev-parse), "$_[0]~0";
 }
 
-sub git_cat_file ($) {
-    my ($objname) = @_;
+sub git_cat_file ($;$) {
+    my ($objname, $etype) = @_;
     # => ($type, $data) or ('missing', undef)
     # in scalar context, just the data
+    # if $etype defined, dies unless type is $etype or in @$etype
     our ($gcf_pid, $gcf_i, $gcf_o);
+    my $chk = sub {
+       my ($gtype, $data) = @_;
+       if ($etype) {
+           $etype = [$etype] unless ref $etype;
+           confess "$objname expected @$etype but is $gtype"
+               unless grep { $gtype eq $_ } @$etype;
+       }
+       return ($gtype, $data);
+    };
     if (!$gcf_pid) {
        my @cmd = qw(git cat-file --batch);
        debugcmd "GCF|", @cmd;
@@ -362,13 +385,26 @@ sub git_cat_file ($) {
     print $gcf_i $objname, "\n" or die $!;
     my $x = <$gcf_o>;
     printdebug "GCF<| ", $x;
-    if ($x =~ m/ (missing)$/) { return ($1, undef); }
+    if ($x =~ m/ (missing)$/) { return $chk->($1, undef); }
     my ($type, $size) = $x =~ m/^.* (\w+) (\d+)\n/ or die "$objname ?";
     my $data;
     (read $gcf_o, $data, $size) == $size or die "$objname $!";
     $x = <$gcf_o>;
     $x eq "\n" or die "$objname ($_) $!";
-    return ($type, $data);
+    return $chk->($type, $data);
+}
+
+sub git_get_symref (;$) {
+    my ($symref) = @_;  $symref //= 'HEAD';
+    # => undef if not a symref, otherwise refs/...
+    my @cmd = (qw(git symbolic-ref -q HEAD));
+    my $branch = cmdoutput_errok @cmd;
+    if (!defined $branch) {
+       $?==256 or failedcmd @cmd;
+    } else {
+       chomp $branch;
+    }
+    return $branch;
 }
 
 sub git_for_each_ref ($$;$) {
@@ -418,6 +454,25 @@ sub git_for_each_tag_referring ($$) {
     });
 }
 
+sub git_check_unmodified () {
+    foreach my $cached (qw(0 1)) {
+       my @cmd = qw(git diff --quiet);
+       push @cmd, qw(--cached) if $cached;
+       push @cmd, qw(HEAD);
+       debugcmd "+",@cmd;
+       $!=0; $?=-1; system @cmd;
+       return if !$?;
+       if ($?==256) {
+           fail
+               $cached
+               ? "git index contains changes (does not match HEAD)"
+               : "working tree is dirty (does not match HEAD)";
+       } else {
+           failedcmd @cmd;
+       }
+    }
+}
+
 sub is_fast_fwd ($$) {
     my ($ancestor,$child) = @_;
     my @cmd = (qw(git merge-base), $ancestor, $child);
@@ -460,12 +515,28 @@ sub git_slurp_config_src ($) {
     return $r;
 }
 
+sub gdr_ffq_prev_branchinfo ($) {
+    my ($symref) = @_;
+    # => ('status', "message", [$symref, $ffq_prev, $gdrlast])
+    # 'status' may be
+    #    branch         message is undef
+    #    weird-symref   } no $symref,
+    #    notbranch      }  no $ffq_prev
+    return ('detached', 'detached HEAD') unless defined $symref;
+    return ('weird-symref', 'HEAD symref is not to refs/')
+       unless $symref =~ m{^refs/};
+    my $ffq_prev = "refs/$ffq_refprefix/$'";
+    my $gdrlast = "refs/$gdrlast_refprefix/$'";
+    printdebug "ffq_prev_branchinfo branch current $symref\n";
+    return ('branch', undef, $symref, $ffq_prev, $gdrlast);
+}
+
 # ========== playground handling ==========
 
 # terminology:
 #
 #   $maindir      user's git working tree
-#   $playground   area in .git/ where we can make files, unpack, etc. etc.
+#   playground    area in .git/ where we can make files, unpack, etc. etc.
 #   playtree      git working tree sharing object store with the user's
 #                 inside playground, or identical to it
 #
@@ -485,28 +556,26 @@ sub git_slurp_config_src ($) {
 #
 #    fresh_playground SUBDIR_PATH_COMPONENTS
 #      e.g fresh_playground 'dgit/unpack' ('.git/' is implied)
-#      default SUBDIR_PATH_COMPONENTS is $playground_subdir
+#      default SUBDIR_PATH_COMPONENTS is playground_subdir
 #      calls record_maindir
 #      sets up a new playground (destroying any old one)
-#      assigns to $playground and returns the same pathname
+#      returns playground pathname
 #      caller may call multiple times with different subdir paths
-#       createing different playgrounds; but $playground global can
-#       refer only to one, obv.
+#       createing different playgrounds
 #
 #    ensure_a_playground SUBDIR_PATH_COMPONENTS
 #      like fresh_playground except:
 #      merely ensures the directory exists; does not delete an existing one
-#      never sets global $playground
 #
 #  then can use
 #
-#    changedir $playground
+#    changedir playground
 #    changedir $maindir
 #
 #    playtree_setup $local_git_cfg
-#            # ^ call in some (perhaps trivial) subdir of $playground
+#            # ^ call in some (perhaps trivial) subdir of playground
 #
-#    rmtree $playground
+#    rmtree playground
 
 # ----- maindir -----
 
@@ -538,8 +607,6 @@ sub record_maindir () {
 
 # ----- playgrounds -----
 
-our $playground;
-
 sub ensure_a_playground_parent ($) {
     my ($spc) = @_;
     record_maindir();
@@ -562,7 +629,7 @@ sub fresh_playground ($) {
     $spc = ensure_a_playground_parent $spc;
     rmtree $spc;
     mkdir $spc or fail "failed to mkdir the playground $spc: $!";
-    return $playground = $spc;
+    return $spc;
 }
 
 # ----- playtrees -----
diff --git a/Debian/Dgit/ExitStatus.pm b/Debian/Dgit/ExitStatus.pm
new file mode 100644 (file)
index 0000000..b69d42d
--- /dev/null
@@ -0,0 +1,26 @@
+# -*- perl -*-
+
+package Debian::Dgit::ExitStatus;
+
+# To use this, at the top (before use strict, even):
+#
+#   END { $? = $Debian::Dgit::ExitStatus::desired // -1; };
+#   use Debian::Dgit::ExitStatus;
+#
+# and then replace every call to `exit' with `finish'.
+# Add a `finish 0' to the end of the program.
+
+BEGIN {
+    use Exporter;
+    @ISA = qw(Exporter);
+    @EXPORT = qw(finish $desired);
+}
+
+our $desired;
+
+sub finish ($) {
+    $desired = $_[0] // 0;
+    exit $desired;
+}
+
+1;
diff --git a/Debian/Dgit/GDR.pm b/Debian/Dgit/GDR.pm
new file mode 100644 (file)
index 0000000..ca7e621
--- /dev/null
@@ -0,0 +1,26 @@
+# -*- perl -*-
+
+package Debian::Dgit::GDR;
+
+use strict;
+use warnings;
+
+# Scripts and programs which are going to `use Debian::Dgit' but which
+# live in git-debrebase (ie are installed with install-gdr)
+# should `use Debian::Dgit::GDR' first.  All this module does is
+# adjust @INC so that the script gets the version of the script from
+# the git-debrebase package (which is installed in a different
+# location and may be a different version).
+
+# To use this with ExitStatus, put at the top (before use strict, even):
+#
+#   END { $? = $Debian::Dgit::ExitStatus::desired // -1; };
+#   use Debian::Dgit::GDR;
+#   use Debian::Dgit::ExitStatus;
+#
+# and then replace every call to `exit' with `finish'.
+# Add a `finish 0' to the end of the program.
+
+# unshift @INC, q{/usr/share/dgit/gdr/perl5}; ###substituted###
+
+1;
index 3eca3121ff89a1ce2981d559c57e53444e193f7a..de28f4d690c967f43d2cd2e913d5cd189fcb532d 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -27,6 +27,7 @@ bindir=$(prefix)/bin
 mandir=$(prefix)/share/man
 perldir=$(prefix)/share/perl5
 man1dir=$(mandir)/man1
+man5dir=$(mandir)/man5
 man7dir=$(mandir)/man7
 infraexamplesdir=$(prefix)/share/doc/dgit-infrastructure/examples
 txtdocdir=$(prefix)/share/doc/dgit
@@ -43,9 +44,17 @@ MAN7PAGES=dgit.7                             \
        dgit-sponsorship.7
 
 TXTDOCS=README.dsc-import
-PERLMODULES=Debian/Dgit.pm
+PERLMODULES=Debian/Dgit.pm Debian/Dgit/ExitStatus.pm
 ABSURDITIES=git
 
+GDR_PROGRAMS=git-debrebase
+GDR_PERLMODULES= \
+       Debian/Dgit.pm \
+       Debian/Dgit/GDR.pm \
+       Debian/Dgit/ExitStatus.pm
+GDR_MAN1PAGES=git-debrebase.1
+GDR_MAN5PAGES=git-debrebase.5
+
 INFRA_PROGRAMS=dgit-repos-server dgit-ssh-dispatch \
        dgit-repos-policy-debian dgit-repos-admin-debian \
        dgit-repos-policy-trusting dgit-mirror-rsync
@@ -55,7 +64,10 @@ INFRA_PERLMODULES= \
        Debian/Dgit/Infra.pm \
        Debian/Dgit/Policy/Debian.pm
 
-all:   $(MAN7PAGES) $(addprefix substituted/,$(PROGRAMS))
+MANPAGES=$(MAN1PAGES) $(MAN5PAGES) $(MAN7PAGES) \
+       $(GDR_MAN1PAGES) $(GDR_MAN5PAGES)
+
+all:   $(MANPAGES) $(addprefix substituted/,$(PROGRAMS))
 
 substituted/%: %
        mkdir -p substituted
@@ -76,10 +88,19 @@ install:    installdirs all
 
 installdirs:
        $(INSTALL_DIR) $(DESTDIR)$(bindir) \
-               $(DESTDIR)$(man1dir) $(DESTDIR)$(man7dir) \
+               $(DESTDIR)$(man1dir) $(DESTDIR)$(man5dir) \
+               $(DESTDIR)$(man7dir) \
                $(DESTDIR)$(txtdocdir) $(DESTDIR)$(absurddir) \
                $(addprefix $(DESTDIR)$(perldir)/, $(dir $(PERLMODULES)))
 
+install-gdr:   installdirs-gdr
+       $(INSTALL_PROGRAM) $(GDR_PROGRAMS) $(DESTDIR)$(bindir)
+       $(INSTALL_DATA) $(GDR_MAN1PAGES) $(DESTDIR)$(man1dir)
+       $(INSTALL_DATA) $(GDR_MAN5PAGES) $(DESTDIR)$(man5dir)
+       set -e; for m in $(GDR_PERLMODULES); do \
+               $(INSTALL_DATA) $$m $(DESTDIR)$(perldir)/$${m%/*}; \
+       done
+
 install-infra: installdirs-infra
        $(INSTALL_PROGRAM) $(addprefix infra/, $(INFRA_PROGRAMS)) \
                $(DESTDIR)$(bindir)
@@ -89,6 +110,11 @@ install-infra:      installdirs-infra
                $(INSTALL_DATA) $$m $(DESTDIR)$(perldir)/$${m%/*}; \
        done
 
+installdirs-gdr:
+       $(INSTALL_DIR) $(DESTDIR)$(bindir) \
+               $(DESTDIR)$(man1dir) $(DESTDIR)$(man5dir) \
+               $(addprefix $(DESTDIR)$(perldir)/, $(dir $(GDR_PERLMODULES)))
+
 installdirs-infra:
        $(INSTALL_DIR) $(DESTDIR)$(bindir) $(DESTDIR)$(infraexamplesdir) \
                $(addprefix $(DESTDIR)$(perldir)/, $(dir $(INFRA_PERLMODULES)))
@@ -97,7 +123,7 @@ check installcheck:
 
 clean distclean mostlyclean maintainer-clean:
        rm -rf tests/tmp substituted
-       set -e; for m in $(MAN7PAGES); do \
+       set -e; for m in $(MANPAGES); do \
                test -e $$m.pod && rm -f $$m; \
        done
 
@@ -106,5 +132,10 @@ clean distclean mostlyclean maintainer-clean:
                --name=$(subst .7,,$@) \
                $^ $@
 
+git-debrebase.%: git-debrebase.%.pod
+       pod2man --section=$* --date="Debian Project" --center="git-debrebase" \
+               --name=$(subst .$*,,$@) \
+               $^ $@
+
 %.view:        %
        man -l $*
diff --git a/NOTES.git-debrebase b/NOTES.git-debrebase
new file mode 100644 (file)
index 0000000..f32cf87
--- /dev/null
@@ -0,0 +1,203 @@
+TODO
+   tutorial
+      dgit-maint-debrebase(7)
+      someone should set branch.<name>.mergeOptions to include --ff-only ?
+
+   arrange for dgit to automatically stitch on push
+     dgit push usually needs to (re)make a pseudomerge.  The "first"
+     git-debrebase stripped out the previous pseudomerge and could
+     remembeed the old HEAD.  But the user has to manually stitch it.
+     To fix this, do we need a new push hook for dgit ?
+
+
+
+workflow
+
+  git-debrebase blah [implies start]       strips pseudomerge(s)
+
+  commit / git-debrebase / etc.
+
+  dgit --damp-run push
+      hook: call git-debrebase prep-push   dgit push does not update remote
+      or something, must add patches at least
+
+  commit / git-debrebase / etc.            strips patches
+
+  dgit push
+      hook: call git-debrebase prep-push   dgit push DOES update remote
+
+  commit / git-debrebase / etc.            strips last pm, but arranges
+                                           that remade pm will incorporate it
+
+
+# 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.)
+#
+#  * Workflow is currently clumsy.  Lots of spurious runes to type.
+#    There's not even a guide.
+#
+#  * new-upstream-v0 has a terrible UI for multiple upstream pieces.
+#    You end up with giant runic command lines.  Does this matter /
+#    One consequence of the lack of richness it can need -f in
+#    fairly sensible situations.
+#
+#  * There should be a good convention for the version number,
+#    and unfinalised or not changelog, after new-upstream.
+#
+#  * 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.
+#
+#  * 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?
+#    maybe the hypothetical git-ffqrebase is part of it too.
+       
+
+# undocumented usages:
+#
+#    git-debrebase [<options>] downstream-rebase-launder-v0  # experimental
+
+
+========================================
+
+Theory for ffq-prev
+
+  refs/ffq-prev/REF    relates to refs/REF
+
+When we strip a pm, we need to maybe record it (or something) as the
+new start point.
+
+When we do a thing
+
+    with no recorded ffq-prev
+
+        ffq-prev is our current tip
+
+        obviously it is safe to say we will overwrite this
+        we do check whether there are not-included changes in the remotes
+        because if the new ffq-prev is not ff from the remotes
+        the later pushes will fail
+
+        this model tends to keep ad-hoc commits made on our
+        tip branch before we did rebase start, in the
+        `interchange view' and also in the rebase stack.
+
+        also we can explicitly preserve with
+        git-debrebase stitch
+
+       It is always safe to rewind ffq-prev: all
+       that does is overwrite _less_ stuff.
+
+        in any case putative ffq-prev must be ff from remote.
+        Otherwise when we push it will not be ff, even though we have
+        made pseudomerge to overwrite ffq-prev.  So if we spot
+        this, report an error.  see above
+
+    with a recorded ffq-prev
+
+        we may need to advance ffq-prev, to allow us to generate
+        future pseudomerges that will be pushable
+
+        advancing ffq-prev is dangerous, since it might
+        effectively cancel the commits that will-ovewrite is advanced
+        over.
+
+        ??? advance it to merge-base( current remote, current tip )
+        if possible (see above), - ie to current remote, subject
+        to the condition that that is an ancestor of current tip
+
+        currently this is not implemented
+
+        better maybe to detect divergence ?  but it is rather late
+        by then!
+
+We check we are ff from remotes before recording new ffq-prev
+
+  ---------- now follows much the same info in different words ----------
+
+Re git-debrebase [--noop-ok] stitch
+
+    we will teach dgit to do
+       git-debrebase stitch
+    or some such ?
+
+following parts are not implemented and maybe aren't the
+best subcommand names etc.
+
+3. git-debrebase push
+
+    like git push only does stitch first
+    ??? command line parsing!
+
+4. git-debrebase release
+
+    stiches, finalises changelog, signs tags, pushes everything
+    for the future, when there is some automatic builder
+
+========================================
+
+import from gbp
+
+what about dgit view branch ?
+ideally, would make pseudomerge over dgit view
+would need to check that dgit view is actually dgit view of
+  ond of our ancestors
+failing that first push will need --overwrite
+that is what is currently implemented
+
+========================================
+
+how to handle divergence and merges (if not detected soon enough)
+
+same problem
+ if merge, look at branches before merge
+ generate new combined branch
+ pseudomerge to overwrite merge
+
+current avaiable strategies:
+
+ maybe launder foreign branch
+
+ if foreign branch is nmuish, can rebase it onto ours
+
+ could merge breakwaters (use analyse to find them)
+ merge breakwaters (assuming same upstream)
+ manually construct new patch queue by inspection of
+  the other two patch queues
+
+ instead of manually constructing patch queue, could use
+  gbp pq export and git merge the patch queues
+  (ie work with interdiffs)
+
+ if upstreams are different and one is ahead
+  simply treat that as "ours" and
+  do the work to import changes from the other
+
+ if upstreams have diverged, can
+  resolve somehow to make new upstream
+  do new-upstream on each branch separately
+  now reduced to previously "solved" problem
+
+ in future, auto patch queue merge algorithm
+  determine next patch to apply
+  there are three versions o..O, l..L, r..R
+  we have already constructed m (previous patch or merged breakwater)
+  try using vector calculus in the implied cube and compute
+   multiple ways to check consistency ?
+
+========================================
+
+For downstreams of Debian, sketch of git-ffqrebase
+
+#    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]
index eb11dc4adf13f98c649f2f4c2dedb52be8eb260c..f5a4025c9586cc5cd250aa452701e083ac45d374 100644 (file)
@@ -1,9 +1,36 @@
-dgit (4.4~) unstable; urgency=low
+dgit (5.0~) unstable; urgency=low
 
-  * 
+  Major new utility:
+  * git-debrebase, a new git workflow tool.
+  * dgit will now, when appropriate, check if it should call
+    git-debrebase.
+
+  dgit bugfixes:
+  * Fix the exit status of programs in dgit.deb, to avoid the Perl
+    misfeature which sometimes copies $! to the exit status.
+  * When checking that the tree is clean, check the git index too.
+  * In quilt_fixup_multipatch, work around git checkout paths
+    not deleting files.  (Hypothetical bug AFAIAA.)
+  * Respect --quilt=nofix even if single-debian-patch.
+
+  dgit minor fixes:
+  * "confess" when we die due to a warning, rather than symply dieing.
+
+  Internal changes:
+  * Move $playground global to dgit.
+  * Break git_get_symref and $extra_orig_namepart_re out into Dgit.pm.
+  * Changes to support git-debrebase.
 
  --
 
+dgit (4.4) unstable; urgency=high
+
+  Test suite bugfix:
+  * Use full key hash rather than short keyid.  Closes:#896653.
+    [ report: Paul Gevers; fix: Chris Lamb ]
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Mon, 23 Apr 2018 13:18:51 +0100
+
 dgit (4.3) unstable; urgency=high
 
   Documentation improvements:
index 4405e141009b8dc147c024642f398189a9c7a8aa..224e52a123d985d0b80992515f618b38e3656661 100644 (file)
@@ -26,6 +26,15 @@ Description: git interoperability with the Debian archive
  .
  dgit clone and dgit fetch construct git commits from uploads.
 
+Package: git-debrebase
+Depends: perl, git-core, libdpkg-perl, libfile-fnmatch-perl
+         ${misc:Depends}
+Recommends: dgit, git-buildpackage
+Architecture: all
+Description: rebasing git workflow tool for Debian packaging
+ git-debrebase is a tool for representing in git, and manpulating,
+ Debian packages based on upstream source code.
+
 Package: dgit-infrastructure
 Depends: ${misc:Depends}, perl, git-core, gpgv, chiark-utils-bin,
          libjson-perl, libdigest-sha-perl, libdbd-sqlite3-perl, sqlite3,
index 9249f8889e26bda0159b9217ed7a1f5db728265d..baff8f897a2b35d12f6c1db02675507b49656141 100755 (executable)
@@ -31,25 +31,33 @@ override_dh_gencontrol:
         perl -i -pe "s/UNRELEASED/$$v/g if m/###substituted###/" usr/bin/dgit
 
 globalperl=/usr/share/perl5
-infraperl=/usr/share/dgit/infra/perl5
 
-override_dh_auto_install:
+override_dh_auto_install: specpkg_install_gdr specpkg_install_infra
        make install prefix=/usr DESTDIR=debian/dgit
-       make install-infra prefix=/usr DESTDIR=debian/dgit-infrastructure \
-               perldir=$(infraperl)
-#      # Most of the Perl modules in dgit-infrastructure live in
-#      # $(infraperl).  The exception is Debian::Dgit::Infra, which
-#      # lives in $(globalperl) and adds $(infraperl) to @INC.
+
+specpkg_install_gdr: p=git-debrebase
+specpkg_install_gdr: pm=GDR
+
+specpkg_install_infra: p=dgit-infrastructure
+specpkg_install_infra: pm=Infra
+
+specpkg_install_%: tok=$*
+specpkg_install_%: specperl=/usr/share/dgit/$(tok)/perl5
+specpkg_install_%:
+       make install-$(tok) prefix=/usr DESTDIR=debian/$(p) perldir=$(specperl)
+#      # Most of the Perl modules in this package live in
+#      # $(specperl).  The exception is Debian::Dgit::Infra, which
+#      # lives in $(globalperl) and adds $(specperl) to @INC.
        set -ex; \
-        base=debian/dgit-infrastructure; \
-        mod=Debian/Dgit/Infra.pm; \
-        src=$${base}$(infraperl)/$${mod}; \
+        base=debian/$(p); \
+        mod=Debian/Dgit/$(pm).pm; \
+        src=$${base}$(specperl)/$${mod}; \
         dst=$${base}$(globalperl)/$${mod}; \
         mkdir -p $${dst%/*}; \
         mv -f $$src $$dst; \
         perl -i -p -e 'next unless m/###substituted###/;' \
                -e 'next unless s/^# (?=unshift \@INC,)//;' \
-               -e 'die unless s{q\{\S+\}}{q{$(infraperl)}};' \
+               -e 'die unless s{q\{\S+\}}{q{$(specperl)}};' \
                 $$dst
 
 debian/tests/control: tests/enumerate-tests debian/tests/control.in
index f3d20f1b4e49a51473dfc1a3774e3abffd89a053..dcc40a78c3dc462e29667d920887714af2afe372 100644 (file)
@@ -16,6 +16,14 @@ Tests-Directory: tests/tests
 Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, chiark-utils-bin
 Restrictions: x-dgit-intree-only x-dgit-git-only
 
+Tests: gdr-diverge-nmu gdr-diverge-nmu-dgit gdr-edits gdr-import-dgit gdr-subcommands
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, chiark-utils-bin, git-debrebase, git-buildpackage, faketime
+
+Tests: gdr-newupstream-v0 gdr-viagit
+Tests-Directory: tests/tests
+Depends: chiark-utils-bin, git-debrebase, git-buildpackage, faketime
+
 Tests: gitattributes
 Tests-Directory: tests/tests
 Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, chiark-utils-bin, bsdgames, man-db, git-man
index 960d3ef033d82124268f81735f2a5b8af20756f8..b558a25934adf7c9d3bcec5bdc177f9d710d9d98 100644 (file)
@@ -1,2 +1,2 @@
 Tests-Directory: tests/tests
-Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, chiark-utils-bin
+Depends: 
diff --git a/dgit b/dgit
index 27dcf1c91e4a9288bb1693e5dc03488f08e5fcbc..ebf44de800ed399aa56644928f01461c9694d67a 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -18,6 +18,9 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+END { $? = $Debian::Dgit::ExitStatus::desired // -1; };
+use Debian::Dgit::ExitStatus;
+
 use strict;
 
 use Debian::Dgit qw(:DEFAULT :playground);
@@ -95,7 +98,7 @@ our %format_ok = map { $_=>1 } ("1.0","3.0 (native)","3.0 (quilt)");
 
 our $suite_re = '[-+.0-9a-z]+';
 our $cleanmode_re = 'dpkg-source(?:-d)?|git|git-ff|check|none';
-our $orig_f_comp_re = 'orig(?:-[-0-9a-z]+)?';
+our $orig_f_comp_re = qr{orig(?:-$extra_orig_namepart_re)?};
 our $orig_f_sig_re = '\\.(?:asc|gpg|pgp)';
 our $orig_f_tail_re = "$orig_f_comp_re\\.tar(?:\\.\\w+)?(?:$orig_f_sig_re)?";
 
@@ -114,6 +117,7 @@ our (@gpg) = qw(gpg);
 our (@sbuild) = qw(sbuild);
 our (@ssh) = 'ssh';
 our (@dgit) = qw(dgit);
+our (@git_debrebase) = qw(git-debrebase);
 our (@aptget) = qw(apt-get);
 our (@aptcache) = qw(apt-cache);
 our (@dpkgbuildpackage) = (qw(dpkg-buildpackage), @dpkg_source_ignores);
@@ -133,6 +137,7 @@ our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
                      'ssh' => \@ssh,
                      'dgit' => \@dgit,
                      'git' => \@git,
+                    'git-debrebase' => \@git_debrebase,
                      'apt-get' => \@aptget,
                      'apt-cache' => \@aptcache,
                      'dpkg-source' => \@dpkgsource,
@@ -153,6 +158,7 @@ sub parseopts_late_defaults();
 sub setup_gitattrs(;$);
 sub check_gitattrs($$);
 
+our $playground;
 our $keyid;
 
 autoflush STDOUT 1;
@@ -235,7 +241,7 @@ END {
     }
 };
 
-sub badcfg { print STDERR "$us: invalid configuration: @_\n"; exit 12; }
+sub badcfg { print STDERR "$us: invalid configuration: @_\n"; finish 12; }
 
 sub forceable_fail ($$) {
     my ($forceoptsl, $msg) = @_;
@@ -253,7 +259,7 @@ sub forceing ($) {
 
 sub no_such_package () {
     print STDERR "$us: package $package does not exist in suite $isuite\n";
-    exit 4;
+    finish 4;
 }
 
 sub deliberately ($) {
@@ -286,6 +292,32 @@ sub dgit_privdir () {
     our $dgit_privdir_made //= ensure_a_playground 'dgit';
 }
 
+sub branch_gdr_info ($$) {
+    my ($symref, $head) = @_;
+    my ($status, $msg, $current, $ffq_prev, $gdrlast) =
+       gdr_ffq_prev_branchinfo($symref);
+    return () unless $status eq 'branch';
+    $ffq_prev = git_get_ref $ffq_prev;
+    $gdrlast  = git_get_ref $gdrlast;
+    $gdrlast &&= is_fast_fwd $gdrlast, $head;
+    return ($ffq_prev, $gdrlast);
+}
+
+sub branch_is_gdr ($$) {
+    my ($symref, $head) = @_;
+    my ($ffq_prev, $gdrlast) = branch_gdr_info($symref, $head);
+    return 0 unless $ffq_prev || $gdrlast;
+    return 1;
+}
+
+sub branch_is_gdr_unstitched_ff ($$$) {
+    my ($symref, $head, $ancestor) = @_;
+    my ($ffq_prev, $gdrlast) = branch_gdr_info($symref, $head);
+    return 0 unless $ffq_prev;
+    return 0 unless is_fast_fwd $ancestor, $ffq_prev;
+    return 1;
+}
+
 #---------- remote protocol support, common ----------
 
 # remote push initiator/responder protocol:
@@ -558,7 +590,7 @@ END
 
 sub badusage {
     print STDERR "$us: @_\n", $helpmsg or die $!;
-    exit 8;
+    finish 8;
 }
 
 sub nextarg {
@@ -571,7 +603,7 @@ sub pre_help () {
 }
 sub cmd_help () {
     print $helpmsg or die $!;
-    exit 0;
+    finish 0;
 }
 
 our $td = $ENV{DGIT_TEST_DUMMY_DIR} || "DGIT_TEST_DUMMY_DIR-unset";
@@ -1683,7 +1715,7 @@ our ($dsc_distro, $dsc_hint_tag, $dsc_hint_url);
 
 sub prep_ud () {
     dgit_privdir(); # ensures that $dgit_privdir_made is based on $maindir
-    fresh_playground 'dgit/unpack';
+    $playground = fresh_playground 'dgit/unpack';
 }
 
 sub mktree_in_ud_here () {
@@ -3507,7 +3539,7 @@ sub fork_for_multisuite ($) {
                                               sub {
             @end = ();
             fetch();
-           exit 0;
+           finish 0;
        });
        # xxx collecte the ref here
 
@@ -3691,15 +3723,7 @@ sub check_not_dirty () {
 
     return if $ignoredirty;
 
-    my @cmd = (@git, qw(diff --quiet HEAD));
-    debugcmd "+",@cmd;
-    $!=0; $?=-1; system @cmd;
-    return if !$?;
-    if ($?==256) {
-       fail "working tree is dirty (does not match HEAD)";
-    } else {
-       failedcmd @cmd;
-    }
+    git_check_unmodified();
 }
 
 sub commit_admin ($) {
@@ -3708,6 +3732,15 @@ sub commit_admin ($) {
     runcmd_ordryrun_local @git, qw(commit -m), $m;
 }
 
+sub quiltify_nofix_bail ($$) {
+    my ($headinfo, $xinfo) = @_;
+    if ($quilt_mode eq 'nofix') {
+       fail "quilt fixup required but quilt mode is \`nofix'\n".
+           "HEAD commit".$headinfo." differs from tree implied by ".
+           " debian/patches".$xinfo;
+    }
+}
+
 sub commit_quilty_patch () {
     my $output = cmdoutput @git, qw(status --porcelain);
     my %adds;
@@ -3722,6 +3755,7 @@ sub commit_quilty_patch () {
        progress "nothing quilty to commit, ok.";
        return;
     }
+    quiltify_nofix_bail "", " (wanted to commit patch update)";
     my @adds = map { s/[][*?\\]/\\$&/g; $_; } sort keys %adds;
     runcmd_ordryrun_local @git, qw(add -f), @adds;
     commit_admin <<END
@@ -3882,6 +3916,8 @@ sub pseudomerge_make_commit ($$$$ $$) {
        : !length  $overwrite_version ? " --overwrite"
        : " --overwrite=".$overwrite_version;
 
+    # Contributing parent is the first parent - that makes
+    # git rev-list --first-parent DTRT.
     my $pmf = dgit_privdir()."/pseudomerge";
     open MC, ">", $pmf or die "$pmf $!";
     print MC <<END or die $!;
@@ -4210,7 +4246,14 @@ END
     my $format = getfield $dsc, 'Format';
     printdebug "format $format\n";
 
+    my $symref = git_get_symref();
     my $actualhead = git_rev_parse('HEAD');
+
+    if (branch_is_gdr_unstitched_ff($symref, $actualhead, $archive_hash)) {
+       runcmd_ordryrun_local @git_debrebase, 'stitch';
+       $actualhead = git_rev_parse('HEAD');
+    }
+
     my $dgithead = $actualhead;
     my $maintviewhead = undef;
 
@@ -4510,13 +4553,8 @@ sub cmd_clone {
 }
 
 sub branchsuite () {
-    my @cmd = (@git, qw(symbolic-ref -q HEAD));
-    my $branch = cmdoutput_errok @cmd;
-    if (!defined $branch) {
-       $?==256 or failedcmd @cmd;
-       return undef;
-    }
-    if ($branch =~ m#$lbranch_re#o) {
+    my $branch = git_get_symref();
+    if (defined $branch && $branch =~ m#$lbranch_re#o) {
        return $1;
     } else {
        return undef;
@@ -4547,7 +4585,7 @@ sub cmd_fetch {
     parseopts();
     fetchpullargs();
     my $multi_fetched = fork_for_multisuite(sub { });
-    exit 0 if $multi_fetched;
+    finish 0 if $multi_fetched;
     fetch();
 }
 
@@ -4756,7 +4794,7 @@ sub i_resp_complete {
 
     i_cleanup();
     printdebug "all done\n";
-    exit 0;
+    finish 0;
 }
 
 sub i_resp_file ($) {
@@ -5206,11 +5244,7 @@ sub quiltify ($$$$) {
            last;
        }
 
-       if ($quilt_mode eq 'nofix') {
-           fail "quilt fixup required but quilt mode is \`nofix'\n".
-               "HEAD commit $c->{Commit} differs from tree implied by ".
-               " debian/patches (tree object $oldtiptree)";
-       }
+       quiltify_nofix_bail " $c->{Commit}", " (tree object $oldtiptree)";
        if ($quilt_mode eq 'smash') {
            printdebug " search quitting smash\n";
            last;
@@ -5424,6 +5458,32 @@ END
 
     my $clogp = parsechangelog();
     my $headref = git_rev_parse('HEAD');
+    my $symref = git_get_symref();
+
+    if ($quilt_mode eq 'linear'
+       && !$fopts->{'single-debian-patch'}
+       && branch_is_gdr($symref, $headref)) {
+       # This is much faster.  It also makes patches that gdr
+       # likes better for future updates without laundering.
+       #
+       # However, it can fail in some casses where we would
+       # succeed: if there are existing patches, which correspond
+       # to a prefix of the branch, but are not in gbp/gdr
+       # format, gdr will fail (exiting status 7), but we might
+       # be able to figure out where to start linearising.  That
+       # will be slower so hopefully there's not much to do.
+       my @cmd = (@git_debrebase,
+                  qw(--noop-ok -funclean-mixed -funclean-ordering
+                     make-patches --quiet-would-amend));
+       # We tolerate soe snags that gdr wouldn't, by default.
+       if (act_local()) {
+           $!=0; $?=-1;
+           failedcmd @cmd if system @cmd and $?!=7;
+       } else {
+           dryrun_report @cmd;
+       }
+       $headref = git_rev_parse('HEAD');
+    }
 
     prep_ud();
     changedir $playground;
@@ -5587,7 +5647,7 @@ sub quilt_check_splitbrain_cache ($$) {
        if (!stat "$maindir_gitcommon/logs/refs/$splitbraincache") {
            $! == ENOENT or die $!;
            printdebug ">(no reflog)\n";
-           exit 0;
+           finish 0;
        }
        exec @cmd; die $!;
     }
@@ -5713,6 +5773,7 @@ sub quilt_fixup_multipatch ($$$) {
 
     rmtree '.pc';
 
+    rmtree 'debian'; # git checkout commitish paths does not delete!
     runcmd @git, qw(checkout -f), $headref, qw(-- debian);
     my $unapplied=git_add_write_tree();
     printdebug "fake orig tree object $unapplied\n";
@@ -5839,7 +5900,7 @@ sub quilt_fixup_editor () {
     }
     I2->error and die $!;
     close O or die $1;
-    exit 0;
+    finish 0;
 }
 
 sub maybe_apply_patches_dirtily () {
@@ -6529,7 +6590,7 @@ sub cmd_setup_new_tree {
 
 sub cmd_version {
     print "dgit version $our_version\n" or die $!;
-    exit 0;
+    finish 0;
 }
 
 our (%valopts_long, %valopts_short);
@@ -6881,7 +6942,7 @@ print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n"
     if $dryrun_level == 1;
 if (!@ARGV) {
     print STDERR $helpmsg or die $!;
-    exit 8;
+    finish 8;
 }
 $cmd = $subcommand = shift @ARGV;
 $cmd =~ y/-/_/;
@@ -6895,3 +6956,5 @@ git_slurp_config();
 my $fn = ${*::}{"cmd_$cmd"};
 $fn or badusage "unknown operation $cmd";
 $fn->();
+
+finish 0;
index 3995ceb6e8c3349ad6565dcbd6157d0c9061a8db..3e4a7182b5422978b2ebe9ff49c78baa21872d21 100755 (executable)
@@ -19,6 +19,8 @@
 #
 # 4. Run the mirror script to push changes, if necessary.
 
+END { $? = $Debian::Dgit::ExitStatus::desired // -1; };
+use Debian::Dgit::ExitStatus;
 
 use strict;
 
@@ -283,7 +285,7 @@ filter_updates();
 
 if (!@updates) {
     print Dumper(\%count), "all is well - nothing to do\n";
-    exit 0;
+    finish 0;
 }
 
 #print Dumper(\@updates);
@@ -325,5 +327,7 @@ if ($real >= 0) {
     print "testing output saved in refs/dgit-badfixuptest/\n" or die $!;
 } else {
     print STDERR "found work to do, exiting status 2\n";
-    exit 2;
+    finish 2;
 }
+
+finish 0;
diff --git a/dgit.1 b/dgit.1
index 6d46b20a4c7887158a567a89f90a82db53176e0a..4cbf10f0dbdc0fc02bb6be7be10d776cbfe2c858 100644 (file)
--- a/dgit.1
+++ b/dgit.1
@@ -185,14 +185,19 @@ archive.
 dgit push always uses the package, suite and version specified in the
 debian/changelog and the .dsc, which must agree.  If the command line
 specifies a suite then that must match too.
+
+With \fB-C\fR, performs a dgit push, additionally ensuring that no
+binary packages are uploaded.
+
+When used on a git-debrebase branch,
+dgit calls git-debrebase
+to prepare the branch
+for source package upload and push.
 .TP
 \fBdgit push-source\fR [\fIsuite\fP]
 Without \fB-C\fR, builds a source package and dgit pushes it.  Saying
 \fBdgit push-source\fR is like saying "update the source code in the
 archive to match my git HEAD, and let the autobuilders do the rest."
-
-With \fB-C\fR, performs a dgit push, additionally ensuring that no
-binary packages are uploaded.
 .TP
 \fBdgit rpush\fR \fIbuild-host\fR\fB:\fR\fIbuild-dir\fR [\fIpush args...\fR]
 Pushes the contents of the specified directory on a remote machine.
@@ -286,6 +291,15 @@ new quilt patch.  dgit cannot convert nontrivial merges, or certain
 other kinds of more exotic history.  If dgit can't find a suitable
 linearisation of your history, by default it will fail, but you can
 ask it to generate a single squashed patch instead.
+
+When used with a git-debrebase branch,
+dgit will ask git-debrebase to prepare patches.
+However,
+dgit can make patches in some situations where git-debrebase fails,
+so dgit quilt-fixup can be useful in its own right.
+To always use dgit's own patch generator
+instead of git-debrebase make-patches,
+pass --git-debrebase=true to dgit.
 .TP
 \fBdgit import-dsc\fR [\fIsub-options\fR] \fI../path/to/.dsc\fR [\fB+\fR|\fB..\fR]branch
 Import a Debian-format source package,
@@ -825,6 +839,7 @@ Specifies a single additional option to pass to
 .BR sbuild ,
 .BR ssh ,
 .BR dgit ,
+.BR git-debrebase ,
 .BR apt-get ,
 .BR apt-cache ,
 .BR gbp-pq ,
@@ -872,6 +887,7 @@ Specifies alternative programs to use instead of
 .BR gpg ,
 .BR ssh ,
 .BR dgit ,
+.BR git-debrebase ,
 .BR apt-get ,
 .BR apt-cache ,
 .BR git ,
diff --git a/git-debrebase b/git-debrebase
new file mode 100755 (executable)
index 0000000..216d7df
--- /dev/null
@@ -0,0 +1,1724 @@
+#!/usr/bin/perl -w
+# git-debrebase
+# Script helping make fast-forwarding histories while still rebasing
+# upstream deltas when working on Debian packaging
+#
+# 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
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+END { $? = $Debian::Dgit::ExitStatus::desired // -1; };
+use Debian::Dgit::GDR;
+use Debian::Dgit::ExitStatus;
+
+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);
+
+our ($opt_force, $opt_noop_ok, @opt_anchors);
+our ($opt_defaultcmd_interactive);
+
+our $us = qw(git-debrebase);
+
+sub badusage ($) {
+    my ($m) = @_;
+    print STDERR "bad usage: $m\n";
+    finish 8;
+}
+
+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_errok @cmd;
+    if (!defined $out) {
+       fail "missing required git config $k" unless $optional;
+       return ();
+    }
+    my @l = split /\0/, $out;
+    return wantarray ? @l : $l[0];
+}
+
+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');
+    $data =~ m/(?<=\n)\n/ or die "$objid ($data) ?";
+    return ($`,$');
+}
+
+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;
+our $workarea;
+
+our @git = qw(git);
+
+sub in_workarea ($) {
+    my ($sub) = @_;
+    changedir $workarea;
+    my $r = eval { $sub->(); };
+    { local $@; changedir $maindir; }
+    die $@ if $@;
+}
+
+sub fresh_workarea () {
+    $workarea = fresh_playground "$playprefix/work";
+    in_workarea sub { playtree_setup };
+}
+
+our $snags_forced = 0;
+our $snags_tripped = 0;
+our $snags_summarised = 0;
+our @deferred_updates;
+our @deferred_update_messages;
+
+sub all_snags_summarised () {
+    $snags_forced + $snags_tripped == $snags_summarised;
+}
+sub run_deferred_updates ($) {
+    my ($mrest) = @_;
+
+    confess 'dangerous internal error' unless all_snags_summarised();
+
+    my @upd_cmd = (@git, qw(update-ref --stdin -m), "debrebase: $mrest");
+    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;
+
+    print $_, "\n" foreach @deferred_update_messages;
+
+    @deferred_updates = ();
+    @deferred_update_messages = ();
+}
+
+sub get_differs ($$) {
+    my ($x,$y) = @_;
+    # This resembles 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/.
+
+    my $differs = 0;
+
+    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->(); }
+    };
+
+    $rundiff->([qw(--name-only)], [], sub {
+        $differs |= $_ eq 'debian' ? DS_DEB : D_UPS;
+    });
+
+    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;
+           }
+           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
+           }
+           $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);
+    }
+
+    printdebug sprintf "get_differs %s, %s = %#x\n", $x, $y, $differs;
+
+    return $differs;
+}
+
+sub commit_pr_info ($) {
+    my ($r) = @_;
+    return Data::Dumper->dump([$r], [qw(commit)]);
+}
+
+sub calculate_committer_authline () {
+    my $c = cmdoutput @git, qw(commit-tree --no-gpg-sign -m),
+       'DUMMY COMMIT (git-debrebase)', "HEAD:";
+    my ($h,$m) = get_commit $c;
+    $h =~ m/^committer .*$/m or confess "($h) ?";
+    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 @snag_force_opts;
+sub snag ($$;@) {
+    my ($tag,$msg) = @_; # ignores extra args, for benefit of keycommits
+    if (grep { $_ eq $tag } @snag_force_opts) {
+       $snags_forced++;
+       print STDERR "git-debrebase: snag ignored (-f$tag): $msg\n";
+    } else {
+       $snags_tripped++;
+       print STDERR "git-debrebase: snag detected (-f$tag): $msg\n";
+    }
+}
+
+# Important: all mainline code must call snags_maybe_bail after
+# any point where snag might be called, but before making changes
+# (eg before any call to run_deferred_updates).  snags_maybe_bail
+# may be called more than once if necessary (but this is not ideal
+# because then the messages about number of snags may be confusing).
+sub snags_maybe_bail () {
+    return if all_snags_summarised();
+    if ($snags_forced) {
+       printf STDERR
+           "%s: snags: %d overriden by individual -f options\n",
+           $us, $snags_forced;
+    }
+    if ($snags_tripped) {
+       if ($opt_force) {
+           printf STDERR
+               "%s: snags: %d overriden by global --force\n",
+               $us, $snags_tripped;
+       } else {
+           fail sprintf
+  "%s: snags: %d blockers (you could -f<tag>, or --force)",
+               $us, $snags_tripped;
+       }
+    }
+    $snags_summarised = $snags_forced + $snags_tripped;
+}
+sub any_snags () {
+    return $snags_forced || $snags_tripped;
+}
+
+# classify returns an info hash like this
+#   CommitId => $objid
+#   Hdr => # commit headers, including 1 final newline
+#   Msg => # commit message (so one newline is dropped)
+#   Tree => $treeobjid
+#   Type => (see below)
+#   Parents = [ {
+#       Ix => $index # ie 0, 1, 2, ...
+#       CommitId
+#       Differs => return value from get_differs
+#       IsOrigin
+#       IsDggitImport => 'orig' 'tarball' 'unpatched' 'package' (as from dgit)
+#     } ...]
+#   NewMsg => # commit message, but with any [dgit import ...] edited
+#             # to say "[was: ...]"
+#
+# Types:
+#   Packaging
+#   Changelog
+#   Upstream
+#   AddPatches
+#   Mixed
+#
+#   Pseudomerge
+#     has additional entres in classification result
+#       Overwritten = [ subset of Parents ]
+#       Contributor = $the_remaining_Parent
+#
+#   DgitImportUnpatched
+#     has additional entry in classification result
+#       OrigParents = [ subset of Parents ]
+#
+#   Anchor
+#     has additional entry in classification result
+#       OrigParents = [ subset of Parents ]  # singleton list
+#
+#   TreatAsAnchor
+#
+#   BreakwaterStart
+#
+#   Unknown
+#     has additional entry in classification result
+#       Why => "prose"
+
+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 $r = {
+       CommitId => $objid,
+       Hdr => $h,
+       Msg => $m,
+        Tree => $t,
+       Parents => $p_ref,
+    };
+
+    foreach my $ph (@ph) {
+       push @$p_ref, {
+            Ix => scalar @$p_ref,
+            CommitId => $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";
+
+    my $classify = sub {
+       my ($type, @rest) = @_;
+       $r = { %$r, Type => $type, @rest };
+       if ($debuglevel) {
+           printdebug " = $type ".(dd $r)."\n";
+       }
+       return $r;
+    };
+    my $unknown = sub {
+       my ($why) = @_;
+       $r = { %$r, Type => qw(Unknown), Why => $why };
+       printdebug " ** Unknown\n";
+       return $r;
+    };
+
+    if (grep { $_ eq $objid } @opt_anchors) {
+       return $classify->('TreatAsAnchor');
+    }
+
+    my @identical = grep { !$_->{Differs} } @p;
+    my ($stype, $series) = git_cat_file "$t:debian/patches/series";
+    my $haspatches = $stype ne 'missing' && $series =~ m/^\s*[^#\n\t ]/m;
+
+    if ($r->{Msg} =~ m{^\[git-debrebase anchor.*\]$}m) {
+       # multi-orig upstreams are represented with an anchor merge
+       # from a single upstream commit which combines the orig tarballs
+
+       # Every anchor tagged this way must be a merge.
+       # We are relying on the
+       #     [git-debrebase anchor: ...]
+       # commit message annotation in "declare" anchor merges (which
+       # do not have any upstream changes), to distinguish those
+       # anchor merges from ordinary pseudomerges (which we might
+       # just try to strip).
+       #
+       # However, the user is going to be doing git-rebase a lot.  We
+       # really don't want them to rewrite an anchor commit.
+       # git-rebase trips up on merges, so that is a useful safety
+       # catch.
+       #
+       # BreakwaterStart commits are also anchors in the terminology
+       # of git-debrebase(5), but they are untagged (and always
+       # manually generated).
+       #
+       # We cannot not tolerate any tagged linear commit (ie,
+       # BreakwaterStart commits tagged `[anchor:') because such a
+       # thing could result from an erroneous linearising raw git
+       # rebase of a merge anchor.  That would represent a corruption
+       # of the branch. and we want to detect and reject the results
+       # of such corruption before it makes it out anywhere.  If we
+       # reject it here then we avoid making the pseudomerge which
+       # would be needed to push it.
+
+       my $badanchor = sub { $unknown->("git-debrebase \`anchor' but @_"); };
+       @p == 2 or return $badanchor->("has other than two parents");
+       $haspatches and return $badanchor->("contains debian/patches");
+
+       # How to decide about l/r ordering of anchors ?  git
+       # --topo-order prefers to expand 2nd parent first.  There's
+       # already an easy rune to look for debian/ history anyway (git log
+       # debian/) so debian breakwater branch should be 1st parent; that
+       # way also there's also an easy rune to look for the upstream
+       # patches (--topo-order).
+
+       # Also this makes --first-parent be slightly more likely to
+       # be useful - it makes it provide a linearised breakwater history.
+
+       # Of course one can say somthing like
+       #  gitk -- ':/' ':!/debian'
+       # to get _just_ the commits touching upstream files, and by
+       # the TREESAME logic in git-rev-list this will leave the
+       # breakwater into upstream at the first anchor.  But that
+       # doesn't report debian/ changes at all.
+
+       # Other observations about gitk: by default, gitk seems to
+       # produce output in a different order to git-rev-list.  I
+       # can't seem to find this documented anywhere.  gitk
+       # --date-order DTRT.  But, gitk always seems to put the
+       # parents from left to right, in order, so it's easy to see
+       # which way round a pseudomerge is.
+
+       $p[0]{IsOrigin} and $badanchor->("is an origin commit");
+       $p[1]{Differs} & ~DS_DEB and
+           $badanchor->("upstream files differ from left parent");
+       $p[0]{Differs} & ~D_UPS and
+           $badanchor->("debian/ differs from right parent");
+
+       return $classify->(qw(Anchor),
+                          OrigParents => [ $p[1] ]);
+    }
+
+    if (@p == 1) {
+       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 & DS_DEB and !($d & ~DS_DEB)) {
+           my ($ty,$dummy) = git_cat_file "$p[0]{CommitId}:debian";
+           if ($ty eq 'tree') {
+               if ($d == D_DEB_CLOG) {
+                   return $classify->(qw(Changelog));
+               } else {
+                   return $classify->(qw(Packaging));
+               }
+           } elsif ($ty eq 'missing') {
+               return $classify->(qw(BreakwaterStart));
+           } else {
+               return $unknown->("parent's debian is not a directory");
+           }
+       } elsif ($d == D_UPS) {
+           return $classify->(qw(Upstream));
+       } 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");
+       } else {
+           confess "internal error $objid ?";
+       }
+    }
+    if (!@p) {
+       return $unknown->("origin commit");
+    }
+
+    if (@p == 2 && @identical == 1) {
+       my @overwritten = grep { $_->{Differs} } @p;
+       confess "internal error $objid ?" unless @overwritten==1;
+       return $classify->(qw(Pseudomerge),
+                          Overwritten => [ $overwritten[0] ],
+                          Contributor => $identical[0]);
+    }
+    if (@p == 2 && @identical == 2) {
+       my $get_t = sub {
+           my ($ph,$pm) = get_commit $_[0]{CommitId};
+           $ph =~ m/^committer .* (\d+) [-+]\d+$/m or die "$_->{CommitId} ?";
+           $1;
+       };
+       my @bytime = @p;
+       my $order = $get_t->($bytime[0]) <=> $get_t->($bytime[1]);
+       if ($order > 0) { # newer first
+       } elsif ($order < 0) {
+           @bytime = reverse @bytime;
+       } else {
+           # same age, default to order made by -s ours
+           # that is, commit was made by someone who preferred L
+       }
+       return $classify->(qw(Pseudomerge),
+                          SubType => qw(Ambiguous),
+                          Contributor => $bytime[0],
+                          Overwritten => [ $bytime[1] ]);
+    }
+    foreach my $p (@p) {
+       my ($p_h, $p_m) = get_commit $p->{CommitId};
+       $p->{IsOrigin} = $p_h !~ m/^parent \w+$/m;
+       ($p->{IsDgitImport},) = $p_m =~ m/^\[dgit import ([0-9a-z]+) .*\]$/m;
+    }
+    my @orig_ps = grep { ($_->{IsDgitImport}//'X') eq 'orig' } @p;
+    my $m2 = $r->{Msg};
+    if (!(grep { !$_->{IsOrigin} } @p) and
+       (@orig_ps >= @p - 1) and
+       $m2 =~ s{^\[(dgit import unpatched .*)\]$}{[was: $1]}m) {
+       $r->{NewMsg} = $m2;
+       return $classify->(qw(DgitImportUnpatched),
+                          OrigParents => \@orig_ps);
+    }
+
+    return $unknown->("complex merge");
+}
+
+sub keycommits ($;$$$$) {
+    my ($head, $furniture, $unclean, $trouble, $fatal) = @_;
+    # => ($anchor, $breakwater)
+
+    # $unclean->("unclean-$tagsfx", $msg, $cl)
+    # $furniture->("unclean-$tagsfx", $msg, $cl)
+    # $dgitimport->("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
+    #   interchange branch (pseudomerge, d/patches, changelog)
+    # $trouble is for things whnich prevent the return of
+    #   anchor and breakwater information; if that is ignored,
+    #   then keycommits returns (undef, undef) instead.
+    # $fatal is for unprocessable commits, and should normally cause
+    #    a failure.  If ignored, agaion, (undef, undef) is returned.
+    #
+    # If a callback is undef, fail is called instead.
+    # If a callback is defined but false, the situation is ignored.
+    # Callbacks may say:
+    #   no warnings qw(exiting); last;
+    # if the answer is no longer wanted.
+
+    my ($anchor, $breakwater);
+    my $clogonly;
+    my $cl;
+    $fatal //= sub { fail $_[2]; };
+    my $x = sub {
+       my ($cb, $tagsfx, $why) = @_;
+       my $m = "branch needs laundering (run git-debrebase): $why";
+       fail $m unless defined $cb;
+       return unless $cb;
+       $cb->("unclean-$tagsfx", $why, $cl);
+    };
+    for (;;) {
+       $cl = classify $head;
+       my $ty = $cl->{Type};
+       if ($ty eq 'Packaging') {
+           $breakwater //= $clogonly;
+           $breakwater //= $head;
+       } elsif ($ty eq 'Changelog') {
+           # this is going to count as the tip of the breakwater
+           # only if it has no upstream stuff before it
+           $clogonly //= $head;
+       } elsif ($ty eq 'Anchor' or
+                $ty eq 'TreatAsAnchor' or
+                $ty eq 'BreakwaterStart') {
+           $anchor = $head;
+           $breakwater //= $clogonly;
+           $breakwater //= $head;
+           last;
+       } elsif ($ty eq 'Upstream') {
+           $x->($unclean, 'ordering',
+ "packaging change ($breakwater) follows upstream change (eg $head)")
+               if defined $breakwater;
+           $clogonly = undef;
+           $breakwater = undef;
+       } elsif ($ty eq 'Mixed') {
+           $x->($unclean, 'mixed',
+                "found mixed upstream/packaging commit ($head)");
+           $clogonly = undef;
+           $breakwater = undef;
+       } elsif ($ty eq 'Pseudomerge' or
+                $ty eq 'AddPatches') {
+           $x->($furniture, (lc $ty),
+                "found interchange bureaucracy commit ($ty, $head)");
+       } elsif ($ty eq 'DgitImportUnpatched') {
+           $x->($trouble, 'dgitimport',
+                "found dgit dsc import ($head)");
+           return (undef,undef);
+       } else {
+           $x->($fatal, 'unprocessable',
+                "found unprocessable commit, cannot cope: $head; $cl->{Why}"
+               );
+           return (undef,undef);
+       }
+       $head = $cl->{Parents}[0]{CommitId};
+    }
+    return ($anchor, $breakwater);
+}
+
+sub walk ($;$$);
+sub walk ($;$$) {
+    my ($input,
+       $nogenerate,$report) = @_;
+    # => ($tip, $breakwater_tip, $last_anchor)
+    # (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
+    my (@brw_cl, @upp_cl, @processed);
+    my %found;
+    my $upp_limit;
+    my @pseudomerges;
+
+    my $cl;
+    my $xmsg = sub {
+       my ($prose, $info) = @_;
+       my $ms = $cl->{Msg};
+       chomp $ms;
+       $info //= '';
+       $ms .= "\n\n[git-debrebase$info: $prose]\n";
+       return (Msg => $ms);
+    };
+    my $rewrite_from_here = sub {
+       my ($cl) = @_;
+       my $sp_cl = { SpecialMethod => 'StartRewrite' };
+       push @$cl, $sp_cl;
+       push @processed, $sp_cl;
+    };
+    my $cur = $input;
+
+    my $prdelim = "";
+    my $prprdelim = sub { print $report $prdelim if $report; $prdelim=""; };
+
+    my $prline = sub {
+       return unless $report;
+       print $report $prdelim, @_;
+       $prdelim = "\n";
+    };
+
+    my $bomb = sub { # usage: return $bomb->();
+       print $report " Unprocessable" if $report;
+       print $report " ($cl->{Why})" if $report && defined $cl->{Why};
+       $prprdelim->();
+       if ($nogenerate) {
+           return (undef,undef);
+       }
+       die "commit $cur: Cannot cope with this commit (d.".
+           (join ' ', map { sprintf "%#x", $_->{Differs} }
+            @{ $cl->{Parents} }).
+           (defined $cl->{Why} ? "; $cl->{Why}": '').
+                ")";
+    };
+
+    my $build;
+    my $breakwater;
+
+    my $build_start = sub {
+       my ($msg, $parent) = @_;
+       $prline->(" $msg");
+       $build = $parent;
+       no warnings qw(exiting); last;
+    };
+
+    my $last_anchor;
+
+    for (;;) {
+       $cl = classify $cur;
+       my $ty = $cl->{Type};
+       my $st = $cl->{SubType};
+       $prline->("$cl->{CommitId} $cl->{Type}");
+       $found{$ty. ( defined($st) ? "-$st" : '' )}++;
+       push @processed, $cl;
+       my $p0 = @{ $cl->{Parents} }==1 ? $cl->{Parents}[0]{CommitId} : undef;
+       if ($ty eq 'AddPatches') {
+           $cur = $p0;
+           $rewrite_from_here->(\@upp_cl);
+           next;
+       } elsif ($ty eq 'Packaging' or $ty eq 'Changelog') {
+           push @brw_cl, $cl;
+           $cur = $p0;
+           next;
+       } elsif ($ty eq 'BreakwaterStart') {
+            $last_anchor = $cur;
+           $build_start->('FirstPackaging', $cur);
+       } elsif ($ty eq 'Upstream') {
+           push @upp_cl, $cl;
+           $cur = $p0;
+           next;
+       } elsif ($ty eq 'Mixed') {
+           my $queue = sub {
+               my ($q, $wh) = @_;
+               my $cls = { %$cl, $xmsg->("split mixed commit: $wh part") };
+               push @$q, $cls;
+           };
+           $queue->(\@brw_cl, "debian");
+           $queue->(\@upp_cl, "upstream");
+           $rewrite_from_here->(\@brw_cl);
+           $cur = $p0;
+           next;
+       } elsif ($ty eq 'Pseudomerge') {
+           my $contrib = $cl->{Contributor}{CommitId};
+           print $report " Contributor=$contrib" if $report;
+           push @pseudomerges, $cl;
+           $rewrite_from_here->(\@upp_cl);
+           $cur = $contrib;
+           next;
+       } elsif ($ty eq 'Anchor' or $ty eq 'TreatAsAnchor') {
+            $last_anchor = $cur;
+           $build_start->("Anchor", $cur);
+       } elsif ($ty eq 'DgitImportUnpatched') {
+           my $pm = $pseudomerges[-1];
+           if (defined $pm) {
+               # To an extent, this is heuristic.  Imports don't have
+               # a useful history of the debian/ branch.  We assume
+               # that the first pseudomerge after an import has a
+               # useful history of debian/, and ignore the histories
+               # from later pseudomerges.  Often the first pseudomerge
+               # will be the dgit import of the upload to the actual
+               # 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->{CommitId}, (scalar @$ovwrs)
+                   if $report;
+               if (@$ovwrs != 1) {
+                    printdebug "*** WALK BOMB DgitImportUnpatched\n";
+                   return $bomb->();
+               }
+               my $ovwr = $ovwrs->[0]{CommitId};
+               printf $report " Overwr=%s", $ovwr if $report;
+               # This import has a tree which is just like a
+               # breakwater tree, but it has the wrong history.  It
+               # ought to have the previous breakwater (which the
+               # pseudomerge overwrote) as an ancestor.  That will
+               # make the history of the debian/ files correct.  As
+               # for the upstream version: either it's the same as
+               # was ovewritten (ie, same as the previous
+               # breakwater), in which case that history is precisely
+               # right; or, otherwise, it was a non-gitish upload of a
+               # new upstream version.  We can tell these apart by
+               # looking at the tree of the supposed upstream.
+               push @brw_cl, {
+                   %$cl,
+                   SpecialMethod => 'DgitImportDebianUpdate',
+                    $xmsg->("convert dgit import: debian changes")
+               }, {
+                   %$cl,
+                   SpecialMethod => 'DgitImportUpstreamUpdate',
+                    $xmsg->("convert dgit import: upstream update",
+                           " anchor")
+               };
+               $prline->(" Import");
+               $rewrite_from_here->(\@brw_cl);
+               $upp_limit //= $#upp_cl; # further, deeper, patches discarded
+               $cur = $ovwr;
+               next;
+           } else {
+               # Everything is from this import.  This kind of import
+               # is already in valid breakwater format, with the
+               # patches as commits.
+               printf $report " NoPM" if $report;
+               # last thing we processed will have been the first patch,
+               # if there is one; which is fine, so no need to rewrite
+               # on account of this import
+               $build_start->("ImportOrigin", $cur);
+           }
+           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
+
+    fresh_workarea();
+
+    my $rewriting = 0;
+
+    my $read_tree_debian = sub {
+       my ($treeish) = @_;
+       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)) {
+           if (!ref $cl) {
+               $current_method = $cl;
+               next;
+           }
+           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') {
+               $read_tree_upstream->($cltree);
+           } elsif ($method eq 'StartRewrite') {
+               $rewriting = 1;
+               next;
+           } elsif ($method eq 'RecordBreakwaterTip') {
+               $breakwater = $build;
+               next;
+           } elsif ($method eq 'DgitImportDebianUpdate') {
+               $read_tree_debian->($cltree);
+           } 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 ?";
+           }
+           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}{}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;
+            if (grep { $method eq $_ } qw(DgitImportUpstreamUpdate)) {
+                $last_anchor = $cur;
+            }
+       }
+    };
+
+    my $final_check = get_differs $build, $input;
+    die sprintf "internal error %#x %s %s", $final_check, $build, $input
+       if $final_check & ~D_PAT_ADD;
+
+    my @r = ($build, $breakwater, $last_anchor);
+    printdebug "*** WALK RETURN @r\n";
+    return @r
+}
+
+sub get_head () {
+    git_check_unmodified();
+    return git_rev_parse qw(HEAD);
+}
+
+sub update_head ($$$) {
+    my ($old, $new, $mrest) = @_;
+    push @deferred_updates, "update HEAD $new $old";
+    run_deferred_updates $mrest;
+}
+
+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 do_launder_head ($) {
+    my ($reflogmsg) = @_;
+    my $old = get_head();
+    record_ffq_auto();
+    my ($tip,$breakwater) = walk $old;
+    snags_maybe_bail();
+    update_head_postlaunder $old, $tip, $reflogmsg;
+    return ($tip,$breakwater);
+}
+
+sub cmd_launder_v0 () {
+    badusage "no arguments to launder-v0 allowed" if @ARGV;
+    my $old = get_head();
+    my ($tip,$breakwater,$last_anchor) = walk $old;
+    update_head_postlaunder $old, $tip, 'launder';
+    printf "# breakwater tip\n%s\n", $breakwater;
+    printf "# working tip\n%s\n", $tip;
+    printf "# last anchor\n%s\n", $last_anchor;
+}
+
+sub defaultcmd_rebase () {
+    push @ARGV, @{ $opt_defaultcmd_interactive // [] };
+    my ($tip,$breakwater) = do_launder_head 'launder for rebase';
+    runcmd @git, qw(rebase), @ARGV, $breakwater if @ARGV;
+}
+
+sub cmd_analyse () {
+    die if ($ARGV[0]//'') =~ m/^-/;
+    badusage "too many arguments to analyse" if @ARGV>1;
+    my ($old) = @ARGV;
+    if (defined $old) {
+       $old = git_rev_parse $old;
+    } else {
+       $old = git_rev_parse 'HEAD';
+    }
+    my ($dummy,$breakwater) = walk $old, 1,*STDOUT;
+    STDOUT->error and die $!;
+}
+
+sub ffq_prev_branchinfo () {
+    my $current = git_get_symref();
+    return gdr_ffq_prev_branchinfo($current);
+}
+
+sub record_ffq_prev_deferred () {
+    # => ('status', "message")
+    # 'status' may be
+    #    deferred          message is undef
+    #    exists
+    #    detached
+    #    weird-symref
+    #    notbranch
+    # if not ff from some branch we should be ff from, is an snag
+    # if "deferred", will have added something about that to
+    #   @deferred_update_messages, and also maybe printed (already)
+    #   some messages about ff checks
+    my ($status, $message, $current, $ffq_prev, $gdrlast)
+       = 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;
+
+    printdebug "ffq check_specs @check_specs\n";
+
+    my $check = sub {
+       my ($lrref, $desc) = @_;
+       printdebug "ffq might check $lrref ($desc)\n";
+       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 length $lrval;
+
+       if (is_fast_fwd $lrval, $currentval) {
+           print "OK, you are ahead of $lrref\n" or die $!;
+           $checked{$lrref} = 1;
+       } elsif (is_fast_fwd $currentval, $lrval) {
+           $checked{$lrref} = -1;
+           snag 'behind', "you are behind $lrref, divergence risk";
+       } else {
+           $checked{$lrref} = -1;
+           snag 'diverged', "you have diverged from $lrref";
+       }
+    };
+
+    my $merge = cfg "branch.$branch.merge",1;
+    if (defined $merge and $merge =~ m{^refs/heads/}) {
+       my $rhs = $';
+       printdebug "ffq merge $rhs\n";
+       my $check_remote = sub {
+           my ($remote, $desc) = @_;
+           printdebug "ffq check_remote ".($remote//'undef')." $desc\n";
+           return unless defined $remote;
+           $check->("refs/remotes/$remote/$rhs", $desc);
+       };
+       $check_remote->((scalar cfg "branch.$branch.remote",1),
+                       'remote fetch/merge branch');
+       $check_remote->((scalar cfg "branch.$branch.pushRemote",1) //
+                       (scalar cfg "branch.$branch.pushDefault",1),
+                       'remote push branch');
+    }
+    if ($branch =~ m{^dgit/}) {
+       $check->("refs/remotes/dgit/$branch", 'remote dgit branch');
+    } elsif ($branch =~ m{^master$}) {
+       $check->("refs/remotes/dgit/dgit/sid", 'remote dgit branch for sid');
+    }
+
+    snags_maybe_bail();
+
+    push @deferred_updates, "update $ffq_prev $currentval $git_null_obj";
+    push @deferred_updates, "delete $gdrlast";
+    push @deferred_update_messages, "Recorded current head for preservation";
+    return ('deferred', undef);
+}
+
+sub record_ffq_auto () {
+    my ($status, $message) = record_ffq_prev_deferred();
+    if ($status eq 'deferred' || $status eq 'exists') {
+    } else {
+       snag $status, "could not record ffq-prev: $message";
+       snags_maybe_bail();
+    }
+}
+
+sub ffq_prev_info () {
+    # => ($ffq_prev, $gdrlast, $ffq_prev_commitish)
+    my ($status, $message, $current, $ffq_prev, $gdrlast)
+       = ffq_prev_branchinfo();
+    if ($status ne 'branch') {
+       snag $status, "could not check ffq-prev: $message";
+       snags_maybe_bail();
+    }
+    my $ffq_prev_commitish = $ffq_prev && git_get_ref $ffq_prev;
+    return ($ffq_prev, $gdrlast, $ffq_prev_commitish);
+}
+
+sub stitch ($$$$$) {
+    my ($old_head, $ffq_prev, $gdrlast, $ffq_prev_commitish, $prose) = @_;
+
+    push @deferred_updates, "delete $ffq_prev $ffq_prev_commitish";
+
+    if (is_fast_fwd $old_head, $ffq_prev_commitish) {
+       my $differs = get_differs $old_head, $ffq_prev_commitish;
+       unless ($differs & ~D_PAT_ADD) {
+           # 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";
+           update_head_checkout $old_head, $ffq_prev_commitish,
+               "stitch (fast forward)";
+           return;
+       }
+    }
+    fresh_workarea();
+    # We make pseudomerges with L as the contributing parent.
+    # This makes git rev-list --first-parent work properly.
+    my $new_head = make_commit [ $old_head, $ffq_prev ], [
+       'Declare fast forward / record previous work',
+        "[git-debrebase pseudomerge: $prose]",
+    ];
+    push @deferred_updates, "update $gdrlast $new_head $git_null_obj";
+    update_head $old_head, $new_head, "stitch: $prose";
+}
+
+sub do_stitch ($;$) {
+    my ($prose, $unclean) = @_;
+
+    my ($ffq_prev, $gdrlast, $ffq_prev_commitish) = ffq_prev_info();
+    if (!$ffq_prev_commitish) {
+       fail "No ffq-prev to stitch." unless $opt_noop_ok;
+       return;
+    }
+    my $dangling_head = get_head();
+
+    keycommits $dangling_head, $unclean,$unclean,$unclean;
+    snags_maybe_bail();
+
+    stitch($dangling_head, $ffq_prev, $gdrlast, $ffq_prev_commitish, $prose);
+}
+
+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 >= 1;
+
+    # 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 = shift @ARGV;
+    if (!defined $new_upstream) {
+       my @tried;
+       # todo: at some point maybe use git-deborig to do this
+       foreach my $tagpfx ('', 'v', 'upstream/') {
+           my $tag = $tagpfx.(dep14_version_mangle $new_upstream_version);
+           $new_upstream = git_get_ref "refs/tags/$tag";
+           last if length $new_upstream;
+           push @tried, $tag;
+       }
+       if (!length $new_upstream) {
+           fail "Could not determine appropriate upstream commitish.\n".
+               " (Tried these tags: @tried)\n".
+               " Check version, and specify upstream commitish explicitly.";
+       }
+    }
+    $new_upstream = git_rev_parse $new_upstream;
+
+    record_ffq_auto();
+
+    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_anchor) = walk $old_head;
+
+    my $old_bw_cl = classify $old_bw;
+    my $old_anchor_cl = classify $old_anchor;
+    my $old_upstream;
+    if (!$old_anchor_cl->{OrigParents}) {
+       snag 'anchor-treated',
+           'old anchor is recognised due to --anchor, cannot check upstream';
+    } else {
+       $old_upstream = parsecommit
+           $old_anchor_cl->{OrigParents}[0]{CommitId};
+       $piece->('', Old => $old_upstream->{CommitId});
+    }
+
+    if ($old_upstream && $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 $old_n_parents = scalar @{ $old_upstream->{Parents} };
+           if (@oldpieces != $old_n_parents) {
+               snag 'upstream-confusing', sprintf
+                   "previous upstream combine %s".
+                   " mentions %d pieces (each implying one orig commit)".
+                   " but has %d parents",
+                   $old_upstream->{CommitId},
+                   (scalar @oldpieces),
+                   $old_n_parents;
+           } elsif ($oldpieces[0] ne '.') {
+               snag 'upstream-confusing', sprintf
+                   "previous upstream combine %s".
+                   " first piece is not \`.'",
+                   $oldpieces[0];
+           } else {
+               $oldpieces[0] = '';
+               foreach my $i (0..$#oldpieces) {
+                   my $n = $oldpieces[$i];
+                   $piece->($n, Old => $old_upstream->{CommitId}.'^'.($i+1));
+               }
+           }
+       } else {
+           snag 'upstream-confusing',
+               "previous upstream $old_upstream->{CommitId} is from".
+               " git-debrebase but not an \`upstream-combine' commit";
+       }
+    }
+
+    foreach my $pc (values %pieces) {
+       if (!$old_upstream) {
+           # we have complained already
+       } elsif (!$pc->{Old}) {
+           snag 'upstream-new-piece',
+               "introducing upstream piece \`$pc->{Name}'";
+       } elsif (!$pc->{New}) {
+           snag 'upstream-rm-piece',
+               "dropping upstream piece \`$pc->{Name}'";
+       } elsif (!is_fast_fwd $pc->{Old}, $pc->{New}) {
+           snag 'upstream-not-ff',
+               "not fast forward: $pc->{Name} $pc->{Old}..$pc->{New}";
+       }
+    }
+
+    printdebug "%pieces = ", (dd \%pieces), "\n";
+    printdebug "\@newpieces = ", (dd \@newpieces), "\n";
+
+    snags_maybe_bail();
+
+    my $new_bw;
+
+    fresh_workarea();
+    in_workarea sub {
+       my @upstream_merge_parents;
+
+       if (!any_snags()) {
+           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 anchor merge.
+
+       read_tree_subdir 'debian', "$old_bw:debian";
+
+       # index now contains the anchor merge contents
+        $new_bw = make_commit [ $old_bw, $new_upstream ],
+            [ "Update to upstream $new_upstream_version",
+ "[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 $!;
+       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_deferred();
+    if ($status eq 'exists' && $opt_noop_ok) {
+       print "Previous head already recorded\n" or die $!;
+    } elsif ($status eq 'deferred') {
+       run_deferred_updates 'record-ffq-prev';
+    } else {
+       fail "Could not preserve: $msg";
+    }
+}
+
+sub cmd_anchor () {
+    badusage "no arguments allowed" if @ARGV;
+    my ($anchor, $bw) = keycommits +(git_rev_parse 'HEAD'), 0,0;
+    print "$bw\n" or die $!;
+}
+
+sub cmd_breakwater () {
+    badusage "no arguments allowed" if @ARGV;
+    my ($anchor, $bw) = keycommits +(git_rev_parse 'HEAD'), 0,0;
+    print "$bw\n" or die $!;
+}
+
+sub cmd_status () {
+    badusage "no arguments allowed" if @ARGV;
+
+    # todo: gdr status should print divergence info
+    # todo: gdr status should print upstream component(s) info
+    # todo: gdr should leave/maintain some refs with this kind of info ?
+
+    my $oldest = [ 0 ];
+    my $newest;
+    my $note = sub {
+       my ($badness, $ourmsg, $snagname, $kcmsg, $cl) = @_;
+       if ($oldest->[0] < $badness) {
+           $oldest = $newest = undef;
+       }
+       $oldest = \@_; # we're walking backwards
+       $newest //= \@_;
+    };
+    my ($anchor, $bw) = keycommits +(git_rev_parse 'HEAD'),
+       sub { $note->(1, 'branch contains furniture (not laundered)', @_); },
+       sub { $note->(2, 'branch is unlaundered', @_); },
+       sub { $note->(3, 'branch needs laundering', @_); },
+       sub { $note->(4, 'branch not in git-debrebase form', @_); };
+
+    my $prcommitinfo = sub {
+       my ($cid) = @_;
+       flush STDOUT or die $!;
+       runcmd @git, qw(--no-pager log -n1),
+           '--pretty=format:    %h %s%n',
+           $cid;
+    };
+
+    print "current branch contents, in git-debrebase terms:\n";
+    if (!$oldest->[0]) {
+       print "  branch is laundered\n";
+    } else {
+       print "  $oldest->[1]\n";
+       my $printed = '';
+       foreach my $info ($oldest, $newest) {
+           my $cid = $info->[4]{CommitId};
+           next if $cid eq $printed;
+           $printed = $cid;
+           print "  $info->[3]\n";
+           $prcommitinfo->($cid);
+       }
+    }
+
+    my $prab = sub {
+       my ($cid, $what) = @_;
+       if (!defined $cid) {
+           print "  $what is not well-defined\n";
+       } else {
+           print "  $what\n";
+           $prcommitinfo->($cid);
+       }
+    };
+    print "key git-debrebase commits:\n";
+    $prab->($anchor, 'anchor');
+    $prab->($bw, 'breakwater');
+
+    my ($ffqstatus, $ffq_msg, $current, $ffq_prev, $gdrlast) =
+       ffq_prev_branchinfo();
+
+    print "branch and ref status, in git-debrebase terms:\n";
+    if ($ffq_msg) {
+       print "  $ffq_msg\n";
+    } else {
+       $ffq_prev = git_get_ref $ffq_prev;
+       $gdrlast = git_get_ref $gdrlast;
+       if ($ffq_prev) {
+           print "  unstitched; previous tip was:\n";
+           $prcommitinfo->($ffq_prev);
+       } elsif (!$gdrlast) {
+           print "  stitched? (no record of git-debrebase work)\n";
+       } elsif (is_fast_fwd $gdrlast, 'HEAD') {
+           print "  stitched\n";
+       } else {
+           print "  not git-debrebase (diverged since last stitch)\n"
+       }
+    }
+}
+
+sub cmd_stitch () {
+    my $prose = 'stitch';
+    GetOptions('prose=s', \$prose) or die badusage("bad options to stitch");
+    badusage "no arguments allowed" if @ARGV;
+    do_stitch $prose, 0;
+}
+sub cmd_prepush () { cmd_stitch(); }
+
+sub cmd_quick () {
+    badusage "no arguments allowed" if @ARGV;
+    do_launder_head 'launder for git-debrebase quick';
+    do_stitch 'quick';
+}
+
+sub cmd_conclude () {
+    my ($ffq_prev, $gdrlast, $ffq_prev_commitish) = ffq_prev_info();
+    if (!$ffq_prev_commitish) {
+       fail "No ongoing git-debrebase session." unless $opt_noop_ok;
+       return;
+    }
+    my $dangling_head = get_head();
+    
+    badusage "no arguments allowed" if @ARGV;
+    do_launder_head 'launder for git-debrebase quick';
+    do_stitch 'quick';
+}
+
+sub make_patches_staged ($) {
+    my ($head) = @_;
+    # Produces the patches that would result from $head if it were
+    # laundered.
+    my ($secret_head, $secret_bw, $last_anchor) = walk $head;
+    fresh_workarea();
+    in_workarea sub {
+       runcmd @git, qw(checkout -q -b bw), $secret_bw;
+       runcmd @git, qw(checkout -q -b patch-queue/bw), $secret_head;
+       runcmd qw(gbp pq export);
+       runcmd @git, qw(add debian/patches);
+    };
+}
+
+sub make_patches ($) {
+    my ($head) = @_;
+    keycommits $head, 0, \&snag;
+    make_patches_staged $head;
+    my $out;
+    in_workarea sub {
+       my $ptree = cmdoutput @git, qw(write-tree --prefix=debian/patches/);
+       runcmd @git, qw(read-tree), $head;
+       read_tree_subdir 'debian/patches', $ptree;
+       $out = make_commit [$head], [
+            'Commit patch queue (exported by git-debrebase)',
+            '[git-debrebase: export and commit patches]',
+        ];
+    };
+    return $out;
+}
+
+sub cmd_make_patches () {
+    my $opt_quiet_would_amend;
+    GetOptions('quiet-would-amend!', \$opt_quiet_would_amend)
+       or die badusage("bad options to make-patches");
+    badusage "no arguments allowed" if @ARGV;
+    my $old_head = get_head();
+    my $new = make_patches $old_head;
+    my $d = get_differs $old_head, $new;
+    if ($d == 0) {
+       fail "No (more) patches to export." unless $opt_noop_ok;
+       return;
+    } elsif ($d == D_PAT_ADD) {
+       snags_maybe_bail();
+       update_head_checkout $old_head, $new, 'make-patches';
+    } else {
+       print STDERR failmsg
+           "Patch export produced patch amendments".
+           " (abandoned output commit $new).".
+           "  Try laundering first."
+           unless $opt_quiet_would_amend;
+       finish 7;
+    }
+}
+
+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) {
+       snag '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) {
+           snag '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') {
+       snag 'upstream-has-debian',
+           "upstream ($upstream) contains debian/ directory";
+    }
+
+    snags_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 anchor merge
+       # the tree is already exactly right
+       $work = make_commit [$work, $upstream], [
+ 'git-debrebase import: declare upstream',
+ 'First breakwater merge.',
+ '[git-debrebase anchor: 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 (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]',
+        ];
+    };
+    if (defined $ffq) {
+       push @deferred_updates, "delete $ffq";
+       push @deferred_updates, "delete $gdrlast";
+    }
+    snags_maybe_bail();
+    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' => \@snag_force_opts,
+          'anchor=s' => \@opt_anchors,
+          'force!',
+          '-i:s' => sub {
+              my ($opt,$val) = @_;
+              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) ];
+              # This access to @ARGV is excessive familiarity with
+              # Getopt::Long, but there isn't another sensible
+              # approach.  '-i=s{0,}' does not work with bundling.
+              push @$opt_defaultcmd_interactive, @ARGV;
+              @ARGV=();
+          }) or die badusage "bad options\n";
+initdebug('git-debrebase ');
+enabledebug if $debuglevel;
+
+my $toplevel = cmdoutput @git, qw(rev-parse --show-toplevel);
+chdir $toplevel or die "chdir $toplevel: $!";
+
+$rd = fresh_playground "$playprefix/misc";
+
+@opt_anchors = map { git_rev_parse $_ } @opt_anchors;
+
+if (!@ARGV || $opt_defaultcmd_interactive || $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->();
+}
+
+finish 0;
diff --git a/git-debrebase.1.pod b/git-debrebase.1.pod
new file mode 100644 (file)
index 0000000..6a98ed2
--- /dev/null
@@ -0,0 +1,475 @@
+=head1 NAME
+
+git-debrebase - delta queue rebase tool for Debian packaging
+
+=head1 SYNOPSYS
+
+ git-debrebase [<options...>] [-- <git-rebase options...>]
+ git-debrebase [<options...>] <operation> [<operation options...>
+
+=head1 INTRODUCTION
+
+git-debrebase is a tool for representing in git,
+and manpulating,
+Debian packages based on upstream source code.
+
+This is the command line reference.
+Please read the tutorial
+L<dgit-maint-debrebase(5)>.
+For background, theory of operation,
+and definitions see L<git-debrebase(5)>.
+
+You should read this manpage in conjunction with
+L<git-debrebase(5)/TERMINOLOGY>,
+which defines many important terms used here.
+
+=head1 PRINCIPAL OPERATIONS
+
+=over
+
+=item git-debrebase [-- <git-rebase options...>]
+
+=item git-debrebase [-i <further git-rebase options...>]
+
+Unstitches and launders the branch.
+(See L</UNSTITCHING AND LAUNDERING> below.)
+
+Then, if any git-rebase options were supplied,
+edits the Debian delta queue,
+using git-rebase, by running
+
+    git rebase <git-rebase options> <breakwater-tip>
+
+Do not pass a base branch argument:
+git-debrebase will supply that.
+Do not use --onto, or --fork-point.
+Useful git-rebase options include -i and --autosquash.
+
+If git-rebase stops for any reason,
+you may git-rebase --abort, --continue, or --skip, as usual.
+If you abort the git-rebase,
+the branch will still have been laundered,
+but everything in the rebase will be undone.
+
+The options for git-rebase must either start with C<-i>,
+or be prececded by C<-->,
+to distinguish them from options for git-debrebase.
+
+=item git-debrebase status
+
+Analyise the current branch,
+both in terms of its conents,
+and the refs which are relevant to git-debrebase,
+and print a human-readable summary.
+
+Please do not attempt to parse the output;
+it may be reformatted or reorganised in the future.
+Instead,
+use one of the L<UNDERLYING AND SUPPLEMENTARY OPERATIONS>
+described below.
+
+=item git-debrebase conclude
+
+Finishes a git-debrebase session,
+tidying up the branch and making it fast forward again.
+
+Specifically: if the branch is unstitched,
+launders and restitches it,
+making a new pseudomerge.
+Otherwise, it is an error,
+unless --noop-ok.
+
+=item git-debrebase quick
+
+Unconditionally launders and restitches the branch,
+consuming any ffq-prev
+and making a new pseudomerge.
+
+If the branch is already laundered and stitched, does nothing.
+
+=item git-debrebase prepush [--prose=<for commit message>]
+
+=item git-debrebase stitch [--prose=<for commit message>]
+
+Stitches the branch,
+consuming ffq-prev.
+This is a good command to run before pushing to a git server.
+
+If there is no ffq-prev, it is an error, unless --noop-ok.
+
+You should consider using B<conclude> instead,
+because that launders the branch too.
+
+=item git-debrebase new-upstream-v0 <new-version> [<upstream-details>...]
+
+Rebases the delta queue
+onto a new upstream version.  In detail:
+
+Firstly, checks that the proposed rebase seems to make sense:
+It is a snag unless the new upstream(s)
+are fast forward from the previous upstream(s)
+as found in the current breakwater anchor.
+And, in the case of a multi-piece upstream
+(a multi-component upstream, in dpkg-source terminology),
+if the pieces are not in the same order, with the same names.
+
+If all seems well, unstitches and launders the branch.
+
+Then,
+generates
+(in a private working area)
+a new anchor merge commit,
+on top of the breakwater tip,
+and on top of that a commit to
+update the version number in debian/changelog.
+
+Finally,
+starts a git-rebase
+of the delta queue onto these new commits.
+
+That git-rebase may complete successfully,
+or it may require your assistance,
+just like a normal git-rebase.
+
+If you git-rebase --abort,
+the whole new upstream operation is aborted,
+except for the laundering.
+
+The <upstream-details> are, optionally, in order:
+
+=over
+
+=item <upstream-commit-ish>
+
+The new upstream branch (or commit-ish).
+The default is to look for one of these tags, in this order:
+U vU upstream/U;
+where U is the new upstream version.
+(This is the same algorithm as L<git-deborig(1)>.)
+
+It is a snag if the upstream contains a debian/ directory;
+if forced to proceed,
+git-debrebase will disregard the upstream's debian/ and
+take (only) the packaging from the current breakwater.
+
+=item <piece-name> <piece-upstream-commit-ish>
+
+Specifies that this is a multi-piece upstream.
+May be repeated.
+
+When such a pair is specified,
+git-debrebase will first combine the pieces of the upstream
+together,
+and then use the result as the combined new upstream.
+
+For each <piece-name>,
+the tree of the <piece-upstream-commit-ish>
+becomes the subdirectory <piece-name>
+in the combined new upstream
+(supplanting any subdirectory that might be there in
+the main upstream branch).
+
+<piece-name> has a restricted syntax:
+it may contain only ASCII alphanumerics and hyphens.
+
+The combined upstream is itself recorded as a commit,
+with each of the upstream pieces' commits as parents.
+The combined commit contains an annotation
+to allow a future git-debrebase new upstream operation
+to make the coherency checks described above.
+
+=item <git-rebase options>
+
+These will be passed to git rebase.
+
+If the upstream rebase is troublesome, -i may be helpful.
+As with plain git-debrebase,
+do not specify a base, or --onto, or --fork-point.
+
+=back
+
+If you are planning to generate a .dsc,
+you will also need to have, or generate,
+actual orig tarball(s),
+which must be identical to the rev-spec(s)
+passed to git-debrebase.
+git-debrebase does not concern itself with source packages
+so neither helps with this, nor checks it.
+L<git-deborig(1)>,
+L<git-archive(1)>, L<dgit(1)> and
+L<gbp-import-orig(1)> may be able to help.
+
+This subcommand has -v0 in its name because we are not yet sure
+that its command line syntax is optimal.
+We may want to introduce an incompatible replacement syntax
+under the name C<new-upstream>.
+
+=item git-debrebase make-patches [--quiet-would-amend]
+
+Generate patches in debian/patches/
+representing the changes made to upstream files.
+
+It is not normally necessary to run this command explicitly.
+When uploading to Debian,
+dgit and git-debrebase
+will cooperate to regenerate patches as necessary.
+When working with pure git remotes,
+the patches are not needed.
+
+Normally git-debrebase make-patches will
+require a laundered branch.
+(A laundered branch does not contain any patches.)
+But if there are already some patches made by
+git-debrebase make-patches,
+and all that has happened is that more
+changes to upstream files have been committed,
+running it again can add the missing patches.
+
+If the patches implied by the current branch
+are not a simple superset of those already in debian/patches,
+make-patches will fail with exit status 7,
+and an error message.
+(The message can be suppress with --quiet-would-amend.)
+If the problem is simply that
+the existing patches were not made by git-debrebase,
+using dgit quilt-fixup instead should succeed.
+
+=item git-debrebase convert-from-gbp [<upstream-commit-ish>]
+
+Cnnverts a gbp patches-unapplied branch
+(not a gbp pq patch queue branch)
+into a git-debrebase interchange branch.
+
+This is done by generating a new anchor merge,
+converting the quilt patches as a delta queue,
+and dropping the patches from the tree.
+
+The upstream commit-ish should correspond to
+the gbp upstream branch, if there is one.
+It is a snag if it is not an ancestor of HEAD,
+or if the history between the upstream and HEAD
+contains commits which make changes to upstream files.
+
+It is also a snag if the specified upstream
+has a debian/ subdirectory.
+This check exists to detect certain likely user errors,
+but if this situation is true and expected,
+forcing it is fine.
+
+The result is a well-formed git-debrebase interchange branch.
+The result is also fast-forward from the gbp branch.
+
+Note that it is dangerous not to know whether you are
+dealing with a gbp patches-unappled branch containing quilt patches,
+or a git-debrebase interchange branch.
+At worst,
+using the wrong tool for the branch format might result in
+a dropped patch queue!
+
+=back
+
+=head1 UNDERLYING AND SUPPLEMENTARY OPERATIONS
+
+=over
+
+=item git-debrebase breakwater
+
+Prints the breakwater tip commitid.
+If your HEAD branch is not fully laundered,
+prints the tip of the so-far-laundered breakwater.
+
+=item git-debrebase anchor
+
+Prints the breakwater anchor commitid.
+
+=item git-debrebase analyse
+
+Walks the history of the current branch,
+most recent commit first,
+back until the most recent anchor,
+printing the commit object id,
+and commit type and info
+(ie the semantics in the git-debrebase model)
+for each commit.
+
+=item git-debrebase record-ffq-prev
+
+Establishes the current branch's ffq-prev,
+as discussed in L</UNSTITCHING AND LAUNDERING>,
+but does not launder the branch or move HEAD.
+
+It is an error if the ffq-prev could not be recorded.
+It is also an error if an ffq-prev has already been recorded,
+unless --noop-ok.
+
+=item git-debrebase launder-v0
+
+Launders the branch without recording anything in ffq-prev.
+Then prints some information about the current branch.
+Do not use this operation;
+it will be withdrawn soon.
+
+=item git-debrebase convert-to-gbp
+
+Converts a laundered branch into a
+gbp patches-unapplied branch containing quilt patches.
+The result is not fast forward from the interchange branch,
+and any ffq-prev is deleted.
+
+This is provided mostly for the test suite
+and for unusual situations.
+It should only be used with a care and 
+with a proper understanding of the underlying theory.
+
+Be sure to not accidentally treat the result as
+a git-debrebase branch,
+or you will drop all the patches!
+
+=back
+
+=head1 OPTIONS
+
+This section documents the general options
+to git-debrebase
+(ie, the ones which immediately follow
+git-debrebase
+or
+git debrebase
+on the command line).
+Individual operations may have their own options which are
+docuented under each operation.
+
+=over
+
+=item -f<snag-id>
+
+Turns snag(s) with id <snag-id> into warnings.
+
+Some troublesome things which git-debrebase encounters
+are B<snag>s.
+(The specific instances are discussed
+in the text for the relvant operation.)
+
+When a snag is detected,
+a message is printed to stderr containing the snag id
+(in the form C<-f<snag-idE<gt>>),
+along with some prose.
+
+If snags are detected, git-debrebase does not continue,
+unless the relevant -f<snag-id> is specified,
+or --force is specified.
+
+=item --force
+
+Turns all snags into warnings.
+See the -f<snag-id> option.
+
+Do not invoke git-debrebase --force in scripts and aliases;
+instead, specify the particular -f<snag-id> for expected snags.
+
+=item --noop-ok
+
+Suppresses the error in
+some situations where git-debrebase does nothing,
+because there is nothing to do.
+
+The specific instances are discussed
+in the text for the relvant operation.
+
+=item --anchor=<commit-ish>
+
+Treats <commit-ish> as an anchor.
+This overrides the usual logic which automatically classifies
+commits as anchors, pseudomerges, delta queue commits, etc.
+
+It also disables some coherency checks
+which depend on metadata extracted from its commit message,
+so
+it is a snag if <commit-ish> is the anchor
+for the previous upstream version in
+git-debrebase new-upstream operations.
+
+=item -D
+
+Requests (more) debugging.  May be repeated.
+
+=back
+
+=head1 UNSTITCHING AND LAUNDERING
+
+Several operations unstitch and launder the branch first.
+In detail this means:
+
+=head2 Establish the current branch's ffq-prev
+
+If ffq-prev is not yet recorded,
+git-debrebase checks that the current branch is ahead of relevant
+remote tracking branches.
+The relevant branches depend on
+the current branch (and its
+git configuration)
+and are as follows:
+
+=over
+
+=item
+
+The branch that git would merge from
+(remote.<branch>.merge, remote.<branch>.remote);
+
+=item
+
+The branch git would push to, if different
+(remote.<branch>.pushRemote etc.);
+
+=item
+
+For local dgit suite branches,
+the corresponding tracking remote;
+
+=item
+
+If you are on C<master>,
+remotes/dgit/dgit/sid.
+
+=back
+
+The apparently relevant ref names to check are filtered through
+branch.<branch>.ffq-ffrefs,
+which is a semicolon-separated list of glob patterns,
+each optionally preceded by !; first match wins.
+
+In each case it is a snag if
+the local HEAD is behind the checked remote,
+or if local HEAD has diverged from it.
+All the checks are done locally using the remote tracking refs:
+git-debrebase does not fetch anything from anywhere.
+
+If these checks pass,
+or are forced,
+git-debrebse then records the current tip as ffq-prev.
+
+=head2 Examine the branch
+
+git-debrebase
+analyses the current HEAD's history to find the anchor
+in its breakwater,
+and the most recent breakwater tip.
+
+=head2 Rewrite the commits into laundered form
+
+Mixed debian+upstream commits are split into two commits each.
+Delta queue (upstream files) commits bubble to the top.
+Pseudomerges,
+and quilt patch additions,
+are dropped.
+
+This rewrite will always succeed, by construction.
+The result is the laundered branch.
+
+=head1 SEE ALSO
+
+git-debrebase(1),
+dgit-maint-rebase(7),
+dgit(1),
+gitglossary(7)
diff --git a/git-debrebase.5.pod b/git-debrebase.5.pod
new file mode 100644 (file)
index 0000000..5cfa376
--- /dev/null
@@ -0,0 +1,610 @@
+=head1 NAME
+
+git-debrebase - git data model for Debian packaging
+
+=head1 INTRODUCTION
+
+git-debrebase is a tool for representing in git,
+and manpulating,
+Debian packages based on upstream source code.
+
+The Debian packaging
+has a fast forwarding history.
+The delta queue (changes to upstream files) is represented
+as a series of individual git commits,
+which can worked on with rebase,
+and also shared.
+
+git-debrebase is designed to work well with dgit.
+git-debrebase can also be used in workflows without source packages,
+for example to work on Debian-format packages outside or alongside Debian.
+
+git-debrebase
+itself is not very suitable for use by Debian derivatives,
+to work on packages inherited from Debian,
+because it assumes that you want to throw away any packaging
+provided by your upstream.
+However, of git-debrebase in Debian does not make anything harder for
+derivatives, and it can make some things easier.
+
+=head1 TERMINOLOGY
+
+=over
+
+=item Pseudomerge
+
+A merge which does not actually merge the trees;
+instead, it is constructed by taking the tree
+from one of the parents
+(ignoring the contents of the other parents).
+These are used to make a rewritten history fast forward
+from a previous tip,
+so that it can be pushed and pulled normally.
+Manual construction of pseudomerges can be done with
+C<git merge -s ours>
+but is not normally needed when using git-debrebase.
+
+=item Packaging files
+
+Files in the source tree within B<debian/>,
+excluding anything in B<debian/patches/>.
+
+=item Upstream
+
+The version of the package without Debian's packaging.
+Typically provided by the actual upstream project,
+and sometimes tracked by Debian contributors in a branch C<upstream>.
+
+Upstream contains upstream files,
+but some upstreams also contain packaging files in B<debian/>.
+Any such non-upstream files found in upstream
+are thrown away by git-debrebase
+each time a new upstream version is incorporated.
+
+=item Upstream files
+
+Files in the source tree outside B<debian/>.
+These may include unmodified source from upstream,
+but also files which have been modified or created for Debian.
+
+=item Delta queue
+
+Debian's changes to upstream files:
+a series of git commits.
+
+=item Quilt patches
+
+Files in B<debian/patches/> generated for the benefit of
+dpkg-source's 3.0 (quilt) .dsc source package format.
+Not used, often deleted, and regenerated when needed
+(such as when uploading to Debian),
+by git-debrebase.
+
+=item Interchange branch; breakwater; stitched; laundered
+
+See L</BRANCHES AND BRANCH STATES - OVERVIEW>.
+
+=item Anchor; Packaging
+
+See L</BRANCH CONTENTS - DETAILED SPECIFICATION>.
+
+=item ffq-prev; debrebase-last
+
+See L</STITCHING, PSEUDO-MERGES, FFQ RECORD>.
+
+=back
+
+=head1 DIAGRAM
+
+           ------/--A!----/--B3!--%--/--> interchange view
+                /        /          /      with debian/ directory
+               %        %          %       entire delta queue applied
+              /        /          /        3.0 (quilt) has debian/patches
+             /        /          3*       "master" on Debian git servers
+            /        /          /
+           2*       2*         2
+          /        /          /
+         1        1          1    breakwater branch, merging baseline
+        /        /          /     unmodified upstream code
+    ---@-----@--A----@--B--C      plus debian/ (but no debian/patches)
+      /     /       /                     no ref refers to this: we
+   --#-----#-------#-----> upstream        reconstruct its identity by
+                                           inspecting interchange branch
+    Key:
+
+      1,2,3   commits touching upstream files only
+      A,B,C   commits touching debian/ only
+      B3      mixed commit (eg made by an NMUer)
+      #       upstream releases
+
+     -@-      anchor merge, takes contents of debian/ from the
+     /         previous `breakwater' commit and rest from upstream
+
+     -/-      pseudomerge; contents are identical to
+     /         parent lower on diagram.
+
+      %       dgit- or git-debrebase- generated commit of debian/patches.
+              `3.0 (quilt)' only; generally dropped by git-debrebase.
+
+      *       Maintainer's HEAD was here while they were editing,
+              before they said they were done, at which point their
+              tools made -/- (and maybe %) to convert to
+              the fast-forwarding interchange branch.
+
+      !       NMUer's HEAD was here when they said `dgit push'.
+              Rebase branch launderer turns each ! into an
+              equivalent *.
+
+=head1 BRANCHES AND BRANCH STATES - OVERVIEW
+
+git-debrebase has one primary branch,
+the B<interchange branch>.
+This branch is found on Debian contributor's workstations
+(typically, a maintainer would call it B<master>),
+in the Debian dgit git server as the suite branch (B<dgit/dgit/sid>)
+and on other git servers which support Debian work
+(eg B<master> on salsa).
+
+The interchange branch is fast-forwarding
+(by virtue of pseudomerges, where necessary).
+
+It is possible to have multiple different interchange branches
+for the same package,
+stored as different local and remote git branches.
+However, divergence should be avoided where possible -
+see L</OTHER MERGES>.
+
+A suitable interchange branch can be used directly with dgit.
+In this case each dgit archive suite branch is a separate
+interchange branch.
+
+Within the ancestry of the interchange branch,
+there is another important, implicit branch, the
+B<breakwater>.
+The breakwater contains unmodified upstream source,
+but with Debian's packaging superimposed
+(replacing any C<debian/> directory that may be in
+the upstream commits).
+The breakwater does not contain any representation of
+the delta queue (not even debian/patches).
+The part of the breakwater processed by git-debrebase
+is the part since the most reecent B<anchor>,
+which is usually a special merge generated by git-debrebase.
+
+When working, locally,
+the user's branch can be in a rebasing state,
+known as B<unstitched>.
+While a branch is unstitched,
+it is not in interchange format.
+The previous interchange branch tip
+tip is recorded,
+so that the previous history
+and the user's work
+can later be
+stitched into the fast-forwarding interchange form.
+
+An unstitched branch may be in
+B<laundered>
+state,
+which means it has a more particular special form
+convenient for manipulating the delta queue.
+
+=head1 BRANCH CONTENTS - DETAILED SPECIFICATION
+
+It is most convenient to describe the
+B<breakwater>
+branch first.
+A breakwater is B<fast-forwarding>,
+but is not usually named by a ref.
+It contains B<in this order> (ancestors first):
+
+=over
+
+=item Anchor
+
+An B<anchor> commit,
+which is usually a special two-parent merge:
+
+The first parent
+contains the most recent version, at that point,
+of the Debian packaging (in debian/);
+it also often contains upstream files,
+but they are to be ignored.
+Often the first parent is a previous breakwater tip.
+
+The second parent
+is an upstream source commit.
+It may sometimes contain a debian/ subdirectory,
+but if so that is to be ignored.
+The second parent's upstream files
+are identical to the anchor's.
+Anchor merges always contain
+C<[git-debrebase anchor: ...]>
+as a line in the commit message.
+
+Alternatively,
+an anchor may be a single-parent commit which introduces
+the C<debian/> directory and makes no other changes:
+ie, the start of Debian packaging.
+
+=item Packaging
+
+Zero or more single-parent commits
+containing only packaging changes.
+(And no quilt patch changes.)
+
+=back
+
+The
+B<laundered>
+branch state is B<rebasing>.
+A laundered branch is based on a breakwater
+but also contains, additionally,
+B<after> the breakwater,
+a representation of the delta queue:
+
+=over
+
+=item Delta queue commits
+
+Zero or more single-parent commits
+contaioning only changes to upstream files.
+
+=back
+
+The merely
+B<unstitched>
+(ie, unstitched but unlaundered)
+branch state is also B<rebasing>.
+It has the same contents as the laundered state,
+except that it may contain,
+additionally,
+in B<in any order but after the breakwater>:
+
+=over
+
+=item Linear commits to the source
+
+Further commit(s) containing changes to
+to upstream files
+and/or
+to packaging,
+possibly mixed within a single commit.
+(But not quilt patch changes.)
+
+=item Quilt patch addition for `3.0 (quilt)'
+
+Commit(s) which add patches to B<debian/patches/>,
+and add those patches to the end of B<series>.
+
+These are only necessary when working with
+packages in C<.dsc 3.0 (quilt)> format.
+For git-debrebase they are purely an output;
+they are deleted when branches are laundered.
+git-debrebase takes care to make a proper patch
+series out of the delta queue,
+so that any resulting source packages are nice.
+
+=back
+
+Finally, an
+B<interchange>
+branch is B<fast forwarding>.
+It has the same contents as an
+unlaundered branch state,
+but may (and usually will) additionally contain
+(in some order,
+possibly intermixed with the extra commits
+which may be found on an unstitched unlaundered branch):
+
+=over
+
+=item Pseudomerge to make fast forward
+
+A pseudomerge making the branch fast forward from
+previous history.
+The contributing parent is itself in interchange format.
+Normally the overwritten parent is
+a previous tip of an interchange branch,
+but this is not necessary as the overwritten
+parent is not examined.
+
+If the two parents have identical trees,
+the one with the later commit date
+(or, if the commit dates are the same,
+the first parent)
+is treated as
+the contributing parent.
+
+=item dgit dsc import pseudomerge
+
+Debian .dsc source package import(s)
+made by dgit
+(during dgit fetch of a package most recently 
+uploaded to Debian without dgit,
+or during dgit import-dsc).
+
+git-debrebase requires that
+each such import is in the fast-forwarding
+format produced by dgit:
+a two-parent pseudomerge,
+whose contributing parent is in the
+non-fast-forwarding
+dgit dsc import format (not described further here),
+and whose overwritten parent is
+the previous interchange tip
+(eg, the previous tip of the dgit suite branch).
+
+=back
+
+=head1 STITCHING, PSEUDO-MERGES, FFQ RECORD
+
+Whenever the branch C<refs/B> is unstitched,
+the previous head is recorded in the git ref C<refs/ffq-prev/B>.
+
+Unstiched branches are not fast forward from the published
+interchange branches [1].
+So before a branch can be pushed,
+the right pseudomerge must be reestablished.
+This is the stitch operation,
+which consumes the ffq-prev ref.
+
+When the user has an unstitched branch,
+they may rewrite it freely,
+from the breakwater tip onwards.
+Such a git rebase is the default operation for git-debrebase.
+Rebases should not go back before the breakwater tip,
+and certainly not before the most recent anchor.
+
+Unstitched branches must not be pushed to interchange branch refs
+(by the use of C<git push -f> or equivalent).
+It is OK to share an unstitched branch
+in similar circumstances and with similar warnings
+to sharing any other rebasing git branch.
+
+[1] Strictly, for a package
+which has never had a Debian delta queue,
+the interchange and breakwater branches may be identical,
+in which case the unstitched branch is fast forward
+from the interchange branch and no pseudomerge is needed.
+
+When ffq-prev is not present,
+C<refs/debrebase-last/B> records some ancestor of refs/B,
+(usually, the result of last stitch).
+This can be used to quickly determine whether refs/B
+is being maintained in git-debrebase form.
+
+=head1 OTHER MERGES
+
+Note that the representation described here does not permit
+general merges on any of the relevant branches.
+For this reason the tools will try to help the user
+avoid divergence of the interchange branch.
+
+See dgit-maint-rebase(7) XXX TBD
+for a discussio of what kinds of behaviours
+should be be avoided
+because
+they might generate such merges.
+
+Automatic resolution of divergent interchange branches
+(or laundering of merges on the interchange branch)
+is thought to be possible,
+but there is no tooling for this yet:
+
+Nonlinear (merging) history in the interchange branch is awkward
+because it (obviously) does not preserve
+the linearity of the delta queue.
+Easy merging of divergent delta queues is a research problem.
+
+Nonlinear (merging) history in the breakwater branch is
+in principle tolerable,
+but each of the parents would have to be, in turn,
+a breakwater,
+and difficult qeustions arise if they don't have the same anchor.
+
+We use the commit message annotation to
+distinguish the special anchor merges from other general merges,
+so we can at least detect unsupported merges.
+
+=head1 LEGAL OPERATIONS
+
+The following basic operations follows from this model
+(refer to the diagram above):
+
+=over
+
+=item Append linear commits
+
+No matter the branch state,
+it is always fine to simply git commit
+(or cherry-pick etc.)
+commits containing upstream file changes, packaging changes,
+or both.
+
+(This may make the branch unlaundered.)
+
+=item Launder branch
+
+Record the previous head in ffq-prev,
+if we were stitched before
+(and delete debrebase-last).
+
+Reorganise the current branch so that the packaging
+changes come first,
+followed by the delta queue,
+turning C<-@-A-1-2-B3> into C<...@-A-B-1-2-3>.
+
+Drop pseudomerges and any quilt patch additions.
+
+=item Interactive rebase
+
+With a laundered branch,
+one can do an interactive git rebase of the delta queue.
+
+=item New upstream rebase
+
+Start rebasing onto a new upstream version,
+turning C<...#..@-A-B-1-2-3> into C<(...#..@-A-B-, ...#'-)@'-1-2>.
+
+This has to be a wrapper around git-rebase,
+which prepares @' and then tries to rebase 1 2 onto @'.
+If the user asks for an interactive rebase,
+@' doesn't appear in the commit list, since
+@' is the newbase of the rebase (see git-rebase(1)).
+
+Note that the construction of @' cannot fail
+because @' simply copies debian/ from B and and everything else from #'.
+(Rebasing A and B is undesirable.
+We want the debian/ files to be non-rebasing
+so that git log shows the packaging history.)
+
+=item Stitch 
+
+Make a pseudomerge,
+whose contributing parent to is the unstitched branch
+and
+whose overwritten parent is ffq-prev,
+consuming ffq-prev in the process
+(and writing debrebase-last instead).
+Ideally the contributing parent would be a laundered branch,
+or perhaps a laundered branch with a quilt patch addition commit.
+
+=item Commit quilt patches
+
+To generate a tree which can be represented as a
+3.0 (quilt) .dsc source packages,
+the delta queue must be reified inside the git tree
+in B<debian/patches/>.
+These patch files can be stripped out and/or regenerated as needed.
+
+=back
+
+=head1 COMMIT MESSAGE ANNOTATIONS
+
+git-debrebase makes annotations
+in the messages of commits it generates.
+
+The general form is
+
+  [git-debrebase[ COMMIT-TYPE [ ARGS...]]: PROSE, MORE PROSE]
+
+git-debrebase treats anything after the colon as a comment,
+paying no attention to PROSE.
+
+The full set of annotations is:
+  [git-debrebase: split mixed commit, debian part]
+  [git-debrebase: split mixed commit, upstream-part]
+  [git-debrebase: convert dgit import, debian changes]
+  [git-debrebase anchor: convert dgit import, upstream changes]
+
+  [git-debrebase upstream-combine . PIECE[ PIECE...]: new upstream]
+  [git-debrebase anchor: new upstream NEW-UPSTREAM-VERSION, merge]
+  [git-debrebase: new upstream NEW-UPSTREAM-VERSION, changelog]
+  [git-debrebase: export and commit patches]
+
+  [git-debrebase convert-from-gbp: drop patches]
+  [git-debrebase anchor: declare upstream]
+  [git-debrebase pseudomerge: stitch]
+
+  [git-debrebase convert-to-gbp: commit patches]
+
+Only anchor merges have the C<[git-debrebase anchor: ...]> tag.
+Single-parent anchors are not generated by git-debrebase,
+and when made manually should not be tagged.
+
+The C<split mixed commit> and C<convert dgit import>
+tags are added to the pre-existing commit message,
+when git-debrebase rewrites the commit.
+
+=head1 APPENDIX - DGIT IMPORT HANDLING
+
+The dgit .dsc import format is not documented or specified
+(so some of the following terms are not defined anywhere).
+The dgit import format it is defined by the implementation in dgit,
+of which git-debrebase has special knowledge.
+
+Consider a non-dgit NMU followed by a dgit NMU:
+
+            interchange --/--B3!--%--//----D*-->
+                         /          /
+                        %          4
+                       /          3
+                      /          2
+                     /          1
+                    2          &_
+                   /          /| \
+                  1          0 00 =XBC%
+                 /
+                /
+          --@--A     breakwater
+           /
+        --#--------> upstream
+
+
+ Supplementary key:
+
+    =XBC%     dgit tarball import of .debian.tar.gz containing
+               Debian packaging including changes B C and quilt patches
+    0         dgit tarball import of upstream tarball
+    00        dgit tarball import of supplementary upstream piece
+    &_        dgit import nearly-breakwater-anchor
+    //        dgit fetch / import-dsc pseudomerge to make fast forward
+
+    &'        git-debrebase converted import (upstream files only)
+    C'        git-debrebase converted packaging change import
+
+    * **      before and after HEAD
+
+We want to transform this into:
+
+=over
+
+=item I. No new upstream version
+
+ (0 + 00 eq #)
+                        --/--B3!--%--//-----D*-------------/-->
+                         /          /                     /
+                        %          4                     4**
+                       /          3                     3
+                      /          2                     2
+                     /          1                     1
+                    2          &_                    /
+                   /          /| \                  /
+                  1          0 00 =XBC%            /
+                 /                                /
+                /                                /
+          --@--A-----B---------------------C'---D
+           /
+        --#----------------------------------------->
+
+=item II. New upstream
+
+ (0 + 00 neq #)
+
+                        --/--B3!--%--//-----D*-------------/-->
+                         /          /                     /
+                        %          4                     4**
+                       /          3                     3
+                      /          2                     2
+                     /          1                     1
+                    2          &_                    /
+                   /          /| \                  /
+                  1          0 00 =XBC%            /
+                 /                                /
+                /                                /
+          --@--A-----B-----------------@---C'---D
+           /                          /
+        --#--------------------- - - / - - --------->
+                                    /
+                                   &'
+                                  /|
+                                 0 00
+
+=back
+
+=head1 SEE ALSO
+
+git-debrebase(1),
+dgit-maint-rebase(7),
+dgit(1)
index 2c00f975a7f88b4fa3104ed60f3d88ad1ec9a6f6..5a4d23571d656db0ff9ac4612596a086adb639bb 100755 (executable)
@@ -42,15 +42,29 @@ finish- () {
 
 test-begin-gencontrol () {
        restrictions=''
-       dependencies=''
+       dependencies='dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, chiark-utils-bin'
 }
 
 restriction-gencontrol () {
        restrictions+=" $r"
 }
 
+gencontrol-add-deps () {
+       for dep in "$@"; do
+               dependencies+="${dependencies:+, }$dep"
+       done
+}
+
 dependencies-gencontrol () {
-       dependencies+=", $deps"
+       for dep in "$deps"; do
+               case "$dep" in
+               NO-DGIT) dependencies='chiark-utils-bin' ;;
+               GDR) gencontrol-add-deps \
+                       git-debrebase git-buildpackage faketime
+                       ;;
+               *) gencontrol-add-deps "$dep" ;;
+               esac
+       done
 }
 
 test-done-gencontrol () {
index e4554e373f9fddef37c7cd0f302e98989e643fd1..bd06d20d21cc7364ce1e056777e5fd03e35b4b29 100644 (file)
--- a/tests/lib
+++ b/tests/lib
@@ -31,8 +31,8 @@ export DGIT_TEST_DEBUG
 
 : ${DGIT_TEST_DISTRO+ ${distro=${DGIT_TEST_DISTRO}}}
 
-export GIT_COMMITTER_DATE='1440253867 +0100'
-export GIT_AUTHOR_DATE='1440253867 +0100'
+export GIT_COMMITTER_DATE='1515000000 +0100'
+export GIT_AUTHOR_DATE='1515000000 +0100'
 
 root=`pwd`
 troot=$root/tests
@@ -189,6 +189,13 @@ t-git-none () {
        (set -e; cd $tmp/git; tar xf $troot/git-template.tar)
 }
 
+t-salsa-add-remote () {
+       local d=$tmp/salsa/$p
+       mkdir -p $d
+       (set -e; cd $d; git init --bare)
+       git remote add ${1-origin} $d
+}
+
 t-git-merge-base () {
        git merge-base $1 $2 || test $? = 1
 }
@@ -415,9 +422,10 @@ t-dgit () {
 {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{'
        $dgit --dgit=$dgit --dget:-u --dput:--config=$tmp/dput.cf \
  ${dgit_config_debian_alias-"--config-lookup-explode=dgit-distro.debian.alias-canon"} \
+ ${DGIT_GITDEBREBASE_TEST+--git-debrebase=}${DGIT_GITDEBREBASE_TEST} \
                ${distro+${distro:+-d}}${distro--dtest-dummy} \
                $DGIT_TEST_OPTS $DGIT_TEST_DEBUG \
-               -k39B13D8A $t_dgit_xopts "$@"
+               -kBCD22CD83243B79D3DFAC33EA3DBCBC039B13D8A $t_dgit_xopts "$@"
        : '}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
 '
 }
@@ -472,12 +480,12 @@ t-setup-done () {
        local savedirs=$2
        local importeval=$3
 
-       local import=IMPORT.${0##*/}
+       local import=IMPORT.${DGIT_TEST_TESTNAME-${0##*/}}
        exec 4>$tmp/$import.new
 
        local vn
        for vn in $savevars; do
-               perl >&4 -I. -MDebian::Dgit -e '
+               perl >&4 -"I$root" -MDebian::Dgit -e '
                        printf "%s=%s\n", $ARGV[0], shellquote $ARGV[1]
                ' $vn "$(eval "printf '%s\n' \"\$$vn\"")"
        done
@@ -997,6 +1005,11 @@ t-commit () {
        revision=$(( ${revision-0} + 1 ))
 }
 
+t-dch-commit () {
+       faketime @"${GIT_AUTHOR_DATE% *}" dch "$@"
+       git commit -m "dch $*" debian/changelog
+}
+
 t-git-config () {
        git config --global "$@"
 }
index d65a1ffeccadcde3e9b9268f35be39620e4221fe..c3a04cbaed1b215cfd62c33b7455976f7a2c4a73 100644 (file)
@@ -12,6 +12,7 @@ t-set-intree () {
        : ${DGIT_REPOS_SERVER_TEST:=$DGIT_TEST_INTREE/infra/dgit-repos-server}
        : ${DGIT_SSH_DISPATCH_TEST:=$DGIT_TEST_INTREE/infra/dgit-ssh-dispatch}
        : ${DGIT_INFRA_PFX:=$DGIT_TEST_INTREE${DGIT_TEST_INTREE:+/infra/}}
+       : ${DGIT_GITDEBREBASE_TEST:=$DGIT_TEST_INTREE/git-debrebase}
        export DGIT_TEST DGIT_BADCOMMIT_FIXUP
        export DGIT_REPOS_SERVER_TEST DGIT_SSH_DISPATCH_TEST
        export PERLLIB="$DGIT_TEST_INTREE${PERLLIB:+:}${PERLLIB}"
diff --git a/tests/lib-gdr b/tests/lib-gdr
new file mode 100644 (file)
index 0000000..9eb7537
--- /dev/null
@@ -0,0 +1,277 @@
+#
+
+: ${GDR_TEST_DEBUG=-D}
+export GDR_TEST_DEBUG
+
+t-git-debrebase () {
+       local gdr=${DGIT_GITDEBREBASE_TEST-git-debrebase}
+       : '
+{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{'
+       $gdr    $GDR_TEST_OPTS $GDR_TEST_DEBUG $t_gdr_xopts "$@"
+       : '}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
+'
+}
+
+t-gdr-good () {
+       local state=$1
+       local beforetag=$2 # say HEAD to skip this check
+       # state should be one of
+       #   laundered
+       #   stitched
+       #   pushed
+
+       git diff --quiet ${beforetag-t.before} -- ':.' ':!debian/patches'
+
+       local etypes bwtip
+
+       LC_MESSAGES=C t-git-debrebase status >../status.check
+       case $state in
+       laundered)
+               egrep '^  *branch is laundered' ../status.check
+               ;;
+       stitched|pushed)
+               egrep \
+ '^  *branch contains furniture|^  *branch is unlaundered|^  *branch needs laundering' ../status.check
+               egrep '^  stitched$' ../status.check
+               ;;
+       esac
+
+       # etypes is either a type,
+       # or   PseudoMerge-<more etypes>
+       # or   AddPatches-<more etypes>
+
+       case $state in
+       laundered)
+                       etypes=Upstream
+                       bwtip=Y:`t-git-debrebase breakwater`
+               ;;
+       stitched)       etypes=Pseudomerge-Upstream ;;
+       pushed)         etypes=AddPatches-Pseudomerge-Upstream ;;
+       pushed-interop) etypes=Pseudomerge-AddPatchesInterop-Upstream ;;
+       esac
+
+       t-git-debrebase analyse >../anal.check
+       expect=`git rev-parse HEAD`
+       exec <../anal.check
+       local cid ctype info nparents
+       while read cid ctype info; do
+               : ===== $cid $ctype $info =====
+               test $cid = $expect
+               local cetype=${etypes%%-*}
+               if [ "x$ctype" = "x$cetype" ]; then cetype=SAME; fi
+               local parents="`git log -n1 --pretty=format:%P $cid`"
+               expect="$parents"
+               enparents=1
+               : "$ctype/$cetype" "$parents"
+
+               case "$ctype/$cetype" in
+               Pseudomerge/SAME)                       ;;
+               Packaging/SAME)                         ;;
+               Packaging/Upstream)                     ;;
+               AddPatches/SAME)                        ;;
+               AddPatches/AddPatchesInterop)           ;;
+               Changelog/Packaging)                    ;;
+               Changelog/Upstream)                     ;;
+               Upstream/SAME)                          ;;
+               Anchor/Upstream)                        ;;
+               Anchor/Packaging)                       ;;
+               *)
+                       fail "etypes=$etypes ctype=$ctype cetype=$cetype $cid"
+                       ;;
+               esac
+
+               case "$ctype/$etypes" in
+               Packaging/Upstream|\
+               Changelog/Upstream)
+                       if [ "x$bwtip" != x ]; then
+                               test "$bwtip" = "Y:$cid"
+                               bwtip=''
+                       fi
+               esac
+
+               case "$cetype" in
+               AddPatchesInterop)
+                       git log -n1 --pretty=format:%B \
+                       | grep '^\[git-debrebase[ :]'
+                       ;;
+               esac
+
+               case "$ctype" in
+               Pseudomerge)
+                       expect=${info#Contributor=}
+                       expect=${expect%% *}
+                       enparents=2
+                       git diff --quiet $expect..$cid
+                       etypes=${etypes#*-}
+
+                       : 'reject pointless pseudomerges'
+                       local overwritten=${parents/$expect/}
+                       overwritten=${overwritten// /}
+                       t-git-debrebase analyse $overwritten >../anal.overwr
+                       local ocid otype oinfo
+                       read <../anal.overwr ocid otype oinfo
+                       case "$otype" in
+                       Pseudomerge) test "x$info" != "x$oinfo" ;;
+                       esac
+                       ;;
+               Packaging)
+                       git diff --quiet $expect..$cid -- ':.' ':!debian' 
+                       git diff --quiet $expect..$cid -- ':debian/patches'
+                       etypes=Packaging
+                       ;;
+               AddPatches)
+                       git diff --quiet $expect..$cid -- \
+                               ':.' ':!debian/patches'
+                       etypes=${etypes#*-}
+                       ;;
+               Changelog)
+                       git diff --quiet $expect..$cid -- \
+                               ':.' ':!debian/changelog'
+                       etypes=Packaging
+                       ;;
+               Upstream/SAME)
+                       git diff --quiet $expect..$cid -- ':debian'
+                       ;;
+               Anchor)
+                       break
+                       ;;
+               esac
+
+               local cnparents=`printf "%s" "$parents" | wc -w`
+               test $cnparents = $enparents
+
+               local cndparents=`
+       for f in $parents; do echo $f; done | sort -u | wc -w
+                               `
+               test $cndparents = $cnparents
+
+               case "$parents" in
+               *"$expect"*)    ;;
+               *)              fail 'unexpected parent' ;;
+               esac
+
+       done
+}
+
+t-some-changes () {
+       local token=$1
+
+       t-git-next-date
+
+       echo >>debian/zorkmid "// debian $token"
+       git add debian/zorkmid
+       git commit -m "DEBIAN add zorkmid ($token)"
+
+       echo >>src.c "// upstream $token"
+       git commit -a -m "UPSTREAM edit src.c ($token)"
+
+       for f in debian/zorkmid src.c; do
+               echo "// both! $token" >>$f
+               git add $f
+       done
+       git commit -m "MIXED add both ($token)"
+
+       t-git-next-date
+}
+
+t-make-new-upstream-tarball () {
+       local uv=$1
+       git checkout make-upstream
+       # leaves ust set to filename of orig tarball
+       echo "upstream $uv" >>docs/README
+       git commit -a -m "upstream $uv tarball"
+       ust=example_$uv.orig.tar.gz
+       git archive -o ../$ust --prefix=example-2.0/ make-upstream
+}
+
+t-nmu-upload-1 () {
+       # usage:
+       #  v=<full version>
+       #  nmu-upload-1 <nmubranch>
+       #  gbp pq import or perhaps other similar things
+       #  nmu-upload-2
+       #  maybe make some dgit-covertible commits
+       #  nmu-upload-3
+
+       t-git-next-date
+       nmubranch=$1
+       git checkout -f -b $nmubranch
+       t-git-debrebase
+       t-git-debrebase convert-to-gbp
+       t-git-next-date
+       # now we are on a gbp patched-unapplied branch
+}
+
+
+t-nmu-upload-2 () {
+       t-git-next-date
+       t-dch-commit -v $v -m "nmu $nmubranch $v"
+}
+
+t-nmu-upload-3 () {
+       t-dch-commit -r sid
+
+       t-dgit -wgf build-source
+
+       cd ..
+       c=${p}_${v}_source.changes
+       debsign -kBCD22CD83243B79D3DFAC33EA3DBCBC039B13D8A $c
+       dput -c $tmp/dput.cf test-dummy $c
+
+       t-archive-process-incoming sid
+       t-git-next-date
+       cd $p
+       git checkout master
+}
+
+t-nmu-commit-an-upstream-change () {
+       echo >>newsrc.c "// more upstream"
+       git add newsrc.c
+       git commit -m 'UPSTREAM NMU'
+}
+
+t-maintainer-commit-some-changes () {
+       t-dch-commit -v$v -m "start $v"
+
+       t-some-changes "maintainer $v"
+       t-git-debrebase
+       t-git-debrebase stitch
+
+       git branch did.maintainer
+
+       t-git-next-date
+}
+
+t-nmu-causes-ff-fail () {
+       t-dgit fetch
+
+       t-expect-fail E:'Not.*fast-forward' \
+       git merge --ff-only dgit/dgit/sid
+
+       t-expect-fail E:'-fdiverged.*refs/remotes/dgit/dgit/sid' \
+       t-git-debrebase
+}
+
+t-nmu-reconciled-good () {
+       local nmutree=$1
+
+       : 'check that what we have is what is expected'
+
+       git checkout -b compare.nmu origin/master~0
+       git checkout $nmutree .
+       git rm -rf debian/patches
+       git commit -m 'rm patches nmu'
+
+       git checkout -b compare.maintainer origin/master~0
+       git checkout did.maintainer .
+       git rm -rf --ignore-unmatch debian/patches
+       git commit --allow-empty -m 'rm patches maintainer'
+
+       git merge compare.nmu
+       git diff --quiet master
+
+       : 'check that dgit still likes it'
+
+       git checkout master
+       t-dgit -wgf quilt-fixup
+}
diff --git a/tests/setup/gdr-convert-gbp b/tests/setup/gdr-convert-gbp
new file mode 100755 (executable)
index 0000000..0b525c8
--- /dev/null
@@ -0,0 +1,100 @@
+#!/bin/bash
+set -e
+. tests/lib
+. $troot/lib-gdr
+
+t-dependencies GDR
+
+t-tstunt-parsechangelog
+
+not-gdr-processable () {
+       t-git-debrebase analyse | grep 'Unknown Unprocessable'
+}
+
+p=example
+t-worktree 1.1
+
+cd example
+
+: 'fake up some kind of upstream'
+git checkout -b upstream quilt-tip
+rm -rf debian
+mkdir debian
+echo junk >debian/rules
+git add debian
+git commit -m "an upstream retcon ($0)"
+
+: 'fake up that our quilt-tip was descended from upstream'
+git checkout quilt-tip
+git merge --no-edit -s ours upstream
+
+: 'fake up that our quilt-tip had the patch queue in it'
+git checkout patch-queue/quilt-tip
+gbp pq export
+git add debian/patches
+git commit -m "patch queue update ($0)"
+
+not-gdr-processable
+
+: 'fake up an upstream 2.0'
+git branch make-upstream upstream
+t-make-new-upstream-tarball 2.0
+
+: 'make branch names more conventional'
+git branch -D master
+git branch -m quilt-tip master
+
+for b in \
+               quilt-tip-2             \
+               gitish-only             \
+               quilt-tip-1.1           \
+               patch-queue/quilt-tip   \
+               indep-arch              \
+; do
+       git branch -D $b
+done
+
+: 'see what gbp import-orig does'
+git checkout master
+gbp import-orig --upstream-version=2.0 ../$ust
+
+not-gdr-processable
+
+t-dch-commit -v 2.0-1 -m 'new upstream (did gbp import-orig)'
+t-dch-commit -r sid
+
+$ifarchive t-archive-none $p
+$ifarchive t-git-none
+$ifarchive t-dgit -wgf --gbp push-source --new
+
+t-salsa-add-remote
+git push --set-upstream origin master
+
+# OK now this looks like something more normal.
+# We have:
+#  maintainer (gbp) view                dgit view
+#    master
+#    debian/2.0-1                          archive/debian/2.0-1
+#    remotes/origin/master                 remotes/dgit/dgit/sid
+
+t-git-debrebase -fupstream-has-debian convert-from-gbp
+
+v=2.0-2
+t-dch-commit -v $v -m 'switch to git-debrebase, no other changes'
+t-dch-commit -r sid
+
+$ifarchive t-dgit -wgf push-source --new --overwrite
+git push
+
+cd ..
+
+$ifarchive t-archive-process-incoming sid
+
+t-setup-done '' "$(echo $p*) salsa $($ifarchive echo git mirror aq)" '
+       . $troot/lib-gdr
+       t-tstunt-parsechangelog
+       p=example
+       t-git-next-date
+'
+
+t-ok
diff --git a/tests/setup/gdr-convert-gbp-noarchive b/tests/setup/gdr-convert-gbp-noarchive
new file mode 100755 (executable)
index 0000000..dfeea3b
--- /dev/null
@@ -0,0 +1,9 @@
+#!/bin/bash
+set -e
+. tests/lib
+. $troot/lib-gdr
+
+t-dependencies GDR
+
+export ifarchive=:
+t-chain-test gdr-convert-gbp
diff --git a/tests/tests/gdr-diverge-nmu b/tests/tests/gdr-diverge-nmu
new file mode 100755 (executable)
index 0000000..15bf901
--- /dev/null
@@ -0,0 +1,61 @@
+#!/bin/bash
+set -e
+. tests/lib
+
+t-dependencies GDR
+
+t-setup-import gdr-convert-gbp
+
+cd $p
+
+t-dgit setup-mergechangelogs
+
+: 'maintainer'
+
+v=2.0-3
+t-maintainer-commit-some-changes
+
+t-git-next-date
+
+: 'non-dgit upload (but we prepare it with dgit anyway)'
+
+t-git-next-date
+git checkout origin/master
+
+v=2.0-2+nmu1
+t-nmu-upload-1 nmu
+gbp pq import
+t-nmu-upload-2
+t-nmu-commit-an-upstream-change
+t-nmu-upload-3
+
+: 'ad hocery'
+
+t-git-next-date
+git checkout master
+t-nmu-causes-ff-fail
+
+git cherry-pick 'dgit/dgit/sid^{/UPSTREAM NMU}'
+
+t-expect-fail 'Automatic merge failed; fix conflicts' \
+git merge --squash -m 'Incorporate NMU' dgit/dgit/sid
+
+git rm -rf debian/patches
+git commit -m 'Incorporate NMU'
+
+git merge -s ours -m 'Declare incorporate NMU' dgit/dgit/sid
+
+: 'right, how are we'
+
+t-git-next-date
+
+t-git-debrebase
+t-gdr-good laundered
+
+t-git-debrebase stitch
+t-gdr-good stitched
+
+
+t-nmu-reconciled-good patch-queue/nmu
+
+t-ok
diff --git a/tests/tests/gdr-diverge-nmu-dgit b/tests/tests/gdr-diverge-nmu-dgit
new file mode 100755 (executable)
index 0000000..4b5907a
--- /dev/null
@@ -0,0 +1,55 @@
+#!/bin/bash
+set -e
+. tests/lib
+
+t-dependencies GDR
+
+t-setup-import gdr-convert-gbp
+
+cd $p
+
+t-dgit setup-mergechangelogs
+
+: 'maintainer'
+
+git checkout master
+
+v=2.0-3
+t-maintainer-commit-some-changes
+
+t-git-next-date
+
+: 'nmu'
+
+git checkout -b nmu origin/master~0
+
+t-git-next-date
+
+v=2.0-2+nmu1
+t-nmu-commit-an-upstream-change
+t-dch-commit -v$v -m finalise
+t-dch-commit -r sid
+
+t-dgit -wgf push-source
+
+t-archive-process-incoming sid
+
+: 'rebase nmu onto our branch'
+
+t-git-next-date
+git checkout master
+t-nmu-causes-ff-fail
+
+git checkout dgit/dgit/sid # detach
+
+t-expect-fail 'E:CONFLICT.*Commit Debian 3\.0 \(quilt\) metadata' \
+git rebase master
+git rebase --skip
+
+git push . HEAD:master
+git checkout master
+
+
+t-nmu-reconciled-good nmu
+
+t-ok
diff --git a/tests/tests/gdr-edits b/tests/tests/gdr-edits
new file mode 100755 (executable)
index 0000000..6c77184
--- /dev/null
@@ -0,0 +1,40 @@
+#!/bin/bash
+set -e
+. tests/lib
+
+t-dependencies GDR
+
+t-setup-import gdr-convert-gbp
+
+cd $p
+
+v=2.0-3
+t-dch-commit -v $v -m testing
+
+t-git-debrebase analyse |tee ../anal.1
+cat ../anal.1
+
+t-some-changes edits
+
+t-dch-commit -r sid
+
+git tag t.before
+
+t-git-debrebase
+t-gdr-good laundered
+
+t-dgit push-source
+t-gdr-good pushed-interop
+
+git branch before-noop
+
+t-git-next-date
+t-git-debrebase
+t-git-debrebase stitch
+t-gdr-good pushed-interop
+
+t-refs-same-start
+t-ref-same refs/heads/before-noop
+t-ref-head
+
+t-ok
diff --git a/tests/tests/gdr-import-dgit b/tests/tests/gdr-import-dgit
new file mode 100755 (executable)
index 0000000..19918d8
--- /dev/null
@@ -0,0 +1,68 @@
+#!/bin/bash
+set -e
+. tests/lib
+
+t-dependencies GDR
+
+t-setup-import gdr-convert-gbp
+
+cd $p
+
+: 'non-dgit upload (but we prepare it with dgit anyway)'
+
+v=2.0-2+nmu1
+t-nmu-upload-1 nmu
+gbp pq import
+t-nmu-upload-2
+t-some-changes $numbranch
+t-nmu-upload-3
+
+: 'done the nmu, switching back to the maintainer hat'
+
+nmu-fold () {
+       t-git-next-date
+       t-dgit fetch
+       t-git-next-date
+       git merge --ff-only dgit/dgit/sid
+
+       git diff --exit-code patch-queue/$nmubranch
+
+       git branch unlaundered.$nmubranch
+
+       t-git-debrebase
+       t-gdr-good laundered
+
+       t-git-debrebase stitch
+       t-gdr-good stitched
+}
+
+nmu-fold
+
+v=2.0-3
+t-dch-commit -v $v -m "incorporate nmu"
+t-dch-commit -r sid
+t-dgit -wgf push-source
+
+: 'now test a new upstream'
+
+t-make-new-upstream-tarball 2.1
+
+git checkout master
+v=2.1-0+nmu1
+t-nmu-upload-1 nmu2
+
+gbp import-orig --upstream-version=2.1 --debian-branch=nmu2 ../$ust
+t-dch-commit -v $v -m "new upstream $v"
+gbp pq import
+
+#t-dgit -wgf build-source
+
+t-nmu-upload-2
+t-some-changes $numbranch
+t-nmu-upload-3
+
+: 'done the nmu, back to the maintainer'
+
+nmu-fold
+
+t-ok
diff --git a/tests/tests/gdr-newupstream-v0 b/tests/tests/gdr-newupstream-v0
new file mode 100755 (executable)
index 0000000..e866edc
--- /dev/null
@@ -0,0 +1,65 @@
+#!/bin/bash
+set -e
+. tests/lib
+
+t-dependencies NO-DGIT GDR
+
+t-setup-import gdr-convert-gbp-noarchive
+
+cd $p
+
+: 'upstream hat'
+
+new-upstream () {
+       uv=$1
+       t-git-next-date
+       git checkout make-upstream
+       git reset --hard upstream
+       t-make-new-upstream-tarball $uv
+       git push . make-upstream:upstream
+       git checkout master
+       t-git-next-date
+}
+
+new-upstream 2.1
+
+: 'maintainer hat'
+
+git branch startpoint
+v=2.1-1
+
+git checkout master
+
+t-expect-fail F:'Could not determine appropriate upstream commitish' \
+t-git-debrebase new-upstream-v0 $v
+
+git tag v2.1 upstream
+
+t-git-debrebase new-upstream-v0 $v
+t-gdr-good laundered
+
+t-git-debrebase stitch
+t-gdr-good stitched
+
+git branch ordinary
+
+: 'with --anchor'
+
+git reset --hard startpoint
+
+t-git-debrebase analyse >../anal.anch
+anchor=$(perl <../anal.anch -ne '
+       next unless m/^(\w+) Anchor\s/;
+       print $1,"\n";
+       exit;
+')
+
+t-git-debrebase --anchor=$anchor -fanchor-treated new-upstream-v0 $v upstream
+t-gdr-good laundered
+
+t-git-debrebase stitch
+t-gdr-good stitched
+
+git diff --quiet ordinary
+
+t-ok
diff --git a/tests/tests/gdr-subcommands b/tests/tests/gdr-subcommands
new file mode 100755 (executable)
index 0000000..e59fc07
--- /dev/null
@@ -0,0 +1,226 @@
+#!/bin/bash
+set -e
+. tests/lib
+
+t-dependencies GDR
+
+t-setup-import gdr-convert-gbp
+
+cd $p
+
+t-dgit setup-mergechangelogs
+
+mix-it () {
+       t-git-next-date
+
+       local m=$(git symbolic-ref HEAD)
+       t-some-changes "subcommands $m 1"
+
+       # we want patches mde by dgit, not gdr, for our test cases
+       t-dgit --git-debrebase=true -wgf quilt-fixup
+       t-git-next-date
+
+       t-some-changes "subcommands $m 2"
+       t-git-next-date
+}
+
+git checkout -b stitched-laundered master
+mix-it
+t-git-debrebase quick
+t-gdr-good stitched HEAD
+
+git checkout -b stitched-mixed master
+mix-it
+
+git checkout -b unstitched-laundered master
+mix-it
+t-git-debrebase
+t-gdr-good laundered
+
+git checkout -b unstitched-mixed master
+t-git-debrebase
+mix-it
+
+t-git-next-date
+
+git show-ref 
+
+subcmd () {
+       local subcmd=$1
+       shift
+       for startbranch in {stitched,unstitched}-{laundered,mixed}; do
+               work="work-$subcmd-$startbranch"
+
+               : "---------- $subcmd $startbranch ----------"
+
+               git for-each-ref "**/$startbranch"{,/**} \
+                       --format='create %(refname) %(objectname)' \
+               | sed "s/$startbranch/$work/" \
+               | git update-ref --stdin
+
+               git checkout $work
+               checkletters=$1; shift
+
+               before=before-$work
+               git branch $before
+
+               local xopts=''
+
+               case "$checkletters" in
+               XX*)
+                       fail "$checkletters" # for debugging
+                       ;;
+               esac
+
+               case "$checkletters" in
+               X*)
+                       t-expect-fail E:'snags: [0-9]* blockers' \
+                       t-git-debrebase $xopts $subcmd
+                       xopts+=' --force'
+                       next_checkletter
+                       ;;
+               esac
+
+               case "$checkletters" in
+               N*)
+                       t-expect-fail E:. \
+                       t-git-debrebase $xopts $subcmd
+                       xopts+=' --noop-ok'
+                       next_checkletter
+                       ;;
+               esac
+
+               case "$checkletters" in
+               [EF]:*)
+                       t-expect-fail "$checkletters" \
+                       t-git-debrebase $xopts $subcmd
+                       continue
+                       ;;
+               *)
+                       t-git-debrebase $xopts $subcmd
+                       ;;
+               esac
+
+               peel=peel-$subcmd-$startbranch
+               git checkout -b $peel
+               t-clean-on-branch $peel
+
+               : "---------- $subcmd $startbranch $checkletters ----------"
+
+               while [ "x$checkletters" != x ]; do
+                       : "---- $subcmd $startbranch ...$checkletters ----"
+                       make_check "$checkletters"
+                       checkletters="${checkletters#?}"
+               done
+       done
+
+}
+
+next_checkletter () {
+       checkletters="${checkletters#?}"
+}
+
+make_check () {
+       case "$1" in
+       [Nn]*)
+               t-refs-same-start
+               t-refs-same refs/heads/$before refs/heads/$work
+               ;;
+       U*)
+               t-refs-same-start
+               t-refs-same refs/heads/$before refs/ffq-prev/heads/$work
+               make_check u
+               ;;
+       u*)
+               t-git-get-ref refs/ffq-prev/heads/$work
+               t-refs-notexist refs/debrebase-last/heads/$work
+               ;;
+       V*)
+               t-refs-same-start
+               t-refs-same refs/ffq-prev/heads/$work \
+                       refs/ffq-prev/heads/$startbranch
+               t-refs-notexist refs/debrebase-last/heads/$work
+               ;;
+       s*)
+               t-refs-notexist refs/ffq-prev/heads/$work
+               t-refs-same-start
+               t-refs-same refs/debrebase-last/heads/$work \
+                       refs/debrebase-last/heads/$startbranch
+               t-has-ancestor HEAD refs/debrebase-last/heads/$work
+               ;;
+       S*)
+               t-refs-notexist refs/ffq-prev/heads/$work
+               t-refs-same-start refs/debrebase-last/heads/$work
+               t-ref-head
+               git diff --quiet HEAD^1
+               git diff HEAD^2 | grep $startbranch
+               git reset --hard HEAD^1
+               ;;
+       P*)
+               t-dgit -wgf --quilt=nofix quilt-fixup
+               git diff HEAD~ debian/patches | egrep .
+               git diff --quiet HEAD~ -- ':.' ':!debian/patches'
+               git reset --hard HEAD~
+               ;;
+       l*)
+               git diff --quiet HEAD refs/heads/$before -- ':.' ':!debian/patches'
+               t-gdr-good laundered
+               ;;
+       t*)
+               git diff --quiet HEAD refs/heads/$before
+               ;;
+       f*)
+               t-has-ancestor HEAD refs/heads/$before
+               ;;
+       *)
+               fail "$1"
+               ;;
+       esac
+}
+
+Ec="F:No ongoing git-debrebase session"
+Ep="F:Patch export produced patch amendments"
+
+# input state:
+#  stitched?           st'd    st'd    unst'd  unst'd
+#  laundered?          laund'd mixed   laund'd mixed
+#
+# "mixed" means an out of order branch
+# containing mixed commits and patch additions,
+# but which needs even more patches
+#
+subcmd ''              Ult     Ull     Vlt     Vl
+subcmd stitch          Ns      Nu      Sltf    Stf
+subcmd prepush         Ns      Nu      Sltf    Stf
+subcmd quick           ns      Sl      Sltf    Sl
+subcmd conclude        "$Ec"   "$Ec"   Sltf    Sl
+subcmd make-patches    sPft    "$Ep"   uPft    "$Ep"
+#subcmd        dgit-upload-hook Psft   "$Ep"   SPft    "$Ep"
+#
+# result codes, each one is a check:
+#   E:$pat     } this is an error (must come first)
+#   F:$pat     } arg is passed to expect-fail
+#
+#   X          should fail due to snags, but succeed when forced
+#   XX         crash out of script for manual debugging
+#
+#   N          this is a noop, error unless --noop-ok
+#   n          this is a silent noop
+# both of these imply tf; but, specify also one of   u s
+#
+# should normally specify one of these:
+#   U          just unstiched: ffq-prev is exactly previous HEAD; implies u
+#   u          result is unstitched
+#   V          ffq-prev remains unchanged; implies also u
+#   s          result is stitched, debrebase-last exists and is unchanged
+#   S          result is stitch just made, remaining letters apply to result~
+#
+#   P          result is add-patches, remaining letters apply to result~
+#
+# should normally specify one or both of these:
+#   l          result is laundered, tree is same as before minus d/patches
+#   t          tree is exactly same as before
+#
+#   f          result is ff from previous HEAD
+
+t-ok
diff --git a/tests/tests/gdr-viagit b/tests/tests/gdr-viagit
new file mode 100755 (executable)
index 0000000..644d2d4
--- /dev/null
@@ -0,0 +1,40 @@
+#!/bin/bash
+set -e
+. tests/lib
+
+t-dependencies NO-DGIT GDR
+
+t-setup-import gdr-convert-gbp-noarchive
+
+: 'set up so t-git-debrebase runs gdr via git'
+
+case "$DGIT_GITDEBREBASE_TEST" in
+''|git-debrebase)      ;;
+*)
+       t-tstunt
+       st=$tmp/tstunt/git-debrebase
+       export DGIT_GITDEBREBASE_TEST_REAL="$DGIT_GITDEBREBASE_TEST"
+       cat <<'END' >$st
+#!/bin/sh
+set -x
+exec "$DGIT_GITDEBREBASE_TEST_REAL" "$@"
+END
+       chmod +x $st
+       ;;
+esac
+
+DGIT_GITDEBREBASE_TEST='git debrebase'
+
+: 'do a simple test'
+
+cd $p
+
+t-some-changes
+
+t-git-debrebase
+t-gdr-good laundered
+
+t-git-debrebase stitch --prose=wombat
+t-gdr-good stitched
+
+t-ok