chiark / gitweb /
po4a: pairwise-pocheck: Force it to be run sometimes
[dgit.git] / git-debrebase
index 7629a148b6acf32f686865a7410a33c9ad44f8ac..5e8a8bb2dc81b979e1e15f95cc5ad744a67da31a 100755 (executable)
@@ -21,6 +21,7 @@
 END { $? = $Debian::Dgit::ExitStatus::desired // -1; };
 use Debian::Dgit::GDR;
 use Debian::Dgit::ExitStatus;
+use Debian::Dgit::I18n;
 
 use strict;
 
@@ -30,6 +31,7 @@ setup_sigwarn();
 use Memoize;
 use Carp;
 use POSIX;
+use Locale::gettext;
 use Data::Dumper;
 use Getopt::Long qw(:config posix_default gnu_compat bundling);
 use Dpkg::Version;
@@ -38,7 +40,7 @@ use File::Copy;
 
 $debugcmd_when_debuglevel = 2;
 
-our ($usage_message) = <<'END';
+our ($usage_message) = i_ <<'END';
 usages:
   git-debrebase [<options>] [--|-i <git rebase options...>]
   git-debrebase [<options>] status
@@ -62,7 +64,7 @@ $|=1;
 
 sub badusage ($) {
     my ($m) = @_;
-    print STDERR "$us: bad usage: $m\n";
+    print STDERR f_ "%s: bad usage: %s\n", $us, $m;
     finish 8;
 }
 
@@ -73,7 +75,7 @@ sub getoptions_main {
 }
 sub getoptions {
     my $sc = shift;
-    getoptions_main "bad options follow \`git-debrebase $sc'", @_;
+    getoptions_main +(f_ "bad options follow \`git-debrebase %s'", $sc), @_;
 }
 
 sub cfg ($;$) {
@@ -84,7 +86,7 @@ sub cfg ($;$) {
     push @cmd, $k;
     my $out = cmdoutput_errok @cmd;
     if (!defined $out) {
-       fail "missing required git config $k" unless $optional;
+       fail f_ "missing required git config %s", $k unless $optional;
        return ();
     }
     my @l = split /\0/, $out;
@@ -103,7 +105,7 @@ sub dd ($) {
 sub get_commit ($) {
     my ($objid) = @_;
     my $data = (git_cat_file $objid, 'commit');
-    $data =~ m/(?<=\n)\n/ or die "$objid ($data) ?";
+    $data =~ m/(?<=\n)\n/ or confess "$objid ($data) ?";
     return ($`,$');
 }
 
@@ -142,10 +144,10 @@ sub run_ref_updates_now ($$) {
 
     my @upd_cmd = (git_update_ref_cmd "debrebase: $mrest", qw(--stdin));
     debugcmd '>|', @upd_cmd;
-    open U, "|-", @upd_cmd or die $!;
+    open U, "|-", @upd_cmd or confess $!;
     foreach (@$updates) {
        printdebug ">= ", $_, "\n";
-       print U $_, "\n" or die $!;
+       print U $_, "\n" or confess $!;
     }
     printdebug ">\$\n";
     close U or failedcmd @upd_cmd;
@@ -171,7 +173,8 @@ sub all_snags_summarised () {
 sub run_deferred_updates ($) {
     my ($mrest) = @_;
 
-    confess 'dangerous internal error' unless all_snags_summarised();
+    my $m = 'dangerous internal error';
+    confess $m.' - '.__ $m unless all_snags_summarised();
 
     merge_wreckage_cleaning \@deferred_updates;
     run_ref_updates_now $mrest, \@deferred_updates;
@@ -196,10 +199,12 @@ sub get_tree ($;$$) {
        return () if $type eq 'missing';
     }
 
+    $recurse = !!$recurse;
+
     confess "get_tree needs object not $x ?" unless $x =~ m{^[0-9a-f]+\:};
 
     our (@get_tree_memo, %get_tree_memo);
-    my $memo = $get_tree_memo{$x};
+    my $memo = $get_tree_memo{$recurse,$x};
     return @$memo if $memo;
 
     local $Debian::Dgit::debugcmd_when_debuglevel = 3;
@@ -216,7 +221,7 @@ sub get_tree ($;$$) {
        push @l, [ $n, $i ];
        confess "$x need $last < $n ?" unless $last lt $n;
     }
-    $get_tree_memo{$x} = \@l;
+    $get_tree_memo{$recurse,$x} = \@l;
     push @get_tree_memo, $x;
     if (@get_tree_memo > 10) {
        delete $get_tree_memo{ shift @get_tree_memo };
@@ -230,12 +235,13 @@ sub trees_diff_walk ($$$;$) {
     # $x and $y are as for get_tree
     # where $name, $ix, $iy are $name and $info from get_tree
     # opts are   all       call even for names same in both
+    #            recurse   call even for names same in both
     my $opts = shift @_ if @_>=4;
     my ($x,$y,$call) = @_;
     my $all = $opts->{all};
     return if !$all and $x eq $y;
-    my @x = get_tree $x;
-    my @y = get_tree $y;
+    my @x = get_tree $x, 0, $opts->{recurse};
+    my @y = get_tree $y, 0, $opts->{recurse};
     printdebug "trees_diff_walk(..$x,$y..) ".Dumper(\@x,\@y)
        if $debuglevel >= 3;
     while (@x || @y) {
@@ -307,13 +313,16 @@ sub get_differs ($$) {
 
            my $xp = $ix && "$xd/patches";
            my $yp = $iy && "$yd/patches";
-           trees_diff_walk $xp, $yp, sub {
+           trees_diff_walk { recurse=>1 }, $xp, $yp, sub {
                my ($n,$ix,$iy) = @_;
 
                # analyse difference in debian/patches
 
                my $ok;
-               if ($n !~ m/\.series$/s && !$ix && $plain->($iy)) {
+               if ($n =~ m{/$}s) {
+                   # we are recursing; directories may appear and disappear
+                   $ok = 1;
+               } elsif ($n !~ m/\.series$/s && !$ix && $plain->($iy)) {
                    $ok = 1;
                } elsif ($n eq 'series' && $plain->($ix) && $plain->($iy)) {
                    my $x_s = (git_cat_file "$xp/series", 'blob');
@@ -389,10 +398,10 @@ 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";
+       print STDERR f_ "%s: snag ignored (-f%s): %s\n", $us, $tag, $msg;
     } else {
        $snags_tripped++;
-       print STDERR "git-debrebase: snag detected (-f$tag): $msg\n";
+       print STDERR f_ "%s: snag detected (-f%s): %s\n", $us, $tag, $msg;
     }
 }
 
@@ -404,17 +413,17 @@ sub snag ($$;@) {
 sub snags_maybe_bail () {
     return if all_snags_summarised();
     if ($snags_forced) {
-       printf STDERR
+       print STDERR f_
            "%s: snags: %d overriden by individual -f options\n",
            $us, $snags_forced;
     }
     if ($snags_tripped) {
        if ($opt_force) {
-           printf STDERR
+           print STDERR f_
                "%s: snags: %d overriden by global --force\n",
                $us, $snags_tripped;
        } else {
-           fail sprintf
+           fail f_
   "%s: snags: %d blocker(s) (you could -f<tag>, or --force)",
                $us, $snags_tripped;
        }
@@ -446,26 +455,26 @@ sub fail_unprocessable ($) {
     changedir $maindir;
     my ($ffqs, $ffqm, $symref, $ffq_prev, $gdrlast) = ffq_prev_branchinfo();
 
-    my $mangled = <<END;
+    my $mangled = __ <<END;
 Branch/history seems mangled - no longer in gdr format.
 See ILLEGAL OPERATIONS in git-debrebase(5).
 END
     chomp $mangled;
 
     if (defined $ffqm) {
-       fail <<END;
-$msg
-Is this meant to be a gdr branch?  $ffqm
+       fail f_ <<END, $msg, $ffqm;
+%s
+Is this meant to be a gdr branch?  %s
 END
     } elsif (git_get_ref $ffq_prev) {
-       fail <<END;
-$msg
-$mangled
+       fail f_ <<END, $msg, $mangled;
+%s
+%s
 Consider git-debrebase scrap, to throw away your recent work.
 END
     } elsif (!git_get_ref $gdrlast) {
-       fail <<END;
-$msg
+       fail f_ <<END, $msg;
+%s
 Branch does not seem to be meant to be a git-debrebase branch?
 Wrong branch, or maybe you needed git-debrebase convert-from-*.
 END
@@ -475,8 +484,8 @@ $msg
 $mangled
 END
     } else {
-       fail <<END;
-$msg
+       fail f_ <<END, $msg;
+%s
 Branch/history mangled, and diverged since last git-debrebase.
 Maybe you reset to, or rebased from, somewhere inappropriate.
 END
@@ -488,6 +497,7 @@ sub gbp_pq_export ($$$) {
     # must be run in a workarea.  $bname and patch-queue/$bname
     # ought not to exist.  Leaves you on patch-queue/$bname with
     # the patches staged but not committed.
+    # returns 1 if there were any patches
     printdebug "gbp_pq_export $bname $base $tip\n";
     runcmd @git, qw(checkout -q -b), $bname, $base;
     runcmd @git, qw(checkout -q -b), "patch-queue/$bname", $tip;
@@ -497,7 +507,9 @@ sub gbp_pq_export ($$$) {
        { local ($!,$?); copy('../gbp-pq-err', \*STDERR); }
        failedcmd @gbp_cmd;
     }
-    runcmd @git, qw(add -f debian/patches) if stat_exists 'debian/patches';
+    return 0 unless stat_exists 'debian/patches';
+    runcmd @git, qw(add -f debian/patches);
+    return 1;
 }
 
 
@@ -594,22 +606,21 @@ sub merge_series ($$$;@) {
        playtree_setup();
        foreach my $q ($base_q, reverse @input_qs) {
            my $s = $q->{MR}{S};
-           gbp_pq_export "p-$s", $q->{SeriesBase}, $q->{SeriesTip};
+           my $any = gbp_pq_export "p-$s", $q->{SeriesBase}, $q->{SeriesTip};
            my @earlier;
-           if (open S, $seriesfile) {
+           if ($any) {
+               open S, $seriesfile or confess "$seriesfile $!";
                while (my $patch = <S>) {
-                   chomp $patch or die $!;
+                   chomp $patch or confess $!;
                    $prereq{$patch} //= {};
                    foreach my $earlier (@earlier) {
-                       $prereq{$patch}{$earlier}{$s}++ and die;
+                       $prereq{$patch}{$earlier}{$s}++ and confess;
                    }
                    push @earlier, $patch;
-                   stat "debian/patches/$patch" or die "$patch ?";
+                   stat "debian/patches/$patch" or confess "$patch ?";
                }
-               S->error and die "$seriesfile $!";
+               S->error and confess "$seriesfile $!";
                close S;
-           } else {
-               die "$seriesfile $!" unless $!==ENOENT;
            }
            read_tree_upstream $newbase, 1;
            my $pec = make_commit [ grep { defined } $base_q->{MR}{PEC} ], [
@@ -687,7 +698,7 @@ sub merge_series ($$$;@) {
        my $authordate = sub {
            my ($f) = @_;
            $authordate{$f} //= do {
-               open PF, "<", "debian/patches/$f" or die "$f $!";
+               open PF, "<", "debian/patches/$f" or confess "$f $!";
                while (<PF>) {
                    return $nodate if m/^$/;
                    last if s{^Date: }{};
@@ -697,7 +708,7 @@ sub merge_series ($$$;@) {
            };
        };
 
-       open NS, '>', $seriesfile or die $!;
+       open NS, '>', $seriesfile or confess $!;
 
        while (keys %prereq) {
            my $best;
@@ -712,7 +723,7 @@ sub merge_series ($$$;@) {
                $best = $try;
            }
            printdebug "merge_series series next $best\n";
-           print NS "$best\n" or die $!;
+           print NS "$best\n" or confess $!;
            delete $prereq{$best};
            foreach my $gp (values %prereq) {
                delete $gp->{$best};
@@ -754,9 +765,9 @@ sub merge_series_patchqueue_convert ($$$) {
            my $tree = cmdoutput @git, qw(write-tree);
            $commit =~ s{^parent (\S+)$}{parent $build}m or confess;
            $commit =~ s{^tree (\S+)$}{tree $tree}m      or confess;
-           open C, ">", "../mcommit" or die $!;
-           print C $commit or die $!;
-           close C or die $!;
+           open C, ">", "../mcommit" or confess $!;
+           print C $commit or confess $!;
+           close C or confess $!;
            $build = cmdoutput @git, qw(hash-object -w -t commit ../mcommit);
        }
        $result = $build;
@@ -827,11 +838,11 @@ sub parsecommit ($;$) {
     #    $p_ref, if provided, must be [] and is used as a base for Parents
 
     $p_ref //= [];
-    die if @$p_ref;
+    confess if @$p_ref;
 
     my ($h,$m) = get_commit $objid;
 
-    my ($t) = $h =~ m/^tree (\w+)$/m or die $objid;
+    my ($t) = $h =~ m/^tree (\w+)$/m or confess $objid;
     my (@ph) = $h =~ m/^parent (\w+)$/mg;
 
     my $r = {
@@ -920,9 +931,11 @@ sub classify ($) {
        # 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");
+       my $badanchor = sub {
+           $unknown->(f_ "git-debrebase \`anchor' but %s", "@_");
+       };
+       @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
@@ -948,11 +961,11 @@ sub classify ($) {
        # 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[0]{IsOrigin} and $badanchor->(__ "is an origin commit");
        $p[1]{Differs} & ~DS_DEB and
-           $badanchor->("upstream files differ from left parent");
+           $badanchor->(__ "upstream files differ from left parent");
        $p[0]{Differs} & ~D_UPS and
-           $badanchor->("debian/ differs from right parent");
+           $badanchor->(__ "debian/ differs from right parent");
 
        return $classify->(qw(Anchor),
                           OrigParents => [ $p[1] ]);
@@ -963,7 +976,7 @@ sub classify ($) {
        if ($d == D_PAT_ADD) {
            return $classify->(qw(AddPatches));
        } elsif ($d & (D_PAT_ADD|D_PAT_OTH)) {
-           return $unknown->("edits debian/patches");
+           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') {
@@ -975,20 +988,20 @@ sub classify ($) {
            } elsif ($ty eq 'missing') {
                return $classify->(qw(BreakwaterStart));
            } else {
-               return $unknown->("parent's debian is not a directory");
+               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");
+           return $unknown->(__ "no changes");
        } else {
            confess "internal error $objid ?";
        }
     }
     if (!@p) {
-       return $unknown->("origin commit");
+       return $unknown->(__ "origin commit");
     }
 
     if (@p == 2 && @identical == 1) {
@@ -1001,7 +1014,8 @@ sub classify ($) {
     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} ?";
+           $ph =~ m/^committer .* (\d+) [-+]\d+$/m
+               or confess "$_->{CommitId} ?";
            $1;
        };
        my @bytime = @p;
@@ -1038,14 +1052,14 @@ sub classify ($) {
        return $classify->("MergedBreakwaters");
     }
     if ($r->{Msg} =~ m{^\[(git-debrebase|dgit)[: ].*\]$}m) {
-       return $unknown->("unknown kind of merge from $1");
+       return $unknown->(f_ "unknown kind of merge from %s", $1);
     }
     if (@p > 2) {
-       return $unknown->("octopus merge");
+       return $unknown->(__ "octopus merge");
     }
 
     if (!$opt_merges) {
-       return $unknown->("general two-parent merge");
+       return $unknown->(__ "general two-parent merge");
     }
 
     return $classify->("VanillaMerge");
@@ -1062,7 +1076,8 @@ sub mergedbreakwaters_anchor ($) {
        $best_anchor = $panchor
            if !defined $best_anchor
            or is_fast_fwd $best_anchor, $panchor;
-       fail "inconsistent anchors in merged-breakwaters $p->{CommitId}"
+       fail f_ "inconsistent anchors in merged-breakwaters %s",
+           $p->{CommitId}
            unless is_fast_fwd $panchor, $best_anchor;
     }
     return $best_anchor;
@@ -1072,9 +1087,8 @@ sub keycommits ($;$$$$$) {
     my ($head, $furniture, $unclean, $trouble, $fatal, $claimed_bw) = @_;
     # => ($anchor, $breakwater)
 
-    # $unclean->("unclean-$tagsfx", $msg, $cl)
     # $furniture->("unclean-$tagsfx", $msg, $cl)
-    # $dgitimport->("unclean-$tagsfx", $msg, $cl))
+    # $unclean->("unclean-$tagsfx", $msg, $cl)
     #   is callled for each situation or commit that
     #   wouldn't be found in a laundered branch
     # $furniture is for furniture commits such as might be found on an
@@ -1102,7 +1116,7 @@ sub keycommits ($;$$$$$) {
     my $x = sub {
        my ($cb, $tagsfx, $mainwhy, $xwhy) = @_;
        my $why = $mainwhy.$xwhy;
-       my $m = "branch needs laundering (run git-debrebase): $why";
+       my $m = f_ "branch needs laundering (run git-debrebase): %s", $why;
        fail $m unless defined $cb;
        return unless $cb;
        $cb->("unclean-$tagsfx", $why, $cl, $mainwhy);
@@ -1130,41 +1144,47 @@ sub keycommits ($;$$$$$) {
            $found_anchor->($head);
        } elsif ($ty eq 'Upstream') {
            $x->($unclean, 'ordering',
- "packaging change ($breakwater) follows upstream change"," (eg $head)")
+ (f_ "packaging change (%s) follows upstream change", $breakwater),
+                (f_ " (eg %s)", $head))
                if defined $breakwater;
            $clogonly = undef;
            $breakwater = undef;
        } elsif ($ty eq 'Mixed') {
            $x->($unclean, 'mixed',
-                "found mixed upstream/packaging commit"," ($head)");
+                (__ "found mixed upstream/packaging commit"),
+                (f_ " (%s)", $head));
            $clogonly = undef;
            $breakwater = undef;
        } elsif ($ty eq 'Pseudomerge' or
                 $ty eq 'AddPatches') {
            my $found_pm = 1;
            $x->($furniture, (lc $ty),
-                "found interchange bureaucracy commit ($ty)"," ($head)");
+                (f_ "found interchange bureaucracy commit (%s)", $ty),
+                (f_ " (%s)", $head));
        } elsif ($ty eq 'DgitImportUnpatched') {
            if ($found_pm) {
                $x->($trouble, 'dgitimport',
-                    "found dgit dsc import"," ($head)");
+                    (__ "found dgit dsc import"),
+                    (f_ " (%s)", $head));
                return (undef,undef);
            } else {
                $x->($fatal, 'unprocessable',
-                    "found bare dgit dsc import with no prior history",
-                    " ($head)");
+                    (__ "found bare dgit dsc import with no prior history"),
+                    (f_ " (%s)", $head));
                return (undef,undef);
            }
        } elsif ($ty eq 'VanillaMerge') {
            $x->($trouble, 'vanillamerge',
-                "found vanilla merge"," ($head)");
+                (__ "found vanilla merge"),
+                (f_ " (%s)", $head));
            return (undef,undef);
        } elsif ($ty eq 'MergedBreakwaters') {
            $found_anchor->(mergedbreakwaters_anchor $cl);
        } else {
            $x->($fatal, 'unprocessable',
-                "found unprocessable commit, cannot cope: $cl->{Why}",
-                " ($head)");
+                (f_ "found unprocessable commit, cannot cope: %s",
+                    $cl->{Why}),
+                (f_ " (%s)", $head));
            return (undef,undef);
        }
        $head = $cl->{Parents}[0]{CommitId};
@@ -1193,10 +1213,12 @@ sub walk ($;$$$) {
     my $cl;
     my $xmsg = sub {
        my ($prose, $info) = @_;
+       # We deliberately do not translate $prose, since this mostly
+       # appears in commits in Debian and they should be in English.
        my $ms = $cl->{Msg};
        chomp $ms;
-       $info //= '';
-       $ms .= "\n\n[git-debrebase$info: $prose]\n";
+       confess unless defined $info;
+       $ms .= "\n\n[git-debrebase $info: $prose]\n";
        return (Msg => $ms);
     };
     my $rewrite_from_here = sub {
@@ -1223,12 +1245,14 @@ sub walk ($;$$$) {
        if ($nogenerate) {
            return (undef,undef);
        }
-       fail_unprocessable "found unprocessable commit, cannot cope".
-           (defined $cl->{Why} ? "; $cl->{Why}:": ':').
-           " (commit $cur) (d.".
-           (join ' ', map { sprintf "%#x", $_->{Differs} }
-            @{ $cl->{Parents} }).
-                ")";
+       my $d =
+           join ' ',
+           map { sprintf "%#x", $_->{Differs} }
+           @{ $cl->{Parents} };
+       fail_unprocessable f_ +(defined $cl->{Why}
+ ? i_ 'found unprocessable commit, cannot cope; %3$s: (commit %1$s) (d.%2$s)'
+ : i_ 'found unprocessable commit, cannot cope: (commit %1$s) (d.%2$s)'),
+                                   $cur, $d, $cl->{Why};
     };
 
     my $build;
@@ -1277,7 +1301,7 @@ sub walk ($;$$$) {
        } elsif ($ty eq 'Mixed') {
            my $queue = sub {
                my ($q, $wh) = @_;
-               my $cls = { %$cl, $xmsg->("split mixed commit: $wh part") };
+               my $cls = { %$cl, $xmsg->("mixed commit: $wh part",'split') };
                push @$q, $cls;
            };
            $queue->(\@brw_cl, "debian");
@@ -1330,12 +1354,12 @@ sub walk ($;$$$) {
                push @brw_cl, {
                    %$cl,
                    SpecialMethod => 'DgitImportDebianUpdate',
-                    $xmsg->("convert dgit import: debian changes")
+                    $xmsg->("debian changes", 'convert dgit import')
                }, {
                    %$cl,
                    SpecialMethod => 'DgitImportUpstreamUpdate',
                     $xmsg->("convert dgit import: upstream update",
-                           " anchor")
+                           "anchor")
                };
                $prline->(" Import");
                $rewrite_from_here->(\@brw_cl);
@@ -1348,10 +1372,10 @@ sub walk ($;$$$) {
                # patches as commits.  Unfortunately it contains
                # debian/patches/.
                printdebug "*** WALK BOMB bare dgit import\n";
-               $cl->{Why} = "bare dgit dsc import";
+               $cl->{Why} = __ "bare dgit dsc import";
                return $bomb->();
            }
-           die "$ty ?";
+           confess "$ty ?";
        } elsif ($ty eq 'MergedBreakwaters') {
            $last_anchor = mergedbreakwaters_anchor $cl;
            $build_start->(' MergedBreakwaters', $cur);
@@ -1528,7 +1552,7 @@ sub walk ($;$$$) {
                 %$cl,
                 SpecialMethod => 'MergeCreateMergedBreakwaters',
                 $xmsg->('constructed from vanilla merge',
-                       ' merged-breakwater'),
+                       'merged-breakwater'),
             };
            push @upp_cl, {
                 %$cl,
@@ -1559,10 +1583,10 @@ sub walk ($;$$$) {
 
     printdebug "WALK REBUILD $build ".(scalar @processed)."\n";
 
-    confess "internal error" unless $build eq (pop @processed)->{CommitId};
+    confess __ "internal error" unless $build eq (pop @processed)->{CommitId};
 
     in_workarea sub {
-       mkdir $rd or $!==EEXIST or die $!;
+       mkdir $rd or $!==EEXIST or confess $!;
        my $current_method;
        my $want_debian = $build;
        my $want_upstream = $build;
@@ -1672,9 +1696,9 @@ sub walk ($;$$$) {
                        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 $!;
+               open CD, ">", $cf or confess $!;
+               print CD $ch, "\n", $cl->{Msg} or confess $!;
+               close CD or confess $!;
                my @cmd = (@git, qw(hash-object));
                push @cmd, qw(-w) if $rewriting;
                push @cmd, qw(-t commit), $cf;
@@ -1688,13 +1712,13 @@ sub walk ($;$$$) {
                    my ($n) = @_;
                    no warnings qw(exiting);
                    next if $n eq 'debian/';
-                   confess "mismatch @_ ?";
+                   confess f_ "mismatch %s ?", "@_";
                };
                trees_diff_walk "$want_debian:debian", "$build:debian", sub {
-                   confess "mismatch @_ ?";
+                   confess f_ "mismatch %s ?", "@_";
                };
                my @old_parents = map { $_->{CommitId} } @{ $cl->{Parents} };
-               confess "mismatch @parents != @old_parents ?"
+               confess f_ "mismatch %s != %s ?", "@parents", "@old_parents"
                    unless "@parents" eq "@old_parents";
            }
             if (grep { $method eq $_ } qw(DgitImportUpstreamUpdate)) {
@@ -1704,7 +1728,7 @@ sub walk ($;$$$) {
     };
 
     my $final_check = get_differs $build, $input;
-    die sprintf "internal error %#x %s %s", $final_check, $input, $build
+    confess f_ "internal error %#x %s %s", $final_check, $input, $build
        if $final_check & ~D_PAT_ADD;
 
     my @r = ($build, $breakwater, $last_anchor);
@@ -1732,7 +1756,7 @@ sub update_head_checkout ($$$) {
 sub update_head_postlaunder ($$$) {
     my ($old, $tip, $reflogmsg) = @_;
     return if $tip eq $old && !@deferred_updates;
-    print "git-debrebase: laundered (head was $old)\n";
+    print f_ "%s: laundered (head was %s)\n", $us, $old;
     update_head $old, $tip, $reflogmsg;
     # no tree changes except debian/patches
     runcmd @git, qw(rm --quiet --ignore-unmatch -rf debian/patches);
@@ -1746,7 +1770,7 @@ sub currently_rebasing() {
 }
 
 sub bail_if_rebasing() {
-    fail "you are in the middle of a git-rebase already"
+    fail __ "you are in the middle of a git-rebase already"
        if currently_rebasing();
 }
 
@@ -1772,14 +1796,14 @@ sub cmd_launder_v0 () {
 
 sub defaultcmd_rebase () {
     push @ARGV, @{ $opt_defaultcmd_interactive // [] };
-    my ($tip,$breakwater) = do_launder_head 'launder for rebase';
+    my ($tip,$breakwater) = do_launder_head __ 'launder for rebase';
     runcmd @git, qw(rebase), @ARGV, $breakwater if @ARGV;
 }
 
 sub cmd_analyse () {
-    badusage "analyse does not support any options"
+    badusage __ "analyse does not support any options"
        if @ARGV and $ARGV[0] =~ m/^-/;
-    badusage "too many arguments to analyse" if @ARGV>1;
+    badusage __ "too many arguments to analyse" if @ARGV>1;
     my ($old) = @ARGV;
     if (defined $old) {
        $old = git_rev_parse $old;
@@ -1787,7 +1811,7 @@ sub cmd_analyse () {
        $old = git_rev_parse 'HEAD';
     }
     my ($dummy,$breakwater) = walk $old, 1,*STDOUT;
-    STDOUT->error and die $!;
+    STDOUT->error and confess $!;
 }
 
 sub ffq_check ($;$$) {
@@ -1804,7 +1828,7 @@ sub ffq_check ($;$$) {
     # normally $currentval should be HEAD
     my ($currentval, $ff, $notff) =@_;
 
-    $ff //= sub { print $_[0] or die $!; };
+    $ff //= sub { print $_[0] or confess $!; };
     $notff //= \&snag;
 
     my ($status, $message, $current, $ffq_prev, $gdrlast)
@@ -1812,9 +1836,9 @@ sub ffq_check ($;$$) {
     return ($status, $message) unless $status eq 'branch';
 
     my $exists = git_get_ref $ffq_prev;
-    return ('exists',"$ffq_prev already exists") if $exists;
+    return ('exists', f_ "%s already exists", $ffq_prev) if $exists;
 
-    return ('not-branch', 'HEAD symref is not to refs/heads/')
+    return ('not-branch', __ 'HEAD symref is not to refs/heads/')
        unless $current =~ m{^refs/heads/};
     my $branch = $';
 
@@ -1837,14 +1861,15 @@ sub ffq_check ($;$$) {
        return unless length $lrval;
 
        if (is_fast_fwd $lrval, $currentval) {
-           $ff->("OK, you are ahead of $lrref\n");
+           $ff->(f_ "OK, you are ahead of %s\n", $lrref);
            $checked{$lrref} = 1;
        } elsif (is_fast_fwd $currentval, $lrval) {
            $checked{$lrref} = -1;
-           $notff->('behind', "you are behind $lrref, divergence risk");
+           $notff->('behind', f_ "you are behind %s, divergence risk",
+                                 $lrref);
        } else {
            $checked{$lrref} = -1;
-           $notff->('diverged', "you have diverged from $lrref");
+           $notff->('diverged', f_ "you have diverged from %s", $lrref);
        }
     };
 
@@ -1865,9 +1890,11 @@ sub ffq_check ($;$$) {
                        'remote push branch');
     }
     if ($branch =~ m{^dgit/}) {
-       $check->("refs/remotes/dgit/$branch", 'remote dgit branch');
+       $check->("refs/remotes/dgit/$branch",
+                __ 'remote dgit branch');
     } elsif ($branch =~ m{^master$}) {
-       $check->("refs/remotes/dgit/dgit/sid", 'remote dgit branch for sid');
+       $check->("refs/remotes/dgit/dgit/sid",
+                __ 'remote dgit branch for sid');
     }
     return (undef, undef, $ffq_prev, $gdrlast);
 }
@@ -1894,7 +1921,8 @@ sub record_ffq_prev_deferred () {
 
     push @deferred_updates, "update $ffq_prev $currentval $git_null_obj";
     push @deferred_updates, "delete $gdrlast";
-    push @deferred_update_messages, "Recorded previous head for preservation";
+    push @deferred_update_messages,
+       __ "Recorded previous head for preservation";
     return ('deferred', undef);
 }
 
@@ -1902,7 +1930,7 @@ 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";
+       snag $status, f_ "could not record ffq-prev: %s", $message;
        snags_maybe_bail();
     }
 }
@@ -1913,7 +1941,7 @@ sub ffq_prev_info () {
     my ($status, $message, $current, $ffq_prev, $gdrlast)
        = ffq_prev_branchinfo();
     if ($status ne 'branch') {
-       snag $status, "could not check ffq-prev: $message";
+       snag $status, f_ "could not check ffq-prev: %s", $message;
        snags_maybe_bail();
     }
     my $ffq_prev_commitish = $ffq_prev && git_get_ref $ffq_prev;
@@ -1933,7 +1961,7 @@ sub stitch ($$$$$) {
            # Just wind forwards rather than making a pointless pseudomerge.
            record_gdrlast $gdrlast, $ffq_prev_commitish;
            update_head_checkout $old_head, $ffq_prev_commitish,
-               "stitch (fast forward)";
+               sprintf "stitch (%s)", __ 'fast forward';
            return;
        }
     }
@@ -1941,7 +1969,9 @@ sub stitch ($$$$$) {
     # 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',
+        # we translate this against the time when this same code is
+        # used outside Debian, for downstreams and users
+       (__ 'Declare fast forward / record previous work'),
         "[git-debrebase pseudomerge: $prose]",
     ];
     record_gdrlast $gdrlast, $new_head;
@@ -1953,7 +1983,7 @@ sub do_stitch ($;$) {
 
     my ($ffq_prev, $gdrlast, $ffq_prev_commitish) = ffq_prev_info();
     if (!$ffq_prev_commitish) {
-       fail "No ffq-prev to stitch." unless $opt_noop_ok;
+       fail __ "No ffq-prev to stitch." unless $opt_noop_ok;
        return;
     }
     my $dangling_head = get_head();
@@ -1982,9 +2012,11 @@ sub resolve_upstream_version ($$) {
        my @tried;
        $new_upstream = upstream_commitish_search $upstream_version, \@tried;
        if (!length $new_upstream) {
-           fail "Could not determine appropriate upstream commitish.\n".
-               " (Tried these tags: @tried)\n".
-               " Check version, and specify upstream commitish explicitly.";
+           fail f_
+               "Could not determine appropriate upstream commitish.\n".
+               " (Tried these tags: %s)\n".
+               " Check version, and specify upstream commitish explicitly.",
+               "@tried";
        }
     }
     $new_upstream = git_rev_parse $new_upstream;
@@ -1998,12 +2030,13 @@ sub cmd_new_upstream () {
 
     my %pieces;
 
-    badusage "need NEW-VERSION [UPS-COMMITTISH]" unless @ARGV >= 1;
+    badusage __ "need NEW-VERSION [UPS-COMMITTISH]" unless @ARGV >= 1;
 
     # parse args - low commitment
     my $spec_version = shift @ARGV;
     my $new_version = (new Dpkg::Version $spec_version, check => 1);
-    fail "bad version number \`$spec_version'" unless defined $new_version;
+    fail f_ "bad version number \`%s'", $spec_version
+       unless defined $new_version;
     if ($new_version->is_native()) {
        $new_version = (new Dpkg::Version "$spec_version-1", check => 1);
     }
@@ -2019,7 +2052,8 @@ sub cmd_new_upstream () {
         my ($n, @x) = @_; # may be ''
         my $pc = $pieces{$n} //= {
            Name => $n,
-           Desc => ($n ? "upstream piece \`$n'" : "upstream (main piece"),
+           Desc => ($n ? (f_ "upstream piece \`%s'", $n)
+                       : (__ "upstream (main piece")),
        };
        while (my $k = shift @x) { $pc->{$k} = shift @x; }
         $pc;
@@ -2039,11 +2073,11 @@ sub cmd_new_upstream () {
     while (@ARGV && $ARGV[0] !~ m{^-}) {
        my $n = shift @ARGV;
 
-        badusage "for each EXTRA-UPS-NAME need EXTRA-UPS-COMMITISH"
+        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$/;
+       confess unless $n =~ m/^$extra_orig_namepart_re$/;
        $newpiece->($n, New => $c);
     }
 
@@ -2057,7 +2091,7 @@ sub cmd_new_upstream () {
     my $old_upstream;
     if (!$old_anchor_cl->{OrigParents}) {
        snag 'anchor-treated',
          'old anchor is recognised due to --anchor, cannot check upstream';
__ 'old anchor is recognised due to --anchor, cannot check upstream';
     } else {
        $old_upstream = parsecommit
            $old_anchor_cl->{OrigParents}[0]{CommitId};
@@ -2072,7 +2106,7 @@ sub cmd_new_upstream () {
            my $old_n_parents = scalar @{ $old_upstream->{Parents} };
            if ($old_n_parents != @oldpieces &&
                $old_n_parents != @oldpieces + 1) {
-               snag 'upstream-confusing', sprintf
+               snag 'upstream-confusing', f_
                    "previous upstream combine %s".
                    " mentions %d pieces (each implying one parent)".
                    " but has %d parents".
@@ -2081,7 +2115,7 @@ sub cmd_new_upstream () {
                    (scalar @oldpieces),
                    $old_n_parents;
            } elsif ($oldpieces[0] ne '.') {
-               snag 'upstream-confusing', sprintf
+               snag 'upstream-confusing', f_
                    "previous upstream combine %s".
                    " first piece is not \`.'",
                    $oldpieces[0];
@@ -2094,9 +2128,10 @@ sub cmd_new_upstream () {
                }
            }
        } else {
-           snag 'upstream-confusing',
-               "previous upstream $old_upstream->{CommitId} is from".
-               " git-debrebase but not an \`upstream-combine' commit";
+           snag 'upstream-confusing', f_
+               "previous upstream %s is from".
+               " git-debrebase but not an \`upstream-combine' commit",
+              $old_upstream->{CommitId};
        }
     }
 
@@ -2105,13 +2140,14 @@ sub cmd_new_upstream () {
            # we have complained already
        } elsif (!$pc->{Old}) {
            snag 'upstream-new-piece',
-               "introducing upstream piece \`$pc->{Name}'";
+               f_ "introducing upstream piece \`%s'", $pc->{Name};
        } elsif (!$pc->{New}) {
            snag 'upstream-rm-piece',
-               "dropping upstream piece \`$pc->{Name}'";
+               f_ "dropping upstream piece \`%s'", $pc->{Name};
        } elsif (!is_fast_fwd $pc->{Old}, $pc->{New}) {
            snag 'upstream-not-ff',
-               "not fast forward: $pc->{Name} $pc->{Old}..$pc->{New}";
+               f_ "not fast forward: %s %s",
+                  $pc->{Name}, "$pc->{Old}..$pc->{New}";
        }
     }
 
@@ -2163,36 +2199,43 @@ sub cmd_new_upstream () {
  "[git-debrebase anchor: new upstream $new_upstream_version, merge]",
             ];
 
-       my $clogsignoff = cmdoutput qw(git show),
-           '--pretty=format:%an <%ae>  %aD',
-           $new_bw;
-
        # Now we have to add a changelog stanza so the Debian version
-       # is right.
-       die if unlink "debian";
-       die $! unless $!==ENOENT or $!==ENOTEMPTY;
-       unlink "debian/changelog" or $!==ENOENT or die $!;
-        mkdir "debian" or die $!;
-       open CN, ">", "debian/changelog" or die $!;
-       my $oldclog = git_cat_file ":debian/changelog";
-       $oldclog =~ m/^($package_re) \(\S+\) / or
-           fail "cannot parse old changelog to get package name";
-       my $p = $1;
-       print CN <<END, $oldclog or die $!;
-$p ($new_version) UNRELEASED; urgency=medium
-
-  * Update to new upstream version $new_upstream_version.
-
- -- $clogsignoff
-
-END
-       close CN or die $!;
+       # is right.  We use debchange to do this.  Invoking debchange
+       # here is a bit fiddly because it has a lot of optional
+       # exciting behaviours, some of which will break stuff, and
+       # some of which won't work in a playtree.
+
+       # Make debchange use git's idea of the user's identity.
+       # That way, if the user never uses debchange et al, configuring
+       # git is enough.
+       my $usetup = sub {
+           my ($e, $k) = @_;
+           my $v = cfg $k, 1;
+           defined $v or return;
+           $ENV{$e} = $v;
+       };
+       $usetup->('DEBEMAIL',    'user.email');
+       $usetup->('DEBFULLNAME', 'user.name');
+
+       my @dch = (qw(debchange
+                     --allow-lower-version .*
+                     --no-auto-nmu
+                     --preserve
+                     --vendor=Unknown-Vendor
+                     --changelog debian/changelog
+                     --check-dirname-level 0
+                     --release-heuristic=changelog
+                     -v), $new_version,
+                  "Update to new upstream version $new_upstream_version.");
+
+       runcmd @git, qw(checkout -q debian/changelog);
+       runcmd @dch;
        runcmd @git, qw(update-index --add --replace), 'debian/changelog';
 
        # Now we have the final new breakwater branch in the index
         $new_bw = make_commit [ $new_bw ],
             [ "Update changelog for new upstream $new_upstream_version",
-              "[git-debrebase: new upstream $new_upstream_version, changelog]",
+              "[git-debrebase changelog: new upstream $new_upstream_version]",
             ];
     };
 
@@ -2214,28 +2257,28 @@ 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 $!;
+       print __ "Previous head already recorded\n" or confess $!;
     } elsif ($status eq 'deferred') {
        run_deferred_updates 'record-ffq-prev';
     } else {
-       fail "Could not preserve: $msg";
+       fail f_ "Could not preserve: %s", $msg;
     }
 }
 
 sub cmd_anchor () {
-    badusage "no arguments allowed" if @ARGV;
+    badusage __ "no arguments allowed" if @ARGV;
     my ($anchor, $bw) = keycommits +(git_rev_parse 'HEAD'), 0,0;
-    print "$bw\n" or die $!;
+    print "$anchor\n" or confess $!;
 }
 
 sub cmd_breakwater () {
-    badusage "no arguments allowed" if @ARGV;
+    badusage __ "no arguments allowed" if @ARGV;
     my ($anchor, $bw) = keycommits +(git_rev_parse 'HEAD'), 0,0;
-    print "$bw\n" or die $!;
+    print "$bw\n" or confess $!;
 }
 
 sub cmd_status () {
-    badusage "no arguments allowed" if @ARGV;
+    badusage __ "no arguments allowed" if @ARGV;
 
     # todo: gdr status should print divergence info
     # todo: gdr status should print upstream component(s) info
@@ -2257,22 +2300,22 @@ sub cmd_status () {
        $newest //= $oldest;
     };
     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', @_); };
+       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 $!;
+       flush STDOUT or confess $!;
        runcmd @git, qw(--no-pager log -n1),
            '--pretty=format:    %h %s%n',
            $cid;
     };
 
-    print "current branch contents, in git-debrebase terms:\n";
+    print __ "current branch contents, in git-debrebase terms:\n";
     if (!$oldest->{Badness}) {
-       print "  branch is laundered\n";
+       print __ "  branch is laundered\n";
     } else {
        print "  $oldest->{OurMsg}\n";
        my $printed = '';
@@ -2288,44 +2331,44 @@ sub cmd_status () {
     my $prab = sub {
        my ($cid, $what) = @_;
        if (!defined $cid) {
-           print "  $what is not well-defined\n";
+           print f_ "  %s is not well-defined\n", $what;
        } else {
            print "  $what\n";
            $prcommitinfo->($cid);
        }
     };
-    print "key git-debrebase commits:\n";
-    $prab->($anchor, 'anchor');
-    $prab->($bw, 'breakwater');
+    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";
+    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";
+           print __ "  unstitched; previous tip was:\n";
            $prcommitinfo->($ffq_prev);
        } elsif (!$gdrlast) {
-           print "  stitched? (no record of git-debrebase work)\n";
+           print __ "  stitched? (no record of git-debrebase work)\n";
        } elsif (is_fast_fwd $gdrlast, 'HEAD') {
-           print "  stitched\n";
+           print __ "  stitched\n";
        } else {
-           print "  not git-debrebase (diverged since last stitch)\n"
+           print __ "  not git-debrebase (diverged since last stitch)\n"
        }
     }
-    print "you are currently rebasing\n" if currently_rebasing();
+    print __ "you are currently rebasing\n" if currently_rebasing();
 }
 
 sub cmd_stitch () {
     my $prose = 'stitch';
     getoptions("stitch",
               'prose=s', \$prose);
-    badusage "no arguments allowed" if @ARGV;
+    badusage __ "no arguments allowed" if @ARGV;
     do_stitch $prose, 0;
 }
 sub cmd_prepush () {
@@ -2334,21 +2377,21 @@ sub cmd_prepush () {
 }
 
 sub cmd_quick () {
-    badusage "no arguments allowed" if @ARGV;
-    do_launder_head 'launder for git-debrebase 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;
+       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_launder_head __ 'launder for git-debrebase quick';
     do_stitch 'quick';
 }
 
@@ -2358,7 +2401,7 @@ sub cmd_scrap () {
        push @deferred_updates, 'verify HEAD HEAD';
        # noop, but stops us complaining that scrap was a noop
     }
-    badusage "no arguments allowed" if @ARGV;
+    badusage __ "no arguments allowed" if @ARGV;
     my ($ffq_prev, $gdrlast, $ffq_prev_commitish) = ffq_prev_info();
     my $scrapping_head;
     if ($ffq_prev_commitish) {
@@ -2372,7 +2415,7 @@ sub cmd_scrap () {
            "delete $merge_cache_ref";
     }
     if (!@deferred_updates) {
-       fail "No ongoing git-debrebase session." unless $opt_noop_ok;
+       fail __ "No ongoing git-debrebase session." unless $opt_noop_ok;
        finish 0;
     }
     snags_maybe_bail();
@@ -2389,23 +2432,30 @@ sub make_patches_staged ($) {
     # laundered.
     my ($secret_head, $secret_bw, $last_anchor) = walk $head;
     fresh_workarea();
+    my $any;
     in_workarea sub {
-       gbp_pq_export 'bw', $secret_bw, $secret_head;
+       $any = gbp_pq_export 'bw', $secret_bw, $secret_head;
     };
+    return $any;
 }
 
 sub make_patches ($) {
     my ($head) = @_;
     keycommits $head, 0, \&snag;
-    make_patches_staged $head;
+    my $any = make_patches_staged $head;
     my $out;
     in_workarea sub {
-       my $ptree = cmdoutput @git, qw(write-tree --prefix=debian/patches/);
+       my $ptree = !$any ? undef :
+           cmdoutput @git, qw(write-tree --prefix=debian/patches/);
        runcmd @git, qw(read-tree), $head;
-       read_tree_subdir 'debian/patches', $ptree;
+       if ($ptree) {
+           read_tree_subdir 'debian/patches', $ptree;
+       } else {
+           rm_subdir_cached 'debian/patches';
+       }
        $out = make_commit [$head], [
-            'Commit patch queue (exported by git-debrebase)',
-            '[git-debrebase: export and commit patches]',
+            (__ 'Commit patch queue (exported by git-debrebase)'),
+            '[git-debrebase make-patches: export and commit patches]',
         ];
     };
     return $out;
@@ -2415,22 +2465,23 @@ sub cmd_make_patches () {
     my $opt_quiet_would_amend;
     getoptions("make-patches",
               'quiet-would-amend!', \$opt_quiet_would_amend);
-    badusage "no arguments allowed" if @ARGV;
+    badusage __ "no arguments allowed" if @ARGV;
     bail_if_rebasing();
     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;
+       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
+       print STDERR failmsg f_
            "Patch export produced patch amendments".
-           " (abandoned output commit $new).".
-           "  Try laundering first."
+           " (abandoned output commit %s).".
+           "  Try laundering first.",
+           $new
            unless $opt_quiet_would_amend;
        finish 7;
     }
@@ -2443,14 +2494,23 @@ sub check_series_has_all_patches ($) {
        [qw(blob missing)];
     $series //= '';
     my %series;
+    our $comments_snagged;
     foreach my $f (grep /\S/, grep {!m/^\s\#/} split /\n/, $series) {
-       fail "patch $f repeated in $seriesfn !" if $series{$f}++;
+       if ($f =~ m/^\s*\#/) {
+           snag 'series-comments', f_
+               "%s contains comments, which will be discarded",
+               $seriesfn
+               unless $comments_snagged++;
+           next;
+       }
+       fail f_ "patch %s repeated in %s !", $f, $seriesfn if $series{$f}++;
     }
     foreach my $patchfile (get_tree "$head:debian/patches", 1,1) {
        my ($f,$i) = @$patchfile;
        next if $series{$f};
        next if $f eq 'series';
-       snag 'unused-patches', "Unused patch file $f will be discarded";
+       snag 'unused-patches', f_
+           "Unused patch file %s will be discarded", $f;
     }
 }
 
@@ -2458,11 +2518,11 @@ sub begin_convert_from () {
     my $head = get_head();
     my ($ffqs, $ffqm, $symref, $ffq_prev, $gdrlast) = ffq_prev_branchinfo();
 
-    fail "ffq-prev exists, this is already managed by git-debrebase!"
+    fail __ "ffq-prev exists, this is already managed by git-debrebase!"
        if $ffq_prev && git_get_ref $ffq_prev;
 
     my $gdrlast_obj = $gdrlast && git_get_ref $gdrlast;
-    snag 'already-converted',
+    snag 'already-converted', __
        "ahead of debrebase-last, this is already managed by git-debrebase!"
        if $gdrlast_obj && is_fast_fwd $gdrlast_obj, $head;
     return ($head, { LastRef => $gdrlast, LastObj => $gdrlast_obj });
@@ -2478,12 +2538,12 @@ sub complete_convert_from ($$$$) {
 }
 
 sub cmd_convert_from_gbp () {
-    badusage "want only 1 optional argument, the upstream git commitish"
+    badusage __ "want only 1 optional argument, the upstream git commitish"
        unless @ARGV<=1;
 
     my $clogp = parsechangelog();
     my $version = $clogp->{'Version'}
-       // die "missing Version from changelog";
+       // fail __ "missing Version from changelog\n";
 
     my ($upstream_spec) = @ARGV;
 
@@ -2498,30 +2558,32 @@ sub cmd_convert_from_gbp () {
        runcmd @git, qw(--no-pager diff --stat),
            $upstream, $old_head,
            qw( -- :!/debian :/);
-       fail <<END;
-upstream ($upstream_spec) and HEAD are not
+       fail f_ <<END, $upstream_spec, $upstream_spec;
+upstream (%s) and HEAD are not
 identical in upstream files.  See diffstat above, or run
-  git diff $upstream_spec HEAD -- :!/debian :/
+  git diff %s HEAD -- :!/debian :/
 END
     }
 
     if (!is_fast_fwd $upstream, $old_head) {
        snag 'upstream-not-ancestor',
-           "upstream ($upstream) is not an ancestor of HEAD";
+           f_ "upstream (%s) is not an ancestor of HEAD", $upstream;
     } 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";
+           snag 'unexpected-upstream-changes', f_
+               "history between upstream (%s) and HEAD contains direct changes to upstream files - are you sure this is a gbp (patches-unapplied) branch?",
+               $upstream;
+           print STDERR f_ "list expected changes with:  %s\n", 
+ "git log --stat --ancestry-path $upstream_spec..HEAD -- :/ ':!/debian'";
        }
     }
 
     if ((git_cat_file "$upstream:debian")[0] ne 'missing') {
        snag 'upstream-has-debian',
-           "upstream ($upstream) contains debian/ directory";
+           f_ "upstream (%s) contains debian/ directory", $upstream;
     }
 
     check_series_has_all_patches $old_head;
@@ -2539,29 +2601,30 @@ END
            $suite = $stz->{Distribution};
            last;
        };
-       die "neither of the first two changelog entries are released\n"
+       die __ "neither of the first two changelog entries are released\n"
            unless defined $lvsn;
        print "last finished-looking changelog entry: ($lvsn) $suite\n";
        my $mtag_pat = debiantag_maintview $lvsn, '*';
        my $mtag = cmdoutput @git, qw(describe --always --abbrev=0 --match),
            $mtag_pat;
-       die "could not find suitable maintainer view tag $mtag_pat\n"
+       die f_ "could not find suitable maintainer view tag %s\n", $mtag_pat
            unless $mtag =~ m{/};
        is_fast_fwd $mtag, 'HEAD' or
-           die "HEAD is not FF from maintainer tag $mtag!";
+           die f_ "HEAD is not FF from maintainer tag %s!", $mtag;
        my $dtag = "archive/$mtag";
        git_get_ref "refs/tags/$dtag" or
-           die "dgit view tag $dtag not found\n";
+           die f_ "dgit view tag %s not found\n", $dtag;
        is_fast_fwd $mtag, $dtag or
-           die "dgit view tag $dtag is not FF from maintainer tag $mtag\n";
-       print "will stitch in dgit view, $dtag\n";
+           die f_ "dgit view tag %s is not FF from maintainer tag %s\n",
+                  $dtag, $mtag;
+       print f_ "will stitch in dgit view, %s\n", $dtag;
        git_rev_parse $dtag;
     };
     if (!$previous_dgit_view) {
        $@ =~ s/^\n+//;
        chomp $@;
-       print STDERR <<END;
-Cannot confirm dgit view: $@
+       print STDERR f_ <<END, "$@";
+Cannot confirm dgit view: %s
 Failed to stitch in dgit view (see messages above).
 dgit --overwrite will be needed on the first dgit push after conversion.
 END
@@ -2606,8 +2669,8 @@ END
     };
 
     complete_convert_from $old_head, $work, $gdrlastinfo, 'convert-from-gbp';
-    print <<END or die $!;
-git-debrebase: converted from patched-unapplied (gbp) branch format, OK
+    print f_ <<END, $us or confess $!;
+%s: converted from patched-unapplied (gbp) branch format, OK
 END
 }
 
@@ -2615,25 +2678,30 @@ sub cmd_convert_to_gbp () {
     badusage "no arguments allowed" if @ARGV;
     my $head = get_head();
     my (undef, undef, undef, $ffq, $gdrlast) = ffq_prev_branchinfo();
-    keycommits $head, 0;
-    my $out;
-    make_patches_staged $head;
-    in_workarea sub {
-       $out = make_commit ['HEAD'], [
-            'Commit patch queue (converted from git-debrebase format)',
-            '[git-debrebase convert-to-gbp: commit patches]',
-        ];
-    };
+    my ($anchor, $breakwater) = keycommits $head, 0;
+    my $out = $breakwater;
+    my $any = make_patches_staged $head;
+    if ($any) {
+       in_workarea sub {
+           $out = make_commit [$out], [
+               'Commit patch queue (converted from git-debrebase format)',
+               '[git-debrebase convert-to-gbp: commit patches]',
+           ];
+       };
+    } else {
+       # in this case, it can be fast forward
+       $out = $head;
+    }
     if (defined $ffq) {
        push @deferred_updates, "delete $ffq";
        push @deferred_updates, "delete $gdrlast";
     }
     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!
+    print f_ <<END, $us,$us,$us or confess $!;
+%s: converted to git-buildpackage branch format
+%s: WARNING: do not now run "git-debrebase" any more
+%s: WARNING: doing so would drop all upstream patches!
 END
 }
 
@@ -2652,7 +2720,7 @@ sub cmd_convert_from_dgit_view () {
               'origs!', \$do_origs,
               'tags!', \$do_tags,
               'always-convert-anyway!', \$always);
-    fail "takes 1 optional argument, the upstream commitish" if @ARGV>1;
+    fail __ "takes 1 optional argument, the upstream commitish" if @ARGV>1;
 
     my @upstreams;
 
@@ -2660,7 +2728,7 @@ sub cmd_convert_from_dgit_view () {
        my $spec = shift @ARGV;
        my $commit = git_rev_parse "$spec^{commit}";
        push @upstreams, { Commit => $commit,
-                          Source => "$ARGV[0], from command line",
+                          Source => (f_ "%s, from command line", $ARGV[0]),
                           Only => 1,
                         };
     }
@@ -2673,12 +2741,13 @@ sub cmd_convert_from_dgit_view () {
        keycommits $head, sub{}, sub{}, $trouble, $trouble;
        printdebug "troubles=$troubles\n";
        if (!$troubles) {
-           print STDERR <<END;
-$us: Branch already seems to be in git-debrebase format!
-$us: --always-convert-anyway would do the conversion operation anyway
-$us: but is probably a bad idea.  Probably, you wanted to do nothing.
+           print STDERR f_ <<END, $us,$us,$us;
+%s: Branch already seems to be in git-debrebase format!
+%s: --always-convert-anyway would do the conversion operation anyway
+%s: but is probably a bad idea.  Probably, you wanted to do nothing.
 END
-           fail "Branch already in git-debrebase format." unless $opt_noop_ok;
+           fail __ "Branch already in git-debrebase format."
+               unless $opt_noop_ok;
            finish 0;
        }
     }
@@ -2688,19 +2757,20 @@ END
     snags_maybe_bail_early();
 
     my $version = upstreamversion $clogp->{Version};
-    print STDERR "Considering possible commits corresponding to upstream:\n";
+    print STDERR __
+       "Considering possible commits corresponding to upstream:\n";
 
     if (!@upstreams) {
        if ($do_tags) {
            my @tried;
            my $ups_tag = upstream_commitish_search $version, \@tried;
            if ($ups_tag) {
-               my $this = "git tag $tried[-1]";
+               my $this = f_ "git tag %s", $tried[-1];
                push @upstreams, { Commit => $ups_tag,
                                   Source => $this,
                                 };
            } else {
-               printf STDERR
+               print STDERR f_
                    " git tag: no suitable tag found (tried %s)\n",
                    "@tried";
            }
@@ -2710,17 +2780,18 @@ END
            # we do a quick check to see if there are plausible origs
            my $something=0;
            if (!opendir BPD, $bpd) {
-               die "$bpd: opendir: $!" unless $!==ENOENT;
+               die f_ "opendir build-products-dir %s: %s", $bpd, $!
+                   unless $!==ENOENT;
            } else {
                while ($!=0, my $f = readdir BPD) {
                    next unless is_orig_file_of_p_v $f, $p, $version;
-                   printf STDERR
+                   print STDERR f_
                        " orig: found what looks like a .orig, %s\n",
                        "$bpd/$f";
                    $something=1;
                    last;
                }
-               die "read $bpd: $!" if $!;
+               confess "read $bpd: $!" if $!;
                closedir BPD;
            }
            if ($something) {
@@ -2737,7 +2808,7 @@ Import effective orig tree for upstream version $version
 END
 This includes the contents of the .orig(s), minus any debian/ directory.
 
-[git-debrebase import-from-dgit-view upstream-import-convert: $version]
+[git-debrebase convert-from-dgit-view upstream-import-convert: $version]
 END
                                                    ];
                    push @upstreams, { Commit => $ups_synth,
@@ -2745,7 +2816,7 @@ END
                                     };
                }
            } else {
-               printf STDERR
+               print STDERR f_
                    " orig: no suitable origs found (looked for %s in %s)\n",
                    "${p}_".(stripeoch $version)."...", $bpd;
            }
@@ -2754,7 +2825,8 @@ END
 
     my $some_patches = stat_exists 'debian/patches/series';
 
-    print STDERR "Evaluating possible commits corresponding to upstream:\n";
+    print STDERR __
+       "Evaluating possible commits corresponding to upstream:\n";
 
     my $result;
     foreach my $u (@upstreams) {
@@ -2771,7 +2843,7 @@ END
  'git-debrebase convert-from-dgit-view: drop upstream changes from breakwater',
  "Drop upstream changes, and delete debian/patches, as part of converting\n".
  "to git-debrebase format.  Upstream changes will appear as commits.",
- '[git-debrebase convert-from-dgit-view: drop patches from tree]'
+ '[git-debrebase convert-from-dgit-view drop-patches]'
                                           ];
            }
            $work = make_commit [ $work, $u->{Commit} ], [
@@ -2790,7 +2862,7 @@ END
                }
                my $r = system @gbp_cmd;
                if ($r) {
-                   printf STDERR
+                   print STDERR f_
                        " %s: couldn't apply patches: gbp pq %s",
                        $u->{Source}, waitstatusmsg();
                    return;
@@ -2799,8 +2871,9 @@ END
            my $work = git_rev_parse qw(HEAD);
            my $diffout = cmdoutput @git, qw(diff-tree --stat HEAD), $work;
            if (length $diffout) {
-               print STDERR
-                   " $u->{Source}: applying patches gives different tree\n";
+               print STDERR f_
+                   " %s: applying patches gives different tree\n",
+                   $u->{Source};
                print STDERR $diffout if $diagnose;
                return;
            }
@@ -2812,19 +2885,33 @@ END
     }
 
     if (!$result) {
-       fail <<END;
+       fail __ <<END;
 Could not find or construct a suitable upstream commit.
 Rerun adding --diagnose after convert-from-dgit-view, or pass a
 upstream commmit explicitly or provide suitable origs.
 END
     }
 
-    printf STDERR "Yes, will base new branch on %s\n", $result->{Source};
+    print STDERR f_ "Yes, will base new branch on %s\n", $result->{Source};
 
     complete_convert_from $head, $result->{Result}, $gdrlastinfo,
        'convert-from-dgit-view';
 }
 
+sub cmd_forget_was_ever_debrebase () {
+    badusage __ "forget-was-ever-debrebase takes no further arguments"
+       if @ARGV;
+    my ($ffqstatus, $ffq_msg, $current, $ffq_prev, $gdrlast) =
+       ffq_prev_branchinfo();
+    fail f_ "Not suitable for recording git-debrebaseness anyway: %s",
+           $ffq_msg
+       if defined $ffq_msg;
+    push @deferred_updates, "delete $ffq_prev";
+    push @deferred_updates, "delete $gdrlast";
+    snags_maybe_bail();
+    run_deferred_updates "forget-was-ever-debrebase";
+}
+
 sub cmd_record_resolved_merge () {
     badusage "record-resolved-merge takes no further arguments" if @ARGV;
     # MERGE-TODO needs documentation
@@ -2914,8 +3001,11 @@ sub cmd_downstream_rebase_launder_v0 () {
     }
 }
 
+setlocale(LC_MESSAGES, "");
+textdomain("git-debrebase");
+
 getoptions_main
-          ("bad options\n",
+          (__ "bad options\n",
           "D+" => \$debuglevel,
           'noop-ok', => \$opt_noop_ok,
           'f=s' => \@snag_force_opts,
@@ -2925,9 +3015,9 @@ getoptions_main
           'experimental-merge-resolution!', \$opt_merges,
           '-i:s' => sub {
               my ($opt,$val) = @_;
-              badusage "git-debrebase: no cuddling to -i for git-rebase"
+              badusage f_ "%s: no cuddling to -i for git-rebase", $us
                   if length $val;
-              die if $opt_defaultcmd_interactive; # should not happen
+              confess 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
@@ -2935,14 +3025,13 @@ getoptions_main
               push @$opt_defaultcmd_interactive, @ARGV;
               @ARGV=();
           },
-          'help' => sub { print $usage_message or die $!; finish 0; },
+          'help' => sub { print __ $usage_message or confess $!; finish 0; },
           );
 
 initdebug('git-debrebase ');
 enabledebug if $debuglevel;
 
-my $toplevel = cmdoutput @git, qw(rev-parse --show-toplevel);
-chdir $toplevel or die "chdir $toplevel: $!";
+changedir_git_toplevel();
 
 $rd = fresh_playground "$playprefix/misc";
 
@@ -2956,7 +3045,7 @@ if (!@ARGV || $opt_defaultcmd_interactive || $ARGV[0] =~ m{^-}) {
     $cmdfn =~ y/-/_/;
     $cmdfn = ${*::}{"cmd_$cmdfn"};
 
-    $cmdfn or badusage "unknown git-debrebase sub-operation $cmd";
+    $cmdfn or badusage f_ "unknown git-debrebase sub-operation %s", $cmd;
     $cmdfn->();
 }