chiark / gitweb /
po4a: pairwise-pocheck: Force it to be run sometimes
[dgit.git] / dgit
diff --git a/dgit b/dgit
index 23e66b2..e104838 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -2,8 +2,8 @@
 # dgit
 # Integration between git and Debian-style archives
 #
-# Copyright (C)2013-2017 Ian Jackson
-# Copyright (C)2017 Sean Whitton
+# Copyright (C)2013-2018 Ian Jackson
+# Copyright (C)2017-2018 Sean Whitton
 #
 # 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
@@ -20,6 +20,7 @@
 
 END { $? = $Debian::Dgit::ExitStatus::desired // -1; };
 use Debian::Dgit::ExitStatus;
+use Debian::Dgit::I18n;
 
 use strict;
 
@@ -37,6 +38,7 @@ use Dpkg::Version;
 use Dpkg::Compression;
 use Dpkg::Compression::Process;
 use POSIX;
+use Locale::gettext;
 use IPC::Open2;
 use Digest::SHA;
 use Digest::MD5;
@@ -78,7 +80,7 @@ our $overwrite_version; # undef: not specified; '': check changelog
 our $quilt_mode;
 our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck|gbp|dpm|unapplied';
 our $dodep14tag;
-our $split_brain_save;
+our %internal_object_save;
 our $we_are_responder;
 our $we_are_initiator;
 our $initiator_tempdir;
@@ -99,10 +101,11 @@ our %forceopts = map { $_=>0 }
 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 = 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)?";
+our $cleanmode_re = qr{(?: dpkg-source (?: -d )? (?: ,no-check | ,all-check )?
+                         | git | git-ff
+                         | check (?: ,ignores )?
+                         | none
+                         )}x;
 
 our $git_authline_re = '^([^<>]+) \<(\S+)\> (\d+ [-+]\d+)$';
 our $splitbraincache = 'dgit-intern/quilt-cache';
@@ -116,7 +119,7 @@ our (@curl) = (qw(curl --proto-redir), '-all,http,https', qw(-L));
 our (@dput) = qw(dput);
 our (@debsign) = qw(debsign);
 our (@gpg) = qw(gpg);
-our (@sbuild) = qw(sbuild);
+our (@sbuild) = (qw(sbuild --no-source));
 our (@ssh) = 'ssh';
 our (@dgit) = qw(dgit);
 our (@git_debrebase) = qw(git-debrebase);
@@ -129,6 +132,8 @@ our (@mergechanges) = qw(mergechanges -f);
 our (@gbp_build) = ('');
 our (@gbp_pq) = ('gbp pq');
 our (@changesopts) = ('');
+our (@pbuilder) = ("sudo -E pbuilder");
+our (@cowbuilder) = ("sudo -E cowbuilder");
 
 our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
                     'curl' => \@curl,
@@ -148,7 +153,9 @@ our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
                      'gbp-build' => \@gbp_build,
                      'gbp-pq' => \@gbp_pq,
                      'ch' => \@changesopts,
-                     'mergechanges' => \@mergechanges);
+                     'mergechanges' => \@mergechanges,
+                     'pbuilder' => \@pbuilder,
+                     'cowbuilder' => \@cowbuilder);
 
 our %opts_opt_cmdonly = ('gpg' => 1, 'git' => 1);
 our %opts_cfg_insertpos = map {
@@ -166,7 +173,6 @@ our $keyid;
 autoflush STDOUT 1;
 
 our $supplementary_message = '';
-our $need_split_build_invocation = 1;
 our $split_brain = 0;
 
 END {
@@ -198,15 +204,13 @@ sub lref () { return "refs/heads/".lbranch(); }
 sub lrref () { return "refs/remotes/$remotename/".server_branch($csuite); }
 sub rrref () { return server_ref($csuite); }
 
-sub stripepoch ($) {
-    my ($vsn) = @_;
-    $vsn =~ s/^\d+\://;
-    return $vsn;
-}
-
 sub srcfn ($$) {
-    my ($vsn,$sfx) = @_;
-    return "${package}_".(stripepoch $vsn).$sfx
+    my ($vsn, $sfx) = @_;
+    return &source_file_leafname($package, $vsn, $sfx);
+}
+sub is_orig_file_of_vsn ($$) {
+    my ($f, $upstreamvsn) = @_;
+    return is_orig_file_of_p_v($f, $package, $upstreamvsn);
 }
 
 sub dscfn ($) {
@@ -219,12 +223,6 @@ sub changespat ($;$) {
     return "${package}_".(stripepoch $vsn)."_".($arch//'*').".changes";
 }
 
-sub upstreamversion ($) {
-    my ($vsn) = @_;
-    $vsn =~ s/-[^-]+$//;
-    return $vsn;
-}
-
 our $us = 'dgit';
 initdebug('');
 
@@ -238,24 +236,29 @@ END {
     }
 };
 
-sub badcfg { print STDERR "$us: invalid configuration: @_\n"; finish 12; }
+sub badcfg {
+    print STDERR f_ "%s: invalid configuration: %s\n", $us, "@_";
+    finish 12;
+}
 
 sub forceable_fail ($$) {
     my ($forceoptsl, $msg) = @_;
     fail $msg unless grep { $forceopts{$_} } @$forceoptsl;
-    print STDERR "warning: overriding problem due to --force:\n". $msg;
+    print STDERR +(__ "warning: overriding problem due to --force:\n"). $msg;
 }
 
 sub forceing ($) {
     my ($forceoptsl) = @_;
     my @got = grep { $forceopts{$_} } @$forceoptsl;
     return 0 unless @got;
-    print STDERR
- "warning: skipping checks or functionality due to --force-$got[0]\n";
+    print STDERR f_
+       "warning: skipping checks or functionality due to --force-%s\n",
+       $got[0];
 }
 
 sub no_such_package () {
-    print STDERR "$us: package $package does not exist in suite $isuite\n";
+    print STDERR f_ "%s: package %s does not exist in suite %s\n",
+       $us, $package, $isuite;
     finish 4;
 }
 
@@ -275,14 +278,16 @@ sub quiltmode_splitbrain () {
 }
 
 sub opts_opt_multi_cmd {
+    my $extra = shift;
     my @cmd;
     push @cmd, split /\s+/, shift @_;
+    push @cmd, @$extra;
     push @cmd, @_;
     @cmd;
 }
 
 sub gbp_pq {
-    return opts_opt_multi_cmd @gbp_pq;
+    return opts_opt_multi_cmd [], @gbp_pq;
 }
 
 sub dgit_privdir () {
@@ -295,6 +300,14 @@ sub bpd_abs () {
     return $r;
 }
 
+sub get_tree_of_commit ($) {
+    my ($commitish) = @_;
+    my $cdata = cmdoutput @git, qw(cat-file commit), $commitish;
+    $cdata =~ m/\n\n/;  $cdata = $`;
+    $cdata =~ m/^tree (\w+)$/m or confess "cdata $cdata ?";
+    return $1;
+}
+
 sub branch_gdr_info ($$) {
     my ($symref, $head) = @_;
     my ($status, $msg, $current, $ffq_prev, $gdrlast) =
@@ -306,21 +319,95 @@ sub branch_gdr_info ($$) {
     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 0 unless !defined $ancestor or is_fast_fwd $ancestor, $ffq_prev;
     return 1;
 }
 
+sub branch_is_gdr ($) {
+    my ($head) = @_;
+    # This is quite like git-debrebase's keycommits.
+    # We have our own implementation because:
+    #  - our algorighm can do fewer tests so is faster
+    #  - it saves testing to see if gdr is installed
+
+    # NB we use this jsut for deciding whether to run gdr make-patches
+    # Before reusing this algorithm for somthing else, its
+    # suitability should be reconsidered.
+
+    my $walk = $head;
+    local $Debian::Dgit::debugcmd_when_debuglevel = 3;
+    printdebug "branch_is_gdr $head...\n";
+    my $get_patches = sub {
+       my $t = git_cat_file "$_[0]:debian/patches", [qw(missing tree)];
+       return $t // '';
+    };
+    my $tip_patches = $get_patches->($head);
+  WALK:
+    for (;;) {
+       my $cdata = git_cat_file $walk, 'commit';
+       my ($hdrs,$msg) = $cdata =~ m{\n\n} ? ($`,$') : ($cdata,'');
+       if ($msg =~ m{^\[git-debrebase\ (
+                         anchor | changelog | make-patches | 
+                         merged-breakwater | pseudomerge
+                     ) [: ] }mx) {
+           # no need to analyse this - it's sufficient
+           # (gdr classifications: Anchor, MergedBreakwaters)
+           # (made by gdr: Pseudomerge, Changelog)
+           printdebug "branch_is_gdr  $walk gdr $1 YES\n";
+           return 1;
+       }
+       my @parents = ($hdrs =~ m/^parent (\w+)$/gm);
+       if (@parents==2) {
+           my $walk_tree = get_tree_of_commit $walk;
+           foreach my $p (@parents) {
+               my $p_tree = get_tree_of_commit $p;
+               if ($p_tree eq $walk_tree) { # pseudomerge contriburor
+                   # (gdr classification: Pseudomerge; not made by gdr)
+                   printdebug "branch_is_gdr  $walk unmarked pseudomerge\n"
+                       if $debuglevel >= 2;
+                   $walk = $p;
+                   next WALK;
+               }
+           }
+           # some other non-gdr merge
+           # (gdr classification: VanillaMerge, DgitImportUnpatched, ?)
+           printdebug "branch_is_gdr  $walk ?-2-merge NO\n";
+           return 0;
+       }
+       if (@parents>2) {
+           # (gdr classification: ?)
+           printdebug "branch_is_gdr  $walk ?-octopus NO\n";
+           return 0;
+       }
+       if (!@parents) {
+           printdebug "branch_is_gdr  $walk origin\n";
+           return 0;
+       }
+       if ($get_patches->($walk) ne $tip_patches) {
+           # Our parent added, removed, or edited patches, and wasn't
+           # a gdr make-patches commit.  gdr make-patches probably
+           # won't do that well, then.
+           # (gdr classification of parent: AddPatches or ?)
+           printdebug "branch_is_gdr  $walk ?-patches NO\n";
+           return 0;
+       }
+       if ($tip_patches eq '' and
+           !defined git_cat_file "$walk:debian") {
+           # (gdr classification of parent: BreakwaterStart
+           printdebug "branch_is_gdr  $walk unmarked BreakwaterStart YES\n";
+           return 1;
+       }
+       # (gdr classification: Upstream Packaging Mixed Changelog)
+       printdebug "branch_is_gdr  $walk plain\n"
+           if $debuglevel >= 2;
+       $walk = $parents[0];
+    }
+}
+
 #---------- remote protocol support, common ----------
 
 # remote push initiator/responder protocol:
@@ -393,28 +480,28 @@ sub i_child_report () {
     die unless $got == $i_child_pid;
     $i_child_pid = undef;
     return undef unless $?;
-    return "build host child ".waitstatusmsg();
+    return f_ "build host child %s", waitstatusmsg();
 }
 
 sub badproto ($$) {
     my ($fh, $m) = @_;
-    fail "connection lost: $!" if $fh->error;
-    fail "protocol violation; $m not expected";
+    fail f_ "connection lost: %s", $! if $fh->error;
+    fail f_ "protocol violation; %s not expected", $m;
 }
 
 sub badproto_badread ($$) {
     my ($fh, $wh) = @_;
-    fail "connection lost: $!" if $!;
+    fail f_ "connection lost: %s", $! if $!;
     my $report = i_child_report();
     fail $report if defined $report;
-    badproto $fh, "eof (reading $wh)";
+    badproto $fh, f_ "eof (reading %s)", $wh;
 }
 
 sub protocol_expect (&$) {
     my ($match, $fh) = @_;
     local $_;
     $_ = <$fh>;
-    defined && chomp or badproto_badread $fh, "protocol message";
+    defined && chomp or badproto_badread $fh, __ "protocol message";
     if (wantarray) {
        my @r = &$match;
        return @r if @r;
@@ -422,7 +509,7 @@ sub protocol_expect (&$) {
        my $r = &$match;
        return $r if $r;
     }
-    badproto $fh, "\`$_'";
+    badproto $fh, f_ "\`%s'", $_;
 }
 
 sub protocol_send_file ($$) {
@@ -433,20 +520,20 @@ sub protocol_send_file ($$) {
        my $got = read PF, $d, 65536;
        die "$ourfn: $!" unless defined $got;
        last if !$got;
-       print $fh "data-block ".length($d)."\n" or die $!;
-       print $fh $d or die $!;
+       print $fh "data-block ".length($d)."\n" or confess $!;
+       print $fh $d or confess $!;
     }
     PF->error and die "$ourfn $!";
-    print $fh "data-end\n" or die $!;
+    print $fh "data-end\n" or confess $!;
     close PF;
 }
 
 sub protocol_read_bytes ($$) {
     my ($fh, $nbytes) = @_;
-    $nbytes =~ m/^[1-9]\d{0,5}$|^0$/ or badproto \*RO, "bad byte count";
+    $nbytes =~ m/^[1-9]\d{0,5}$|^0$/ or badproto \*RO, __ "bad byte count";
     my $d;
     my $got = read $fh, $d, $nbytes;
-    $got==$nbytes or badproto_badread $fh, "data block";
+    $got==$nbytes or badproto_badread $fh, __ "data block";
     return $d;
 }
 
@@ -462,9 +549,9 @@ sub protocol_receive_file ($$) {
        } $fh;
        last unless $y;
        my $d = protocol_read_bytes $fh, $l;
-       print PF $d or die $!;
+       print PF $d or confess $!;
     }
-    close PF or die $!;
+    close PF or confess $!;
 }
 
 #---------- remote protocol support, responder ----------
@@ -474,7 +561,7 @@ sub responder_send_command ($) {
     return unless $we_are_responder;
     # called even without $we_are_responder
     printdebug ">> $command\n";
-    print PO $command, "\n" or die $!;
+    print PO $command, "\n" or confess $!;
 }    
 
 sub responder_send_file ($$) {
@@ -509,8 +596,8 @@ sub initiator_expect (&) {
 sub progress {
     if ($we_are_responder) {
        my $m = join '', @_;
-       responder_send_command "progress ".length($m) or die $!;
-       print PO $m or die $!;
+       responder_send_command "progress ".length($m) or confess $!;
+       print PO $m or confess $!;
     } else {
        print @_, "\n";
     }
@@ -525,9 +612,10 @@ sub url_get {
     }
     my $what = $_[$#_];
     progress "downloading $what...";
-    my $r = $ua->get(@_) or die $!;
+    my $r = $ua->get(@_) or confess $!;
     return undef if $r->code == 404;
-    $r->is_success or fail "failed to fetch $what: ".$r->status_line;
+    $r->is_success or fail f_ "failed to fetch %s: %s",
+       $what, $r->status_line;
     return $r->decoded_content(charset => 'none');
 }
 
@@ -538,9 +626,9 @@ sub act_scary () { return !$dryrun_level; }
 
 sub printdone {
     if (!$dryrun_level) {
-       progress "$us ok: @_";
+       progress f_ "%s ok: %s", $us, "@_";
     } else {
-       progress "would be ok: @_ (but dry run only)";
+       progress f_ "would be ok: %s (but dry run only)", "@_";
     }
 }
 
@@ -564,12 +652,13 @@ sub runcmd_ordryrun_local {
     }
 }
 
-our $helpmsg = <<END;
+our $helpmsg = i_ <<END;
 main usages:
   dgit [dgit-opts] clone [dgit-opts] package [suite] [./dir|/dir]
   dgit [dgit-opts] fetch|pull [dgit-opts] [suite]
   dgit [dgit-opts] build [dpkg-buildpackage-opts]
   dgit [dgit-opts] sbuild [sbuild-opts]
+  dgit [dgit-opts] pbuilder|cowbuilder [debbuildopts]
   dgit [dgit-opts] push [dgit-opts] [suite]
   dgit [dgit-opts] push-source [dgit-opts] [suite]
   dgit [dgit-opts] rpush build-host:build-dir ...
@@ -582,17 +671,17 @@ important dgit options:
   -c<name>=<value>    set git config option (used directly by dgit too)
 END
 
-our $later_warning_msg = <<END;
+our $later_warning_msg = i_ <<END;
 Perhaps the upload is stuck in incoming.  Using the version from git.
 END
 
 sub badusage {
-    print STDERR "$us: @_\n", $helpmsg or die $!;
+    print STDERR f_ "%s: %s\n%s", $us, "@_", __ $helpmsg or confess $!;
     finish 8;
 }
 
 sub nextarg {
-    @ARGV or badusage "too few arguments";
+    @ARGV or badusage __ "too few arguments";
     return scalar shift @ARGV;
 }
 
@@ -600,7 +689,7 @@ sub pre_help () {
     not_necessarily_a_tree();
 }
 sub cmd_help () {
-    print $helpmsg or die $!;
+    print __ $helpmsg or confess $!;
     finish 0;
 }
 
@@ -702,8 +791,12 @@ sub git_get_config ($) {
                           "undef")."\n"
            if $debuglevel >= 4;
        $l or next;
-       @$l==1 or badcfg "multiple values for $c".
-           " (in $src git config)" if @$l > 1;
+       @$l==1 or badcfg
+           f_ "multiple values for %s (in %s git config)", $c, $src
+           if @$l > 1;
+       $l->[0] =~ m/\n/ and badcfg f_
+ "value for config option %s (in %s git config) contains newline(s)!",
+            $c, $src;
        return $l->[0];
     }
     return undef;
@@ -721,8 +814,10 @@ sub cfg {
            return $dv;
        }
     }
-    badcfg "need value for one of: @_\n".
-       "$us: distro or suite appears not to be (properly) supported";
+    badcfg f_
+       "need value for one of: %s\n".
+       "%s: distro or suite appears not to be (properly) supported",
+       "@_", $us;
 }
 
 sub not_necessarily_a_tree () {
@@ -761,7 +856,8 @@ sub access_nomdistro () {
     my $base = access_basedistro();
     my $r = cfg("dgit-distro.$base.nominal-distro",'RETURN-UNDEF') // $base;
     $r =~ m/^$distro_re$/ or badcfg
- "bad syntax for (nominal) distro \`$r' (does not match /^$distro_re$/)";
+       f_ "bad syntax for (nominal) distro \`%s' (does not match %s)",
+       $r, "/^$distro_re$/";
     return $r;
 }
 
@@ -775,7 +871,7 @@ sub access_quirk () {
        $re =~ s/[^-0-9a-z_\%*()]/\\$&/ig;
        $re =~ s/\*/.*/g;
        $re =~ s/\%/([-0-9a-z_]+)/
-           or $re =~ m/[()]/ or badcfg "backports-quirk needs \% or ( )";
+           or $re =~ m/[()]/ or badcfg __ "backports-quirk needs \% or ( )";
        if ($isuite =~ m/^$re$/) {
            return ('backports',"$basedistro-backports",$1);
        }
@@ -791,7 +887,8 @@ sub parse_cfg_bool ($$$) {
     return
        $v =~ m/^[ty1]/ ? 1 :
        $v =~ m/^[fn0]/ ? 0 :
-       badcfg "$what needs t (true, y, 1) or f (false, n, 0) not \`$v'";
+       badcfg f_ "%s needs t (true, y, 1) or f (false, n, 0) not \`%s'",
+           $what, $v;
 }      
 
 sub access_forpush_config () {
@@ -809,7 +906,8 @@ sub access_forpush_config () {
        $v =~ m/^[ty1]/ ? 0 : # force readonly,    forpush = 0
        $v =~ m/^[fn0]/ ? 1 : # force nonreadonly, forpush = 1
        $v =~ m/^[a]/  ? '' : # auto,              forpush = ''
-       badcfg "readonly needs t (true, y, 1) or f (false, n, 0) or a (auto)";
+       badcfg __
+           "readonly needs t (true, y, 1) or f (false, n, 0) or a (auto)";
 }
 
 sub access_forpush () {
@@ -818,11 +916,12 @@ sub access_forpush () {
 }
 
 sub pushing () {
-    die "$access_forpush ?" if ($access_forpush // 1) ne 1;
-    badcfg "pushing but distro is configured readonly"
+    confess +(__ 'internal error').' '.Dumper($access_forpush)," ?" if
+       defined $access_forpush and !$access_forpush;
+    badcfg __ "pushing but distro is configured readonly"
        if access_forpush_config() eq '0';
     $access_forpush = 1;
-    $supplementary_message = <<'END' unless $we_are_responder;
+    $supplementary_message = __ <<'END' unless $we_are_responder;
 Push failed, before we got started.
 You can retry the push, after fixing the problem, if you like.
 END
@@ -840,8 +939,8 @@ sub supplementary_message ($) {
        return;
     } elsif ($protovsn >= 3) {
        responder_send_command "supplementary-message ".length($msg)
-           or die $!;
-       print PO $msg or die $!;
+           or confess $!;
+       print PO $msg or confess $!;
     }
 }
 
@@ -978,7 +1077,7 @@ sub commit_getclogp ($) {
 }
 
 sub parse_dscdata () {
-    my $dscfh = new IO::File \$dscdata, '<' or die $!;
+    my $dscfh = new IO::File \$dscdata, '<' or confess $!;
     printdebug Dumper($dscdata) if $debuglevel>1;
     $dsc = parsecontrolfh($dscfh,$dscurl,1);
     printdebug Dumper($dsc) if $debuglevel>1;
@@ -988,7 +1087,7 @@ our %rmad;
 
 sub archive_query ($;@) {
     my ($method) = shift @_;
-    fail "this operation does not support multiple comma-separated suites"
+    fail __ "this operation does not support multiple comma-separated suites"
        if $isuite =~ m/,/;
     my $query = access_cfg('archive-query','RETURN-UNDEF');
     $query =~ s/^(\w+):// or badcfg "invalid archive-query method \`$query'";
@@ -1034,8 +1133,9 @@ sub archive_api_query_cmd ($) {
                fail "for $url: stat $key: $!" unless $!==ENOENT;
                next;
            }
-           fail "config requested specific TLS key but do not know".
-               " how to get curl to use exactly that EE key ($key)";
+           fail f_ "config requested specific TLS key but do not know".
+                   " how to get curl to use exactly that EE key (%s)",
+                   $key;
 #          push @cmd, "--cacert", $key, "--capath", "/dev/enoent";
 #           # Sadly the above line does not work because of changes
 #           # to gnutls.   The real fix for #790093 may involve
@@ -1054,7 +1154,7 @@ sub archive_api_query_cmd ($) {
 sub api_query ($$;$) {
     use JSON;
     my ($data, $subpath, $ok404) = @_;
-    badcfg "ftpmasterapi archive query method takes no data part"
+    badcfg __ "ftpmasterapi archive query method takes no data part"
        if length $data;
     my @cmd = archive_api_query_cmd($subpath);
     my $url = $cmd[$#cmd];
@@ -1062,11 +1162,11 @@ sub api_query ($$;$) {
     my $json = cmdoutput @cmd;
     unless ($json =~ s/\d+\d+\d$//) {
        failedcmd_report_cmd undef, @cmd;
-       fail "curl failed to print 3-digit HTTP code";
+       fail __ "curl failed to print 3-digit HTTP code";
     }
     my $code = $&;
     return undef if $code eq '404' && $ok404;
-    fail "fetch of $url gave HTTP code $code"
+    fail f_ "fetch of %s gave HTTP code %s", $url, $code
        unless $url =~ m#^file://# or $code =~ m/^2/;
     return decode_json($json);
 }
@@ -1082,15 +1182,17 @@ sub canonicalise_suite_ftpmasterapi {
        } qw(codename name);
        push @matched, $entry;
     }
-    fail "unknown suite $isuite" unless @matched;
+    fail f_ "unknown suite %s, maybe -d would help", $isuite
+       unless @matched;
     my $cn;
     eval {
-       @matched==1 or die "multiple matches for suite $isuite\n";
+       @matched==1 or die f_ "multiple matches for suite %s\n", $isuite;
        $cn = "$matched[0]{codename}";
-       defined $cn or die "suite $isuite info has no codename\n";
-       $cn =~ m/^$suite_re$/ or die "suite $isuite maps to bad codename\n";
+       defined $cn or die f_ "suite %s info has no codename\n", $isuite;
+       $cn =~ m/^$suite_re$/
+           or die f_ "suite %s maps to bad codename\n", $isuite;
     };
-    die "bad ftpmaster api response: $@\n".Dumper(\@matched)
+    die +(__ "bad ftpmaster api response: ")."$@\n".Dumper(\@matched)
        if length $@;
     return $cn;
 }
@@ -1104,18 +1206,18 @@ sub archive_query_ftpmasterapi {
        eval {
            my $vsn = "$entry->{version}";
            my ($ok,$msg) = version_check $vsn;
-           die "bad version: $msg\n" unless $ok;
+           die f_ "bad version: %s\n", $msg unless $ok;
            my $component = "$entry->{component}";
-           $component =~ m/^$component_re$/ or die "bad component";
+           $component =~ m/^$component_re$/ or die __ "bad component";
            my $filename = "$entry->{filename}";
            $filename && $filename !~ m#[^-+:._~0-9a-zA-Z/]|^[/.]|/[/.]#
-               or die "bad filename";
+               or die __ "bad filename";
            my $sha256sum = "$entry->{sha256sum}";
-           $sha256sum =~ m/^[0-9a-f]+$/ or die "bad sha256sum";
+           $sha256sum =~ m/^[0-9a-f]+$/ or die __ "bad sha256sum";
            push @rows, [ $vsn, "/pool/$component/$filename",
                          $digester, $sha256sum ];
        };
-       die "bad ftpmaster api response: $@\n".Dumper($entry)
+       die +(__ "bad ftpmaster api response: ")."$@\n".Dumper($entry)
            if length $@;
     }
     @rows = sort { -version_compare($a->[0],$b->[0]) } @rows;
@@ -1154,15 +1256,15 @@ sub aptget_cache_clean {
 
 sub aptget_lock_acquire () {
     my $lockfile = "$aptget_base/lock";
-    open APTGET_LOCK, '>', $lockfile or die "open $lockfile: $!";
-    flock APTGET_LOCK, LOCK_EX or die "lock $lockfile: $!";
+    open APTGET_LOCK, '>', $lockfile or confess "open $lockfile: $!";
+    flock APTGET_LOCK, LOCK_EX or confess "lock $lockfile: $!";
 }
 
 sub aptget_prep ($) {
     my ($data) = @_;
     return if defined $aptget_base;
 
-    badcfg "aptget archive query method takes no data part"
+    badcfg __ "aptget archive query method takes no data part"
        if length $data;
 
     my $cache = $ENV{XDG_CACHE_DIR} // "$ENV{HOME}/.cache";
@@ -1177,7 +1279,7 @@ sub aptget_prep ($) {
     ensuredir $aptget_base;
 
     my $quoted_base = $aptget_base;
-    die "$quoted_base contains bad chars, cannot continue"
+    confess "$quoted_base contains bad chars, cannot continue"
        if $quoted_base =~ m/["\\]/; # apt.conf(5) says no escaping :-/
 
     ensuredir $aptget_base;
@@ -1193,17 +1295,17 @@ sub aptget_prep ($) {
     cfg_apply_map(\$aptsuites, 'suite map',
                  access_cfg('aptget-suite-map', 'RETURN-UNDEF'));
 
-    open SRCS, ">", "$aptget_base/$sourceslist" or die $!;
+    open SRCS, ">", "$aptget_base/$sourceslist" or confess $!;
     printf SRCS "deb-src %s %s %s\n",
        access_cfg('mirror'),
        $aptsuites,
        access_cfg('aptget-components')
-       or die $!;
+       or confess $!;
 
     ensuredir "$aptget_base/cache";
     ensuredir "$aptget_base/lists";
 
-    open CONF, ">", $aptget_configpath or die $!;
+    open CONF, ">", $aptget_configpath or confess $!;
     print CONF <<END;
 Debug::NoLocking "true";
 APT::Get::List-Cleanup "false";
@@ -1223,10 +1325,10 @@ END
                        Dir::Etc::preferencesparts
                      )) {
        ensuredir "$aptget_base/$key";
-       print CONF "$key \"$quoted_base/$key\";\n" or die $!;
+       print CONF "$key \"$quoted_base/$key\";\n" or confess $!;
     };
 
-    my $oldatime = (time // die $!) - 1;
+    my $oldatime = (time // confess $!) - 1;
     foreach my $oldlist (<$aptget_base/lists/*Release>) {
        next unless stat_exists $oldlist;
        my ($mtime) = (stat _)[9];
@@ -1245,13 +1347,13 @@ END
     my @inreleasefiles = grep { m#/InRelease$# } @releasefiles;
     @releasefiles = @inreleasefiles if @inreleasefiles;
     if (!@releasefiles) {
-       fail <<END;
-apt seemed to not to update dgit's cached Release files for $isuite.
-(Perhaps $cache
+       fail f_ <<END, $isuite, $cache;
+apt seemed to not to update dgit's cached Release files for %s.
+(Perhaps %s
  is on a filesystem mounted `noatime'; if so, please use `relatime'.)
 END
     }
-    die "apt updated too many Release files (@releasefiles), erk"
+    confess "apt updated too many Release files (@releasefiles), erk"
        unless @releasefiles == 1;
 
     ($aptget_releasefile) = @releasefiles;
@@ -1267,8 +1369,9 @@ sub canonicalise_suite_aptget {
        my $val = $release->{$name};
        if (defined $val) {
            printdebug "release file $name: $val\n";
-           $val =~ m/^$suite_re$/o or fail
- "Release file ($aptget_releasefile) specifies intolerable $name";
+           $val =~ m/^$suite_re$/o or fail f_
+               "Release file (%s) specifies intolerable %s",
+               $aptget_releasefile, $name;
            cfg_apply_map(\$val, 'suite rmap',
                          access_cfg('aptget-suite-rmap', 'RETURN-UNDEF'));
            return $val
@@ -1295,8 +1398,9 @@ sub archive_query_aptget {
        aptget_aptget(), qw(--download-only --only-source source), $package;
 
     my @dscs = <$aptget_base/source/*.dsc>;
-    fail "apt-get source did not produce a .dsc" unless @dscs;
-    fail "apt-get source produced several .dscs (@dscs)" unless @dscs==1;
+    fail __ "apt-get source did not produce a .dsc" unless @dscs;
+    fail f_ "apt-get source produced several .dscs (%s)", "@dscs"
+       unless @dscs==1;
 
     my $pre_dsc = parsecontrol $dscs[0], $dscs[0], 1;
 
@@ -1310,6 +1414,7 @@ sub file_in_archive_aptget () { return undef; }
 sub package_not_wholly_new_aptget () { return undef; }
 
 #---------- `dummyapicat' archive query method ----------
+# (untranslated, because this is for testing purposes etc.)
 
 sub archive_query_dummycatapi { archive_query_ftpmasterapi @_; }
 sub canonicalise_suite_dummycatapi { canonicalise_suite_ftpmasterapi @_; }
@@ -1323,7 +1428,7 @@ sub dummycatapi_run_in_mirror ($@) {
     my @cmd = (qw(sh -ec), 'cd "$1"; shift'."\n".$rune,
               qw(x), $mirror, @$argl);
     debugcmd "-|", @cmd;
-    open FIA, "-|", @cmd or die $!;
+    open FIA, "-|", @cmd or confess $!;
     my $r = $fn->();
     close FIA or ($!==0 && $?==141) or die failedcmd @cmd;
     return $r;
@@ -1398,10 +1503,11 @@ sub madison_get_parse {
 sub canonicalise_suite_madison {
     # madison canonicalises for us
     my @r = madison_get_parse(@_);
-    @r or fail
-       "unable to canonicalise suite using package $package".
-       " which does not appear to exist in suite $isuite;".
-       " --existing-package may help";
+    @r or fail f_
+       "unable to canonicalise suite using package %s".
+       " which does not appear to exist in suite %s;".
+       " --existing-package may help",
+       $package, $isuite;
     return $r[0][2];
 }
 
@@ -1409,6 +1515,7 @@ sub file_in_archive_madison { return undef; }
 sub package_not_wholly_new_madison { return undef; }
 
 #---------- `sshpsql' archive query method ----------
+# (untranslated, because this is obsolete)
 
 sub sshpsql ($$$) {
     my ($data,$runeinfo,$sql) = @_;
@@ -1424,7 +1531,7 @@ sub sshpsql ($$$) {
               " export LC_MESSAGES=C; export LC_CTYPE=C;".
               " ".shellquote qw(psql -A), $dbname, qw(-c), $sql);
     debugcmd "|",@cmd;
-    open P, "-|", @cmd or die $!;
+    open P, "-|", @cmd or confess $!;
     while (<P>) {
        chomp or die;
        printdebug(">|$_|\n");
@@ -1487,6 +1594,7 @@ sub file_in_archive_sshpsql ($$$) { return undef; }
 sub package_not_wholly_new_sshpsql ($$$) { return undef; }
 
 #---------- `dummycat' archive query method ----------
+# (untranslated, because this is for testing purposes etc.)
 
 sub canonicalise_suite_dummycat ($$) {
     my ($proto,$data) = @_;
@@ -1532,6 +1640,7 @@ sub file_in_archive_dummycat () { return undef; }
 sub package_not_wholly_new_dummycat () { return undef; }
 
 #---------- tag format handling ----------
+# (untranslated, because everything should be new tag format by now)
 
 sub access_cfg_tagformats () {
     split /\,/, access_cfg('dgit-tag-format');
@@ -1586,12 +1695,12 @@ sub select_tagformat () {
 
 sub canonicalise_suite () {
     return if defined $csuite;
-    fail "cannot operate on $isuite suite" if $isuite eq 'UNRELEASED';
+    fail f_ "cannot operate on %s suite", $isuite if $isuite eq 'UNRELEASED';
     $csuite = archive_query('canonicalise_suite');
     if ($isuite ne $csuite) {
-       progress "canonical suite name for $isuite is $csuite";
+       progress f_ "canonical suite name for %s is %s", $isuite, $csuite;
     } else {
-       progress "canonical suite name is $csuite";
+       progress f_ "canonical suite name is %s", $csuite;
     }
 }
 
@@ -1611,13 +1720,13 @@ sub get_archive_dsc () {
            $digester->add($dscdata);
            my $got = $digester->hexdigest();
            $got eq $digest or
-               fail "$dscurl has hash $got but".
-                   " archive told us to expect $digest";
+               fail f_ "%s has hash %s but archive told us to expect %s",
+                       $dscurl, $got, $digest;
        }
        parse_dscdata();
        my $fmt = getfield $dsc, 'Format';
        $format_ok{$fmt} or forceable_fail [qw(unsupported-source-format)],
-           "unsupported source format $fmt, sorry";
+           f_ "unsupported source format %s, sorry", $fmt;
            
        $dsc_checked = !!$digester;
        printdebug "get_archive_dsc: Version ".(getfield $dsc, 'Version')."\n";
@@ -1644,7 +1753,8 @@ sub check_for_git () {
            # NB that if we are pushing, $usedistro will be $distro/push
            $instead_distro= cfg("dgit-distro.$usedistro.diverts.$divert");
            $instead_distro =~ s{^/}{ access_basedistro()."/" }e;
-           progress "diverting to $divert (using config for $instead_distro)";
+           progress f_ "diverting to %s (using config for %s)",
+                       $divert, $instead_distro;
            return check_for_git();
        }
        failedcmd @cmd unless defined $r and $r =~ m/^[01]$/;
@@ -1660,7 +1770,7 @@ sub check_for_git () {
        # curl -sS -I with https_proxy prints
        # HTTP/1.0 200 Connection established
        $result =~ m/^\S+ (404|200) /s or
-           fail "unexpected results from git check query - ".
+           fail +(__ "unexpected results from git check query - ").
                Dumper($prefix, $result);
        my $code = $1;
        if ($code eq '404') {
@@ -1675,7 +1785,7 @@ sub check_for_git () {
     } elsif ($how eq 'false') {
        return 0;
     } else {
-       badcfg "unknown git-check \`$how'";
+       badcfg f_ "unknown git-check \`%s'", $how;
     }
 }
 
@@ -1690,7 +1800,7 @@ sub create_remote_git_repo () {
     } elsif ($how eq 'true') {
        # nothing to do
     } else {
-       badcfg "unknown git-create \`$how'";
+       badcfg f_ "unknown git-create \`%s'", $how;
     }
 }
 
@@ -1722,13 +1832,13 @@ sub remove_stray_gits ($) {
     my ($what) = @_;
     my @gitscmd = qw(find -name .git -prune -print0);
     debugcmd "|",@gitscmd;
-    open GITS, "-|", @gitscmd or die $!;
+    open GITS, "-|", @gitscmd or confess $!;
     {
        local $/="\0";
        while (<GITS>) {
            chomp or die;
-           print STDERR "$us: warning: removing from $what: ",
-               (messagequote $_), "\n";
+           print STDERR f_ "%s: warning: removing from %s: %s\n",
+               $us, $what, (messagequote $_);
            rmtree $_;
        }
     }
@@ -1740,7 +1850,7 @@ sub mktree_in_ud_from_only_subdir ($;$) {
     # changes into the subdir
 
     my (@dirs) = <*/.>;
-    die "expected one subdir but found @dirs ?" unless @dirs==1;
+    confess "expected one subdir but found @dirs ?" unless @dirs==1;
     $dirs[0] =~ m#^([^/]+)/\.$# or die;
     my $dir = $1;
     changedir $dir;
@@ -1773,7 +1883,7 @@ sub dsc_files_info () {
        foreach (split /\n/, $field) {
            next unless m/\S/;
            m/^(\w+) (\d+) (\S+)$/ or
-               fail "could not parse .dsc $fname line \`$_'";
+               fail f_ "could not parse .dsc %s line \`%s'", $fname, $_;
            my $digester = eval "$module"."->$method;" or die $@;
            push @out, {
                Hash => $1,
@@ -1784,8 +1894,8 @@ sub dsc_files_info () {
        }
        return @out;
     }
-    fail "missing any supported Checksums-* or Files field in ".
-       $dsc->get_option('name');
+    fail f_ "missing any supported Checksums-* or Files field in %s",
+           $dsc->get_option('name');
 }
 
 sub dsc_files () {
@@ -1829,8 +1939,9 @@ sub files_compare_inputs (@) {
                if (defined $$re) {
                    $fchecked{$f}{$in_name} = 1;
                    $$re eq $info or
-                       fail "hash or size of $f varies in $fname fields".
-                       " (between: ".$showinputs->().")";
+                       fail f_
+              "hash or size of %s varies in %s fields (between: %s)",
+                                $f, $fname, $showinputs->();
                } else {
                    $$re = $info;
                }
@@ -1838,17 +1949,18 @@ sub files_compare_inputs (@) {
            @files = sort @files;
            $expected_files //= \@files;
            "@$expected_files" eq "@files" or
-               fail "file list in $in_name varies between hash fields!";
+               fail f_ "file list in %s varies between hash fields!",
+                       $in_name;
        }
        $expected_files or
-           fail "$in_name has no files list field(s)";
+           fail f_ "%s has no files list field(s)", $in_name;
     }
     printdebug "files_compare_inputs ".Dumper(\%fchecked, \%record)
        if $debuglevel>=2;
 
     grep { keys %$_ == @$inputs-1 } values %fchecked
-       or fail "no file appears in all file lists".
-       " (looked in: ".$showinputs->().")";
+       or fail f_ "no file appears in all file lists (looked in: %s)",
+                  $showinputs->();
 }
 
 sub is_orig_file_in_dsc ($$) {
@@ -1860,13 +1972,6 @@ sub is_orig_file_in_dsc ($$) {
     return 1;
 }
 
-sub is_orig_file_of_vsn ($$) {
-    my ($f, $upstreamvsn) = @_;
-    my $base = srcfn $upstreamvsn, '';
-    return 0 unless $f =~ m/^\Q$base\E\.$orig_f_tail_re$/;
-    return 1;
-}
-
 # This function determines whether a .changes file is source-only from
 # the point of view of dak.  Thus, it permits *_source.buildinfo
 # files.
@@ -1894,7 +1999,7 @@ sub test_source_only_changes ($) {
         $l =~ m/\S+$/ or next;
         # \.tar\.[a-z0-9]+ covers orig.tar and the tarballs in native packages
         unless ($& =~ m/(?:\.dsc|\.diff\.gz|\.tar\.[a-z0-9]+|_source\.buildinfo)$/) {
-            print "purportedly source-only changes polluted by $&\n";
+            print f_ "purportedly source-only changes polluted by %s\n", $&;
             return 0;
         }
     }
@@ -1907,7 +2012,7 @@ sub changes_update_origs_from_dsc ($$$$) {
     printdebug "checking origs needed ($upstreamvsn)...\n";
     $_ = getfield $changes, 'Files';
     m/^\w+ \d+ (\S+ \S+) \S+$/m or
-       fail "cannot find section/priority from .changes Files field";
+       fail __ "cannot find section/priority from .changes Files field";
     my $placementinfo = $1;
     my %changed;
     printdebug "checking origs needed placement '$placementinfo'...\n";
@@ -1919,7 +2024,7 @@ sub changes_update_origs_from_dsc ($$$$) {
        printdebug "origs $file is_orig\n";
        my $have = archive_query('file_in_archive', $file);
        if (!defined $have) {
-           print STDERR <<END;
+           print STDERR __ <<END;
 archive does not support .orig check; hope you used --ch:--sa/-sd if needed
 END
            return;
@@ -1936,25 +2041,27 @@ END
                $_ = $dsc->{$fname};
                next unless defined;
                m/^(\w+) .* \Q$file\E$/m or
-                   fail ".dsc $fname missing entry for $file";
+                   fail f_ ".dsc %s missing entry for %s", $fname, $file;
                if ($h->{$archivefield} eq $1) {
                    $same++;
                } else {
-                   push @differ,
- "$archivefield: $h->{$archivefield} (archive) != $1 (local .dsc)";
+                   push @differ, f_
+                       "%s: %s (archive) != %s (local .dsc)",
+                       $archivefield, $h->{$archivefield}, $1;
                }
            }
-           die "$file ".Dumper($h)." ?!" if $same && @differ;
+           confess "$file ".Dumper($h)." ?!" if $same && @differ;
            $found_same++
                if $same;
-           push @found_differ, "archive $h->{filename}: ".join "; ", @differ
+           push @found_differ,
+               f_ "archive %s: %s", $h->{filename}, join "; ", @differ
                if @differ;
        }
        printdebug "origs $file f.same=$found_same".
            " #f._differ=$#found_differ\n";
        if (@found_differ && !$found_same) {
            fail join "\n",
-               "archive contains $file with different checksum",
+               (f_ "archive contains %s with different checksum", $file),
                @found_differ;
        }
        # Now we edit the changes file to add or remove it
@@ -1972,7 +2079,7 @@ END
                $dsc_data =~ m/^(.* \Q$file\E$)$/m or die "$dsc_data $file ?";
                my $extra = $1;
                $extra =~ s/ \d+ /$&$placementinfo /
-                   or die "$fname $extra >$dsc_data< ?"
+                   or confess "$fname $extra >$dsc_data< ?"
                    if $fname eq 'Files';
                $changes->{$fname} .= "\n". $extra;
                $changed{$file} = "added";
@@ -1981,7 +2088,7 @@ END
     }
     if (%changed) {
        foreach my $file (keys %changed) {
-           progress sprintf
+           progress f_
                "edited .changes for archive .orig contents: %s %s",
                $changed{$file}, $file;
        }
@@ -1990,10 +2097,11 @@ END
        if (act_local()) {
            rename $chtmp,$changesfile or die "$changesfile $!";
        } else {
-           progress "[new .changes left in $changesfile]";
+           progress f_ "[new .changes left in %s]", $changesfile;
        }
     } else {
-       progress "$changesfile already has appropriate .orig(s) (if any)";
+       progress f_ "%s already has appropriate .orig(s) (if any)",
+                   $changesfile;
     }
 }
 
@@ -2002,28 +2110,6 @@ sub make_commit ($) {
     return cmdoutput @git, qw(hash-object -w -t commit), $file;
 }
 
-sub make_commit_text ($) {
-    my ($text) = @_;
-    my ($out, $in);
-    my @cmd = (@git, qw(hash-object -w -t commit --stdin));
-    debugcmd "|",@cmd;
-    print Dumper($text) if $debuglevel > 1;
-    my $child = open2($out, $in, @cmd) or die $!;
-    my $h;
-    eval {
-       print $in $text or die $!;
-       close $in or die $!;
-       $h = <$out>;
-       $h =~ m/^\w+$/ or die;
-       $h = $&;
-       printdebug "=> $h\n";
-    };
-    close $out;
-    waitpid $child, 0 == $child or die "$child $!";
-    $? and failedcmd @cmd;
-    return $h;
-}
-
 sub clogp_authline ($) {
     my ($clogp) = @_;
     my $author = getfield $clogp, 'Maintainer';
@@ -2038,8 +2124,9 @@ sub clogp_authline ($) {
     my $date = cmdoutput qw(date), '+%s %z', qw(-d), getfield($clogp,'Date');
     my $authline = "$author $date";
     $authline =~ m/$git_authline_re/o or
-       fail "unexpected commit author line format \`$authline'".
-       " (was generated from changelog Maintainer field)";
+       fail f_ "unexpected commit author line format \`%s'".
+               " (was generated from changelog Maintainer field)",
+               $authline;
     return ($1,$2,$3) if wantarray;
     return $authline;
 }
@@ -2052,14 +2139,14 @@ sub vendor_patches_distro ($$) {
     printdebug "checking for vendor-specific $series ($what)\n";
 
     if (!open SERIES, "<", $series) {
-       die "$series $!" unless $!==ENOENT;
+       confess "$series $!" unless $!==ENOENT;
        return;
     }
     while (<SERIES>) {
        next unless m/\S/;
        next if m/^\s+\#/;
 
-       print STDERR <<END;
+       print STDERR __ <<END;
 
 Unfortunately, this source package uses a feature of dpkg-source where
 the same source package unpacks to different source code on different
@@ -2072,8 +2159,9 @@ different packages, if different distros are supposed to have
 different code).
 
 END
-       fail "Found active distro-specific series file for".
-           " $checkdistro ($what): $series, cannot continue";
+       fail f_ "Found active distro-specific series file for".
+               " %s (%s): %s, cannot continue",
+               $checkdistro, $what, $series;
     }
     die "$series $!" if SERIES->error;
     close SERIES;
@@ -2102,11 +2190,11 @@ sub check_for_vendor_patches () {
     use Dpkg::Vendor;
     vendor_patches_distro($ENV{DEB_VENDOR}, "DEB_VENDOR");
     vendor_patches_distro(Dpkg::Vendor::get_current_vendor(),
-                        "Dpkg::Vendor \`current vendor'");
+                         __ "Dpkg::Vendor \`current vendor'");
     vendor_patches_distro(access_basedistro(),
-                         "(base) distro being accessed");
+                         __ "(base) distro being accessed");
     vendor_patches_distro(access_nomdistro(),
-                         "(nominal) distro being accessed");
+                         __ "(nominal) distro being accessed");
 }
 
 sub generate_commits_from_dsc () {
@@ -2127,12 +2215,12 @@ sub generate_commits_from_dsc () {
            printdebug "linked (using ...,fetch).\n";
        } elsif ((printdebug "($!) "),
                 $! != ENOENT) {
-           fail "accessing $buildproductsdir/$f,fetch: $!";
+           fail f_ "accessing %s: %s", "$buildproductsdir/$f,fetch", $!;
        } elsif (link_ltarget $upper_f, $f) {
            printdebug "linked.\n";
        } elsif ((printdebug "($!) "),
                 $! != ENOENT) {
-           fail "accessing $buildproductsdir/$f: $!";
+           fail f_ "accessing %s: %s", "$buildproductsdir/$f", $!;
        } else {
            printdebug "absent.\n";
        }
@@ -2143,18 +2231,18 @@ sub generate_commits_from_dsc () {
 
        printdebug "considering saving $f: ";
 
-       if (link $f, $upper_f) {
+       if (rename_link_xf 1, $f, $upper_f) {
            printdebug "linked.\n";
-       } elsif ((printdebug "($!) "),
+       } elsif ((printdebug "($@) "),
                 $! != EEXIST) {
-           fail "saving $buildproductsdir/$f: $!";
+           fail f_ "saving %s: %s", "$buildproductsdir/$f", $@;
        } elsif (!$refetched) {
            printdebug "no need.\n";
-       } elsif (link $f, "$upper_f,fetch") {
+       } elsif (rename_link_xf 1, $f, "$upper_f,fetch") {
            printdebug "linked (using ...,fetch).\n";
-       } elsif ((printdebug "($!) "),
+       } elsif ((printdebug "($@) "),
                 $! != EEXIST) {
-           fail "saving $buildproductsdir/$f,fetch: $!";
+           fail f_ "saving %s: %s", "$buildproductsdir/$f,fetch", $@;
        } else {
            printdebug "cannot.\n";
        }
@@ -2201,9 +2289,9 @@ sub generate_commits_from_dsc () {
                new Dpkg::Compression::Process compression => $cname;
            @compr_cmd = $compr_proc->get_uncompress_cmdline();
            my $compr_fh = new IO::Handle;
-           my $compr_pid = open $compr_fh, "-|" // die $!;
+           my $compr_pid = open $compr_fh, "-|" // confess $!;
            if (!$compr_pid) {
-               open STDIN, "<&", $input or die $!;
+               open STDIN, "<&", $input or confess $!;
                exec @compr_cmd;
                die "dgit (child): exec $compr_cmd[0]: $!\n";
            }
@@ -2211,23 +2299,23 @@ sub generate_commits_from_dsc () {
        }
 
        rmtree "_unpack-tar";
-       mkdir "_unpack-tar" or die $!;
+       mkdir "_unpack-tar" or confess $!;
        my @tarcmd = qw(tar -x -f -
                        --no-same-owner --no-same-permissions
                        --no-acls --no-xattrs --no-selinux);
-       my $tar_pid = fork // die $!;
+       my $tar_pid = fork // confess $!;
        if (!$tar_pid) {
-           chdir "_unpack-tar" or die $!;
-           open STDIN, "<&", $input or die $!;
+           chdir "_unpack-tar" or confess $!;
+           open STDIN, "<&", $input or confess $!;
            exec @tarcmd;
-           die "dgit (child): exec $tarcmd[0]: $!";
+           die f_ "dgit (child): exec %s: %s", $tarcmd[0], $!;
        }
-       $!=0; (waitpid $tar_pid, 0) == $tar_pid or die $!;
+       $!=0; (waitpid $tar_pid, 0) == $tar_pid or confess $!;
        !$? or failedcmd @tarcmd;
 
        close $input or
            (@compr_cmd ? ($?==SIGPIPE || failedcmd @compr_cmd)
-            : die $!);
+            : confess $!);
        # finally, we have the results in "tarball", but maybe
        # with the wrong permissions
 
@@ -2284,7 +2372,7 @@ sub generate_commits_from_dsc () {
     push @cmd, qw(-x --), $dscfn;
     runcmd @cmd;
 
-    my ($tree,$dir) = mktree_in_ud_from_only_subdir("source package");
+    my ($tree,$dir) = mktree_in_ud_from_only_subdir(__ "source package");
     if (madformat $dsc->{format}) { 
        check_for_vendor_patches();
     }
@@ -2302,7 +2390,7 @@ sub generate_commits_from_dsc () {
     my $r1clogp;
 
     printdebug "import clog search...\n";
-    parsechangelog_loop \@clogcmd, "package changelog", sub {
+    parsechangelog_loop \@clogcmd, (__ "package changelog"), sub {
        my ($thisstanza, $desc) = @_;
        no warnings qw(exiting);
 
@@ -2342,7 +2430,7 @@ sub generate_commits_from_dsc () {
        printdebug "import clog $r1clogp->{version} becomes r1\n";
     };
 
-    $clogp or fail "package changelog has no entries!";
+    $clogp or fail __ "package changelog has no entries!";
 
     my $authline = clogp_authline $clogp;
     my $changes = getfield $clogp, 'Changes';
@@ -2361,12 +2449,13 @@ sub generate_commits_from_dsc () {
        foreach my $tt (@tartrees) {
            printdebug "import tartree $tt->{F} $tt->{Tree}\n";
 
+           my $mbody = f_ "Import %s", $tt->{F};
            $tt->{Commit} = make_commit_text($tt->{Orig} ? <<END_O : <<END_T);
 tree $tt->{Tree}
 author $r1authline
 committer $r1authline
 
-Import $tt->{F}
+$mbody
 
 [dgit import orig $tt->{F}]
 END_O
@@ -2374,7 +2463,7 @@ tree $tt->{Tree}
 author $authline
 committer $authline
 
-Import $tt->{F}
+$mbody
 
 [dgit import tarball $package $cversion $tt->{F}]
 END_T
@@ -2383,14 +2472,14 @@ END_T
 
     printdebug "import main commit\n";
 
-    open C, ">../commit.tmp" or die $!;
-    print C <<END or die $!;
+    open C, ">../commit.tmp" or confess $!;
+    print C <<END or confess $!;
 tree $tree
 END
-    print C <<END or die $! foreach @tartrees;
+    print C <<END or confess $! foreach @tartrees;
 parent $_->{Commit}
 END
-    print C <<END or die $!;
+    print C <<END or confess $!;
 author $authline
 committer $authline
 
@@ -2399,7 +2488,7 @@ $changes
 [dgit import $treeimporthow $package $cversion]
 END
 
-    close C or die $!;
+    close C or confess $!;
     my $rawimport_hash = make_commit qw(../commit.tmp);
 
     if (madformat $dsc->{format}) {
@@ -2441,10 +2530,10 @@ END
                chomp $@;
                progress "warning: $@";
                $path = "$absurdity:$path";
-               progress "$us: trying slow absurd-git-apply...";
+               progress f_ "%s: trying slow absurd-git-apply...", $us;
                rename "../../gbp-pq-output","../../gbp-pq-output.0"
                    or $!==ENOENT
-                   or die $!;
+                   or confess $!;
            }
            eval {
                die "forbid absurd git-apply\n" if $use_absurd
@@ -2460,19 +2549,19 @@ END
                    'exec >/dev/null 2>>../../gbp-pq-output', @showcmd;
                debugcmd "+",@realcmd;
                if (system @realcmd) {
-                   die +(shellquote @showcmd).
-                       " failed: ".
-                       failedcmd_waitstatus()."\n";
+                   die f_ "%s failed: %s\n",
+                       +(shellquote @showcmd),
+                       failedcmd_waitstatus();
                }
 
                my $gapplied = git_rev_parse('HEAD');
                my $gappliedtree = cmdoutput @git, qw(rev-parse HEAD:);
                $gappliedtree eq $dappliedtree or
-                   fail <<END;
+                   fail f_ <<END, $gapplied, $gappliedtree, $dappliedtree;
 gbp-pq import and dpkg-source disagree!
- gbp-pq import gave commit $gapplied
- gbp-pq import gave tree $gappliedtree
- dpkg-source --before-build gave tree $dappliedtree
+ gbp-pq import gave commit %s
+ gbp-pq import gave tree %s
+ dpkg-source --before-build gave tree %s
 END
                $rawimport_hash = $gapplied;
            };
@@ -2484,11 +2573,11 @@ END
        }
     }
 
-    progress "synthesised git commit from .dsc $cversion";
+    progress f_ "synthesised git commit from .dsc %s", $cversion;
 
     my $rawimport_mergeinput = {
         Commit => $rawimport_hash,
-        Info => "Import of source package",
+        Info => __ "Import of source package",
     };
     my @output = ($rawimport_mergeinput);
 
@@ -2499,16 +2588,18 @@ END
            version_compare($oversion, $cversion);
        if ($vcmp < 0) {
            @output = ($rawimport_mergeinput, $lastpush_mergeinput,
-               { Message => <<END, ReverseParents => 1 });
-Record $package ($cversion) in archive suite $csuite
+               { ReverseParents => 1,
+                 Message => (f_ <<END, $package, $cversion, $csuite) });
+Record %s (%s) in archive suite %s
 END
        } elsif ($vcmp > 0) {
-           print STDERR <<END or die $!;
+           print STDERR f_ <<END, $cversion, $oversion,
 
-Version actually in archive:   $cversion (older)
-Last version pushed with dgit: $oversion (newer or same)
-$later_warning_msg
+Version actually in archive:   %s (older)
+Last version pushed with dgit: %s (newer or same)
+%s
 END
+               __ $later_warning_msg or confess $!;
             @output = $lastpush_mergeinput;
         } else {
            # Same version.  Use what's in the server git branch,
@@ -2538,22 +2629,22 @@ sub complete_file_from_dsc ($$;$) {
        open F, "<", "$tf" or die "$tf: $!";
        $fi->{Digester}->reset();
        $fi->{Digester}->addfile(*F);
-       F->error and die $!;
+       F->error and confess $!;
        $got = $fi->{Digester}->hexdigest();
        return $got eq $fi->{Hash};
     };
 
     if (stat_exists $tf) {
        if ($checkhash->()) {
-           progress "using existing $f";
+           progress f_ "using existing %s", $f;
            return 1;
        }
        if (!$refetched) {
-           fail "file $f has hash $got but .dsc".
-               " demands hash $fi->{Hash} ".
-               "(perhaps you should delete this file?)";
+           fail f_ "file %s has hash %s but .dsc demands hash %s".
+                   " (perhaps you should delete this file?)",
+                   $f, $got, $fi->{Hash};
        }
-       progress "need to fetch correct version of $f";
+       progress f_ "need to fetch correct version of %s", $f;
        unlink $tf or die "$tf $!";
        $$refetched = 1;
     } else {
@@ -2569,9 +2660,9 @@ sub complete_file_from_dsc ($$;$) {
     return 0 if !act_local();
 
     $checkhash->() or
-       fail "file $f has hash $got but .dsc".
-           " demands hash $fi->{Hash} ".
-           "(got wrong file from archive!)";
+       fail f_ "file %s has hash %s but .dsc demands hash %s".
+               " (got wrong file from archive!)",
+               $f, $got, $fi->{Hash};
 
     return 1;
 }
@@ -2664,7 +2755,7 @@ sub git_lrfetch_sane {
     for (;;) {
        printdebug "git_lrfetch_sane iteration $fetch_iteration\n";
         if (++$fetch_iteration > 10) {
-           fail "too many iterations trying to get sane fetch!";
+           fail __ "too many iterations trying to get sane fetch!";
        }
 
        my @look = map { "refs/$_" } @specs;
@@ -2672,14 +2763,14 @@ sub git_lrfetch_sane {
        debugcmd "|",@lcmd;
 
        my %wantr;
-       open GITLS, "-|", @lcmd or die $!;
+       open GITLS, "-|", @lcmd or confess $!;
        while (<GITLS>) {
            printdebug "=> ", $_;
            m/^(\w+)\s+(\S+)\n/ or die "ls-remote $_ ?";
            my ($objid,$rrefname) = ($1,$2);
            if (!$wanted_rref->($rrefname)) {
-               print STDERR <<END;
-warning: git ls-remote @look reported $rrefname; this is silly, ignoring it.
+               print STDERR f_ <<END, "@look", $rrefname;
+warning: git ls-remote %s reported %s; this is silly, ignoring it.
 END
                next;
            }
@@ -2722,8 +2813,8 @@ END
 git-fetch @fspecs created $lrefname which git ls-remote @look didn't list.
 END
                } else {
-                   print STDERR <<END
-warning: git fetch @fspecs created $lrefname; this is silly, deleting it.
+                   print STDERR f_ <<END, "@fspecs", $lrefname
+warning: git fetch %s created %s; this is silly, deleting it.
 END
                }
                runcmd_ordryrun_local @git, qw(update-ref -d), $lrefname;
@@ -2737,14 +2828,14 @@ END
            my $want = $wantr{$rrefname};
            next if $got eq $want;
            if (!defined $objgot{$want}) {
-               fail <<END unless act_local();
+               fail __ <<END unless act_local();
 --dry-run specified but we actually wanted the results of git fetch,
 so this is not going to work.  Try running dgit fetch first,
 or using --damp-run instead of --dry-run.
 END
-               print STDERR <<END;
-warning: git ls-remote suggests we want $lrefname
-warning:  and it should refer to $want
+               print STDERR f_ <<END, $lrefname, $want;
+warning: git ls-remote suggests we want %s
+warning:  and it should refer to %s
 warning:  but git fetch didn't fetch that object to any relevant ref.
 warning:  This may be due to a race with someone updating the server.
 warning:  Will try again...
@@ -2810,8 +2901,8 @@ sub git_fetch_us () {
        } elsif ($here{$lref} eq $objid) {
            lrfetchref_used $fullrefname;
        } else {
-           print STDERR
-               "Not updating $lref from $here{$lref} to $objid.\n";
+           print STDERR f_ "Not updating %s from %s to %s.\n",
+                           $lref, $here{$lref}, $objid;
        }
     });
 }
@@ -2859,20 +2950,20 @@ sub parse_dsc_field ($$) {
     }
 
     if (!defined $f) {
-       progress "$what: NO git hash";
+       progress f_ "%s: NO git hash", $what;
        parse_dsc_field_def_dsc_distro();
     } elsif (($dsc_hash, $dsc_distro, $dsc_hint_tag, $dsc_hint_url)
             = $f =~ m/^(\w+)\s+($distro_re)\s+($versiontag_re)\s+(\S+)(?:\s|$)/) {
-       progress "$what: specified git info ($dsc_distro)";
+       progress f_ "%s: specified git info (%s)", $what, $dsc_distro;
        $dsc_hint_tag = [ $dsc_hint_tag ];
     } elsif ($f =~ m/^\w+\s*$/) {
        $dsc_hash = $&;
        parse_dsc_field_def_dsc_distro();
        $dsc_hint_tag = [ debiantags +(getfield $dsc, 'Version'),
                          $dsc_distro ];
-       progress "$what: specified git hash";
+       progress f_ "%s: specified git hash", $what;
     } else {
-       fail "$what: invalid Dgit info";
+       fail f_ "%s: invalid Dgit info", $what;
     }
 }
 
@@ -2894,18 +2985,17 @@ sub resolve_dsc_field_commit ($$) {
        my $lrf = lrfetchrefs;
 
        if (!$chase_dsc_distro) {
-           progress
-               "not chasing .dsc distro $dsc_distro: not fetching $what";
+           progress f_ "not chasing .dsc distro %s: not fetching %s",
+                       $dsc_distro, $what;
            return 0;
        }
 
-       progress
-           ".dsc names distro $dsc_distro: fetching $what";
+       progress f_ ".dsc names distro %s: fetching %s", $dsc_distro, $what;
 
        my $url = access_giturl();
        if (!defined $url) {
-           defined $dsc_hint_url or fail <<END;
-.dsc Dgit metadata is in context of distro $dsc_distro
+           defined $dsc_hint_url or fail f_ <<END, $dsc_distro;
+.dsc Dgit metadata is in context of distro %s
 for which we have no configured url and .dsc provides no hint
 END
            my $proto =
@@ -2914,10 +3004,10 @@ END
            parse_cfg_bool "dsc-url-proto-ok", 'false',
                cfg("dgit.dsc-url-proto-ok.$proto",
                    "dgit.default.dsc-url-proto-ok")
-               or fail <<END;
-.dsc Dgit metadata is in context of distro $dsc_distro
+               or fail f_ <<END, $dsc_distro, $proto;
+.dsc Dgit metadata is in context of distro %s
 for which we have no configured url;
-.dsc provides hinted url with protocol $proto which is unsafe.
+.dsc provides hinted url with protocol %s which is unsafe.
 (can be overridden by config - consult documentation)
 END
            $url = $dsc_hint_url;
@@ -2935,30 +3025,30 @@ END
 
     if (parse_cfg_bool 'rewrite-map-enable', 'true', $rewrite_enable) {
        if (!defined $mapref) {
-           my $lrf = $do_fetch->("rewrite map", $rewritemap) or return;
+           my $lrf = $do_fetch->((__ "rewrite map"), $rewritemap) or return;
            $mapref = $lrf.'/'.$rewritemap;
        }
        my $rewritemapdata = git_cat_file $mapref.':map';
        if (defined $rewritemapdata
            && $rewritemapdata =~ m/^$dsc_hash(?:[ \t](\w+))/m) {
-           progress
+           progress __
                "server's git history rewrite map contains a relevant entry!";
 
            $dsc_hash = $1;
            if (defined $dsc_hash) {
-               progress "using rewritten git hash in place of .dsc value";
+               progress __ "using rewritten git hash in place of .dsc value";
            } else {
-               progress "server data says .dsc hash is to be disregarded";
+               progress __ "server data says .dsc hash is to be disregarded";
            }
        }
     }
 
     if (!defined git_cat_file $dsc_hash) {
        my @tags = map { "tags/".$_ } @$dsc_hint_tag;
-       my $lrf = $do_fetch->("additional commits", @tags) &&
+       my $lrf = $do_fetch->((__ "additional commits"), @tags) &&
            defined git_cat_file $dsc_hash
-           or fail <<END;
-.dsc Dgit metadata requires commit $dsc_hash
+           or fail f_ <<END, $dsc_hash;
+.dsc Dgit metadata requires commit %s
 but we could not obtain that object anywhere.
 END
        foreach my $t (@tags) {
@@ -2982,11 +3072,11 @@ sub fetch_from_archive () {
     get_archive_dsc();
 
     if ($dsc) {
-       parse_dsc_field($dsc, 'last upload to archive');
+       parse_dsc_field($dsc, __ 'last upload to archive');
        resolve_dsc_field_commit access_basedistro,
            lrfetchrefs."/".$rewritemap
     } else {
-       progress "no version available from the archive";
+       progress __ "no version available from the archive";
     }
 
     # If the archive's .dsc has a Dgit field, there are three
@@ -3069,19 +3159,19 @@ sub fetch_from_archive () {
     printdebug "previous reference hash=$lastpush_hash\n";
     $lastpush_mergeinput = $lastpush_hash && {
         Commit => $lastpush_hash,
-       Info => "dgit suite branch on dgit git server",
+       Info => (__ "dgit suite branch on dgit git server"),
     };
 
     my $lastfetch_hash = git_get_ref(lrref());
     printdebug "fetch_from_archive: lastfetch=$lastfetch_hash\n";
     my $lastfetch_mergeinput = $lastfetch_hash && {
        Commit => $lastfetch_hash,
-       Info => "dgit client's archive history view",
+       Info => (__ "dgit client's archive history view"),
     };
 
     my $dsc_mergeinput = $dsc_hash && {
         Commit => $dsc_hash,
-        Info => "Dgit field in .dsc from archive",
+        Info => (__ "Dgit field in .dsc from archive"),
     };
 
     my $cwd = getcwd();
@@ -3094,7 +3184,7 @@ sub fetch_from_archive () {
            printdebug "del_lrfetchrefs: $objid $fullrefname\n";
            if (!$gur) {
                $gur ||= new IO::Handle;
-               open $gur, "|-", qw(git update-ref --stdin) or die $!;
+               open $gur, "|-", qw(git update-ref --stdin) or confess $!;
            }
            printf $gur "delete %s %s\n", $fullrefname, $objid;
        }
@@ -3108,23 +3198,24 @@ sub fetch_from_archive () {
        if (!$lastpush_hash || $dsc_hash eq $lastpush_hash) {
            @mergeinputs = $dsc_mergeinput
        } elsif (is_fast_fwd($dsc_hash,$lastpush_hash)) {
-           print STDERR <<END or die $!;
+           print STDERR f_ <<END, $dsc_hash, $lastpush_hash,
 
 Git commit in archive is behind the last version allegedly pushed/uploaded.
-Commit referred to by archive: $dsc_hash
-Last version pushed with dgit: $lastpush_hash
-$later_warning_msg
+Commit referred to by archive: %s
+Last version pushed with dgit: %s
+%s
 END
+               __ $later_warning_msg or confess $!;
            @mergeinputs = ($lastpush_mergeinput);
        } else {
            # Archive has .dsc which is not a descendant of the last dgit
            # push.  This can happen if the archive moves .dscs about.
            # Just follow its lead.
            if (is_fast_fwd($lastpush_hash,$dsc_hash)) {
-               progress "archive .dsc names newer git commit";
+               progress __ "archive .dsc names newer git commit";
                @mergeinputs = ($dsc_mergeinput);
            } else {
-               progress "archive .dsc names other git commit, fixing up";
+               progress __ "archive .dsc names other git commit, fixing up";
                @mergeinputs = ($dsc_mergeinput, $lastpush_mergeinput);
            }
        }
@@ -3144,18 +3235,19 @@ END
     } elsif ($lastpush_hash) {
        # only in git, not in the archive yet
        @mergeinputs = ($lastpush_mergeinput);
-       print STDERR <<END or die $!;
+       print STDERR f_ <<END,
 
 Package not found in the archive, but has allegedly been pushed using dgit.
-$later_warning_msg
+%s
 END
+           __ $later_warning_msg or confess $!;
     } else {
        printdebug "nothing found!\n";
        if (defined $skew_warning_vsn) {
-           print STDERR <<END or die $!;
+           print STDERR f_ <<END, $skew_warning_vsn or confess $!;
 
 Warning: relevant archive skew detected.
-Archive allegedly contains $skew_warning_vsn
+Archive allegedly contains %s
 But we were not able to obtain any version from the archive or git.
 
 END
@@ -3197,10 +3289,7 @@ END
        # here we go, then:
        my $tree_commit = $mergeinputs[0]{Commit};
 
-       my $tree = cmdoutput @git, qw(cat-file commit), $tree_commit;
-       $tree =~ m/\n\n/;  $tree = $`;
-       $tree =~ m/^tree (\w+)$/m or die "$dsc_hash tree ?";
-       $tree = $1;
+       my $tree = get_tree_of_commit $tree_commit;;
 
        # We use the changelog author of the package in question the
        # author of this pseudo-merge.  This is (roughly) correct if
@@ -3221,27 +3310,27 @@ END
 
        my $mcf = dgit_privdir()."/mergecommit";
        open MC, ">", $mcf or die "$mcf $!";
-       print MC <<END or die $!;
+       print MC <<END or confess $!;
 tree $tree
 END
 
        my @parents = grep { $_->{Commit} } @mergeinputs;
        @parents = reverse @parents if $compat_info->{ReverseParents};
-       print MC <<END or die $! foreach @parents;
+       print MC <<END or confess $! foreach @parents;
 parent $_->{Commit}
 END
 
-       print MC <<END or die $!;
+       print MC <<END or confess $!;
 author $author
 committer $author
 
 END
 
        if (defined $compat_info->{Message}) {
-           print MC $compat_info->{Message} or die $!;
+           print MC $compat_info->{Message} or confess $!;
        } else {
-           print MC <<END or die $!;
-Record $package ($cversion) in archive suite $csuite
+           print MC f_ <<END, $package, $cversion, $csuite or confess $!;
+Record %s (%s) in archive suite %s
 
 Record that
 END
@@ -3249,17 +3338,17 @@ END
                my ($mi) = (@_);
                my $mversion = mergeinfo_version $mi;
                printf MC "  %-20s %s\n", $mversion, $mi->{Info}
-                   or die $!;
+                   or confess $!;
            };
 
            $message_add_info->($mergeinputs[0]);
-           print MC <<END or die $!;
+           print MC __ <<END or confess $!;
 should be treated as descended from
 END
            $message_add_info->($_) foreach @mergeinputs[1..$#mergeinputs];
        }
 
-       close MC or die $!;
+       close MC or confess $!;
        $hash = make_commit $mcf;
     } else {
        $hash = $mergeinputs[0]{Commit};
@@ -3269,12 +3358,12 @@ END
     my $chkff = sub {
        my ($lasth, $what) = @_;
        return unless $lasth;
-       die "$lasth $hash $what ?" unless is_fast_fwd($lasth, $hash);
+       confess "$lasth $hash $what ?" unless is_fast_fwd($lasth, $hash);
     };
 
-    $chkff->($lastpush_hash, 'dgit repo server tip (last push)')
+    $chkff->($lastpush_hash, __ 'dgit repo server tip (last push)')
        if $lastpush_hash;
-    $chkff->($lastfetch_hash, 'local tracking tip (last fetch)');
+    $chkff->($lastfetch_hash, __ 'local tracking tip (last fetch)');
 
     fetch_from_archive_record_1($hash);
 
@@ -3284,11 +3373,11 @@ END
        my $got_vsn = getfield $gotclogp, 'Version';
        printdebug "SKEW CHECK GOT $got_vsn\n";
        if (version_compare($got_vsn, $skew_warning_vsn) < 0) {
-           print STDERR <<END or die $!;
+           print STDERR f_ <<END, $skew_warning_vsn, $got_vsn or confess $!;
 
 Warning: archive skew detected.  Using the available version:
-Archive allegedly contains    $skew_warning_vsn
-We were able to obtain only   $got_vsn
+Archive allegedly contains    %s
+We were able to obtain only   %s
 
 END
        }
@@ -3300,7 +3389,7 @@ END
 
     lrfetchref_used lrfetchref();
 
-    check_gitattrs($hash, "fetched source tree");
+    check_gitattrs($hash, __ "fetched source tree");
 
     unshift @end, $del_lrfetchrefs;
     return $hash;
@@ -3328,15 +3417,15 @@ sub setup_mergechangelogs (;$) {
        while (<ATTRS>) {
            chomp;
            next if m{^debian/changelog\s};
-           print NATTRS $_, "\n" or die $!;
+           print NATTRS $_, "\n" or confess $!;
        }
-       ATTRS->error and die $!;
+       ATTRS->error and confess $!;
        close ATTRS;
     }
-    print NATTRS "debian/changelog merge=$driver\n" or die $!;
+    print NATTRS "debian/changelog merge=$driver\n" or confess $!;
     close NATTRS;
 
-    set_local_git_config "$cb.name", 'debian/changelog merge driver';
+    set_local_git_config "$cb.name", __ 'debian/changelog merge driver';
     set_local_git_config "$cb.driver", 'dpkg-mergechangelogs -m %O %A %B %A';
 
     rename "$attrs.new", "$attrs" or die "$attrs: $!";
@@ -3389,7 +3478,7 @@ sub is_gitattrs_setup () {
        printdebug "is_gitattrs_setup: found old macro\n";
        return 0;
     }
-    $gai->error and die $!;
+    $gai->error and confess $!;
     printdebug "is_gitattrs_setup: found nothing\n";
     return undef;
 }    
@@ -3400,7 +3489,7 @@ sub setup_gitattrs (;$) {
 
     my $already = is_gitattrs_setup();
     if ($already) {
-       progress <<END;
+       progress __ <<END;
 [attr]dgit-defuse-attrs already found, and proper, in .git/info/attributes
  not doing further gitattributes setup
 END
@@ -3410,12 +3499,13 @@ END
     my $af = "$maindir_gitcommon/info/attributes";
     ensuredir "$maindir_gitcommon/info";
 
-    open GAO, "> $af.new" or die $!;
-    print GAO <<END or die $! unless defined $already;
+    open GAO, "> $af.new" or confess $!;
+    print GAO <<END, __ <<ENDT or confess $! unless defined $already;
 *      dgit-defuse-attrs
 $new
-# ^ see GITATTRIBUTES in dgit(7) and dgit setup-new-tree in dgit(1)
 END
+# ^ see GITATTRIBUTES in dgit(7) and dgit setup-new-tree in dgit(1)
+ENDT
     my $gai = open_main_gitattrs();
     if ($gai) {
        while (<$gai>) {
@@ -3424,12 +3514,12 @@ END
                $_ = $new;
            }
            chomp;
-           print GAO $_, "\n" or die $!;
+           print GAO $_, "\n" or confess $!;
        }
-       $gai->error and die $!;
+       $gai->error and confess $!;
     }
-    close GAO or die $!;
-    rename "$af.new", "$af" or die "install $af: $!";
+    close GAO or confess $!;
+    rename "$af.new", "$af" or fail f_ "install %s: %s", $af, $!;
 }
 
 sub setup_new_tree () {
@@ -3447,7 +3537,7 @@ sub check_gitattrs ($$) {
     my @cmd = (@git, qw(ls-tree -lrz --), "${treeish}:");
     debugcmd "|",@cmd;
     my $gafl = new IO::File;
-    open $gafl, "-|", @cmd or die $!;
+    open $gafl, "-|", @cmd or confess $!;
     while (<$gafl>) {
        chomp or die;
        s/^\d+\s+\w+\s+\w+\s+(\d+)\t// or die;
@@ -3455,8 +3545,8 @@ sub check_gitattrs ($$) {
        next unless m{(?:^|/)\.gitattributes$};
 
        # oh dear, found one
-       print STDERR <<END;
-dgit: warning: $what contains .gitattributes
+       print STDERR f_ <<END, $what;
+dgit: warning: %s contains .gitattributes
 dgit: .gitattributes not (fully) defused.  Recommended: dgit setup-new-tree.
 END
        close $gafl;
@@ -3468,34 +3558,35 @@ END
 
 
 sub multisuite_suite_child ($$$) {
-    my ($tsuite, $merginputs, $fn) = @_;
+    my ($tsuite, $mergeinputs, $fn) = @_;
     # in child, sets things up, calls $fn->(), and returns undef
     # in parent, returns canonical suite name for $tsuite
     my $canonsuitefh = IO::File::new_tmpfile;
-    my $pid = fork // die $!;
+    my $pid = fork // confess $!;
     if (!$pid) {
        forkcheck_setup();
        $isuite = $tsuite;
        $us .= " [$isuite]";
        $debugprefix .= " ";
-       progress "fetching $tsuite...";
+       progress f_ "fetching %s...", $tsuite;
        canonicalise_suite();
-       print $canonsuitefh $csuite, "\n" or die $!;
-       close $canonsuitefh or die $!;
+       print $canonsuitefh $csuite, "\n" or confess $!;
+       close $canonsuitefh or confess $!;
        $fn->();
        return undef;
     }
-    waitpid $pid,0 == $pid or die $!;
-    fail "failed to obtain $tsuite: ".waitstatusmsg() if $? && $?!=256*4;
-    seek $canonsuitefh,0,0 or die $!;
+    waitpid $pid,0 == $pid or confess $!;
+    fail f_ "failed to obtain %s: %s", $tsuite, waitstatusmsg()
+       if $? && $?!=256*4;
+    seek $canonsuitefh,0,0 or confess $!;
     local $csuite = <$canonsuitefh>;
-    die $! unless defined $csuite && chomp $csuite;
+    confess $! unless defined $csuite && chomp $csuite;
     if ($? == 256*4) {
        printdebug "multisuite $tsuite missing\n";
        return $csuite;
     }
     printdebug "multisuite $tsuite ok (canon=$csuite)\n";
-    push @$merginputs, {
+    push @$mergeinputs, {
         Ref => lrref,
         Info => $csuite,
     };
@@ -3524,7 +3615,7 @@ sub fork_for_multisuite ($) {
                                            sub { });
     return 0 unless defined $cbasesuite;
 
-    fail "package $package missing in (base suite) $cbasesuite"
+    fail f_ "package %s missing in (base suite) %s", $package, $cbasesuite
        unless @mergeinputs;
 
     my @csuites = ($cbasesuite);
@@ -3539,7 +3630,6 @@ sub fork_for_multisuite ($) {
             fetch_one();
            finish 0;
        });
-       # xxx collecte the ref here
 
        $csubsuite =~ s/^\Q$cbasesuite\E-/-/;
        push @csuites, $csubsuite;
@@ -3557,9 +3647,9 @@ sub fork_for_multisuite ($) {
     if ($previous) {
        unshift @mergeinputs, {
             Commit => $previous,
-            Info => "local combined tracking branch",
-            Warning =>
- "archive seems to have rewound: local tracking branch is ahead!",
+            Info => (__ "local combined tracking branch"),
+            Warning => (__
+ "archive seems to have rewound: local tracking branch is ahead!"),
         };
     }
 
@@ -3598,8 +3688,9 @@ sub fork_for_multisuite ($) {
        my $tree = cmdoutput qw(git rev-parse), $needed[0]{Commit}.':';
 
        my $commit = "tree $tree\n";
-       my $msg = "Combine archive branches $csuite [dgit]\n\n".
-           "Input branches:\n";
+       my $msg = f_ "Combine archive branches %s [dgit]\n\n".
+                    "Input branches:\n",
+                    $csuite;
 
        foreach my $mi (sort { $a->{Index} <=> $b->{Index} } @mergeinputs) {
            printdebug "multisuite merge include $mi->{Info}\n";
@@ -3611,9 +3702,10 @@ sub fork_for_multisuite ($) {
                $mi->{Info};
        }
        my $authline = clogp_authline mergeinfo_getclogp $needed[0];
-       $msg .= "\nKey\n".
+       $msg .= __ "\nKey\n".
            " * marks the highest version branch, which choose to use\n".
-           " + marks each branch which was not already an ancestor\n\n".
+           " + marks each branch which was not already an ancestor\n\n";
+       $msg .=
            "[dgit multi-suite $csuite]\n";
        $commit .=
            "author $authline\n".
@@ -3625,15 +3717,15 @@ sub fork_for_multisuite ($) {
     fetch_from_archive_record_1($output);
     fetch_from_archive_record_2($output);
 
-    progress "calculated combined tracking suite $csuite";
+    progress f_ "calculated combined tracking suite %s", $csuite;
 
     return 1;
 }
 
 sub clone_set_head () {
-    open H, "> .git/HEAD" or die $!;
-    print H "ref: ".lref()."\n" or die $!;
-    close H or die $!;
+    open H, "> .git/HEAD" or confess $!;
+    print H "ref: ".lref()."\n" or confess $!;
+    close H or confess $!;
 }
 sub clone_finish ($) {
     my ($dstdir) = @_;
@@ -3643,7 +3735,7 @@ sub clone_finish ($) {
         git ls-tree -r --name-only -z HEAD | \
         xargs -0r touch -h -r . --
 END
-    printdone "ready for work in $dstdir";
+    printdone f_ "ready for work in %s", $dstdir;
 }
 
 sub clone ($) {
@@ -3651,7 +3743,7 @@ sub clone ($) {
     # once in parent after first suite fetched,
     # and then again in child after everything is finished
     my ($dstdir) = @_;
-    badusage "dry run makes no sense with clone" unless act_local();
+    badusage __ "dry run makes no sense with clone" unless act_local();
 
     my $multi_fetched = fork_for_multisuite(sub {
         printdebug "multi clone before fetch merge\n";
@@ -3668,7 +3760,7 @@ sub clone ($) {
 
     canonicalise_suite();
     my $hasgit = check_for_git();
-    mkdir $dstdir or fail "create \`$dstdir': $!";
+    mkdir $dstdir or fail f_ "create \`%s': %s", $dstdir, $!;
     changedir $dstdir;
     runcmd @git, qw(init -q);
     record_maindir();
@@ -3679,11 +3771,11 @@ sub clone ($) {
        runcmd @git, qw(remote add), 'origin', $giturl;
     }
     if ($hasgit) {
-       progress "fetching existing git history";
+       progress __ "fetching existing git history";
        git_fetch_us();
        runcmd_ordryrun_local @git, qw(fetch origin);
     } else {
-       progress "starting new git history";
+       progress __ "starting new git history";
     }
     fetch_from_archive() or no_such_package;
     my $vcsgiturl = $dsc->{'Vcs-Git'};
@@ -3708,13 +3800,13 @@ sub fetch_one () {
         cfg 'dgit.vcs-git.suites')) {
        my $current = cfg 'remote.vcs-git.url', 'RETURN-UNDEF';
        if (defined $current && $current ne $vcsgiturl) {
-           print STDERR <<END;
-FYI: Vcs-Git in $csuite has different url to your vcs-git remote.
+           print STDERR f_ <<END, $csuite;
+FYI: Vcs-Git in %s has different url to your vcs-git remote.
  Your vcs-git remote url may be out of date.  Use dgit update-vcs-git ?
 END
        }
     }
-    printdone "fetched into ".lrref();
+    printdone f_ "fetched into %s", lrref();
 }
 
 sub dofetch () {
@@ -3725,18 +3817,32 @@ sub dofetch () {
 
 sub pull () {
     dofetch();
-    runcmd_ordryrun_local @git, qw(merge -m),"Merge from $csuite [dgit]",
+    runcmd_ordryrun_local @git, qw(merge -m),
+       (f_ "Merge from %s [dgit]", $csuite),
         lrref();
-    printdone "fetched to ".lrref()." and merged into HEAD";
+    printdone f_ "fetched to %s and merged into HEAD", lrref();
 }
 
 sub check_not_dirty () {
-    foreach my $f (qw(local-options local-patch-header)) {
-       if (stat_exists "debian/source/$f") {
-           fail "git tree contains debian/source/$f";
+    my @forbid = qw(local-options local-patch-header);
+    @forbid = map { "debian/source/$_" } @forbid;
+    foreach my $f (@forbid) {
+       if (stat_exists $f) {
+           fail f_ "git tree contains %s", $f;
        }
     }
 
+    my @cmd = (@git, qw(status -uall --ignored --porcelain));
+    push @cmd, qw(debian/source/format debian/source/options);
+    push @cmd, @forbid;
+
+    my $bad = cmdoutput @cmd;
+    if (length $bad) {
+       fail +(__
+ "you have uncommitted changes to critical files, cannot continue:\n").
+              $bad;
+    }
+
     return if $includedirty;
 
     git_check_unmodified();
@@ -3751,9 +3857,10 @@ sub commit_admin ($) {
 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;
+       fail f_
+           "quilt fixup required but quilt mode is \`nofix'\n".
+           "HEAD commit%s differs from tree implied by debian/patches%s",
+           $headinfo, $xinfo;
     }
 }
 
@@ -3768,15 +3875,16 @@ sub commit_quilty_patch () {
     }
     delete $adds{'.pc'}; # if there wasn't one before, don't add it
     if (!%adds) {
-       progress "nothing quilty to commit, ok.";
+       progress __ "nothing quilty to commit, ok.";
        return;
     }
-    quiltify_nofix_bail "", " (wanted to commit patch update)";
+    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
+    commit_admin +(__ <<ENDT).<<END
 Commit Debian 3.0 (quilt) metadata
 
+ENDT
 [dgit ($our_version) quilt-fixup]
 END
 }
@@ -3796,18 +3904,18 @@ sub get_source_format () {
                $options{$_} = 1;
            }
        }
-       F->error and die $!;
+       F->error and confess $!;
        close F;
     } else {
-       die $! unless $!==&ENOENT;
+       confess $! unless $!==&ENOENT;
     }
 
     if (!open F, "debian/source/format") {
-       die $! unless $!==&ENOENT;
+       confess $! unless $!==&ENOENT;
        return '';
     }
     $_ = <F>;
-    F->error and die $!;
+    F->error and confess $!;
     chomp;
     return ($_, \%options);
 }
@@ -3817,12 +3925,12 @@ sub madformat_wantfixup ($) {
     return 0 unless $format eq '3.0 (quilt)';
     our $quilt_mode_warned;
     if ($quilt_mode eq 'nocheck') {
-       progress "Not doing any fixup of \`$format' due to".
-           " ----no-quilt-fixup or --quilt=nocheck"
+       progress f_ "Not doing any fixup of \`%s'".
+           " due to ----no-quilt-fixup or --quilt=nocheck", $format
            unless $quilt_mode_warned++;
        return 0;
     }
-    progress "Format \`$format', need to check/update patch stack"
+    progress f_ "Format \`%s', need to check/update patch stack", $format
        unless $quilt_mode_warned++;
     return 1;
 }
@@ -3830,13 +3938,15 @@ sub madformat_wantfixup ($) {
 sub maybe_split_brain_save ($$$) {
     my ($headref, $dgitview, $msg) = @_;
     # => message fragment "$saved" describing disposition of $dgitview
-    return "commit id $dgitview" unless defined $split_brain_save;
+    #    (used inside parens, in the English texts)
+    my $save = $internal_object_save{'dgit-view'};
+    return f_ "commit id %s", $dgitview unless defined $save;
     my @cmd = (shell_cmd 'cd "$1"; shift', $maindir,
               git_update_ref_cmd
               "dgit --dgit-view-save $msg HEAD=$headref",
-              $split_brain_save, $dgitview);
+              $save, $dgitview);
     runcmd @cmd;
-    return "and left in $split_brain_save";
+    return f_ "and left in %s", $save;
 }
 
 # An "infopair" is a tuple [ $thing, $what ]
@@ -3861,17 +3971,19 @@ sub infopair_lrf_tag_lookup ($$) {
        printdebug "infopair_lrfetchref_tag_lookup $tagobj $tagname $what\n";
        return [ git_rev_parse($tagobj), $what ];
     }
-    fail @tagnames==1 ? <<END : <<END;
-Wanted tag $what (@tagnames) on dgit server, but not found
+    fail @tagnames==1 ? (f_ <<END, $what, "@tagnames")
+Wanted tag %s (%s) on dgit server, but not found
 END
-Wanted tag $what (one of: @tagnames) on dgit server, but not found
+                     : (f_ <<END, $what, "@tagnames");
+Wanted tag %s (one of: %s) on dgit server, but not found
 END
 }
 
 sub infopair_cond_ff ($$) {
     my ($anc,$desc) = @_;
-    is_fast_fwd($anc->[0], $desc->[0]) or fail <<END;
-$anc->[1] ($anc->[0]) .. $desc->[1] ($desc->[0]) is not fast forward
+    is_fast_fwd($anc->[0], $desc->[0]) or
+       fail f_ <<END, $anc->[1], $anc->[0], $desc->[1], $desc->[0];
+%s (%s) .. %s (%s) is not fast forward
 END
 };
 
@@ -3880,7 +3992,7 @@ sub pseudomerge_version_check ($$) {
 
     my $arch_clogp = commit_getclogp $archive_hash;
     my $i_arch_v = [ (getfield $arch_clogp, 'Version'),
-                    'version currently in archive' ];
+                    __ 'version currently in archive' ];
     if (defined $overwrite_version) {
        if (length $overwrite_version) {
            infopair_cond_equal([ $overwrite_version,
@@ -3888,7 +4000,8 @@ sub pseudomerge_version_check ($$) {
                                $i_arch_v);
        } else {
            my $v = $i_arch_v->[0];
-           progress "Checking package changelog for archive version $v ...";
+           progress f_
+               "Checking package changelog for archive version %s ...", $v;
            my $cd;
            eval {
                my @xa = ("-f$v", "-t$v");
@@ -3896,7 +4009,8 @@ sub pseudomerge_version_check ($$) {
                my $gf = sub {
                    my ($fn) = @_;
                    [ (getfield $vclogp, $fn),
-                     "$fn field from dpkg-parsechangelog @xa" ];
+                     (f_ "%s field from dpkg-parsechangelog %s",
+                         $fn, "@xa") ];
                };
                my $cv = $gf->('Version');
                infopair_cond_equal($i_arch_v, $cv);
@@ -3905,12 +4019,13 @@ sub pseudomerge_version_check ($$) {
            if ($@) {
                $@ =~ s/^dgit: //gm;
                fail "$@".
-                   "Perhaps debian/changelog does not mention $v ?";
+                   f_ "Perhaps debian/changelog does not mention %s ?", $v;
            }
-           fail <<END if $cd->[0] =~ m/UNRELEASED/;
-$cd->[1] is $cd->[0]
-Your tree seems to based on earlier (not uploaded) $v.
+           fail f_ <<END, $cd->[1], $cd->[0], $v
+%s is %s
+Your tree seems to based on earlier (not uploaded) %s.
 END
+               if $cd->[0] =~ m/UNRELEASED/;
        }
     }
     
@@ -3921,7 +4036,8 @@ END
 sub pseudomerge_make_commit ($$$$ $$) {
     my ($clogp, $dgitview, $archive_hash, $i_arch_v,
        $msg_cmd, $msg_msg) = @_;
-    progress "Declaring that HEAD inciudes all changes in $i_arch_v->[0]...";
+    progress f_ "Declaring that HEAD includes all changes in %s...",
+                $i_arch_v->[0];
 
     my $tree = cmdoutput qw(git rev-parse), "${dgitview}:";
     my $authline = clogp_authline $clogp;
@@ -3936,7 +4052,7 @@ sub pseudomerge_make_commit ($$$$ $$) {
     # git rev-list --first-parent DTRT.
     my $pmf = dgit_privdir()."/pseudomerge";
     open MC, ">", $pmf or die "$pmf $!";
-    print MC <<END or die $!;
+    print MC <<END or confess $!;
 tree $tree
 parent $dgitview
 parent $archive_hash
@@ -3947,7 +4063,7 @@ $msg_msg
 
 [$msg_cmd]
 END
-    close MC or die $!;
+    close MC or confess $!;
 
     return make_commit($pmf);
 }
@@ -3976,7 +4092,7 @@ sub splitbrain_pseudomerge ($$$$) {
     my $i_arch_v = pseudomerge_version_check($clogp, $archive_hash);
 
     if (!defined $overwrite_version) {
-       progress "Checking that HEAD inciudes all changes in archive...";
+       progress __ "Checking that HEAD includes all changes in archive...";
     }
 
     return $dgitview if is_fast_fwd $archive_hash, $dgitview;
@@ -3984,10 +4100,11 @@ sub splitbrain_pseudomerge ($$$$) {
     if (defined $overwrite_version) {
     } elsif (!eval {
        my $t_dep14 = debiantag_maintview $i_arch_v->[0], access_nomdistro;
-       my $i_dep14 = infopair_lrf_tag_lookup($t_dep14, "maintainer view tag");
+       my $i_dep14 = infopair_lrf_tag_lookup($t_dep14,
+                                             __ "maintainer view tag");
        my $t_dgit = debiantag_new $i_arch_v->[0], access_nomdistro;
-       my $i_dgit = infopair_lrf_tag_lookup($t_dgit, "dgit view tag");
-       my $i_archive = [ $archive_hash, "current archive contents" ];
+       my $i_dgit = infopair_lrf_tag_lookup($t_dgit, __ "dgit view tag");
+       my $i_archive = [ $archive_hash, __ "current archive contents" ];
 
        printdebug "splitbrain_pseudomerge i_archive @$i_archive\n";
 
@@ -3997,25 +4114,25 @@ sub splitbrain_pseudomerge ($$$$) {
        1;
     }) {
         $@ =~ s/^\n//; chomp $@;
-       print STDERR <<END;
+       print STDERR <<END.(__ <<ENDT);
 $@
-| Not fast forward; maybe --overwrite is needed ?  Please see dgit(1).
 END
+| Not fast forward; maybe --overwrite is needed ?  Please see dgit(1).
+ENDT
        finish -1;
     }
 
+    my $arch_v = $i_arch_v->[0];
     my $r = pseudomerge_make_commit
        $clogp, $dgitview, $archive_hash, $i_arch_v,
        "dgit --quilt=$quilt_mode",
-       (defined $overwrite_version ? <<END_OVERWR : <<END_MAKEFF);
-Declare fast forward from $i_arch_v->[0]
-END_OVERWR
-Make fast forward from $i_arch_v->[0]
-END_MAKEFF
+       (defined $overwrite_version
+        ? f_ "Declare fast forward from %s\n", $arch_v
+        : f_ "Make fast forward from %s\n",    $arch_v);
 
     maybe_split_brain_save $maintview, $r, "pseudomerge";
 
-    progress "Made pseudo-merge of $i_arch_v->[0] into dgit view.";
+    progress f_ "Made pseudo-merge of %s into dgit view.", $arch_v;
     return $r;
 }      
 
@@ -4028,7 +4145,7 @@ sub plain_overwrite_pseudomerge ($$$) {
 
     return $head if is_fast_fwd $archive_hash, $head;
 
-    my $m = "Declare fast forward from $i_arch_v->[0]";
+    my $m = f_ "Declare fast forward from %s", $i_arch_v->[0];
 
     my $r = pseudomerge_make_commit
        $clogp, $head, $archive_hash, $i_arch_v,
@@ -4036,7 +4153,7 @@ sub plain_overwrite_pseudomerge ($$$) {
 
     runcmd git_update_ref_cmd $m, 'HEAD', $r, $head;
 
-    progress "Make pseudo-merge of $i_arch_v->[0] into your HEAD.";
+    progress f_ "Make pseudo-merge of %s into your HEAD.", $i_arch_v->[0];
     return $r;
 }
 
@@ -4048,7 +4165,8 @@ sub push_parse_changelog ($) {
 
     my $clogpackage = getfield $clogp, 'Source';
     $package //= $clogpackage;
-    fail "-p specified $package but changelog specified $clogpackage"
+    fail f_ "-p specified %s but changelog specified %s",
+           $package, $clogpackage
        unless $package eq $clogpackage;
     my $cversion = getfield $clogp, 'Version';
 
@@ -4069,8 +4187,9 @@ sub push_parse_dsc ($$$) {
     my $dversion = getfield $dsc, 'Version';
     my $dscpackage = getfield $dsc, 'Source';
     ($dscpackage eq $package && $dversion eq $cversion) or
-       fail "$dscfn is for $dscpackage $dversion".
-           " but debian/changelog is for $package $cversion";
+       fail f_ "%s is for %s %s but debian/changelog is for %s %s",
+               $dscfn, $dscpackage, $dversion,
+                       $package,    $cversion;
 }
 
 sub push_tagwants ($$$$) {
@@ -4124,13 +4243,13 @@ sub push_mktags ($$ $$ $) {
     $dsc->{$ourdscfield[0]} = join " ",
        $tagwants->[0]{Objid}, $declaredistro, $tagwants->[0]{Tag},
        $reader_giturl;
-    $dsc->save("$dscfn.tmp") or die $!;
+    $dsc->save("$dscfn.tmp") or confess $!;
 
     my $changes = parsecontrol($changesfile,$changesfilewhat);
     foreach my $field (qw(Source Distribution Version)) {
        $changes->{$field} eq $clogp->{$field} or
-           fail "changes field $field \`$changes->{$field}'".
-               " does not match changelog \`$clogp->{$field}'";
+           fail f_ "changes field %s \`%s' does not match changelog \`%s'",
+                   $field, $changes->{$field}, $clogp->{$field};
     }
 
     my $cversion = getfield $clogp, 'Version';
@@ -4147,8 +4266,8 @@ sub push_mktags ($$ $$ $) {
        my $head = $tw->{Objid};
        my $tag = $tw->{Tag};
 
-       open TO, '>', $tfn->('.tmp') or die $!;
-       print TO <<END or die $!;
+       open TO, '>', $tfn->('.tmp') or confess $!;
+       print TO <<END or confess $!;
 object $head
 type commit
 tag $tag
@@ -4156,25 +4275,30 @@ tagger $authline
 
 END
        if ($tw->{View} eq 'dgit') {
-           print TO <<END or die $!;
-$package release $cversion for $clogsuite ($csuite) [dgit]
+           print TO f_ <<ENDT, $package, $cversion, $clogsuite, $csuite
+%s release %s for %s (%s) [dgit]
+ENDT
+               or confess $!;
+           print TO <<END or confess $!;
 [dgit distro=$declaredistro$delibs]
 END
            foreach my $ref (sort keys %previously) {
-               print TO <<END or die $!;
+               print TO <<END or confess $!;
 [dgit previously:$ref=$previously{$ref}]
 END
            }
        } elsif ($tw->{View} eq 'maint') {
-           print TO <<END or die $!;
-$package release $cversion for $clogsuite ($csuite)
-(maintainer view tag generated by dgit --quilt=$quilt_mode)
+           print TO f_ <<END, $package, $cversion, $clogsuite, $csuite,
+%s release %s for %s (%s)
+(maintainer view tag generated by dgit --quilt=%s)
 END
+               $quilt_mode
+               or confess $!;
        } else {
-           die Dumper($tw)."?";
+           confess Dumper($tw)."?";
        }
 
-       close TO or die $!;
+       close TO or confess $!;
 
        my $tagobjfn = $tfn->('.tmp');
        if ($sign) {
@@ -4184,7 +4308,7 @@ END
            if (!defined $keyid) {
                $keyid = getfield $clogp, 'Maintainer';
            }
-           unlink $tfn->('.tmp.asc') or $!==&ENOENT or die $!;
+           unlink $tfn->('.tmp.asc') or $!==&ENOENT or confess $!;
            my @sign_cmd = (@gpg, qw(--detach-sign --armor));
            push @sign_cmd, qw(-u),$keyid if defined $keyid;
            push @sign_cmd, $tfn->('.tmp');
@@ -4216,7 +4340,7 @@ sub sign_changes ($) {
 sub dopush () {
     printdebug "actually entering push\n";
 
-    supplementary_message(<<'END');
+    supplementary_message(__ <<'END');
 Push failed, while checking state of the archive.
 You can retry the push, after fixing the problem, if you like.
 END
@@ -4226,11 +4350,11 @@ END
     my $archive_hash = fetch_from_archive();
     if (!$archive_hash) {
        $new_package or
-           fail "package appears to be new in this suite;".
-               " if this is intentional, use --new";
+           fail __ "package appears to be new in this suite;".
+                   " if this is intentional, use --new";
     }
 
-    supplementary_message(<<'END');
+    supplementary_message(__ <<'END');
 Push failed, while preparing your push.
 You can retry the push, after fixing the problem, if you like.
 END
@@ -4254,8 +4378,8 @@ END
 
     my $dscpath = "$buildproductsdir/$dscfn";
     stat_exists $dscpath or
-       fail "looked for .dsc $dscpath, but $!;".
-           " maybe you forgot to build";
+       fail f_ "looked for .dsc %s, but %s; maybe you forgot to build",
+               $dscpath, $!;
 
     responder_send_file('dsc', $dscpath);
 
@@ -4268,6 +4392,15 @@ END
     my $actualhead = git_rev_parse('HEAD');
 
     if (branch_is_gdr_unstitched_ff($symref, $actualhead, $archive_hash)) {
+       if (quiltmode_splitbrain()) {
+           my ($ffq_prev, $gdrlast) = branch_gdr_info($symref, $actualhead);
+           fail f_ <<END, $ffq_prev, $quilt_mode;
+Branch is managed by git-debrebase (%s
+exists), but quilt mode (%s) implies a split view.
+Pass the right --quilt option or adjust your git config.
+Or, maybe, run git-debrebase forget-was-ever-debrebase.
+END
+       }
        runcmd_ordryrun_local @git_debrebase, 'stitch';
        $actualhead = git_rev_parse('HEAD');
     }
@@ -4285,9 +4418,10 @@ END
            my $cachekey;
            ($dgithead, $cachekey) =
                quilt_check_splitbrain_cache($actualhead, $upstreamversion);
-           $dgithead or fail
- "--quilt=$quilt_mode but no cached dgit view:
- perhaps HEAD changed since dgit build[-source] ?";
+           $dgithead or fail f_
+ "--quilt=%s but no cached dgit view:
+ perhaps HEAD changed since dgit build[-source] ?",
+                              $quilt_mode;
            $split_brain = 1;
            $dgithead = splitbrain_pseudomerge($clogp,
                                               $actualhead, $dgithead,
@@ -4316,7 +4450,7 @@ END
        } elsif (deliberately_not_fast_forward) {
            $forceflag = '+';
        } else {
-           fail "dgit push: HEAD is not a descendant".
+           fail __ "dgit push: HEAD is not a descendant".
                " of the archive's version.\n".
                "To overwrite the archive's contents,".
                " pass --overwrite[=VERSION].\n".
@@ -4326,7 +4460,7 @@ END
     }
 
     changedir $playground;
-    progress "checking that $dscfn corresponds to HEAD";
+    progress f_ "checking that %s corresponds to HEAD", $dscfn;
     runcmd qw(dpkg-source -x --),
         $dscpath =~ m#^/# ? $dscpath : "$maindir/$dscpath";
     my ($tree,$dir) = mktree_in_ud_from_only_subdir("source package");
@@ -4359,22 +4493,27 @@ END
                }
            }
            if (@mode_changes) {
-               fail <<END.(join '', @mode_changes).<<END;
-HEAD specifies a different tree to $dscfn:
+               fail +(f_ <<ENDT, $dscfn).<<END
+HEAD specifies a different tree to %s:
+ENDT
 $diffs
 END
+                   .(join '', @mode_changes)
+                   .(f_ <<ENDT, $tree, $referent);
 There is a problem with your source tree (see dgit(7) for some hints).
-To see a full diff, run git diff $tree $referent
-END
+To see a full diff, run git diff %s %s
+ENDT
            }
 
-           fail <<END;
-HEAD specifies a different tree to $dscfn:
+           fail +(f_ <<ENDT, $dscfn).<<END.(f_ <<ENDT, $tree, $referent);
+HEAD specifies a different tree to %s:
+ENDT
 $diffs
+END
 Perhaps you forgot to build.  Or perhaps there is a problem with your
  source tree (see dgit(7) for some hints).  To see a full diff, run
-   git diff $tree $referent
-END
+   git diff %s %s
+ENDT
        } else {
            failedcmd @diffcmd;
        }
@@ -4382,9 +4521,10 @@ END
     if (!$changesfile) {
        my $pat = changespat $cversion;
        my @cs = glob "$buildproductsdir/$pat";
-       fail "failed to find unique changes file".
-           " (looked for $pat in $buildproductsdir);".
-           " perhaps you need to use dgit -C"
+       fail f_ "failed to find unique changes file".
+               " (looked for %s in %s);".
+               " perhaps you need to use dgit -C",
+               $pat, $buildproductsdir
            unless @cs==1;
        ($changesfile) = @cs;
     } else {
@@ -4403,21 +4543,23 @@ END
     if ($sourceonlypolicy eq 'ok') {
     } elsif ($sourceonlypolicy eq 'always') {
        forceable_fail [qw(uploading-binaries)],
-           "uploading binaries, although distroy policy is source only"
+           __ "uploading binaries, although distro policy is source only"
            if $hasdebs;
     } elsif ($sourceonlypolicy eq 'never') {
        forceable_fail [qw(uploading-source-only)],
-           "source-only upload, although distroy policy requires .debs"
+           __ "source-only upload, although distro policy requires .debs"
            if !$hasdebs;
     } elsif ($sourceonlypolicy eq 'not-wholly-new') {
        forceable_fail [qw(uploading-source-only)],
-           "source-only upload, even though package is entirely NEW\n".
-           "(this is contrary to policy in ".(access_nomdistro()).")"
+           f_ "source-only upload, even though package is entirely NEW\n".
+              "(this is contrary to policy in %s)",
+              access_nomdistro()
            if !$hasdebs
            && $new_package
            && !(archive_query('package_not_wholly_new', $package) // 1);
     } else {
-       badcfg "unknown source-only-uploads policy \`$sourceonlypolicy'";
+       badcfg f_ "unknown source-only-uploads policy \`%s'",
+                 $sourceonlypolicy;
     }
 
     # Perhaps adjust .dsc to contain right set of origs
@@ -4433,7 +4575,8 @@ END
     responder_send_command("param isuite $isuite");
     responder_send_command("param tagformat $tagformat");
     if (defined $maintviewhead) {
-       die unless ($protovsn//4) >= 4;
+       confess "internal error (protovsn=$protovsn)"
+           if defined $protovsn and $protovsn < 4;
        responder_send_command("param maint-view $maintviewhead");
     }
 
@@ -4458,7 +4601,7 @@ END
                                 dgit_privdir()."/tag");
     my @tagobjfns;
 
-    supplementary_message(<<'END');
+    supplementary_message(__ <<'END');
 Push failed, while signing the tag.
 You can retry the push, after fixing the problem, if you like.
 END
@@ -4471,7 +4614,7 @@ END
                              $changesfile,$changesfile,
                              \@tagwants);
     }
-    supplementary_message(<<'END');
+    supplementary_message(__ <<'END');
 Push failed, *after* signing the tag.
 If you want to try again, you should use a new version number.
 END
@@ -4488,7 +4631,7 @@ END
            @git, qw(update-ref), "refs/tags/$tag", $tag_obj_hash;
     }
 
-    supplementary_message(<<'END');
+    supplementary_message(__ <<'END');
 Push failed, while updating the remote git repository - see messages above.
 If you want to try again, you should use a new version number.
 END
@@ -4505,12 +4648,11 @@ END
        qw(-c push.followTags=false push), access_giturl(), @pushrefs;
     runcmd_ordryrun git_update_ref_cmd 'dgit push', lrref(), $dgithead;
 
-    supplementary_message(<<'END');
+    supplementary_message(__ <<'END');
 Push failed, while obtaining signatures on the .changes and .dsc.
 If it was just that the signature failed, you may try again by using
-debsign by hand to sign the changes
-   $changesfile
-and then dput to complete the upload.
+debsign by hand to sign the changes file (see the command dgit tried,
+above), and then dput that changes file to complete the upload.
 If you need to change the package, you must use a new version number.
 END
     if ($we_are_responder) {
@@ -4523,22 +4665,22 @@ END
        if (act_local()) {
            rename "$dscpath.tmp",$dscpath or die "$dscfn $!";
        } else {
-           progress "[new .dsc left in $dscpath.tmp]";
+           progress f_ "[new .dsc left in %s.tmp]", $dscpath;
        }
        sign_changes $changesfile;
     }
 
-    supplementary_message(<<END);
+    supplementary_message(f_ <<END, $changesfile);
 Push failed, while uploading package(s) to the archive server.
 You can retry the upload of exactly these same files with dput of:
-  $changesfile
+  %s
 If that .changes file is broken, you will need to use a new version
 number for your next attempt at the upload.
 END
     my $host = access_cfg('upload-host','RETURN-UNDEF');
     my @hostarg = defined($host) ? ($host,) : ();
     runcmd_ordryrun @dput, @hostarg, $changesfile;
-    printdone "pushed and uploaded $cversion";
+    printdone f_ "pushed and uploaded %s", $cversion;
 
     supplementary_message('');
     responder_send_command("complete");
@@ -4550,7 +4692,7 @@ sub pre_clone () {
 sub cmd_clone {
     parseopts();
     my $dstdir;
-    badusage "-p is not allowed with clone; specify as argument instead"
+    badusage __ "-p is not allowed with clone; specify as argument instead"
        if defined $package;
     if (@ARGV==1) {
        ($package) = @ARGV;
@@ -4561,13 +4703,13 @@ sub cmd_clone {
     } elsif (@ARGV==3) {
        ($package,$isuite,$dstdir) = @ARGV;
     } else {
-       badusage "incorrect arguments to dgit clone";
+       badusage __ "incorrect arguments to dgit clone";
     }
     notpushing();
 
     $dstdir ||= "$package";
     if (stat_exists $dstdir) {
-       fail "$dstdir already exists";
+       fail f_ "%s already exists", $dstdir;
     }
 
     my $cwd_remove;
@@ -4577,15 +4719,16 @@ sub cmd_clone {
            return unless defined $cwd_remove;
            if (!chdir "$cwd_remove") {
                return if $!==&ENOENT;
-               die "chdir $cwd_remove: $!";
+               confess "chdir $cwd_remove: $!";
            }
            printdebug "clone rmonerror removing $dstdir\n";
            if (stat $dstdir) {
-               rmtree($dstdir) or die "remove $dstdir: $!\n";
+               rmtree($dstdir) or fail f_ "remove %s: %s\n", $dstdir, $!;
            } elsif (grep { $! == $_ }
                     (ENOENT, ENOTDIR, EACCES, EPERM, ELOOP)) {
            } else {
-               print STDERR "check whether to remove $dstdir: $!\n";
+               print STDERR f_ "check whether to remove %s: %s\n",
+                               $dstdir, $!;
            }
        };
     }
@@ -4622,7 +4765,7 @@ sub fetchpullargs () {
     } elsif (@ARGV==1) {
        ($isuite) = @ARGV;
     } else {
-       badusage "incorrect arguments to dgit fetch or dgit pull";
+       badusage __ "incorrect arguments to dgit fetch or dgit pull";
     }
     notpushing();
 }
@@ -4638,8 +4781,8 @@ sub cmd_pull {
     fetchpullargs();
     if (quiltmode_splitbrain()) {
        my ($format, $fopts) = get_source_format();
-       madformat($format) and fail <<END
-dgit pull not yet supported in split view mode (--quilt=$quilt_mode)
+       madformat($format) and fail f_ <<END, $quilt_mode
+dgit pull not yet supported in split view mode (--quilt=%s)
 END
     }
     pull();
@@ -4648,7 +4791,7 @@ END
 sub cmd_checkout {
     parseopts();
     package_from_d_control();
-    @ARGV==1 or badusage "dgit checkout needs a suite argument";
+    @ARGV==1 or badusage __ "dgit checkout needs a suite argument";
     ($isuite) = @ARGV;
     notpushing();
 
@@ -4710,17 +4853,17 @@ sub cmd_update_vcs_git () {
     my @cmd;
     my $orgurl = cfg 'remote.vcs-git.url', 'RETURN-UNDEF';
     if (!defined $orgurl) {
-       print STDERR "setting up vcs-git: $url\n";
+       print STDERR f_ "setting up vcs-git: %s\n", $url;
        @cmd = (@git, qw(remote add vcs-git), $url);
     } elsif ($orgurl eq $url) {
-       print STDERR "vcs git already configured: $url\n";
+       print STDERR f_ "vcs git already configured: %s\n", $url;
     } else {
-       print STDERR "changing vcs-git url to: $url\n";
+       print STDERR f_ "changing vcs-git url to: %s\n", $url;
        @cmd = (@git, qw(remote set-url vcs-git), $url);
     }
     runcmd_ordryrun_local @cmd;
     if ($dofetch) {
-       print "fetching (@ARGV)\n";
+       print f_ "fetching (%s)\n", "@ARGV";
        runcmd_ordryrun_local @git, qw(fetch vcs-git), @ARGV;
     }
 }
@@ -4735,7 +4878,7 @@ sub prep_push () {
     } elsif (@ARGV==1) {
        ($specsuite) = (@ARGV);
     } else {
-       badusage "incorrect arguments to dgit $subcommand";
+       badusage f_ "incorrect arguments to dgit %s", $subcommand;
     }
     if ($new_package) {
        local ($package) = $existing_package; # this is a hack
@@ -4746,8 +4889,9 @@ sub prep_push () {
     if (defined $specsuite &&
        $specsuite ne $isuite &&
        $specsuite ne $csuite) {
-           fail "dgit $subcommand: changelog specifies $isuite ($csuite)".
-               " but command line specifies $specsuite";
+           fail f_ "dgit %s: changelog specifies %s (%s)".
+                   " but command line specifies %s",
+                   $subcommand, $isuite, $csuite, $specsuite;
     }
 }
 
@@ -4756,8 +4900,6 @@ sub cmd_push {
     dopush();
 }
 
-our $sourcechanges;
-
 #---------- remote commands' implementation ----------
 
 sub pre_remote_push_build_host {
@@ -4773,11 +4915,11 @@ sub pre_remote_push_build_host {
     $we_are_responder = 1;
     $us .= " (build host)";
 
-    open PI, "<&STDIN" or die $!;
-    open STDIN, "/dev/null" or die $!;
-    open PO, ">&STDOUT" or die $!;
+    open PI, "<&STDIN" or confess $!;
+    open STDIN, "/dev/null" or confess $!;
+    open PO, ">&STDOUT" or confess $!;
     autoflush PO 1;
-    open STDOUT, ">&STDERR" or die $!;
+    open STDOUT, ">&STDERR" or confess $!;
     autoflush STDOUT 1;
 
     $vsnwant //= 1;
@@ -4785,9 +4927,9 @@ sub pre_remote_push_build_host {
        $vsnwant =~ m{^(?:.*,)?$_(?:,.*)?$}
     } @rpushprotovsn_support;
 
-    fail "build host has dgit rpush protocol versions ".
-       (join ",", @rpushprotovsn_support).
-        " but invocation host has $vsnwant"
+    fail f_ "build host has dgit rpush protocol versions %s".
+            " but invocation host has %s",
+           (join ",", @rpushprotovsn_support), $vsnwant
        unless defined $protovsn;
 
     changedir $dir;
@@ -4864,7 +5006,8 @@ sub cmd_rpush {
 
     if (defined $initiator_tempdir) {
        rmtree $initiator_tempdir;
-       mkdir $initiator_tempdir, 0700 or die "$initiator_tempdir: $!";
+       mkdir $initiator_tempdir, 0700
+           or fail f_ "create %s: %s", $initiator_tempdir, $!;
        $i_tmp = $initiator_tempdir;
     } else {
        $i_tmp = tempdir();
@@ -4900,11 +5043,11 @@ sub i_resp_complete {
     $i_child_pid = undef; # prevents killing some other process with same pid
     printdebug "waiting for build host child $pid...\n";
     my $got = waitpid $pid, 0;
-    die $! unless $got == $pid;
-    die "build host child failed $?" if $?;
+    confess $! unless $got == $pid;
+    fail f_ "build host child failed: %s", waitstatusmsg() if $?;
 
     i_cleanup();
-    printdebug "all done\n";
+    printdebug __ "all done\n";
     finish 0;
 }
 
@@ -4913,7 +5056,7 @@ sub i_resp_file ($) {
     my $localname = i_method "i_localname", $keyword;
     my $localpath = "$i_tmp/$localname";
     stat_exists $localpath and
-       badproto \*RO, "file $keyword ($localpath) twice";
+       badproto \*RO, f_ "file %s (%s) twice", $keyword, $localpath;
     protocol_receive_file \*RO, $localpath;
     i_method "i_file", $keyword;
 }
@@ -4921,15 +5064,15 @@ sub i_resp_file ($) {
 our %i_param;
 
 sub i_resp_param ($) {
-    $_[0] =~ m/^(\S+) (.*)$/ or badproto \*RO, "bad param spec";
+    $_[0] =~ m/^(\S+) (.*)$/ or badproto \*RO, __ "bad param spec";
     $i_param{$1} = $2;
 }
 
 sub i_resp_previously ($) {
     $_[0] =~ m#^(refs/tags/\S+)=(\w+)$#
-       or badproto \*RO, "bad previously spec";
+       or badproto \*RO, __ "bad previously spec";
     my $r = system qw(git check-ref-format), $1;
-    die "bad previously ref spec ($r)" if $r;
+    confess "bad previously ref spec ($r)" if $r;
     $previously{$1} = $2;
 }
 
@@ -4946,8 +5089,9 @@ sub i_resp_want ($) {
     pushing();
     rpush_handle_protovsn_bothends();
 
-    fail "rpush negotiated protocol version $protovsn".
-       " which does not support quilt mode $quilt_mode"
+    fail f_ "rpush negotiated protocol version %s".
+       " which does not support quilt mode %s",
+       $protovsn, $quilt_mode
        if quiltmode_splitbrain;
 
     my @localpaths = i_method "i_want", $keyword;
@@ -4955,7 +5099,7 @@ sub i_resp_want ($) {
     foreach my $localpath (@localpaths) {
        protocol_send_file \*RI, $localpath;
     }
-    print RI "files-end\n" or die $!;
+    print RI "files-end\n" or confess $!;
 }
 
 our ($i_clogp, $i_version, $i_dscfn, $i_changesfn, @i_buildinfos);
@@ -4990,10 +5134,10 @@ sub i_file_buildinfo {
     if (!forceing [qw(buildinfo-changes-mismatch)]) {
        files_compare_inputs($bd, $ch);
        (getfield $bd, $_) eq (getfield $ch, $_) or
-           fail "buildinfo mismatch $_"
+           fail f_ "buildinfo mismatch in field %s", $_
            foreach qw(Source Version);
        !defined $bd->{$_} or
-           fail "buildinfo contains $_"
+           fail f_ "buildinfo contains forbidden field %s", $_
            foreach qw(Changes Changed-by Distribution);
     }
     push @i_buildinfos, $bi;
@@ -5034,7 +5178,7 @@ sub i_want_signed_tag {
 
     return
        push_mktags $i_clogp, $i_dscfn,
-           $i_changesfn, 'remote changes',
+           $i_changesfn, (__ 'remote changes file'),
            \@tagwants;
 }
 
@@ -5047,6 +5191,7 @@ sub i_want_signed_dsc_changes {
 #---------- building etc. ----------
 
 our $version;
+our $sourcechanges;
 our $dscfn;
 
 #----- `3.0 (quilt)' handling -----
@@ -5059,15 +5204,15 @@ sub quiltify_dpkg_commit ($$$;$) {
 
     mkpath '.git/dgit'; # we are in playtree
     my $descfn = ".git/dgit/quilt-description.tmp";
-    open O, '>', $descfn or die "$descfn: $!";
+    open O, '>', $descfn or confess "$descfn: $!";
     $msg =~ s/\n+/\n\n/;
-    print O <<END or die $!;
+    print O <<END or confess $!;
 From: $author
 ${xinfo}Subject: $msg
 ---
 
 END
-    close O or die $!;
+    close O or confess $!;
 
     {
        local $ENV{'EDITOR'} = cmdoutput qw(realpath --), $0;
@@ -5108,21 +5253,21 @@ sub quiltify_trees_differ ($$;$$$) {
 
        if ($unrepres) {
            eval {
-               die "not a plain file or symlink\n"
+               die __ "not a plain file or symlink\n"
                    unless $newmode =~ m/^(?:10|12)\d{4}$/ ||
                           $oldmode =~ m/^(?:10|12)\d{4}$/;
                if ($oldmode =~ m/[^0]/ &&
                    $newmode =~ m/[^0]/) {
                    # both old and new files exist
-                   die "mode or type changed\n" if $oldmode ne $newmode;
-                   die "modified symlink\n" unless $newmode =~ m/^10/;
+                   die __ "mode or type changed\n" if $oldmode ne $newmode;
+                   die __ "modified symlink\n" unless $newmode =~ m/^10/;
                } elsif ($oldmode =~ m/[^0]/) {
                    # deletion
-                   die "deletion of symlink\n"
+                   die __ "deletion of symlink\n"
                        unless $oldmode =~ m/^10/;
                } else {
                    # creation
-                   die "creation with non-default mode\n"
+                   die __ "creation with non-default mode\n"
                        unless $newmode =~ m/^100644$/ or
                               $newmode =~ m/^120000$/;
                }
@@ -5152,7 +5297,7 @@ sub quiltify_tree_sentinelfiles ($) {
 
 sub quiltify_splitbrain_needed () {
     if (!$split_brain) {
-       progress "dgit view: changes are required...";
+       progress __ "dgit view: changes are required...";
        runcmd @git, qw(checkout -q -b dgit-view);
        $split_brain = 1;
     }
@@ -5181,32 +5326,34 @@ sub quiltify_splitbrain ($$$$$$$) {
        my ($x,$y) = @_;
        my $cmd = "git diff $x $y -- :/ ':!debian'";
        $cmd .= " ':!/.gitignore' ':!*/.gitignore'" if $gitignore_special;
-       return "\nFor full diff showing the problem(s), type:\n $cmd\n";
+       return f_ "\nFor full diff showing the problem(s), type:\n %s\n",
+                 $cmd;
     };
 
     if ($quilt_mode =~ m/gbp|unapplied/ &&
        ($diffbits->{O2H} & 01)) {
-       my $msg =
- "--quilt=$quilt_mode specified, implying patches-unapplied git tree\n".
- " but git tree differs from orig in upstream files.";
+       my $msg = f_
+ "--quilt=%s specified, implying patches-unapplied git tree\n".
+ " but git tree differs from orig in upstream files.",
+                     $quilt_mode;
        $msg .= $fulldiffhint->($unapplied, 'HEAD');
        if (!stat_exists "debian/patches") {
-           $msg .=
+           $msg .= __
  "\n ... debian/patches is missing; perhaps this is a patch queue branch?";
        }  
        fail $msg;
     }
     if ($quilt_mode =~ m/dpm/ &&
        ($diffbits->{H2A} & 01)) {
-       fail <<END. $fulldiffhint->($oldtiptree,'HEAD');
---quilt=$quilt_mode specified, implying patches-applied git tree
+       fail +(f_ <<END, $quilt_mode). $fulldiffhint->($oldtiptree,'HEAD');
+--quilt=%s specified, implying patches-applied git tree
  but git tree differs from result of applying debian/patches to upstream
 END
     }
     if ($quilt_mode =~ m/gbp|unapplied/ &&
        ($diffbits->{O2A} & 01)) { # some patches
        quiltify_splitbrain_needed();
-       progress "dgit view: creating patches-applied version using gbp pq";
+       progress __ "dgit view: creating patches-applied version using gbp pq";
        runcmd shell_cmd 'exec >/dev/null', gbp_pq, qw(import);
        # gbp pq import creates a fresh branch; push back to dgit-view
        runcmd @git, qw(update-ref refs/heads/dgit-view HEAD);
@@ -5214,8 +5361,8 @@ END
     }
     if ($quilt_mode =~ m/gbp|dpm/ &&
        ($diffbits->{O2A} & 02)) {
-       fail <<END;
---quilt=$quilt_mode specified, implying that HEAD is for use with a
+       fail f_ <<END, $quilt_mode;
+--quilt=%s specified, implying that HEAD is for use with a
  tool which does not create patches for changes to upstream
  .gitignores: but, such patches exist in debian/patches.
 END
@@ -5223,72 +5370,55 @@ END
     if (($diffbits->{O2H} & 02) && # user has modified .gitignore
        !($diffbits->{O2A} & 02)) { # patches do not change .gitignore
        quiltify_splitbrain_needed();
-       progress "dgit view: creating patch to represent .gitignore changes";
+       progress __
+           "dgit view: creating patch to represent .gitignore changes";
         ensuredir "debian/patches";
        my $gipatch = "debian/patches/auto-gitignore";
-       open GIPATCH, ">>", "$gipatch" or die "$gipatch: $!";
-       stat GIPATCH or die "$gipatch: $!";
-       fail "$gipatch already exists; but want to create it".
-           " to record .gitignore changes" if (stat _)[7];
-       print GIPATCH <<END or die "$gipatch: $!";
+       open GIPATCH, ">>", "$gipatch" or confess "$gipatch: $!";
+       stat GIPATCH or confess "$gipatch: $!";
+       fail f_ "%s already exists; but want to create it".
+               " to record .gitignore changes",
+               $gipatch
+           if (stat _)[7];
+       print GIPATCH +(__ <<END).<<ENDU or die "$gipatch: $!";
 Subject: Update .gitignore from Debian packaging branch
 
 The Debian packaging git branch contains these updates to the upstream
 .gitignore file(s).  This patch is autogenerated, to provide these
 updates to users of the official Debian archive view of the package.
+END
 
 [dgit ($our_version) update-gitignore]
 ---
-END
+ENDU
         close GIPATCH or die "$gipatch: $!";
         runcmd shell_cmd "exec >>$gipatch", @git, qw(diff),
             $unapplied, $headref, "--", sort keys %$editedignores;
-        open SERIES, "+>>", "debian/patches/series" or die $!;
-        defined seek SERIES, -1, 2 or $!==EINVAL or die $!;
+        open SERIES, "+>>", "debian/patches/series" or confess $!;
+        defined seek SERIES, -1, 2 or $!==EINVAL or confess $!;
         my $newline;
-        defined read SERIES, $newline, 1 or die $!;
-       print SERIES "\n" or die $! unless $newline eq "\n";
-       print SERIES "auto-gitignore\n" or die $!;
+        defined read SERIES, $newline, 1 or confess $!;
+       print SERIES "\n" or confess $! unless $newline eq "\n";
+       print SERIES "auto-gitignore\n" or confess $!;
        close SERIES or die  $!;
         runcmd @git, qw(add -f -- debian/patches/series), $gipatch;
-        commit_admin <<END
+        commit_admin +(__ <<END).<<ENDU
 Commit patch to update .gitignore
+END
 
 [dgit ($our_version) update-gitignore-quilt-fixup]
-END
+ENDU
     }
 
     my $dgitview = git_rev_parse 'HEAD';
 
     changedir $maindir;
-    # When we no longer need to support squeeze, use --create-reflog
-    # instead of this:
-    ensuredir "$maindir_gitcommon/logs/refs/dgit-intern";
-    my $makelogfh = new IO::File "$maindir_gitcommon/logs/refs/$splitbraincache", '>>'
-      or die $!;
-
-    my $oldcache = git_get_ref "refs/$splitbraincache";
-    if ($oldcache eq $dgitview) {
-       my $tree = cmdoutput qw(git rev-parse), "$dgitview:";
-       # git update-ref doesn't always update, in this case.  *sigh*
-       my $dummy = make_commit_text <<END;
-tree $tree
-parent $dgitview
-author Dgit <dgit\@example.com> 1000000000 +0000
-committer Dgit <dgit\@example.com> 1000000000 +0000
-
-Dummy commit - do not use
-END
-       runcmd @git, qw(update-ref -m), "dgit $our_version - dummy",
-           "refs/$splitbraincache", $dummy;
-    }
-    runcmd @git, qw(update-ref -m), $cachekey, "refs/$splitbraincache",
-       $dgitview;
+    reflog_cache_insert "refs/$splitbraincache", $cachekey, $dgitview;
 
     changedir "$playground/work";
 
-    my $saved = maybe_split_brain_save $headref, $dgitview, "converted";
-    progress "dgit view: created ($saved)";
+    my $saved = maybe_split_brain_save $headref, $dgitview, __ "converted";
+    progress f_ "dgit view: created (%s)", $saved;
 }
 
 sub quiltify ($$$$) {
@@ -5353,7 +5483,7 @@ sub quiltify ($$$$) {
        my $c = shift @todo;
        next if $considered{$c->{Commit}}++;
 
-       $not->($c, "maximum search space exceeded") if --$max_work <= 0;
+       $not->($c, __ "maximum search space exceeded") if --$max_work <= 0;
 
        printdebug "quiltify investigate $c->{Commit}\n";
 
@@ -5371,7 +5501,7 @@ sub quiltify ($$$$) {
        }
 
        my $c_sentinels = quiltify_tree_sentinelfiles $c->{Commit};
-       $not->($c, "has $c_sentinels not $t_sentinels")
+       $not->($c, f_ "has %s not %s", $c_sentinels, $t_sentinels)
            if $c_sentinels ne $t_sentinels;
 
        my $commitdata = cmdoutput @git, qw(cat-file commit), $c->{Commit};
@@ -5380,23 +5510,25 @@ sub quiltify ($$$$) {
        my @parents = ($commitdata =~ m/^parent (\w+)$/gm);
        @parents = map { { Commit => $_, Child => $c } } @parents;
 
-       $not->($c, "root commit") if !@parents;
+       $not->($c, __ "root commit") if !@parents;
 
        foreach my $p (@parents) {
            $p->{Nontrivial}= quiltify_trees_differ $p->{Commit},$c->{Commit};
        }
        my $ndiffers = grep { $_->{Nontrivial} } @parents;
-       $not->($c, "merge ($ndiffers nontrivial parents)") if $ndiffers > 1;
+       $not->($c, f_ "merge (%s nontrivial parents)", $ndiffers)
+           if $ndiffers > 1;
 
        foreach my $p (@parents) {
            printdebug "considering C=$c->{Commit} P=$p->{Commit}\n";
 
            my @cmd= (@git, qw(diff-tree -r --name-only),
-                     $p->{Commit},$c->{Commit}, qw(-- debian/patches .pc));
+                     $p->{Commit},$c->{Commit},
+                     qw(-- debian/patches .pc debian/source/format));
            my $patchstackchange = cmdoutput @cmd;
            if (length $patchstackchange) {
                $patchstackchange =~ s/\n/,/g;
-               $not->($p, "changed $patchstackchange");
+               $not->($p, f_ "changed %s", $patchstackchange);
            }
 
            printdebug " search queue P=$p->{Commit} ",
@@ -5413,28 +5545,33 @@ sub quiltify ($$$$) {
            $x =~ s/(.*?[0-9a-z]{8})[0-9a-z]*$/$1/;
            return $x;
        };
-       my $reportnot = sub {
-           my ($notp) = @_;
-           my $s = $abbrev->($notp);
-           my $c = $notp->{Child};
-           $s .= "..".$abbrev->($c) if $c;
-           $s .= ": ".$notp->{Whynot};
-           return $s;
-       };
        if ($quilt_mode eq 'linear') {
-           print STDERR "\n$us: error: quilt fixup cannot be linear.  Stopped at:\n";
+           print STDERR f_
+               "\n%s: error: quilt fixup cannot be linear.  Stopped at:\n",
+               $us;
+           my $all_gdr = !!@nots;
            foreach my $notp (@nots) {
-               print STDERR "$us:  ", $reportnot->($notp), "\n";
+               my $c = $notp->{Child};
+               my $cprange = $abbrev->($notp);
+               $cprange .= "..".$abbrev->($c) if $c;
+               print STDERR f_ "%s:  %s: %s\n",
+                   $us, $cprange, $notp->{Whynot};
+               $all_gdr &&= $notp->{Child} &&
+                   (git_cat_file $notp->{Child}{Commit}, 'commit')
+                   =~ m{^\[git-debrebase(?! split[: ]).*\]$}m;
            }
-           print STDERR "$us: $_\n" foreach @$failsuggestion;
-           fail
- "quilt history linearisation failed.  Search \`quilt fixup' in dgit(7).\n".
- "Use dpkg-source --commit by hand; or, --quilt=smash for one ugly patch";
+           print STDERR "\n";
+           $failsuggestion =
+               [ grep { $_->[0] ne 'quilt-mode' } @$failsuggestion ]
+               if $all_gdr;
+           print STDERR "$us: $_->[1]\n" foreach @$failsuggestion;
+           fail __
+ "quilt history linearisation failed.  Search \`quilt fixup' in dgit(7).\n";
        } elsif ($quilt_mode eq 'smash') {
        } elsif ($quilt_mode eq 'auto') {
-           progress "quilt fixup cannot be linear, smashing...";
+           progress __ "quilt fixup cannot be linear, smashing...";
        } else {
-           die "$quilt_mode ?";
+           confess "$quilt_mode ?";
        }
 
        my $time = $ENV{'GIT_COMMITTER_DATE'} || time;
@@ -5444,12 +5581,14 @@ sub quiltify ($$$$) {
 
        quiltify_dpkg_commit "auto-$version-$target-$time",
            (getfield $clogp, 'Maintainer'),
-           "Automatically generated patch ($clogp->{Version})\n".
-           "Last (up to) $ncommits git changes, FYI:\n\n". $msg;
+           (f_ "Automatically generated patch (%s)\n".
+            "Last (up to) %s git changes, FYI:\n\n",
+            $clogp->{Version}, $ncommits).
+            $msg;
        return;
     }
 
-    progress "quiltify linearisation planning successful, executing...";
+    progress __ "quiltify linearisation planning successful, executing...";
 
     for (my $p = $sref_S;
         my $c = $p->{Child};
@@ -5483,15 +5622,16 @@ sub quiltify ($$$$) {
            my ($what) = @_;
 
            eval {
-               die "contains unexpected slashes\n" if m{//} || m{/$};
-               die "contains leading punctuation\n" if m{^\W} || m{/\W};
-               die "contains bad character(s)\n" if m{[^-a-z0-9_.+=~/]}i;
-               die "is series file\n" if m{$series_filename_re}o;
-               die "too long" if length > 200;
+               die __ "contains unexpected slashes\n" if m{//} || m{/$};
+               die __ "contains leading punctuation\n" if m{^\W} || m{/\W};
+               die __ "contains bad character(s)\n" if m{[^-a-z0-9_.+=~/]}i;
+               die __ "is series file\n" if m{$series_filename_re}o;
+               die __ "too long\n" if length > 200;
            };
            return $_ unless $@;
-           print STDERR "quiltifying commit $cc:".
-               " ignoring/dropping Gbp-Pq $what: $@";
+           print STDERR f_
+               "quiltifying commit %s: ignoring/dropping Gbp-Pq %s: %s",
+               $cc, $what, $@;
            return undef;
        };
 
@@ -5519,7 +5659,7 @@ sub quiltify ($$$$) {
                $patchname = $translitname;
            };
            print STDERR
-               "dgit: patch title transliteration error: $@"
+               +(f_ "dgit: patch title transliteration error: %s", $@)
                if $@;
            $patchname =~ y/ A-Z/-a-z/;
            $patchname =~ y/-a-z0-9_.+=~//cd;
@@ -5541,7 +5681,7 @@ sub quiltify ($$$$) {
        for ($index='';
             stat "debian/patches/$patchname$index";
             $index++) { }
-       $!==ENOENT or die "$patchname$index $!";
+       $!==ENOENT or confess "$patchname$index $!";
 
        runcmd @git, qw(checkout -q), $cc;
 
@@ -5583,7 +5723,7 @@ END
 
     if ($quilt_mode eq 'linear'
        && !$fopts->{'single-debian-patch'}
-       && branch_is_gdr($symref, $headref)) {
+       && branch_is_gdr($headref)) {
        # This is much faster.  It also makes patches that gdr
        # likes better for future updates without laundering.
        #
@@ -5600,7 +5740,10 @@ END
        if (act_local()) {
            debugcmd "+",@cmd;
            $!=0; $?=-1;
-           failedcmd @cmd if system @cmd and $?!=7*256;
+           failedcmd @cmd
+               if system @cmd
+               and not ($? == 7*256 or
+                        $? == -1 && $!==ENOENT);
        } else {
            dryrun_report @cmd;
        }
@@ -5618,8 +5761,6 @@ END
        quilt_fixup_multipatch($clogp, $headref, $upstreamversion);
     }
 
-    die 'bug' if $split_brain && !$need_split_build_invocation;
-
     changedir $maindir;
     runcmd_ordryrun_local
         @git, qw(pull --ff-only -q), "$playground/work", qw(master);
@@ -5628,7 +5769,7 @@ END
 sub unpack_playtree_mkwork ($) {
     my ($headref) = @_;
 
-    mkdir "work" or die $!;
+    mkdir "work" or confess $!;
     changedir "work";
     mktree_in_ud_here();
     runcmd @git, qw(reset -q --hard), $headref;
@@ -5657,17 +5798,18 @@ sub unpack_playtree_linkorigs ($$) {
 
 sub quilt_fixup_delete_pc () {
     runcmd @git, qw(rm -rqf .pc);
-    commit_admin <<END
+    commit_admin +(__ <<END).<<ENDU
 Commit removal of .pc (quilt series tracking data)
+END
 
 [dgit ($our_version) upgrade quilt-remove-pc]
-END
+ENDU
 }
 
 sub quilt_fixup_singlepatch ($$$) {
     my ($clogp, $headref, $upstreamversion) = @_;
 
-    progress "starting quiltify (single-debian-patch)";
+    progress __ "starting quiltify (single-debian-patch)";
 
     # dpkg-source --commit generates new patches even if
     # single-debian-patch is in debian/source/options.  In order to
@@ -5683,7 +5825,9 @@ sub quilt_fixup_singlepatch ($$$) {
     changedir "..";
     runcmd @dpkgsource, qw(-x), (srcfn $version, ".dsc");
     rename srcfn("$upstreamversion", "/debian/patches"), 
-           "work/debian/patches";
+       "work/debian/patches"
+       or $!==ENOENT
+       or confess "install d/patches: $!";
 
     changedir "work";
     commit_quilty_patch();
@@ -5694,8 +5838,8 @@ sub quilt_make_fake_dsc ($) {
 
     my $fakeversion="$upstreamversion-~~DGITFAKE";
 
-    my $fakedsc=new IO::File 'fake.dsc', '>' or die $!;
-    print $fakedsc <<END or die $!;
+    my $fakedsc=new IO::File 'fake.dsc', '>' or confess $!;
+    print $fakedsc <<END or confess $!;
 Format: 3.0 (quilt)
 Source: $package
 Version: $fakeversion
@@ -5708,11 +5852,11 @@ END
        my $md = new Digest::MD5;
 
        my $fh = new IO::File $b, '<' or die "$b $!";
-       stat $fh or die $!;
+       stat $fh or confess $!;
        my $size = -s _;
 
        $md->addfile($fh);
-       print $fakedsc " ".$md->hexdigest." $size $b\n" or die $!;
+       print $fakedsc " ".$md->hexdigest." $size $b\n" or confess $!;
     };
 
     unpack_playtree_linkorigs($upstreamversion, $dscaddfile);
@@ -5729,9 +5873,34 @@ END
     runcmd qw(env GZIP=-1n tar -zcf), "./$debtar", qw(-C), $maindir, @files;
 
     $dscaddfile->($debtar);
-    close $fakedsc or die $!;
+    close $fakedsc or confess $!;
 }
 
+sub quilt_fakedsc2unapplied ($$) {
+    my ($headref, $upstreamversion) = @_;
+    # must be run in the playground
+    # quilt_make_fake_dsc must have been called
+
+    runcmd qw(sh -ec),
+        'exec dpkg-source --no-check --skip-patches -x fake.dsc >/dev/null';
+
+    my $fakexdir= $package.'-'.(stripepoch $upstreamversion);
+    rename $fakexdir, "fake" or die "$fakexdir $!";
+
+    changedir 'fake';
+
+    remove_stray_gits(__ "source package");
+    mktree_in_ud_here();
+
+    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";
+    return $unapplied;
+}    
+
 sub quilt_check_splitbrain_cache ($$) {
     my ($headref, $upstreamversion) = @_;
     # Called only if we are in (potentially) split brain mode.
@@ -5741,8 +5910,9 @@ sub quilt_check_splitbrain_cache ($$) {
 
     my $splitbrain_cachekey;
     
-    progress
- "dgit: split brain (separate dgit view) may be needed (--quilt=$quilt_mode).";
+    progress f_
+ "dgit: split brain (separate dgit view) may be needed (--quilt=%s).",
+                $quilt_mode;
     # we look in the reflog of dgit-intern/quilt-cache
     # we look for an entry whose message is the key for the cache lookup
     my @cachekey = (qw(dgit), $our_version);
@@ -5763,39 +5933,23 @@ sub quilt_check_splitbrain_cache ($$) {
     push @cachekey, $srcshash->hexdigest();
     $splitbrain_cachekey = "@cachekey";
 
-    my @cmd = (@git, qw(log -g), '--pretty=format:%H %gs',
-              $splitbraincache);
     printdebug "splitbrain cachekey $splitbrain_cachekey\n";
-    debugcmd "|(probably)",@cmd;
-    my $child = open GC, "-|";  defined $child or die $!;
-    if (!$child) {
-       chdir $maindir or die $!;
-       if (!stat "$maindir_gitcommon/logs/refs/$splitbraincache") {
-           $! == ENOENT or die $!;
-           printdebug ">(no reflog)\n";
-           finish 0;
-       }
-       exec @cmd; die $!;
-    }
-    while (<GC>) {
-       chomp;
-       printdebug ">| ", $_, "\n" if $debuglevel > 1;
-       next unless m/^(\w+) (\S.*\S)$/ && $2 eq $splitbrain_cachekey;
-           
-       my $cachehit = $1;
+
+    my $cachehit = reflog_cache_lookup
+       "refs/$splitbraincache", $splitbrain_cachekey;
+
+    if ($cachehit) {
        unpack_playtree_mkwork($headref);
        my $saved = maybe_split_brain_save $headref, $cachehit, "cache-hit";
        if ($cachehit ne $headref) {
-           progress "dgit view: found cached ($saved)";
+           progress f_ "dgit view: found cached (%s)", $saved;
            runcmd @git, qw(checkout -q -b dgit-view), $cachehit;
            $split_brain = 1;
            return ($cachehit, $splitbrain_cachekey);
        }
-       progress "dgit view: found cached, no changes required";
+       progress __ "dgit view: found cached, no changes required";
        return ($headref, $splitbrain_cachekey);
     }
-    die $! if GC->error;
-    failedcmd unless close GC;
 
     printdebug "splitbrain cache miss\n";
     return (undef, $splitbrain_cachekey);
@@ -5804,7 +5958,8 @@ sub quilt_check_splitbrain_cache ($$) {
 sub quilt_fixup_multipatch ($$$) {
     my ($clogp, $headref, $upstreamversion) = @_;
 
-    progress "examining quilt state (multiple patches, $quilt_mode mode)";
+    progress f_ "examining quilt state (multiple patches, %s mode)",
+               $quilt_mode;
 
     # Our objective is:
     #  - honour any existing .pc in case it has any strangeness
@@ -5885,24 +6040,7 @@ sub quilt_fixup_multipatch ($$$) {
            quilt_check_splitbrain_cache($headref, $upstreamversion);
        return if $cachehit;
     }
-
-    runcmd qw(sh -ec),
-        'exec dpkg-source --no-check --skip-patches -x fake.dsc >/dev/null';
-
-    my $fakexdir= $package.'-'.(stripepoch $upstreamversion);
-    rename $fakexdir, "fake" or die "$fakexdir $!";
-
-    changedir 'fake';
-
-    remove_stray_gits("source package");
-    mktree_in_ud_here();
-
-    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";
+    my $unapplied=quilt_fakedsc2unapplied($headref, $upstreamversion);
 
     ensuredir '.pc';
 
@@ -5910,7 +6048,7 @@ sub quilt_fixup_multipatch ($$$) {
     $!=0; $?=-1;
     if (system @bbcmd) {
        failedcmd @bbcmd if $? < 0;
-       fail <<END;
+       fail __ <<END;
 failed to apply your git tree's patch stack (from debian/patches/) to
  the corresponding upstream tarball(s).  Your source tree and .orig
  are probably too inconsistent.  dgit can only fix up certain kinds of
@@ -5925,10 +6063,10 @@ END
     my $mustdeletepc=0;
     if (stat_exists ".pc") {
         -d _ or die;
-       progress "Tree already contains .pc - will use it then delete it.";
+       progress __ "Tree already contains .pc - will use it then delete it.";
         $mustdeletepc=1;
     } else {
-        rename '../fake/.pc','.pc' or die $!;
+        rename '../fake/.pc','.pc' or confess $!;
     }
 
     changedir '../fake';
@@ -5961,31 +6099,41 @@ END
     }
     printdebug "differences \@dl @dl.\n";
 
-    progress sprintf
-"$us: base trees orig=%.20s o+d/p=%.20s",
-              $unapplied, $oldtiptree;
-    progress sprintf
-"$us: quilt differences: src:  %s orig %s     gitignores:  %s orig %s\n".
-"$us: quilt differences:      HEAD %s o+d/p               HEAD %s o+d/p",
-                             $dl[0], $dl[1],              $dl[3], $dl[4],
-                                 $dl[2],                     $dl[5];
+    progress f_
+"%s: base trees orig=%.20s o+d/p=%.20s",
+              $us, $unapplied, $oldtiptree;
+    progress f_
+"%s: quilt differences: src:  %s orig %s     gitignores:  %s orig %s\n".
+"%s: quilt differences:      HEAD %s o+d/p               HEAD %s o+d/p",
+  $us,                      $dl[0], $dl[1],              $dl[3], $dl[4],
+  $us,                          $dl[2],                     $dl[5];
 
     if (@unrepres) {
-       print STDERR "dgit:  cannot represent change: $_->[1]: $_->[0]\n"
+       print STDERR f_ "dgit:  cannot represent change: %s: %s\n",
+                       $_->[1], $_->[0]
            foreach @unrepres;
-       forceable_fail [qw(unrepresentable)], <<END;
+       forceable_fail [qw(unrepresentable)], __ <<END;
 HEAD has changes to .orig[s] which are not representable by `3.0 (quilt)'
 END
     }
 
     my @failsuggestion;
     if (!($diffbits->{O2H} & $diffbits->{O2A})) {
-        push @failsuggestion, "This might be a patches-unapplied branch.";
-    }  elsif (!($diffbits->{H2A} & $diffbits->{O2A})) {
-        push @failsuggestion, "This might be a patches-applied branch.";
+        push @failsuggestion, [ 'unapplied', __
+ "This might be a patches-unapplied branch." ];
+    } elsif (!($diffbits->{H2A} & $diffbits->{O2A})) {
+        push @failsuggestion, [ 'applied', __
+ "This might be a patches-applied branch." ];
     }
-    push @failsuggestion, "Maybe you need to specify one of".
-        " --[quilt=]gbp --[quilt=]dpm --quilt=unapplied ?";
+    push @failsuggestion, [ 'quilt-mode', __
+ "Maybe you need one of --[quilt=]gbp --[quilt=]dpm --quilt=unapplied ?" ];
+
+    push @failsuggestion, [ 'gitattrs', __
+ "Warning: Tree has .gitattributes.  See GITATTRIBUTES in dgit(7)." ]
+       if stat_exists '.gitattributes';
+
+    push @failsuggestion, [ 'origs', __
+ "Maybe orig tarball(s) are not identical to git representation?" ];
 
     if (quiltmode_splitbrain()) {
        quiltify_splitbrain($clogp, $unapplied, $headref, $oldtiptree,
@@ -5994,11 +6142,11 @@ END
        return;
     }
 
-    progress "starting quiltify (multiple patches, $quilt_mode mode)";
+    progress f_ "starting quiltify (multiple patches, %s mode)", $quilt_mode;
     quiltify($clogp,$headref,$oldtiptree,\@failsuggestion);
 
     if (!open P, '>>', ".pc/applied-patches") {
-       $!==&ENOENT or die $!;
+       $!==&ENOENT or confess $!;
     } else {
        close P;
     }
@@ -6013,25 +6161,25 @@ END
 sub quilt_fixup_editor () {
     my $descfn = $ENV{$fakeeditorenv};
     my $editing = $ARGV[$#ARGV];
-    open I1, '<', $descfn or die "$descfn: $!";
-    open I2, '<', $editing or die "$editing: $!";
-    unlink $editing or die "$editing: $!";
-    open O, '>', $editing or die "$editing: $!";
-    while (<I1>) { print O or die $!; } I1->error and die $!;
+    open I1, '<', $descfn or confess "$descfn: $!";
+    open I2, '<', $editing or confess "$editing: $!";
+    unlink $editing or confess "$editing: $!";
+    open O, '>', $editing or confess "$editing: $!";
+    while (<I1>) { print O or confess $!; } I1->error and confess $!;
     my $copying = 0;
     while (<I2>) {
        $copying ||= m/^\-\-\- /;
        next unless $copying;
-       print O or die $!;
+       print O or confess $!;
     }
-    I2->error and die $!;
+    I2->error and confess $!;
     close O or die $1;
     finish 0;
 }
 
 sub maybe_apply_patches_dirtily () {
     return unless $quilt_mode =~ m/gbp|unapplied/;
-    print STDERR <<END or die $!;
+    print STDERR __ <<END or confess $!;
 
 dgit: Building, or cleaning with rules target, in patches-unapplied tree.
 dgit: Have to apply the patches - making the tree dirty.
@@ -6044,7 +6192,7 @@ END
 }
 
 sub maybe_unapply_patches_again () {
-    progress "dgit: Unapplying patches again to tidy up the tree."
+    progress __ "dgit: Unapplying patches again to tidy up the tree."
        if $patches_applied_dirtily;
     runcmd qw(dpkg-source --after-build .)
        if $patches_applied_dirtily & 01;
@@ -6055,38 +6203,76 @@ sub maybe_unapply_patches_again () {
 
 #----- other building -----
 
-our $clean_using_builder;
-# ^ tree is to be cleaned by dpkg-source's builtin idea that it should
-#   clean the tree before building (perhaps invoked indirectly by
-#   whatever we are using to run the build), rather than separately
-#   and explicitly by us.
+sub clean_tree_check_git ($$) {
+    my ($honour_ignores, $message) = @_;
+    my @cmd = (@git, qw(clean -dn));
+    push @cmd, qw(-x) unless $honour_ignores;
+    my $leftovers = cmdoutput @cmd;
+    if (length $leftovers) {
+       print STDERR $leftovers, "\n" or confess $!;
+       fail $message;
+    }
+}
+
+sub clean_tree_check_git_wd ($) {
+    my ($message) = @_;
+    return if $cleanmode =~ m{no-check};
+    return if $patches_applied_dirtily; # yuk
+    clean_tree_check_git +($cleanmode !~ m{all-check}),
+                         (f_ <<END, $message);
+%s
+If this is just missing .gitignore entries, use a different clean
+mode, eg --clean=dpkg-source,no-check (-wdu/-wddu) to ignore them
+or --clean=git (-wg/-wgf) to use \`git clean' instead.
+END
+}
+
+sub clean_tree_check () {
+    # Not yet fully implemented.
+    # This function needs to not care about modified but tracked files.
+    # That was done by check_not_dirty, and by now we may have run
+    # the rules clean target which might modify tracked files (!)
+    if ($cleanmode =~ m{^check}) {
+       clean_tree_check_git +($cleanmode =~ m{ignores}), __
+ "tree contains uncommitted files and --clean=check specified";
+    } elsif ($cleanmode =~ m{^dpkg-source}) {
+       clean_tree_check_git_wd __
+ "tree contains uncommitted files (NB dgit didn't run rules clean)";
+    } elsif ($cleanmode =~ m{^git}) {
+       # If we were actually cleaning these files would be summarily
+       # deleted.  Since we're not, and not using the working tree
+       # anyway, we can just ignore them - nothing will use them.
+    } elsif ($cleanmode eq 'none') {
+    } else {
+       confess "$cleanmode ?";
+    }
+}
 
 sub clean_tree () {
-    return if $clean_using_builder;
-    if ($cleanmode eq 'dpkg-source') {
-       maybe_apply_patches_dirtily();
-       runcmd_ordryrun_local @dpkgbuildpackage, qw(-T clean);
-    } elsif ($cleanmode eq 'dpkg-source-d') {
+    # We always clean the tree ourselves, rather than leave it to the
+    # builder (dpkg-source, or soemthing which calls dpkg-source).
+    if ($cleanmode =~ m{^dpkg-source}) {
+       my @cmd = @dpkgbuildpackage;
+       push @cmd, qw(-d) if $cleanmode =~ m{^dpkg-source-d};
+       push @cmd, qw(-T clean);
        maybe_apply_patches_dirtily();
-       runcmd_ordryrun_local @dpkgbuildpackage, qw(-d -T clean);
+       runcmd_ordryrun_local @cmd;
+       clean_tree_check_git_wd __
+ "tree contains uncommitted files (after running rules clean)";
     } elsif ($cleanmode eq 'git') {
        runcmd_ordryrun_local @git, qw(clean -xdf);
     } elsif ($cleanmode eq 'git-ff') {
        runcmd_ordryrun_local @git, qw(clean -xdff);
-    } elsif ($cleanmode eq 'check') {
-       my $leftovers = cmdoutput @git, qw(clean -xdn);
-       if (length $leftovers) {
-           print STDERR $leftovers, "\n" or die $!;
-           fail "tree contains uncommitted files and --clean=check specified";
-       }
+    } elsif ($cleanmode =~ m{^check}) {
+       clean_tree_check();
     } elsif ($cleanmode eq 'none') {
     } else {
-       die "$cleanmode ?";
+       confess "$cleanmode ?";
     }
 }
 
 sub cmd_clean () {
-    badusage "clean takes no additional arguments" if @ARGV;
+    badusage __ "clean takes no additional arguments" if @ARGV;
     notpushing();
     clean_tree();
     maybe_unapply_patches_again();
@@ -6099,11 +6285,13 @@ sub WANTSRC_BUILDER () { 02; } # caller should run dpkg-buildpackage
 sub build_or_push_prep_early () {
     our $build_or_push_prep_early_done //= 0;
     return if $build_or_push_prep_early_done++;
-    badusage "-p is not allowed with dgit $subcommand" if defined $package;
+    badusage f_ "-p is not allowed with dgit %s", $subcommand
+       if defined $package;
     my $clogp = parsechangelog();
     $isuite = getfield $clogp, 'Distribution';
     $package = getfield $clogp, 'Source';
     $version = getfield $clogp, 'Version';
+    $dscfn = dscfn($version);
 }
 
 sub build_prep_early () {
@@ -6115,17 +6303,27 @@ sub build_prep_early () {
 sub build_prep ($) {
     my ($wantsrc) = @_;
     build_prep_early();
-    # clean the tree if we're trying to include dirty changes in the
-    # source package, or we are running the builder in $maindir
-    clean_tree() if $includedirty || ($wantsrc & WANTSRC_BUILDER);
+    if (!building_source_in_playtree() || ($wantsrc & WANTSRC_BUILDER)) {
+       # Clean the tree because we're going to use the contents of
+       # $maindir.  (We trying to include dirty changes in the source
+       # package, or we are running the builder in $maindir.)
+       clean_tree();
+    } else {
+       # We don't actually need to do anything in $maindir, but we
+       # should do some kind of cleanliness check because (i) the
+       # user may have forgotten a `git add', and (ii) if the user
+       # said -wc we should still do the check.
+       clean_tree_check();
+    }
     build_maybe_quilt_fixup();
     if ($rmchanges) {
        my $pat = changespat $version;
        foreach my $f (glob "$buildproductsdir/$pat") {
            if (act_local()) {
-               unlink $f or fail "remove old changes file $f: $!";
+               unlink $f or
+                   fail f_ "remove old changes file %s: %s", $f, $!;
            } else {
-               progress "would remove $f";
+               progress f_ "would remove %s", $f;
            }
        }
     }
@@ -6150,17 +6348,17 @@ sub changesopts_version () {
            1;
        }) {
            print STDERR $@;
-           fail
+           fail __
  "archive query failed (queried because --since-version not specified)";
        }
        if (@vsns) {
            @vsns = map { $_->[0] } @vsns;
            @vsns = sort { -version_compare($a, $b) } @vsns;
            $changes_since_version = $vsns[0];
-           progress "changelog will contain changes since $vsns[0]";
+           progress f_ "changelog will contain changes since %s", $vsns[0];
        } else {
            $changes_since_version = '_';
-           progress "package seems new, not specifying -v<version>";
+           progress __ "package seems new, not specifying -v<version>";
        }
     }
     if ($changes_since_version ne '_') {
@@ -6176,23 +6374,11 @@ sub changesopts () {
 
 sub massage_dbp_args ($;$) {
     my ($cmd,$xargs) = @_;
-    # We need to:
-    #
-    #  - if we're going to split the source build out so we can
-    #    do strange things to it, massage the arguments to dpkg-buildpackage
-    #    so that the main build doessn't build source (or add an argument
-    #    to stop it building source by default).
-    #
-    #  - add -nc to stop dpkg-source cleaning the source tree,
-    #    unless we're not doing a split build and want dpkg-source
-    #    as cleanmode, in which case we can do nothing
-    #
+    # Since we split the source build out so we can do strange things
+    # to it, massage the arguments to dpkg-buildpackage so that the
+    # main build doessn't build source (or add an argument to stop it
+    # building source by default).
     debugcmd '#massaging#', @$cmd if $debuglevel>1;
-#print STDERR "MASS0 ",Dumper($cmd, $xargs, $need_split_build_invocation);
-    if ($cleanmode eq 'dpkg-source' && !$need_split_build_invocation) {
-       $clean_using_builder = 1;
-       return WANTSRC_BUILDER;
-    }
     # -nc has the side effect of specifying -b if nothing else specified
     # and some combinations of -S, -b, et al, are errors, rather than
     # later simply overriding earlie.  So we need to:
@@ -6203,17 +6389,26 @@ sub massage_dbp_args ($;$) {
     my $dmode = '-F';
     foreach my $l ($cmd, $xargs) {
        next unless $l;
-       @$l = grep { !(m/^-[SgGFABb]$/s and $dmode=$_) } @$l;
+       @$l = grep { !(m/^-[SgGFABb]$|^--build=/s and $dmode=$_) } @$l;
     }
     push @$cmd, '-nc';
 #print STDERR "MASS1 ",Dumper($cmd, $xargs, $dmode);
     my $r = WANTSRC_BUILDER;
-    if ($need_split_build_invocation) {
-       printdebug "massage split $dmode.\n";
-       $r = $dmode =~ m/[S]/     ?  WANTSRC_SOURCE :
-            $dmode =~ y/gGF/ABb/ ?  WANTSRC_SOURCE | WANTSRC_BUILDER :
-            $dmode =~ m/[ABb]/   ?                   WANTSRC_BUILDER :
-            die "$dmode ?";
+    printdebug "massage split $dmode.\n";
+    if ($dmode =~ s/^--build=//) {
+       $r = 0;
+       my @d = split /,/, $dmode;
+       $r |= WANTSRC_SOURCE  if grep { s/^full$/binary/ } @d;
+       $r |= WANTSRC_SOURCE  if grep { s/^source$// } @d;
+       $r |= WANTSRC_BUILDER if grep { m/./ } @d;
+       fail __ "Wanted to build nothing!" unless $r;
+       $dmode = '--build='. join ',', grep m/./, @d;
+    } else {
+       $r =
+         $dmode =~ m/[S]/     ?  WANTSRC_SOURCE :
+         $dmode =~ y/gGF/ABb/ ?  WANTSRC_SOURCE | WANTSRC_BUILDER :
+         $dmode =~ m/[ABb]/   ?                   WANTSRC_BUILDER :
+         confess "$dmode ?";
     }
     printdebug "massage done $r $dmode.\n";
     push @$cmd, $dmode;
@@ -6243,30 +6438,35 @@ sub postbuild_mergechanges ($) {
     } @changesfiles;
     my $result;
     if (@changesfiles==1) {
-       fail <<END.$msg_if_onlyone if defined $msg_if_onlyone;
-only one changes file from build (@changesfiles)
+       fail +(f_ <<END, "@changesfiles").$msg_if_onlyone
+only one changes file from build (%s)
 END
+           if defined $msg_if_onlyone;
        $result = $changesfiles[0];
     } elsif (@changesfiles==2) {
        my $binchanges = parsecontrol($changesfiles[1], "binary changes file");
        foreach my $l (split /\n/, getfield $binchanges, 'Files') {
-           fail "$l found in binaries changes file $binchanges"
+           fail f_ "%s found in binaries changes file %s", $l, $binchanges
                if $l =~ m/\.dsc$/;
        }
        runcmd_ordryrun_local @mergechanges, @changesfiles;
        my $multichanges = changespat $version,'multi';
        if (act_local()) {
-           stat_exists $multichanges or fail "$multichanges: $!";
+           stat_exists $multichanges or fail f_
+               "%s unexpectedly not created by build", $multichanges;
            foreach my $cf (glob $pat) {
                next if $cf eq $multichanges;
-               rename "$cf", "$cf.inmulti" or fail "$cf\{,.inmulti}: $!";
+               rename "$cf", "$cf.inmulti" or fail f_
+                   "install new changes %s\{,.inmulti}: %s", $cf, $!;
            }
        }
        $result = $multichanges;
     } else {
-       fail "wrong number of different changes files (@changesfiles)";
+       fail f_ "wrong number of different changes files (%s)",
+               "@changesfiles";
     }
-    printdone "build successful, results in $result\n" or die $!;
+    printdone f_ "build successful, results in %s\n", $result
+       or confess $!;
 }
 
 sub midbuild_checkchanges () {
@@ -6277,9 +6477,9 @@ sub midbuild_checkchanges () {
        $_ ne changespat $version,'source' and
        $_ ne changespat $version,'multi'
     } @unwanted;
-    fail <<END
-changes files other than source matching $pat already present; building would result in ambiguity about the intended results.
-Suggest you delete @unwanted.
+    fail +(f_ <<END, $pat, "@unwanted")
+changes files other than source matching %s already present; building would result in ambiguity about the intended results.
+Suggest you delete %s.
 END
        if @unwanted;
 }
@@ -6296,12 +6496,17 @@ sub postbuild_mergechanges_vanilla ($) {
            postbuild_mergechanges(undef);
        };
     } else {
-       printdone "build successful\n";
+       printdone __ "build successful\n";
     }
 }
 
 sub cmd_build {
     build_prep_early();
+    $buildproductsdir eq '..' or print STDERR +(f_ <<END, $us, $us);
+%s: warning: build-products-dir set, but not supported by dpkg-buildpackage
+%s: warning: build-products-dir will be ignored; files will go to ..
+END
+    $buildproductsdir = '..';
     my @dbp = (@dpkgbuildpackage, qw(-us -uc), changesopts_initial(), @ARGV);
     my $wantsrc = massage_dbp_args \@dbp;
     build_prep($wantsrc);
@@ -6338,7 +6543,6 @@ sub cmd_gbp_build {
     if ($gbp_make_orig) {
        clean_tree();
        $cleanmode = 'none'; # don't do it again
-       $need_split_build_invocation = 1;
     }
 
     my @dbp = @dpkgbuildpackage;
@@ -6352,7 +6556,7 @@ sub cmd_gbp_build {
            $gbp_build[0] = 'gbp buildpackage';
        }
     }
-    my @cmd = opts_opt_multi_cmd @gbp_build;
+    my @cmd = opts_opt_multi_cmd [], @gbp_build;
 
     push @cmd, (qw(-us -uc --git-no-sign-tags),
                "--git-builder=".(shellquote @dbp));
@@ -6360,7 +6564,7 @@ sub cmd_gbp_build {
     if ($gbp_make_orig) {
        my $priv = dgit_privdir();
        my $ok = "$priv/origs-gen-ok";
-       unlink $ok or $!==&ENOENT or die $!;
+       unlink $ok or $!==&ENOENT or confess $!;
        my @origs_cmd = @cmd;
        push @origs_cmd, qw(--git-cleaner=true);
        push @origs_cmd, "--git-prebuild=".
@@ -6381,9 +6585,7 @@ sub cmd_gbp_build {
        build_source();
        midbuild_checkchanges_vanilla $wantsrc;
     } else {
-       if (!$clean_using_builder) {
-           push @cmd, '--git-cleaner=true';
-       }
+       push @cmd, '--git-cleaner=true';
     }
     maybe_unapply_patches_again();
     if ($wantsrc & WANTSRC_BUILDER) {
@@ -6394,22 +6596,6 @@ sub cmd_gbp_build {
 }
 sub cmd_git_build { cmd_gbp_build(); } # compatibility with <= 1.0
 
-sub move_dsc_to_bpd ($) {
-    my ($dscfn) = @_;
-    printdebug "moving $dscfn and all referenced files to ".bpd_abs()."\n";
-    $dsc = parsecontrol($dscfn, "source package");
-    foreach my $l (split /\n/, getfield $dsc, 'Files') {
-        $l =~ m/\S+$/ or next;
-        $l =~ s/.* //;
-        printdebug "found $l - renaming\n";
-        rename "$l", bpd_abs()."/$l"
-          or fail "put in place new source file ($l): $!";
-    }
-    printdebug "moving $dscfn to ".bpd_abs()."/$dscfn\n";
-    rename "$dscfn", bpd_abs()."/$dscfn"
-      or fail "put in place new source file ($dscfn): $!";
-}
-
 sub building_source_in_playtree {
     # If $includedirty, we have to build the source package from the
     # working tree, not a playtree, so that uncommitted changes are
@@ -6427,9 +6613,8 @@ sub build_source {
     $sourcechanges = changespat $version,'source';
     if (act_local()) {
        unlink "$buildproductsdir/$sourcechanges" or $!==ENOENT
-           or fail "remove $sourcechanges: $!";
+           or fail f_ "remove %s: %s", $sourcechanges, $!;
     }
-    $dscfn = dscfn($version);
     my @cmd = (@dpkgsource, qw(-b --));
     my $leafdir;
     if (building_source_in_playtree()) {
@@ -6448,6 +6633,24 @@ sub build_source {
         }
     } else {
         $leafdir = basename $maindir;
+
+       if ($buildproductsdir ne '..') {
+           # Well, we are going to run dpkg-source -b which consumes
+           # origs from .. and generates output there.  To make this
+           # work when the bpd is not .. , we would have to (i) link
+           # origs from bpd to .. , (ii) check for files that
+           # dpkg-source -b would/might overwrite, and afterwards
+           # (iii) move all the outputs back to the bpd (iv) except
+           # for the origs which should be deleted from .. if they
+           # weren't there beforehand.  And if there is an error and
+           # we don't run to completion we would necessarily leave a
+           # mess.  This is too much.  The real way to fix this
+           # is for dpkg-source to have bpd support.
+           confess unless $includedirty;
+           fail __
+ "--include-dirty not supported with --build-products-dir, sorry";
+       }
+
         changedir '..';
     }
     runcmd_ordryrun_local @cmd, $leafdir;
@@ -6457,61 +6660,118 @@ sub build_source {
       'exec >../$1; shift; exec "$@"','x', $sourcechanges,
       @dpkggenchanges, qw(-S), changesopts();
     changedir '..';
-    move_dsc_to_bpd($dscfn);
-    rename "$sourcechanges", bpd_abs()."/$sourcechanges"
-       or fail "put in place source changes file ($sourcechanges): $!";
+
+    printdebug "moving $dscfn, $sourcechanges, etc. to ".bpd_abs()."\n";
+    $dsc = parsecontrol($dscfn, "source package");
+
+    my $mv = sub {
+       my ($why, $l) = @_;
+        printdebug " renaming ($why) $l\n";
+        rename_link_xf 0, "$l", bpd_abs()."/$l"
+           or fail f_ "put in place new built file (%s): %s", $l, $@;
+    };
+    foreach my $l (split /\n/, getfield $dsc, 'Files') {
+        $l =~ m/\S+$/ or next;
+       $mv->('Files', $&);
+    }
+    $mv->('dsc', $dscfn);
+    $mv->('changes', $sourcechanges);
+
     changedir $maindir;
 }
 
 sub cmd_build_source {
-    badusage "build-source takes no additional arguments" if @ARGV;
+    badusage __ "build-source takes no additional arguments" if @ARGV;
     build_prep(WANTSRC_SOURCE);
     build_source();
     maybe_unapply_patches_again();
-    printdone "source built, results in $dscfn and $sourcechanges";
+    printdone f_ "source built, results in %s and %s",
+                $dscfn, $sourcechanges;
 }
 
 sub cmd_push_source {
     prep_push();
-    fail "dgit push-source: --include-dirty/--ignore-dirty does not make".
-      "sense with push-source!" if $includedirty;
+    fail __
+       "dgit push-source: --include-dirty/--ignore-dirty does not make".
+       "sense with push-source!"
+       if $includedirty;
     build_maybe_quilt_fixup();
     if ($changesfile) {
         my $changes = parsecontrol("$buildproductsdir/$changesfile",
-                                   "source changes file");
+                                   __ "source changes file");
         unless (test_source_only_changes($changes)) {
-            fail "user-specified changes file is not source-only";
+            fail __ "user-specified changes file is not source-only";
         }
     } else {
         # Building a source package is very fast, so just do it
        build_source();
-       die "er, patches are applied dirtily but shouldn't be.."
+       confess "er, patches are applied dirtily but shouldn't be.."
            if $patches_applied_dirtily;
        $changesfile = $sourcechanges;
     }
     dopush();
 }
 
-sub cmd_sbuild {
-    build_prep(WANTSRC_SOURCE); # not BUILDER because sbuild uses the .dsc
+sub binary_builder {
+    my ($bbuilder, $pbmc_msg, @args) = @_;
+    build_prep(WANTSRC_SOURCE);
     build_source();
     midbuild_checkchanges();
     in_bpd {
        if (act_local()) {
-           stat_exists $dscfn or fail "$dscfn (in parent directory): $!";
-           stat_exists $sourcechanges
-               or fail "$sourcechanges (in parent directory): $!";
+           stat_exists $dscfn or fail f_
+               "%s (in build products dir): %s", $dscfn, $!;
+           stat_exists $sourcechanges or fail f_
+               "%s (in build products dir): %s", $sourcechanges, $!;
        }
-       runcmd_ordryrun_local @sbuild, qw(-d), $isuite, @ARGV, $dscfn;
+       runcmd_ordryrun_local @$bbuilder, @args;
     };
     maybe_unapply_patches_again();
     in_bpd {
-       postbuild_mergechanges(<<END);
+       postbuild_mergechanges($pbmc_msg);
+    };
+}
+
+sub cmd_sbuild {
+    build_prep_early();
+    binary_builder(\@sbuild, (__ <<END), qw(-d), $isuite, @ARGV, $dscfn);
 perhaps you need to pass -A ?  (sbuild's default is to build only
 arch-specific binaries; dgit 1.4 used to override that.)
 END
-    };
-}    
+}
+
+sub pbuilder ($) {
+    my ($pbuilder) = @_;
+    build_prep_early();
+    # @ARGV is allowed to contain only things that should be passed to
+    # pbuilder under debbuildopts; just massage those
+    my $wantsrc = massage_dbp_args \@ARGV;
+    fail __
+       "you asked for a builder but your debbuildopts didn't ask for".
+       " any binaries -- is this really what you meant?"
+       unless $wantsrc & WANTSRC_BUILDER;
+    fail __
+       "we must build a .dsc to pass to the builder but your debbuiltopts".
+       " forbids the building of a source package; cannot continue"
+      unless $wantsrc & WANTSRC_SOURCE;
+    # We do not want to include the verb "build" in @pbuilder because
+    # the user can customise @pbuilder and they shouldn't be required
+    # to include "build" in their customised value.  However, if the
+    # user passes any additional args to pbuilder using the dgit
+    # option --pbuilder:foo, such args need to come after the "build"
+    # verb.  opts_opt_multi_cmd does all of that.
+    binary_builder([opts_opt_multi_cmd ["build"], @$pbuilder], undef,
+                   qw(--debbuildopts), "@ARGV", qw(--distribution), $isuite,
+                   $dscfn);
+}
+
+sub cmd_pbuilder {
+    pbuilder(\@pbuilder);
+}
+
+sub cmd_cowbuilder {
+    pbuilder(\@cowbuilder);
+}
 
 sub cmd_quilt_fixup {
     badusage "incorrect arguments to dgit quilt-fixup" if @ARGV;
@@ -6520,13 +6780,32 @@ sub cmd_quilt_fixup {
     build_maybe_quilt_fixup();
 }
 
+sub cmd_print_unapplied_treeish {
+    badusage __ "incorrect arguments to dgit print-unapplied-treeish"
+       if @ARGV;
+    my $headref = git_rev_parse('HEAD');
+    my $clogp = commit_getclogp $headref;
+    $package = getfield $clogp, 'Source';
+    $version = getfield $clogp, 'Version';
+    $isuite = getfield $clogp, 'Distribution';
+    $csuite = $isuite; # we want this to be offline!
+    notpushing();
+
+    prep_ud();
+    changedir $playground;
+    my $uv = upstreamversion $version;
+    quilt_make_fake_dsc($uv);
+    my $u = quilt_fakedsc2unapplied($headref, $uv);
+    print $u, "\n" or confess $!;
+}
+
 sub import_dsc_result {
     my ($dstref, $newhash, $what_log, $what_msg) = @_;
     my @cmd = (git_update_ref_cmd $what_log, $dstref, $newhash);
     runcmd @cmd;
-    check_gitattrs($newhash, "source tree");
+    check_gitattrs($newhash, __ "source tree");
 
-    progress "dgit: import-dsc: $what_msg";
+    progress f_ "dgit: import-dsc: %s", $what_msg;
 }
 
 sub cmd_import_dsc {
@@ -6539,14 +6818,16 @@ sub cmd_import_dsc {
        if (m/^--require-valid-signature$/) {
            $needsig = 1;
        } else {
-           badusage "unknown dgit import-dsc sub-option \`$_'";
+           badusage f_ "unknown dgit import-dsc sub-option \`%s'", $_;
        }
     }
 
-    badusage "usage: dgit import-dsc .../PATH/TO/.DSC BRANCH" unless @ARGV==2;
+    badusage __ "usage: dgit import-dsc .../PATH/TO/.DSC BRANCH"
+       unless @ARGV==2;
     my ($dscfn, $dstbranch) = @ARGV;
 
-    badusage "dry run makes no sense with import-dsc" unless act_local();
+    badusage __ "dry run makes no sense with import-dsc"
+       unless act_local();
 
     my $force = $dstbranch =~ s/^\+//   ? +1 :
                $dstbranch =~ s/^\.\.// ? -1 :
@@ -6562,14 +6843,14 @@ sub cmd_import_dsc {
     my $chead = cmdoutput_errok @symcmd;
     defined $chead or $?==256 or failedcmd @symcmd;
 
-    fail "$dstbranch is checked out - will not update it"
+    fail f_ "%s is checked out - will not update it", $dstbranch
        if defined $chead and $chead eq $dstbranch;
 
     my $oldhash = git_get_ref $dstbranch;
 
-    open D, "<", $dscfn or fail "open import .dsc ($dscfn): $!";
+    open D, "<", $dscfn or fail f_ "open import .dsc (%s): %s", $dscfn, $!;
     $dscdata = do { local $/ = undef; <D>; };
-    D->error and fail "read $dscfn: $!";
+    D->error and fail f_ "read %s: %s", $dscfn, $!;
     close C;
 
     # we don't normally need this so import it here
@@ -6580,13 +6861,13 @@ sub cmd_import_dsc {
        local $SIG{__WARN__} = sub {
            print STDERR $_[0];
            return unless $needsig;
-           fail "import-dsc signature check failed";
+           fail __ "import-dsc signature check failed";
        };
        if (!$dp->is_signed()) {
-           warn "$us: warning: importing unsigned .dsc\n";
+           warn f_ "%s: warning: importing unsigned .dsc\n", $us;
        } else {
            my $r = $dp->check_signature();
-           die "->check_signature => $r" if $needsig && $r;
+           confess "->check_signature => $r" if $needsig && $r;
        }
     }
 
@@ -6594,7 +6875,7 @@ sub cmd_import_dsc {
 
     $package = getfield $dsc, 'Source';
 
-    parse_dsc_field($dsc, "Dgit metadata in .dsc")
+    parse_dsc_field($dsc, __ "Dgit metadata in .dsc")
        unless forceing [qw(import-dsc-with-dgit-field)];
     parse_dsc_field_def_dsc_distro();
 
@@ -6604,7 +6885,8 @@ sub cmd_import_dsc {
     notpushing();
 
     if (defined $dsc_hash) {
-       progress "dgit: import-dsc of .dsc with Dgit field, using git hash";
+       progress __
+           "dgit: import-dsc of .dsc with Dgit field, using git hash";
        resolve_dsc_field_commit undef, undef;
     }
     if (defined $dsc_hash) {
@@ -6612,29 +6894,29 @@ sub cmd_import_dsc {
                   "echo $dsc_hash | git cat-file --batch-check");
        my $objgot = cmdoutput @cmd;
        if ($objgot =~ m#^\w+ missing\b#) {
-           fail <<END
-.dsc contains Dgit field referring to object $dsc_hash
+           fail f_ <<END, $dsc_hash
+.dsc contains Dgit field referring to object %s
 Your git tree does not have that object.  Try `git fetch' from a
-plausible server (browse.dgit.d.o? alioth?), and try the import-dsc again.
+plausible server (browse.dgit.d.o? salsa?), and try the import-dsc again.
 END
        }
        if ($oldhash && !is_fast_fwd $oldhash, $dsc_hash) {
            if ($force > 0) {
-               progress "Not fast forward, forced update.";
+               progress __ "Not fast forward, forced update.";
            } else {
-               fail "Not fast forward to $dsc_hash";
+               fail f_ "Not fast forward to %s", $dsc_hash;
            }
        }
        import_dsc_result $dstbranch, $dsc_hash,
            "dgit import-dsc (Dgit): $info",
-           "updated git ref $dstbranch";
+           f_ "updated git ref %s", $dstbranch;
        return 0;
     }
 
-    fail <<END
-Branch $dstbranch already exists
-Specify ..$specbranch for a pseudo-merge, binding in existing history
-Specify  +$specbranch to overwrite, discarding existing history
+    fail f_ <<END, $dstbranch, $specbranch, $specbranch
+Branch %s already exists
+Specify ..%s for a pseudo-merge, binding in existing history
+Specify  +%s to overwrite, discarding existing history
 END
        if $oldhash && !$force;
 
@@ -6644,24 +6926,29 @@ END
        my $here = "$buildproductsdir/$f";
        if (lstat $here) {
            next if stat $here;
-           fail "lstat $here works but stat gives $! !";
+           fail f_ "lstat %s works but stat gives %s !", $here, $!;
        }
-       fail "stat $here: $!" unless $! == ENOENT;
+       fail f_ "stat %s: %s", $here, $! unless $! == ENOENT;
        my $there = $dscfn;
        if ($dscfn =~ m#^(?:\./+)?\.\./+#) {
            $there = $';
        } elsif ($dscfn =~ m#^/#) {
            $there = $dscfn;
        } else {
-           fail "cannot import $dscfn which seems to be inside working tree!";
+           fail f_
+               "cannot import %s which seems to be inside working tree!",
+               $dscfn;
        }
-       $there =~ s#/+[^/]+$## or
-           fail "import $dscfn requires ../$f, but it does not exist";
+       $there =~ s#/+[^/]+$## or fail f_
+           "import %s requires .../%s, but it does not exist",
+           $dscfn, $f;
        $there .= "/$f";
        my $test = $there =~ m{^/} ? $there : "../$there";
-       stat $test or fail "import $dscfn requires $test, but: $!";
-       symlink $there, $here or fail "symlink $there to $here: $!";
-       progress "made symlink $here -> $there";
+       stat $test or fail f_
+           "import %s requires %s, but: %s", $dscfn, $test, $!;
+       symlink $there, $here or fail f_
+           "symlink %s to %s: %s", $there, $here, $!;
+       progress f_ "made symlink %s -> %s", $here, $there;
 #      print STDERR Dumper($fi);
     }
     my @mergeinputs = generate_commits_from_dsc();
@@ -6671,21 +6958,24 @@ END
 
     if ($oldhash) {
        if ($force > 0) {
-           progress "Import, forced update - synthetic orphan git history.";
+           progress __
+               "Import, forced update - synthetic orphan git history.";
        } elsif ($force < 0) {
-           progress "Import, merging.";
+           progress __ "Import, merging.";
            my $tree = cmdoutput @git, qw(rev-parse), "$newhash:";
            my $version = getfield $dsc, 'Version';
            my $clogp = commit_getclogp $newhash;
            my $authline = clogp_authline $clogp;
-           $newhash = make_commit_text <<END;
+           $newhash = make_commit_text <<ENDU
 tree $tree
 parent $newhash
 parent $oldhash
 author $authline
 committer $authline
 
-Merge $package ($version) import into $dstbranch
+ENDU
+               .(f_ <<END, $package, $version, $dstbranch);
+Merge %s (%s) import into %s
 END
        } else {
            die; # caught earlier
@@ -6694,20 +6984,20 @@ END
 
     import_dsc_result $dstbranch, $newhash,
        "dgit import-dsc: $info",
-       "results are in in git ref $dstbranch";
+       f_ "results are in git ref %s", $dstbranch;
 }
 
 sub pre_archive_api_query () {
     not_necessarily_a_tree();
 }
 sub cmd_archive_api_query {
-    badusage "need only 1 subpath argument" unless @ARGV==1;
+    badusage __ "need only 1 subpath argument" unless @ARGV==1;
     my ($subpath) = @ARGV;
     local $isuite = 'DGIT-API-QUERY-CMD';
     my @cmd = archive_api_query_cmd($subpath);
     push @cmd, qw(-f);
     debugcmd ">",@cmd;
-    exec @cmd or fail "exec curl: $!\n";
+    exec @cmd or fail f_ "exec curl: %s\n", $!;
 }
 
 sub repos_server_url () {
@@ -6721,53 +7011,56 @@ sub pre_clone_dgit_repos_server () {
     not_necessarily_a_tree();
 }
 sub cmd_clone_dgit_repos_server {
-    badusage "need destination argument" unless @ARGV==1;
+    badusage __ "need destination argument" unless @ARGV==1;
     my ($destdir) = @ARGV;
     my $url = repos_server_url();
     my @cmd = (@git, qw(clone), $url, $destdir);
     debugcmd ">",@cmd;
-    exec @cmd or fail "exec git clone: $!\n";
+    exec @cmd or fail f_ "exec git clone: %s\n", $!;
 }
 
 sub pre_print_dgit_repos_server_source_url () {
     not_necessarily_a_tree();
 }
 sub cmd_print_dgit_repos_server_source_url {
-    badusage "no arguments allowed to dgit print-dgit-repos-server-source-url"
+    badusage __
+       "no arguments allowed to dgit print-dgit-repos-server-source-url"
        if @ARGV;
     my $url = repos_server_url();
-    print $url, "\n" or die $!;
+    print $url, "\n" or confess $!;
 }
 
 sub pre_print_dpkg_source_ignores {
     not_necessarily_a_tree();
 }
 sub cmd_print_dpkg_source_ignores {
-    badusage "no arguments allowed to dgit print-dpkg-source-ignores"
+    badusage __
+       "no arguments allowed to dgit print-dpkg-source-ignores"
        if @ARGV;
-    print "@dpkg_source_ignores\n" or die $!;
+    print "@dpkg_source_ignores\n" or confess $!;
 }
 
 sub cmd_setup_mergechangelogs {
-    badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
+    badusage __ "no arguments allowed to dgit setup-mergechangelogs"
+       if @ARGV;
     local $isuite = 'DGIT-SETUP-TREE';
     setup_mergechangelogs(1);
 }
 
 sub cmd_setup_useremail {
-    badusage "no arguments allowed to dgit setup-useremail" if @ARGV;
+    badusage __ "no arguments allowed to dgit setup-useremail" if @ARGV;
     local $isuite = 'DGIT-SETUP-TREE';
     setup_useremail(1);
 }
 
 sub cmd_setup_gitattributes {
-    badusage "no arguments allowed to dgit setup-useremail" if @ARGV;
+    badusage __ "no arguments allowed to dgit setup-useremail" if @ARGV;
     local $isuite = 'DGIT-SETUP-TREE';
     setup_gitattrs(1);
 }
 
 sub cmd_setup_new_tree {
-    badusage "no arguments allowed to dgit setup-tree" if @ARGV;
+    badusage __ "no arguments allowed to dgit setup-tree" if @ARGV;
     local $isuite = 'DGIT-SETUP-TREE';
     setup_new_tree();
 }
@@ -6775,7 +7068,7 @@ sub cmd_setup_new_tree {
 #---------- argument parsing and main program ----------
 
 sub cmd_version {
-    print "dgit version $our_version\n" or die $!;
+    print "dgit version $our_version\n" or confess $!;
     finish 0;
 }
 
@@ -6814,8 +7107,8 @@ defvalopt '', '-C', '.+', sub {
 defvalopt '--initiator-tempdir','','.*', sub {
     ($initiator_tempdir) = (@_);
     $initiator_tempdir =~ m#^/# or
-       badusage "--initiator-tempdir must be used specify an".
-       " absolute, not relative, directory."
+       badusage __ "--initiator-tempdir must be used specify an".
+                   " absolute, not relative, directory."
 };
 
 sub defoptmodes ($@) {
@@ -6853,11 +7146,11 @@ sub parseopts () {
        my ($what) = @_;
        @rvalopts = ($_);
        if (!defined $val) {
-           badusage "$what needs a value" unless @ARGV;
+           badusage f_ "%s needs a value", $what unless @ARGV;
            $val = shift @ARGV;
            push @rvalopts, $val;
        }
-       badusage "bad value \`$val' for $what" unless
+       badusage f_ "bad value \`%s' for %s", $val, $what unless
            $val =~ m/^$oi->{Re}$(?!\n)/s;
        my $how = $oi->{How};
        if (ref($how) eq 'SCALAR') {
@@ -6923,10 +7216,13 @@ sub parseopts () {
            } elsif (m/^--delayed=(\d+)$/s) {
                push @ropts, $_;
                push @dput, $_;
-           } elsif (m/^--dgit-view-save=(.+)$/s) {
+           } elsif (my ($k,$v) =
+                    m/^--save-(dgit-view)=(.+)$/s ||
+                    m/^--(dgit-view)-save=(.+)$/s
+                    ) {
                push @ropts, $_;
-               $split_brain_save = $1;
-               $split_brain_save =~ s#^(?!refs/)#refs/heads/#;
+               $v =~ s#^(?!refs/)#refs/heads/#;
+               $internal_object_save{$k} = $v;
            } elsif (m/^--(no-)?rm-old-changes$/s) {
                push @ropts, $_;
                $rmchanges = !$1;
@@ -6939,17 +7235,14 @@ sub parseopts () {
                $_='';
            } elsif (m/^--force-/) {
                print STDERR
-                   "$us: warning: ignoring unknown force option $_\n";
+                   f_ "%s: warning: ignoring unknown force option %s\n",
+                      $us, $_;
                $_='';
            } elsif (m/^--dgit-tag-format=(old|new)$/s) {
                # undocumented, for testing
                push @ropts, $_;
                $tagformat_want = [ $1, 'command line', 1 ];
                # 1 menas overrides distro configuration
-           } elsif (m/^--always-split-source-build$/s) {
-               # undocumented, was once for testing, now a no-op
-               push @ropts, $_;
-               $need_split_build_invocation = 1;
            } elsif (m/^--config-lookup-explode=(.+)$/s) {
                # undocumented, for testing
                push @ropts, $_;
@@ -6962,7 +7255,7 @@ sub parseopts () {
                push @ropts, $_;
                $funcopts_long{$_}();
            } else {
-               badusage "unknown long option \`$_'";
+               badusage f_ "unknown long option \`%s'", $_;
            }
        } else {
            while (m/^-./s) {
@@ -6994,15 +7287,18 @@ sub parseopts () {
                } elsif (s/^-wgf$//s) {
                    push @ropts, $&;
                    $cleanmode = 'git-ff';
-               } elsif (s/^-wd$//s) {
+               } elsif (s/^-wd(d?)([na]?)$//s) {
                    push @ropts, $&;
                    $cleanmode = 'dpkg-source';
-               } elsif (s/^-wdd$//s) {
-                   push @ropts, $&;
-                   $cleanmode = 'dpkg-source-d';
+                   $cleanmode .= '-d' if $1;
+                   $cleanmode .= ',no-check' if $2 eq 'n';
+                   $cleanmode .= ',all-check' if $2 eq 'a';
                } elsif (s/^-wc$//s) {
                    push @ropts, $&;
                    $cleanmode = 'check';
+               } elsif (s/^-wci$//s) {
+                   push @ropts, $&;
+                   $cleanmode = 'check,ignores';
                } elsif (s/^-c([^=]*)\=(.*)$//s) {
                    push @git, '-c', $&;
                    $gitcfgs{cmdline}{$1} = [ $2 ];
@@ -7015,7 +7311,7 @@ sub parseopts () {
                    $valopt->($oi->{Short});
                    $_ = '';
                } else {
-                   badusage "unknown short option \`$_'";
+                   badusage f_ "unknown short option \`%s'", $_;
                }
            }
        }
@@ -7024,23 +7320,24 @@ sub parseopts () {
 
 sub check_env_sanity () {
     my $blocked = new POSIX::SigSet;
-    sigprocmask SIG_UNBLOCK, $blocked, $blocked or die $!;
+    sigprocmask SIG_UNBLOCK, $blocked, $blocked or confess $!;
 
     eval {
        foreach my $name (qw(PIPE CHLD)) {
            my $signame = "SIG$name";
            my $signum = eval "POSIX::$signame" // die;
-           ($SIG{$name} // 'DEFAULT') eq 'DEFAULT' or
-               die "$signame is set to something other than SIG_DFL\n";
+           die f_ "%s is set to something other than SIG_DFL\n",
+               $signame
+               if defined $SIG{$name} and $SIG{$name} ne 'DEFAULT';
            $blocked->ismember($signum) and
-               die "$signame is blocked\n";
+               die f_ "%s is blocked\n", $signame;
        }
     };
     return unless $@;
     chomp $@;
-    fail <<END;
-On entry to dgit, $@
-This is a bug produced by something in in your execution environment.
+    fail f_ <<END, $@;
+On entry to dgit, %s
+This is a bug produced by something in your execution environment.
 Giving up.
 END
 }
@@ -7056,7 +7353,7 @@ sub parseopts_late_defaults () {
 
        my $v = access_cfg("cmd-$k", 'RETURN-UNDEF');
        if (defined $v) {
-           badcfg "cannot set command for $k"
+           badcfg f_ "cannot set command for %s", $k
                unless length $om->[0];
            $om->[0] = $v;
        }
@@ -7069,7 +7366,7 @@ sub parseopts_late_defaults () {
            printdebug "CL $c ", (join " ", map { shellquote } @vl),
                "\n" if $debuglevel >= 4;
            next unless @vl;
-           badcfg "cannot configure options for $k"
+           badcfg f_ "cannot configure options for %s", $k
                if $opts_opt_cmdonly{$k};
            my $insertpos = $opts_cfg_insertpos{$k};
            @$om = ( @$om[0..$insertpos-1],
@@ -7089,7 +7386,7 @@ sub parseopts_late_defaults () {
            // access_cfg('quilt-mode', 'RETURN-UNDEF')
            // 'linear';
        $quilt_mode =~ m/^($quilt_modes_re)$/ 
-           or badcfg "unknown quilt-mode \`$quilt_mode'";
+           or badcfg f_ "unknown quilt-mode \`%s'", $quilt_mode;
        $quilt_mode = $1;
     }
 
@@ -7099,22 +7396,24 @@ sub parseopts_late_defaults () {
        next if defined $$vr;
        $$vr = access_cfg($moc->{Key}, 'RETURN-UNDEF') // $moc->{Default};
        my $v = $moc->{Vals}{$$vr};
-       badcfg "unknown $moc->{Key} setting \`$$vr'" unless defined $v;
+       badcfg f_ "unknown %s setting \`%s'", $moc->{Key}, $$vr
+           unless defined $v;
        $$vr = $v;
     }
 
-    $need_split_build_invocation ||= quiltmode_splitbrain();
-
-    fail "dgit: --include-dirty is not supported in split view quilt mode"
+    fail __ "dgit: --include-dirty is not supported in split view quilt mode"
        if $split_brain && $includedirty;
 
     if (!defined $cleanmode) {
        local $access_forpush;
-       $cleanmode = access_cfg('clean-mode', 'RETURN-UNDEF');
+       $cleanmode = access_cfg('clean-mode-newer', 'RETURN-UNDEF');
+       $cleanmode = undef if $cleanmode && $cleanmode !~ m/^$cleanmode_re$/;
+
+       $cleanmode //= access_cfg('clean-mode', 'RETURN-UNDEF');
        $cleanmode //= 'dpkg-source';
 
-       badcfg "unknown clean-mode \`$cleanmode'" unless
-           $cleanmode =~ m/^($cleanmode_re)$(?!\n)/s;
+       badcfg f_ "unknown clean-mode \`%s'", $cleanmode unless
+           $cleanmode =~ m/$cleanmode_re/;
     }
 
     $buildproductsdir //= access_cfg('build-products-dir', 'RETURN-UNDEF');
@@ -7123,6 +7422,9 @@ sub parseopts_late_defaults () {
     $bpd_glob =~ s#[][\\{}*?~]#\\$&#g;
 }
 
+setlocale(LC_MESSAGES, "");
+textdomain("dgit");
+
 if ($ENV{$fakeeditorenv}) {
     git_slurp_config();
     quilt_fixup_editor();
@@ -7131,11 +7433,11 @@ if ($ENV{$fakeeditorenv}) {
 parseopts();
 check_env_sanity();
 
-print STDERR "DRY RUN ONLY\n" if $dryrun_level > 1;
-print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n"
+print STDERR __ "DRY RUN ONLY\n" if $dryrun_level > 1;
+print STDERR __ "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n"
     if $dryrun_level == 1;
 if (!@ARGV) {
-    print STDERR $helpmsg or die $!;
+    print STDERR __ $helpmsg or confess $!;
     finish 8;
 }
 $cmd = $subcommand = shift @ARGV;
@@ -7144,11 +7446,14 @@ $cmd =~ y/-/_/;
 my $pre_fn = ${*::}{"pre_$cmd"};
 $pre_fn->() if $pre_fn;
 
-record_maindir if $invoked_in_git_tree;
+if ($invoked_in_git_tree) {
+    changedir_git_toplevel();
+    record_maindir();
+}
 git_slurp_config();
 
 my $fn = ${*::}{"cmd_$cmd"};
-$fn or badusage "unknown operation $cmd";
+$fn or badusage f_ "unknown operation %s", $cmd;
 $fn->();
 
 finish 0;