chiark / gitweb /
dgit: Refactor and simplify `quilt fixup cannot be linear' generation
[dgit.git] / dgit
diff --git a/dgit b/dgit
index 42813f1f8f026c00ef79e463a4fda1401e18a822..2ac6d51c6506af9980dc418c767e7cfe4fa0720c 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -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;
@@ -113,7 +115,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);
@@ -230,24 +232,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;
 }
 
@@ -289,6 +296,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) =
@@ -300,21 +315,91 @@ 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 ($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:
@@ -387,28 +472,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;
@@ -416,7 +501,7 @@ sub protocol_expect (&$) {
        my $r = &$match;
        return $r if $r;
     }
-    badproto $fh, "\`$_'";
+    badproto $fh, f_ "\`%s'", $_;
 }
 
 sub protocol_send_file ($$) {
@@ -437,10 +522,10 @@ sub protocol_send_file ($$) {
 
 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;
 }
 
@@ -521,7 +606,8 @@ sub url_get {
     progress "downloading $what...";
     my $r = $ua->get(@_) or die $!;
     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');
 }
 
@@ -532,9 +618,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)", "@_";
     }
 }
 
@@ -558,7 +644,7 @@ 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]
@@ -577,17 +663,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 die $!;
     finish 8;
 }
 
 sub nextarg {
-    @ARGV or badusage "too few arguments";
+    @ARGV or badusage __ "too few arguments";
     return scalar shift @ARGV;
 }
 
@@ -595,7 +681,7 @@ sub pre_help () {
     not_necessarily_a_tree();
 }
 sub cmd_help () {
-    print $helpmsg or die $!;
+    print __ $helpmsg or die $!;
     finish 0;
 }
 
@@ -697,8 +783,9 @@ 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;
        return $l->[0];
     }
     return undef;
@@ -716,8 +803,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 () {
@@ -756,7 +845,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;
 }
 
@@ -770,7 +860,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);
        }
@@ -786,7 +876,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 () {
@@ -804,7 +895,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 () {
@@ -813,12 +905,12 @@ sub access_forpush () {
 }
 
 sub pushing () {
-    confess 'internal error '.Dumper($access_forpush)," ?" if
+    confess +(__ 'internal error').' '.Dumper($access_forpush)," ?" if
        defined $access_forpush and !$access_forpush;
-    badcfg "pushing but distro is configured readonly"
+    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
@@ -984,7 +1076,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'";
@@ -1030,8 +1122,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
@@ -1050,7 +1143,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];
@@ -1058,11 +1151,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);
 }
@@ -1078,15 +1171,17 @@ sub canonicalise_suite_ftpmasterapi {
        } qw(codename name);
        push @matched, $entry;
     }
-    fail "unknown suite $isuite, maybe -d would help" 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;
 }
@@ -1100,18 +1195,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;
@@ -1150,15 +1245,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";
@@ -1173,7 +1268,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;
@@ -1189,7 +1284,7 @@ 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,
@@ -1241,13 +1336,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;
@@ -1263,8 +1358,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
@@ -1291,8 +1387,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;
 
@@ -1306,6 +1403,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 @_; }
@@ -1394,10 +1492,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];
 }
 
@@ -1405,6 +1504,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) = @_;
@@ -1483,6 +1583,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) = @_;
@@ -1528,6 +1629,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');
@@ -1582,12 +1684,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;
     }
 }
 
@@ -1607,13 +1709,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";
@@ -1640,7 +1742,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]$/;
@@ -1656,7 +1759,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') {
@@ -1671,7 +1774,7 @@ sub check_for_git () {
     } elsif ($how eq 'false') {
        return 0;
     } else {
-       badcfg "unknown git-check \`$how'";
+       badcfg f_ "unknown git-check \`%s'", $how;
     }
 }
 
@@ -1686,7 +1789,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;
     }
 }
 
@@ -1723,8 +1826,8 @@ sub remove_stray_gits ($) {
        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 $_;
        }
     }
@@ -1736,7 +1839,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;
@@ -1769,7 +1872,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,
@@ -1780,8 +1883,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 () {
@@ -1825,8 +1928,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;
                }
@@ -1834,17 +1938,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 ($$) {
@@ -1883,7 +1988,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;
         }
     }
@@ -1896,7 +2001,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";
@@ -1908,7 +2013,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;
@@ -1925,25 +2030,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
@@ -1961,7 +2068,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";
@@ -1970,7 +2077,7 @@ END
     }
     if (%changed) {
        foreach my $file (keys %changed) {
-           progress sprintf
+           progress f_
                "edited .changes for archive .orig contents: %s %s",
                $changed{$file}, $file;
        }
@@ -1979,10 +2086,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;
     }
 }
 
@@ -2005,8 +2113,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;
 }
@@ -2019,14 +2128,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
@@ -2039,8 +2148,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;
@@ -2069,11 +2179,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 () {
@@ -2094,12 +2204,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";
        }
@@ -2114,14 +2224,14 @@ sub generate_commits_from_dsc () {
            printdebug "linked.\n";
        } 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") {
            printdebug "linked (using ...,fetch).\n";
        } elsif ((printdebug "($!) "),
                 $! != EEXIST) {
-           fail "saving $buildproductsdir/$f,fetch: $!";
+           fail f_ "saving %s: %s", "$buildproductsdir/$f,fetch", $!;
        } else {
            printdebug "cannot.\n";
        }
@@ -2187,7 +2297,7 @@ sub generate_commits_from_dsc () {
            chdir "_unpack-tar" or die $!;
            open STDIN, "<&", $input or die $!;
            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 $!;
        !$? or failedcmd @tarcmd;
@@ -2251,7 +2361,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();
     }
@@ -2269,7 +2379,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);
 
@@ -2309,7 +2419,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';
@@ -2328,12 +2438,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
@@ -2341,7 +2452,7 @@ tree $tt->{Tree}
 author $authline
 committer $authline
 
-Import $tt->{F}
+$mbody
 
 [dgit import tarball $package $cversion $tt->{F}]
 END_T
@@ -2408,7 +2519,7 @@ 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 $!;
@@ -2427,19 +2538,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;
            };
@@ -2451,11 +2562,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);
 
@@ -2466,16 +2577,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 die $!;
             @output = $lastpush_mergeinput;
         } else {
            # Same version.  Use what's in the server git branch,
@@ -2512,15 +2625,15 @@ sub complete_file_from_dsc ($$;$) {
 
     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 {
@@ -2536,9 +2649,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;
 }
@@ -2631,7 +2744,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;
@@ -2645,8 +2758,8 @@ sub git_lrfetch_sane {
            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;
            }
@@ -2689,8 +2802,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;
@@ -2704,14 +2817,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...
@@ -2777,8 +2890,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;
        }
     });
 }
@@ -2826,20 +2939,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;
     }
 }
 
@@ -2861,18 +2974,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 =
@@ -2881,10 +2993,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;
@@ -2902,30 +3014,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) {
@@ -2949,11 +3061,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
@@ -3036,19 +3148,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();
@@ -3075,23 +3187,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 die $!;
            @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);
            }
        }
@@ -3111,18 +3224,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 die $!;
     } else {
        printdebug "nothing found!\n";
        if (defined $skew_warning_vsn) {
-           print STDERR <<END or die $!;
+           print STDERR f_ <<END, $skew_warning_vsn or die $!;
 
 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
@@ -3164,10 +3278,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
@@ -3207,8 +3318,8 @@ END
        if (defined $compat_info->{Message}) {
            print MC $compat_info->{Message} or die $!;
        } else {
-           print MC <<END or die $!;
-Record $package ($cversion) in archive suite $csuite
+           print MC f_ <<END, $package, $cversion, $csuite or die $!;
+Record %s (%s) in archive suite %s
 
 Record that
 END
@@ -3220,7 +3331,7 @@ END
            };
 
            $message_add_info->($mergeinputs[0]);
-           print MC <<END or die $!;
+           print MC __ <<END or die $!;
 should be treated as descended from
 END
            $message_add_info->($_) foreach @mergeinputs[1..$#mergeinputs];
@@ -3236,12 +3347,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);
 
@@ -3251,11 +3362,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 die $!;
 
 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
        }
@@ -3267,7 +3378,7 @@ END
 
     lrfetchref_used lrfetchref();
 
-    check_gitattrs($hash, "fetched source tree");
+    check_gitattrs($hash, __ "fetched source tree");
 
     unshift @end, $del_lrfetchrefs;
     return $hash;
@@ -3303,7 +3414,7 @@ sub setup_mergechangelogs (;$) {
     print NATTRS "debian/changelog merge=$driver\n" or die $!;
     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: $!";
@@ -3367,7 +3478,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
@@ -3378,11 +3489,12 @@ END
     ensuredir "$maindir_gitcommon/info";
 
     open GAO, "> $af.new" or die $!;
-    print GAO <<END or die $! unless defined $already;
+    print GAO <<END, __ <<ENDT or die $! 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>) {
@@ -3396,7 +3508,7 @@ END
        $gai->error and die $!;
     }
     close GAO or die $!;
-    rename "$af.new", "$af" or die "install $af: $!";
+    rename "$af.new", "$af" or fail f_ "install %s: %s", $af, $!;
 }
 
 sub setup_new_tree () {
@@ -3422,8 +3534,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;
@@ -3445,7 +3557,7 @@ sub multisuite_suite_child ($$$) {
        $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 $!;
@@ -3453,7 +3565,8 @@ sub multisuite_suite_child ($$$) {
        return undef;
     }
     waitpid $pid,0 == $pid or die $!;
-    fail "failed to obtain $tsuite: ".waitstatusmsg() if $? && $?!=256*4;
+    fail f_ "failed to obtain %s: %s", $tsuite, waitstatusmsg()
+       if $? && $?!=256*4;
     seek $canonsuitefh,0,0 or die $!;
     local $csuite = <$canonsuitefh>;
     die $! unless defined $csuite && chomp $csuite;
@@ -3491,7 +3604,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);
@@ -3523,9 +3636,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!"),
         };
     }
 
@@ -3564,8 +3677,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";
@@ -3577,9 +3691,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".
@@ -3591,7 +3706,7 @@ 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;
 }
@@ -3609,7 +3724,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 ($) {
@@ -3617,7 +3732,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";
@@ -3634,7 +3749,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();
@@ -3645,11 +3760,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'};
@@ -3674,13 +3789,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 () {
@@ -3691,15 +3806,16 @@ 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";
+           fail f_ "git tree contains debian/source/%s", $f;
        }
     }
 
@@ -3717,9 +3833,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;
     }
 }
 
@@ -3734,15 +3851,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
 }
@@ -3783,12 +3901,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;
 }
@@ -3796,14 +3914,15 @@ sub madformat_wantfixup ($) {
 sub maybe_split_brain_save ($$$) {
     my ($headref, $dgitview, $msg) = @_;
     # => message fragment "$saved" describing disposition of $dgitview
+    #    (used inside parens, in the English texts)
     my $save = $internal_object_save{'dgit-view'};
-    return "commit id $dgitview" unless defined $save;
+    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",
               $save, $dgitview);
     runcmd @cmd;
-    return "and left in $save";
+    return f_ "and left in %s", $save;
 }
 
 # An "infopair" is a tuple [ $thing, $what ]
@@ -3828,17 +3947,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
 };
 
@@ -3847,7 +3968,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,
@@ -3855,7 +3976,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");
@@ -3863,7 +3985,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);
@@ -3872,12 +3995,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/;
        }
     }
     
@@ -3888,7 +4012,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 inciudes all changes in %s...",
+                $i_arch_v->[0];
 
     my $tree = cmdoutput qw(git rev-parse), "${dgitview}:";
     my $authline = clogp_authline $clogp;
@@ -3943,7 +4068,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 inciudes all changes in archive...";
     }
 
     return $dgitview if is_fast_fwd $archive_hash, $dgitview;
@@ -3951,10 +4076,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";
 
@@ -3964,25 +4090,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;
 }      
 
@@ -3995,7 +4121,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,
@@ -4003,7 +4129,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;
 }
 
@@ -4015,7 +4141,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';
 
@@ -4036,8 +4163,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 ($$$$) {
@@ -4096,8 +4224,8 @@ sub push_mktags ($$ $$ $) {
     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';
@@ -4123,8 +4251,11 @@ tagger $authline
 
 END
        if ($tw->{View} eq 'dgit') {
+           print TO f_ <<ENDT, $package, $cversion, $clogsuite, $csuite
+%s release %s for %s (%s) [dgit]
+ENDT
+               or die $!;
            print TO <<END or die $!;
-$package release $cversion for $clogsuite ($csuite) [dgit]
 [dgit distro=$declaredistro$delibs]
 END
            foreach my $ref (sort keys %previously) {
@@ -4133,12 +4264,14 @@ END
 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 die $!;
        } else {
-           die Dumper($tw)."?";
+           confess Dumper($tw)."?";
        }
 
        close TO or die $!;
@@ -4183,7 +4316,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
@@ -4193,11 +4326,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
@@ -4221,8 +4354,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);
 
@@ -4235,6 +4368,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');
     }
@@ -4252,9 +4394,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,
@@ -4283,7 +4426,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".
@@ -4293,7 +4436,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");
@@ -4326,22 +4469,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;
        }
@@ -4349,9 +4497,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 {
@@ -4370,21 +4519,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 distroy 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 distroy 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
@@ -4426,7 +4577,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
@@ -4439,7 +4590,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
@@ -4456,7 +4607,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
@@ -4473,12 +4624,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) {
@@ -4491,22 +4641,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");
@@ -4518,7 +4668,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;
@@ -4529,13 +4679,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;
@@ -4545,15 +4695,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, $!;
            }
        };
     }
@@ -4590,7 +4741,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();
 }
@@ -4606,8 +4757,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();
@@ -4616,7 +4767,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();
 
@@ -4678,17 +4829,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;
     }
 }
@@ -4703,7 +4854,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
@@ -4714,8 +4865,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;
     }
 }
 
@@ -4751,9 +4903,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;
@@ -4830,7 +4982,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();
@@ -4867,10 +5020,10 @@ sub i_resp_complete {
     printdebug "waiting for build host child $pid...\n";
     my $got = waitpid $pid, 0;
     die $! unless $got == $pid;
-    die "build host child failed $?" if $?;
+    fail f_ "build host child failed: %s", waitstatusmsg() if $?;
 
     i_cleanup();
-    printdebug "all done\n";
+    printdebug __ "all done\n";
     finish 0;
 }
 
@@ -4879,7 +5032,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;
 }
@@ -4887,15 +5040,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;
 }
 
@@ -4912,8 +5065,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;
@@ -4956,10 +5110,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;
@@ -5000,7 +5154,7 @@ sub i_want_signed_tag {
 
     return
        push_mktags $i_clogp, $i_dscfn,
-           $i_changesfn, 'remote changes',
+           $i_changesfn, (__ 'remote changes file'),
            \@tagwants;
 }
 
@@ -5026,7 +5180,7 @@ 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 $!;
 From: $author
@@ -5075,21 +5229,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$/;
                }
@@ -5119,7 +5273,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;
     }
@@ -5148,32 +5302,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);
@@ -5181,8 +5337,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
@@ -5190,23 +5346,27 @@ 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;
@@ -5218,11 +5378,12 @@ END
        print SERIES "auto-gitignore\n" or die $!;
        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';
@@ -5358,23 +5519,25 @@ 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";
+           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 "$us:  $cprange: $notp->{Whynot}\n";
+               $all_gdr &&= $notp->{Child} &&
+                   (git_cat_file $notp->{Child}{Commit}, 'commit')
+                   =~ m{^\[git-debrebase(?! split[: ]).*\]$}m;
            }
-           print STDERR "$us: $_\n" foreach @$failsuggestion;
+           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".
- "Use dpkg-source --commit by hand; or, --quilt=smash for one ugly patch";
+ "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...";
@@ -5432,7 +5595,7 @@ sub quiltify ($$$$) {
                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 "too long\n" if length > 200;
            };
            return $_ unless $@;
            print STDERR "quiltifying commit $cc:".
@@ -5528,7 +5691,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,11 +5763,12 @@ 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 ($$$) {
@@ -5915,12 +6079,21 @@ END
 
     my @failsuggestion;
     if (!($diffbits->{O2H} & $diffbits->{O2A})) {
-        push @failsuggestion, "This might be a patches-unapplied branch.";
+        push @failsuggestion, [ 'unapplied',
+                              "This might be a patches-unapplied branch." ];
     } elsif (!($diffbits->{H2A} & $diffbits->{O2A})) {
-        push @failsuggestion, "This might be a patches-applied branch.";
+        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,
@@ -6127,16 +6300,27 @@ 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;
     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 ?";
+    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 :
+         die "$dmode ?";
+    }
     printdebug "massage done $r $dmode.\n";
     push @$cmd, $dmode;
 #print STDERR "MASS2 ",Dumper($cmd, $xargs, $r);
@@ -6225,9 +6409,10 @@ sub postbuild_mergechanges_vanilla ($) {
 sub cmd_build {
     build_prep_early();
     $buildproductsdir eq '..' or print STDERR <<END;
-$us: warning: build-products-dir set, but not supported by dgit build
-$us: warning: things may go wrong or files may go to the wrong place
+$us: warning: build-products-dir set, but not supported by dpkg-buildpackage
+$us: 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);
@@ -7079,7 +7264,7 @@ sub parseopts_late_defaults () {
        $$vr = $v;
     }
 
-    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) {
@@ -7097,6 +7282,9 @@ sub parseopts_late_defaults () {
     $bpd_glob =~ s#[][\\{}*?~]#\\$&#g;
 }
 
+setlocale(LC_MESSAGES, "");
+textdomain("dgit");
+
 if ($ENV{$fakeeditorenv}) {
     git_slurp_config();
     quilt_fixup_editor();
@@ -7109,7 +7297,7 @@ 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 die $!;
     finish 8;
 }
 $cmd = $subcommand = shift @ARGV;