chiark / gitweb /
dgit: Use a regexp to match clean mode check
[dgit.git] / dgit
diff --git a/dgit b/dgit
index 8ce43899bc1e74b9a8c8216cbc8b8b673d00d921..2d7b8d4a7cb987ed207fd584e1bd9c54e7587bc1 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -101,7 +101,7 @@ our %forceopts = map { $_=>0 }
 our %format_ok = map { $_=>1 } ("1.0","3.0 (native)","3.0 (quilt)");
 
 our $suite_re = '[-+.0-9a-z]+';
-our $cleanmode_re = 'dpkg-source(?:-d)?|git|git-ff|check|none';
+our $cleanmode_re = qr{(?:dpkg-source(?:-d)?|git|git-ff|check|none)};
 
 our $git_authline_re = '^([^<>]+) \<(\S+)\> (\d+ [-+]\d+)$';
 our $splitbraincache = 'dgit-intern/quilt-cache';
@@ -379,6 +379,10 @@ sub branch_is_gdr ($) {
            printdebug "branch_is_gdr  $walk ?-octopus NO\n";
            return 0;
        }
+       if (!@parents) {
+           printdebug "branch_is_gdr  $walk origin\n";
+           return 0;
+       }
        if ($get_patches->($walk) ne $tip_patches) {
            # Our parent added, removed, or edited patches, and wasn't
            # a gdr make-patches commit.  gdr make-patches probably
@@ -786,6 +790,9 @@ sub git_get_config ($) {
        @$l==1 or badcfg
            f_ "multiple values for %s (in %s git config)", $c, $src
            if @$l > 1;
+       $l->[0] =~ m/\n/ and badcfg f_
+ "value for config option %s (in %s git config) contains newline(s)!",
+            $c, $src;
        return $l->[0];
     }
     return undef;
@@ -2220,18 +2227,18 @@ sub generate_commits_from_dsc () {
 
        printdebug "considering saving $f: ";
 
-       if (link $f, $upper_f) {
+       if (rename_link_xf 1, $f, $upper_f) {
            printdebug "linked.\n";
-       } elsif ((printdebug "($!) "),
+       } elsif ((printdebug "($@) "),
                 $! != EEXIST) {
-           fail f_ "saving %s: %s", "$buildproductsdir/$f", $!;
+           fail f_ "saving %s: %s", "$buildproductsdir/$f", $@;
        } elsif (!$refetched) {
            printdebug "no need.\n";
-       } elsif (link $f, "$upper_f,fetch") {
+       } elsif (rename_link_xf 1, $f, "$upper_f,fetch") {
            printdebug "linked (using ...,fetch).\n";
-       } elsif ((printdebug "($!) "),
+       } elsif ((printdebug "($@) "),
                 $! != EEXIST) {
-           fail f_ "saving %s: %s", "$buildproductsdir/$f,fetch", $!;
+           fail f_ "saving %s: %s", "$buildproductsdir/$f,fetch", $@;
        } else {
            printdebug "cannot.\n";
        }
@@ -3813,12 +3820,25 @@ sub pull () {
 }
 
 sub check_not_dirty () {
-    foreach my $f (qw(local-options local-patch-header)) {
-       if (stat_exists "debian/source/$f") {
-           fail f_ "git tree contains debian/source/%s", $f;
+    my @forbid = qw(local-options local-patch-header);
+    @forbid = map { "debian/source/$_" } @forbid;
+    foreach my $f (@forbid) {
+       if (stat_exists $f) {
+           fail f_ "git tree contains %s", $f;
        }
     }
 
+    my @cmd = (@git, qw(status -uall --ignored --porcelain));
+    push @cmd, qw(debian/source/format debian/source/options);
+    push @cmd, @forbid;
+
+    my $bad = cmdoutput @cmd;
+    if (length $bad) {
+       fail +(__
+ "you have uncommitted changes to critical files, cannot continue:\n").
+              $bad;
+    }
+
     return if $includedirty;
 
     git_check_unmodified();
@@ -4012,7 +4032,7 @@ END
 sub pseudomerge_make_commit ($$$$ $$) {
     my ($clogp, $dgitview, $archive_hash, $i_arch_v,
        $msg_cmd, $msg_msg) = @_;
-    progress f_ "Declaring that HEAD inciudes all changes in %s...",
+    progress f_ "Declaring that HEAD includes all changes in %s...",
                 $i_arch_v->[0];
 
     my $tree = cmdoutput qw(git rev-parse), "${dgitview}:";
@@ -4068,7 +4088,7 @@ sub splitbrain_pseudomerge ($$$$) {
     my $i_arch_v = pseudomerge_version_check($clogp, $archive_hash);
 
     if (!defined $overwrite_version) {
-       progress __ "Checking that HEAD inciudes all changes in archive...";
+       progress __ "Checking that HEAD includes all changes in archive...";
     }
 
     return $dgitview if is_fast_fwd $archive_hash, $dgitview;
@@ -4519,11 +4539,11 @@ ENDT
     if ($sourceonlypolicy eq 'ok') {
     } elsif ($sourceonlypolicy eq 'always') {
        forceable_fail [qw(uploading-binaries)],
-           __ "uploading binaries, although distroy policy is source only"
+           __ "uploading binaries, although distro policy is source only"
            if $hasdebs;
     } elsif ($sourceonlypolicy eq 'never') {
        forceable_fail [qw(uploading-source-only)],
-           __ "source-only upload, although distroy policy requires .debs"
+           __ "source-only upload, although distro policy requires .debs"
            if !$hasdebs;
     } elsif ($sourceonlypolicy eq 'not-wholly-new') {
        forceable_fail [qw(uploading-source-only)],
@@ -5499,7 +5519,8 @@ sub quiltify ($$$$) {
            printdebug "considering C=$c->{Commit} P=$p->{Commit}\n";
 
            my @cmd= (@git, qw(diff-tree -r --name-only),
-                     $p->{Commit},$c->{Commit}, qw(-- debian/patches .pc));
+                     $p->{Commit},$c->{Commit},
+                     qw(-- debian/patches .pc debian/source/format));
            my $patchstackchange = cmdoutput @cmd;
            if (length $patchstackchange) {
                $patchstackchange =~ s/\n/,/g;
@@ -5715,7 +5736,10 @@ END
        if (act_local()) {
            debugcmd "+",@cmd;
            $!=0; $?=-1;
-           failedcmd @cmd if system @cmd and $?!=7*256;
+           failedcmd @cmd
+               if system @cmd
+               and not ($? == 7*256 or
+                        $? == -1 && $!==ENOENT);
        } else {
            dryrun_report @cmd;
        }
@@ -5797,7 +5821,9 @@ sub quilt_fixup_singlepatch ($$$) {
     changedir "..";
     runcmd @dpkgsource, qw(-x), (srcfn $version, ".dsc");
     rename srcfn("$upstreamversion", "/debian/patches"), 
-           "work/debian/patches";
+       "work/debian/patches"
+       or $!==ENOENT
+       or confess "install d/patches: $!";
 
     changedir "work";
     commit_quilty_patch();
@@ -6116,7 +6142,7 @@ END
     quiltify($clogp,$headref,$oldtiptree,\@failsuggestion);
 
     if (!open P, '>>', ".pc/applied-patches") {
-       $!==&ENOENT or  confess $!;
+       $!==&ENOENT or confess $!;
     } else {
        close P;
     }
@@ -6149,7 +6175,7 @@ sub quilt_fixup_editor () {
 
 sub maybe_apply_patches_dirtily () {
     return unless $quilt_mode =~ m/gbp|unapplied/;
-    print STDERR <<END or confess $!;
+    print STDERR __ <<END or confess $!;
 
 dgit: Building, or cleaning with rules target, in patches-unapplied tree.
 dgit: Have to apply the patches - making the tree dirty.
@@ -6162,7 +6188,7 @@ END
 }
 
 sub maybe_unapply_patches_again () {
-    progress "dgit: Unapplying patches again to tidy up the tree."
+    progress __ "dgit: Unapplying patches again to tidy up the tree."
        if $patches_applied_dirtily;
     runcmd qw(dpkg-source --after-build .)
        if $patches_applied_dirtily & 01;
@@ -6173,30 +6199,33 @@ sub maybe_unapply_patches_again () {
 
 #----- other building -----
 
-our $clean_using_builder;
-# ^ tree is to be cleaned by dpkg-source's builtin idea that it should
-#   clean the tree before building (perhaps invoked indirectly by
-#   whatever we are using to run the build), rather than separately
-#   and explicitly by us.
+sub clean_tree_check () {
+    # Not yet fully implemented.
+    if ($cleanmode =~ m{^check}) {
+       my $leftovers = cmdoutput @git, qw(clean -xdn);
+       if (length $leftovers) {
+           print STDERR $leftovers, "\n" or confess $!;
+           fail __
+ "tree contains uncommitted files and --clean=check specified";
+       }
+    }
+}
 
 sub clean_tree () {
-    return if $clean_using_builder;
-    if ($cleanmode eq 'dpkg-source') {
+    # We always clean the tree ourselves, rather than leave it to the
+    # builder (dpkg-source, or soemthing which calls dpkg-source).
+    if ($cleanmode =~ m{^dpkg-source}) {
+       my @cmd = @dpkgbuildpackage;
+       push @cmd, qw(-d) if $cleanmode =~ m{^dpkg-source-d};
+       push @cmd, qw(-T clean);
        maybe_apply_patches_dirtily();
-       runcmd_ordryrun_local @dpkgbuildpackage, qw(-T clean);
-    } elsif ($cleanmode eq 'dpkg-source-d') {
-       maybe_apply_patches_dirtily();
-       runcmd_ordryrun_local @dpkgbuildpackage, qw(-d -T clean);
+       runcmd_ordryrun_local @cmd;
     } elsif ($cleanmode eq 'git') {
        runcmd_ordryrun_local @git, qw(clean -xdf);
     } elsif ($cleanmode eq 'git-ff') {
        runcmd_ordryrun_local @git, qw(clean -xdff);
-    } elsif ($cleanmode eq 'check') {
-       my $leftovers = cmdoutput @git, qw(clean -xdn);
-       if (length $leftovers) {
-           print STDERR $leftovers, "\n" or confess $!;
-           fail "tree contains uncommitted files and --clean=check specified";
-       }
+    } elsif ($cleanmode =~ m{^check}) {
+       clean_tree_check();
     } elsif ($cleanmode eq 'none') {
     } else {
        die "$cleanmode ?";
@@ -6204,7 +6233,7 @@ sub clean_tree () {
 }
 
 sub cmd_clean () {
-    badusage "clean takes no additional arguments" if @ARGV;
+    badusage __ "clean takes no additional arguments" if @ARGV;
     notpushing();
     clean_tree();
     maybe_unapply_patches_again();
@@ -6217,7 +6246,8 @@ sub WANTSRC_BUILDER () { 02; } # caller should run dpkg-buildpackage
 sub build_or_push_prep_early () {
     our $build_or_push_prep_early_done //= 0;
     return if $build_or_push_prep_early_done++;
-    badusage "-p is not allowed with dgit $subcommand" if defined $package;
+    badusage f_ "-p is not allowed with dgit %s", $subcommand
+       if defined $package;
     my $clogp = parsechangelog();
     $isuite = getfield $clogp, 'Distribution';
     $package = getfield $clogp, 'Source';
@@ -6234,17 +6264,27 @@ sub build_prep_early () {
 sub build_prep ($) {
     my ($wantsrc) = @_;
     build_prep_early();
-    # clean the tree if we're trying to include dirty changes in the
-    # source package, or we are running the builder in $maindir
-    clean_tree() if $includedirty || ($wantsrc & WANTSRC_BUILDER);
+    if (!building_source_in_playtree() || ($wantsrc & WANTSRC_BUILDER)) {
+       # Clean the tree because we're going to use the contents of
+       # $maindir.  (We trying to include dirty changes in the source
+       # package, or we are running the builder in $maindir.)
+       clean_tree();
+    } else {
+       # We don't actually need to do anything in $maindir, but we
+       # should do some kind of cleanliness check because (i) the
+       # user may have forgotten a `git add', and (ii) if the user
+       # said -wc we should still do the check.
+       clean_tree_check();
+    }
     build_maybe_quilt_fixup();
     if ($rmchanges) {
        my $pat = changespat $version;
        foreach my $f (glob "$buildproductsdir/$pat") {
            if (act_local()) {
-               unlink $f or fail "remove old changes file $f: $!";
+               unlink $f or
+                   fail f_ "remove old changes file %s: %s", $f, $!;
            } else {
-               progress "would remove $f";
+               progress f_ "would remove %s", $f;
            }
        }
     }
@@ -6269,17 +6309,17 @@ sub changesopts_version () {
            1;
        }) {
            print STDERR $@;
-           fail
+           fail __
  "archive query failed (queried because --since-version not specified)";
        }
        if (@vsns) {
            @vsns = map { $_->[0] } @vsns;
            @vsns = sort { -version_compare($a, $b) } @vsns;
            $changes_since_version = $vsns[0];
-           progress "changelog will contain changes since $vsns[0]";
+           progress f_ "changelog will contain changes since %s", $vsns[0];
        } else {
            $changes_since_version = '_';
-           progress "package seems new, not specifying -v<version>";
+           progress __ "package seems new, not specifying -v<version>";
        }
     }
     if ($changes_since_version ne '_') {
@@ -6322,14 +6362,14 @@ sub massage_dbp_args ($;$) {
        $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;
+       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 ?";
+         confess "$dmode ?";
     }
     printdebug "massage done $r $dmode.\n";
     push @$cmd, $dmode;
@@ -6359,30 +6399,35 @@ sub postbuild_mergechanges ($) {
     } @changesfiles;
     my $result;
     if (@changesfiles==1) {
-       fail <<END.$msg_if_onlyone if defined $msg_if_onlyone;
-only one changes file from build (@changesfiles)
+       fail +(f_ <<END, "@changesfiles").$msg_if_onlyone
+only one changes file from build (%s)
 END
+           if defined $msg_if_onlyone;
        $result = $changesfiles[0];
     } elsif (@changesfiles==2) {
        my $binchanges = parsecontrol($changesfiles[1], "binary changes file");
        foreach my $l (split /\n/, getfield $binchanges, 'Files') {
-           fail "$l found in binaries changes file $binchanges"
+           fail f_ "%s found in binaries changes file %s", $l, $binchanges
                if $l =~ m/\.dsc$/;
        }
        runcmd_ordryrun_local @mergechanges, @changesfiles;
        my $multichanges = changespat $version,'multi';
        if (act_local()) {
-           stat_exists $multichanges or fail "$multichanges: $!";
+           stat_exists $multichanges or fail f_
+               "%s unexpectedly not created by build", $multichanges;
            foreach my $cf (glob $pat) {
                next if $cf eq $multichanges;
-               rename "$cf", "$cf.inmulti" or fail "$cf\{,.inmulti}: $!";
+               rename "$cf", "$cf.inmulti" or fail f_
+                   "install new changes %s\{,.inmulti}: %s", $cf, $!;
            }
        }
        $result = $multichanges;
     } else {
-       fail "wrong number of different changes files (@changesfiles)";
+       fail f_ "wrong number of different changes files (%s)",
+               "@changesfiles";
     }
-    printdone "build successful, results in $result\n" or confess $!;
+    printdone f_ "build successful, results in %s\n", $result
+       or confess $!;
 }
 
 sub midbuild_checkchanges () {
@@ -6393,9 +6438,9 @@ sub midbuild_checkchanges () {
        $_ ne changespat $version,'source' and
        $_ ne changespat $version,'multi'
     } @unwanted;
-    fail <<END
-changes files other than source matching $pat already present; building would result in ambiguity about the intended results.
-Suggest you delete @unwanted.
+    fail +(f_ <<END, $pat, "@unwanted")
+changes files other than source matching %s already present; building would result in ambiguity about the intended results.
+Suggest you delete %s.
 END
        if @unwanted;
 }
@@ -6412,15 +6457,15 @@ sub postbuild_mergechanges_vanilla ($) {
            postbuild_mergechanges(undef);
        };
     } else {
-       printdone "build successful\n";
+       printdone __ "build successful\n";
     }
 }
 
 sub cmd_build {
     build_prep_early();
-    $buildproductsdir eq '..' or print STDERR <<END;
-$us: warning: build-products-dir set, but not supported by dpkg-buildpackage
-$us: warning: build-products-dir will be ignored; files will go to ..
+    $buildproductsdir eq '..' or print STDERR +(f_ <<END, $us, $us);
+%s: warning: build-products-dir set, but not supported by dpkg-buildpackage
+%s: warning: build-products-dir will be ignored; files will go to ..
 END
     $buildproductsdir = '..';
     my @dbp = (@dpkgbuildpackage, qw(-us -uc), changesopts_initial(), @ARGV);
@@ -6501,9 +6546,7 @@ sub cmd_gbp_build {
        build_source();
        midbuild_checkchanges_vanilla $wantsrc;
     } else {
-       if (!$clean_using_builder) {
-           push @cmd, '--git-cleaner=true';
-       }
+       push @cmd, '--git-cleaner=true';
     }
     maybe_unapply_patches_again();
     if ($wantsrc & WANTSRC_BUILDER) {
@@ -6531,7 +6574,7 @@ sub build_source {
     $sourcechanges = changespat $version,'source';
     if (act_local()) {
        unlink "$buildproductsdir/$sourcechanges" or $!==ENOENT
-           or fail "remove $sourcechanges: $!";
+           or fail f_ "remove %s: %s", $sourcechanges, $!;
     }
     my @cmd = (@dpkgsource, qw(-b --));
     my $leafdir;
@@ -6551,6 +6594,24 @@ sub build_source {
         }
     } else {
         $leafdir = basename $maindir;
+
+       if ($buildproductsdir ne '..') {
+           # Well, we are going to run dpkg-source -b which consumes
+           # origs from .. and generates output there.  To make this
+           # work when the bpd is not .. , we would have to (i) link
+           # origs from bpd to .. , (ii) check for files that
+           # dpkg-source -b would/might overwrite, and afterwards
+           # (iii) move all the outputs back to the bpd (iv) except
+           # for the origs which should be deleted from .. if they
+           # weren't there beforehand.  And if there is an error and
+           # we don't run to completion we would necessarily leave a
+           # mess.  This is too much.  The real way to fix this
+           # is for dpkg-source to have bpd support.
+           confess unless $includedirty;
+           fail __
+ "--include-dirty not supported with --build-products-dir, sorry";
+       }
+
         changedir '..';
     }
     runcmd_ordryrun_local @cmd, $leafdir;
@@ -6567,8 +6628,8 @@ sub build_source {
     my $mv = sub {
        my ($why, $l) = @_;
         printdebug " renaming ($why) $l\n";
-        rename "$l", bpd_abs()."/$l"
-           or fail "put in place new built file ($l): $!";
+        rename_link_xf 0, "$l", bpd_abs()."/$l"
+           or fail f_ "put in place new built file (%s): %s", $l, $@;
     };
     foreach my $l (split /\n/, getfield $dsc, 'Files') {
         $l =~ m/\S+$/ or next;
@@ -6581,28 +6642,31 @@ sub build_source {
 }
 
 sub cmd_build_source {
-    badusage "build-source takes no additional arguments" if @ARGV;
+    badusage __ "build-source takes no additional arguments" if @ARGV;
     build_prep(WANTSRC_SOURCE);
     build_source();
     maybe_unapply_patches_again();
-    printdone "source built, results in $dscfn and $sourcechanges";
+    printdone f_ "source built, results in %s and %s",
+                $dscfn, $sourcechanges;
 }
 
 sub cmd_push_source {
     prep_push();
-    fail "dgit push-source: --include-dirty/--ignore-dirty does not make".
-      "sense with push-source!" if $includedirty;
+    fail __
+       "dgit push-source: --include-dirty/--ignore-dirty does not make".
+       "sense with push-source!"
+       if $includedirty;
     build_maybe_quilt_fixup();
     if ($changesfile) {
         my $changes = parsecontrol("$buildproductsdir/$changesfile",
-                                   "source changes file");
+                                   __ "source changes file");
         unless (test_source_only_changes($changes)) {
-            fail "user-specified changes file is not source-only";
+            fail __ "user-specified changes file is not source-only";
         }
     } else {
         # Building a source package is very fast, so just do it
        build_source();
-       die "er, patches are applied dirtily but shouldn't be.."
+       confess "er, patches are applied dirtily but shouldn't be.."
            if $patches_applied_dirtily;
        $changesfile = $sourcechanges;
     }
@@ -6616,9 +6680,10 @@ sub binary_builder {
     midbuild_checkchanges();
     in_bpd {
        if (act_local()) {
-           stat_exists $dscfn or fail "$dscfn (in build products dir): $!";
-           stat_exists $sourcechanges
-               or fail "$sourcechanges (in build products dir): $!";
+           stat_exists $dscfn or fail f_
+               "%s (in build products dir): %s", $dscfn, $!;
+           stat_exists $sourcechanges or fail f_
+               "%s (in build products dir): %s", $sourcechanges, $!;
        }
        runcmd_ordryrun_local @$bbuilder, @args;
     };
@@ -6630,7 +6695,7 @@ sub binary_builder {
 
 sub cmd_sbuild {
     build_prep_early();
-    binary_builder(\@sbuild, <<END, qw(-d), $isuite, @ARGV, $dscfn);
+    binary_builder(\@sbuild, (__ <<END), qw(-d), $isuite, @ARGV, $dscfn);
 perhaps you need to pass -A ?  (sbuild's default is to build only
 arch-specific binaries; dgit 1.4 used to override that.)
 END
@@ -6642,11 +6707,13 @@ sub pbuilder ($) {
     # @ARGV is allowed to contain only things that should be passed to
     # pbuilder under debbuildopts; just massage those
     my $wantsrc = massage_dbp_args \@ARGV;
-    fail "you asked for a builder but your debbuildopts didn't ask for".
-      " any binaries -- is this really what you meant?"
-      unless $wantsrc & WANTSRC_BUILDER;
-    fail "we must build a .dsc to pass to the builder but your debbuiltopts".
-      " forbids the building of a source package; cannot continue"
+    fail __
+       "you asked for a builder but your debbuildopts didn't ask for".
+       " any binaries -- is this really what you meant?"
+       unless $wantsrc & WANTSRC_BUILDER;
+    fail __
+       "we must build a .dsc to pass to the builder but your debbuiltopts".
+       " forbids the building of a source package; cannot continue"
       unless $wantsrc & WANTSRC_SOURCE;
     # We do not want to include the verb "build" in @pbuilder because
     # the user can customise @pbuilder and they shouldn't be required
@@ -6675,7 +6742,8 @@ sub cmd_quilt_fixup {
 }
 
 sub cmd_print_unapplied_treeish {
-    badusage "incorrect arguments to dgit print-unapplied-treeish" if @ARGV;
+    badusage __ "incorrect arguments to dgit print-unapplied-treeish"
+       if @ARGV;
     my $headref = git_rev_parse('HEAD');
     my $clogp = commit_getclogp $headref;
     $package = getfield $clogp, 'Source';
@@ -6696,9 +6764,9 @@ sub import_dsc_result {
     my ($dstref, $newhash, $what_log, $what_msg) = @_;
     my @cmd = (git_update_ref_cmd $what_log, $dstref, $newhash);
     runcmd @cmd;
-    check_gitattrs($newhash, "source tree");
+    check_gitattrs($newhash, __ "source tree");
 
-    progress "dgit: import-dsc: $what_msg";
+    progress f_ "dgit: import-dsc: %s", $what_msg;
 }
 
 sub cmd_import_dsc {
@@ -6711,14 +6779,16 @@ sub cmd_import_dsc {
        if (m/^--require-valid-signature$/) {
            $needsig = 1;
        } else {
-           badusage "unknown dgit import-dsc sub-option \`$_'";
+           badusage f_ "unknown dgit import-dsc sub-option \`%s'", $_;
        }
     }
 
-    badusage "usage: dgit import-dsc .../PATH/TO/.DSC BRANCH" unless @ARGV==2;
+    badusage __ "usage: dgit import-dsc .../PATH/TO/.DSC BRANCH"
+       unless @ARGV==2;
     my ($dscfn, $dstbranch) = @ARGV;
 
-    badusage "dry run makes no sense with import-dsc" unless act_local();
+    badusage __ "dry run makes no sense with import-dsc"
+       unless act_local();
 
     my $force = $dstbranch =~ s/^\+//   ? +1 :
                $dstbranch =~ s/^\.\.// ? -1 :
@@ -6734,14 +6804,14 @@ sub cmd_import_dsc {
     my $chead = cmdoutput_errok @symcmd;
     defined $chead or $?==256 or failedcmd @symcmd;
 
-    fail "$dstbranch is checked out - will not update it"
+    fail f_ "%s is checked out - will not update it", $dstbranch
        if defined $chead and $chead eq $dstbranch;
 
     my $oldhash = git_get_ref $dstbranch;
 
-    open D, "<", $dscfn or fail "open import .dsc ($dscfn): $!";
+    open D, "<", $dscfn or fail f_ "open import .dsc (%s): %s", $dscfn, $!;
     $dscdata = do { local $/ = undef; <D>; };
-    D->error and fail "read $dscfn: $!";
+    D->error and fail f_ "read %s: %s", $dscfn, $!;
     close C;
 
     # we don't normally need this so import it here
@@ -6752,13 +6822,13 @@ sub cmd_import_dsc {
        local $SIG{__WARN__} = sub {
            print STDERR $_[0];
            return unless $needsig;
-           fail "import-dsc signature check failed";
+           fail __ "import-dsc signature check failed";
        };
        if (!$dp->is_signed()) {
-           warn "$us: warning: importing unsigned .dsc\n";
+           warn f_ "%s: warning: importing unsigned .dsc\n", $us;
        } else {
            my $r = $dp->check_signature();
-           die "->check_signature => $r" if $needsig && $r;
+           confess "->check_signature => $r" if $needsig && $r;
        }
     }
 
@@ -6766,7 +6836,7 @@ sub cmd_import_dsc {
 
     $package = getfield $dsc, 'Source';
 
-    parse_dsc_field($dsc, "Dgit metadata in .dsc")
+    parse_dsc_field($dsc, __ "Dgit metadata in .dsc")
        unless forceing [qw(import-dsc-with-dgit-field)];
     parse_dsc_field_def_dsc_distro();
 
@@ -6776,7 +6846,8 @@ sub cmd_import_dsc {
     notpushing();
 
     if (defined $dsc_hash) {
-       progress "dgit: import-dsc of .dsc with Dgit field, using git hash";
+       progress __
+           "dgit: import-dsc of .dsc with Dgit field, using git hash";
        resolve_dsc_field_commit undef, undef;
     }
     if (defined $dsc_hash) {
@@ -6784,29 +6855,29 @@ sub cmd_import_dsc {
                   "echo $dsc_hash | git cat-file --batch-check");
        my $objgot = cmdoutput @cmd;
        if ($objgot =~ m#^\w+ missing\b#) {
-           fail <<END
-.dsc contains Dgit field referring to object $dsc_hash
+           fail f_ <<END, $dsc_hash
+.dsc contains Dgit field referring to object %s
 Your git tree does not have that object.  Try `git fetch' from a
-plausible server (browse.dgit.d.o? alioth?), and try the import-dsc again.
+plausible server (browse.dgit.d.o? salsa?), and try the import-dsc again.
 END
        }
        if ($oldhash && !is_fast_fwd $oldhash, $dsc_hash) {
            if ($force > 0) {
-               progress "Not fast forward, forced update.";
+               progress __ "Not fast forward, forced update.";
            } else {
-               fail "Not fast forward to $dsc_hash";
+               fail f_ "Not fast forward to %s", $dsc_hash;
            }
        }
        import_dsc_result $dstbranch, $dsc_hash,
            "dgit import-dsc (Dgit): $info",
-           "updated git ref $dstbranch";
+           f_ "updated git ref %s", $dstbranch;
        return 0;
     }
 
-    fail <<END
-Branch $dstbranch already exists
-Specify ..$specbranch for a pseudo-merge, binding in existing history
-Specify  +$specbranch to overwrite, discarding existing history
+    fail f_ <<END, $dstbranch, $specbranch, $specbranch
+Branch %s already exists
+Specify ..%s for a pseudo-merge, binding in existing history
+Specify  +%s to overwrite, discarding existing history
 END
        if $oldhash && !$force;
 
@@ -6816,24 +6887,29 @@ END
        my $here = "$buildproductsdir/$f";
        if (lstat $here) {
            next if stat $here;
-           fail "lstat $here works but stat gives $! !";
+           fail f_ "lstat %s works but stat gives %s !", $here, $!;
        }
-       fail "stat $here: $!" unless $! == ENOENT;
+       fail f_ "stat %s: %s", $here, $! unless $! == ENOENT;
        my $there = $dscfn;
        if ($dscfn =~ m#^(?:\./+)?\.\./+#) {
            $there = $';
        } elsif ($dscfn =~ m#^/#) {
            $there = $dscfn;
        } else {
-           fail "cannot import $dscfn which seems to be inside working tree!";
+           fail f_
+               "cannot import %s which seems to be inside working tree!",
+               $dscfn;
        }
-       $there =~ s#/+[^/]+$## or
-           fail "import $dscfn requires ../$f, but it does not exist";
+       $there =~ s#/+[^/]+$## or fail f_
+           "import %s requires .../%s, but it does not exist",
+           $dscfn, $f;
        $there .= "/$f";
        my $test = $there =~ m{^/} ? $there : "../$there";
-       stat $test or fail "import $dscfn requires $test, but: $!";
-       symlink $there, $here or fail "symlink $there to $here: $!";
-       progress "made symlink $here -> $there";
+       stat $test or fail f_
+           "import %s requires %s, but: %s", $dscfn, $test, $!;
+       symlink $there, $here or fail f_
+           "symlink %s to %s: %s", $there, $here, $!;
+       progress f_ "made symlink %s -> %s", $here, $there;
 #      print STDERR Dumper($fi);
     }
     my @mergeinputs = generate_commits_from_dsc();
@@ -6843,21 +6919,24 @@ END
 
     if ($oldhash) {
        if ($force > 0) {
-           progress "Import, forced update - synthetic orphan git history.";
+           progress __
+               "Import, forced update - synthetic orphan git history.";
        } elsif ($force < 0) {
-           progress "Import, merging.";
+           progress __ "Import, merging.";
            my $tree = cmdoutput @git, qw(rev-parse), "$newhash:";
            my $version = getfield $dsc, 'Version';
            my $clogp = commit_getclogp $newhash;
            my $authline = clogp_authline $clogp;
-           $newhash = make_commit_text <<END;
+           $newhash = make_commit_text <<ENDU
 tree $tree
 parent $newhash
 parent $oldhash
 author $authline
 committer $authline
 
-Merge $package ($version) import into $dstbranch
+ENDU
+               .(f_ <<END, $package, $version, $dstbranch);
+Merge %s (%s) import into %s
 END
        } else {
            die; # caught earlier
@@ -6866,20 +6945,20 @@ END
 
     import_dsc_result $dstbranch, $newhash,
        "dgit import-dsc: $info",
-       "results are in in git ref $dstbranch";
+       f_ "results are in git ref %s", $dstbranch;
 }
 
 sub pre_archive_api_query () {
     not_necessarily_a_tree();
 }
 sub cmd_archive_api_query {
-    badusage "need only 1 subpath argument" unless @ARGV==1;
+    badusage __ "need only 1 subpath argument" unless @ARGV==1;
     my ($subpath) = @ARGV;
     local $isuite = 'DGIT-API-QUERY-CMD';
     my @cmd = archive_api_query_cmd($subpath);
     push @cmd, qw(-f);
     debugcmd ">",@cmd;
-    exec @cmd or fail "exec curl: $!\n";
+    exec @cmd or fail f_ "exec curl: %s\n", $!;
 }
 
 sub repos_server_url () {
@@ -6893,19 +6972,20 @@ sub pre_clone_dgit_repos_server () {
     not_necessarily_a_tree();
 }
 sub cmd_clone_dgit_repos_server {
-    badusage "need destination argument" unless @ARGV==1;
+    badusage __ "need destination argument" unless @ARGV==1;
     my ($destdir) = @ARGV;
     my $url = repos_server_url();
     my @cmd = (@git, qw(clone), $url, $destdir);
     debugcmd ">",@cmd;
-    exec @cmd or fail "exec git clone: $!\n";
+    exec @cmd or fail f_ "exec git clone: %s\n", $!;
 }
 
 sub pre_print_dgit_repos_server_source_url () {
     not_necessarily_a_tree();
 }
 sub cmd_print_dgit_repos_server_source_url {
-    badusage "no arguments allowed to dgit print-dgit-repos-server-source-url"
+    badusage __
+       "no arguments allowed to dgit print-dgit-repos-server-source-url"
        if @ARGV;
     my $url = repos_server_url();
     print $url, "\n" or confess $!;
@@ -6915,31 +6995,33 @@ sub pre_print_dpkg_source_ignores {
     not_necessarily_a_tree();
 }
 sub cmd_print_dpkg_source_ignores {
-    badusage "no arguments allowed to dgit print-dpkg-source-ignores"
+    badusage __
+       "no arguments allowed to dgit print-dpkg-source-ignores"
        if @ARGV;
     print "@dpkg_source_ignores\n" or confess $!;
 }
 
 sub cmd_setup_mergechangelogs {
-    badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
+    badusage __ "no arguments allowed to dgit setup-mergechangelogs"
+       if @ARGV;
     local $isuite = 'DGIT-SETUP-TREE';
     setup_mergechangelogs(1);
 }
 
 sub cmd_setup_useremail {
-    badusage "no arguments allowed to dgit setup-useremail" if @ARGV;
+    badusage __ "no arguments allowed to dgit setup-useremail" if @ARGV;
     local $isuite = 'DGIT-SETUP-TREE';
     setup_useremail(1);
 }
 
 sub cmd_setup_gitattributes {
-    badusage "no arguments allowed to dgit setup-useremail" if @ARGV;
+    badusage __ "no arguments allowed to dgit setup-useremail" if @ARGV;
     local $isuite = 'DGIT-SETUP-TREE';
     setup_gitattrs(1);
 }
 
 sub cmd_setup_new_tree {
-    badusage "no arguments allowed to dgit setup-tree" if @ARGV;
+    badusage __ "no arguments allowed to dgit setup-tree" if @ARGV;
     local $isuite = 'DGIT-SETUP-TREE';
     setup_new_tree();
 }
@@ -6986,8 +7068,8 @@ defvalopt '', '-C', '.+', sub {
 defvalopt '--initiator-tempdir','','.*', sub {
     ($initiator_tempdir) = (@_);
     $initiator_tempdir =~ m#^/# or
-       badusage "--initiator-tempdir must be used specify an".
-       " absolute, not relative, directory."
+       badusage __ "--initiator-tempdir must be used specify an".
+                   " absolute, not relative, directory."
 };
 
 sub defoptmodes ($@) {
@@ -7025,11 +7107,11 @@ sub parseopts () {
        my ($what) = @_;
        @rvalopts = ($_);
        if (!defined $val) {
-           badusage "$what needs a value" unless @ARGV;
+           badusage f_ "%s needs a value", $what unless @ARGV;
            $val = shift @ARGV;
            push @rvalopts, $val;
        }
-       badusage "bad value \`$val' for $what" unless
+       badusage f_ "bad value \`%s' for %s", $val, $what unless
            $val =~ m/^$oi->{Re}$(?!\n)/s;
        my $how = $oi->{How};
        if (ref($how) eq 'SCALAR') {
@@ -7114,7 +7196,8 @@ sub parseopts () {
                $_='';
            } elsif (m/^--force-/) {
                print STDERR
-                   "$us: warning: ignoring unknown force option $_\n";
+                   f_ "%s: warning: ignoring unknown force option %s\n",
+                      $us, $_;
                $_='';
            } elsif (m/^--dgit-tag-format=(old|new)$/s) {
                # undocumented, for testing
@@ -7133,7 +7216,7 @@ sub parseopts () {
                push @ropts, $_;
                $funcopts_long{$_}();
            } else {
-               badusage "unknown long option \`$_'";
+               badusage f_ "unknown long option \`%s'", $_;
            }
        } else {
            while (m/^-./s) {
@@ -7186,7 +7269,7 @@ sub parseopts () {
                    $valopt->($oi->{Short});
                    $_ = '';
                } else {
-                   badusage "unknown short option \`$_'";
+                   badusage f_ "unknown short option \`%s'", $_;
                }
            }
        }
@@ -7201,17 +7284,18 @@ sub check_env_sanity () {
        foreach my $name (qw(PIPE CHLD)) {
            my $signame = "SIG$name";
            my $signum = eval "POSIX::$signame" // die;
-           die "$signame is set to something other than SIG_DFL\n"
+           die f_ "%s is set to something other than SIG_DFL\n",
+               $signame
                if defined $SIG{$name} and $SIG{$name} ne 'DEFAULT';
            $blocked->ismember($signum) and
-               die "$signame is blocked\n";
+               die f_ "%s is blocked\n", $signame;
        }
     };
     return unless $@;
     chomp $@;
-    fail <<END;
-On entry to dgit, $@
-This is a bug produced by something in in your execution environment.
+    fail f_ <<END, $@;
+On entry to dgit, %s
+This is a bug produced by something in your execution environment.
 Giving up.
 END
 }
@@ -7227,7 +7311,7 @@ sub parseopts_late_defaults () {
 
        my $v = access_cfg("cmd-$k", 'RETURN-UNDEF');
        if (defined $v) {
-           badcfg "cannot set command for $k"
+           badcfg f_ "cannot set command for %s", $k
                unless length $om->[0];
            $om->[0] = $v;
        }
@@ -7240,7 +7324,7 @@ sub parseopts_late_defaults () {
            printdebug "CL $c ", (join " ", map { shellquote } @vl),
                "\n" if $debuglevel >= 4;
            next unless @vl;
-           badcfg "cannot configure options for $k"
+           badcfg f_ "cannot configure options for %s", $k
                if $opts_opt_cmdonly{$k};
            my $insertpos = $opts_cfg_insertpos{$k};
            @$om = ( @$om[0..$insertpos-1],
@@ -7260,7 +7344,7 @@ sub parseopts_late_defaults () {
            // access_cfg('quilt-mode', 'RETURN-UNDEF')
            // 'linear';
        $quilt_mode =~ m/^($quilt_modes_re)$/ 
-           or badcfg "unknown quilt-mode \`$quilt_mode'";
+           or badcfg f_ "unknown quilt-mode \`%s'", $quilt_mode;
        $quilt_mode = $1;
     }
 
@@ -7270,7 +7354,8 @@ sub parseopts_late_defaults () {
        next if defined $$vr;
        $$vr = access_cfg($moc->{Key}, 'RETURN-UNDEF') // $moc->{Default};
        my $v = $moc->{Vals}{$$vr};
-       badcfg "unknown $moc->{Key} setting \`$$vr'" unless defined $v;
+       badcfg f_ "unknown %s setting \`%s'", $moc->{Key}, $$vr
+           unless defined $v;
        $$vr = $v;
     }
 
@@ -7279,11 +7364,14 @@ sub parseopts_late_defaults () {
 
     if (!defined $cleanmode) {
        local $access_forpush;
-       $cleanmode = access_cfg('clean-mode', 'RETURN-UNDEF');
+       $cleanmode = access_cfg('clean-mode-newer', 'RETURN-UNDEF');
+       $cleanmode = undef if $cleanmode && $cleanmode !~ m/^$cleanmode_re$/;
+
+       $cleanmode //= access_cfg('clean-mode', 'RETURN-UNDEF');
        $cleanmode //= 'dpkg-source';
 
-       badcfg "unknown clean-mode \`$cleanmode'" unless
-           $cleanmode =~ m/^($cleanmode_re)$(?!\n)/s;
+       badcfg f_ "unknown clean-mode \`%s'", $cleanmode unless
+           $cleanmode =~ m/$cleanmode_re/;
     }
 
     $buildproductsdir //= access_cfg('build-products-dir', 'RETURN-UNDEF');
@@ -7303,8 +7391,8 @@ if ($ENV{$fakeeditorenv}) {
 parseopts();
 check_env_sanity();
 
-print STDERR "DRY RUN ONLY\n" if $dryrun_level > 1;
-print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n"
+print STDERR __ "DRY RUN ONLY\n" if $dryrun_level > 1;
+print STDERR __ "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n"
     if $dryrun_level == 1;
 if (!@ARGV) {
     print STDERR __ $helpmsg or confess $!;
@@ -7316,11 +7404,14 @@ $cmd =~ y/-/_/;
 my $pre_fn = ${*::}{"pre_$cmd"};
 $pre_fn->() if $pre_fn;
 
-record_maindir if $invoked_in_git_tree;
+if ($invoked_in_git_tree) {
+    changedir_git_toplevel();
+    record_maindir();
+}
 git_slurp_config();
 
 my $fn = ${*::}{"cmd_$cmd"};
-$fn or badusage "unknown operation $cmd";
+$fn or badusage f_ "unknown operation %s", $cmd;
 $fn->();
 
 finish 0;