chiark / gitweb /
changelog: Tidy a bit
[dgit.git] / dgit
diff --git a/dgit b/dgit
index 9a28d81d75387edfcef84207c680817367ea0b2a..d62694a90818c7d461324381f57a3eb4d6b3afc2 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -48,7 +48,7 @@ our $absurdity = undef; ###substituted###
 our @rpushprotovsn_support = qw(4 3 2); # 4 is new tag format
 our $protovsn;
 
-our $isuite = 'unstable';
+our $isuite;
 our $idistro;
 our $package;
 our @ropts;
@@ -69,6 +69,8 @@ our $rmchanges;
 our $overwrite_version; # undef: not specified; '': check changelog
 our $quilt_mode;
 our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck|gbp|dpm|unapplied';
+our $dodep14tag;
+our $dodep14tag_re = 'want|no|always';
 our $split_brain_save;
 our $we_are_responder;
 our $initiator_tempdir;
@@ -76,6 +78,7 @@ our $patches_applied_dirtily = 00;
 our $tagformat_want;
 our $tagformat;
 our $tagformatfn;
+our $chase_dsc_distro=1;
 
 our %forceopts = map { $_=>0 }
     qw(unrepresentable unsupported-source-format
@@ -94,6 +97,7 @@ our $orig_f_tail_re = "$orig_f_comp_re\\.tar(?:\\.\\w+)?(?:$orig_f_sig_re)?";
 
 our $git_authline_re = '^([^<>]+) \<(\S+)\> (\d+ [-+]\d+)$';
 our $splitbraincache = 'dgit-intern/quilt-cache';
+our $rewritemap = 'dgit-rewrite/map';
 
 our (@git) = qw(git);
 our (@dget) = qw(dget);
@@ -139,7 +143,7 @@ our %opts_cfg_insertpos = map {
     scalar @{ $opts_opt_map{$_} }
 } keys %opts_opt_map;
 
-sub finalise_opts_opts();
+sub parseopts_late_defaults();
 
 our $keyid;
 
@@ -171,8 +175,7 @@ sub debiantag ($$) {
 
 sub debiantag_maintview ($$) { 
     my ($v,$distro) = @_;
-    $v =~ y/~:/_%/;
-    return "$distro/$v";
+    return "$distro/".dep14_version_mangle $v;
 }
 
 sub madformat ($) { $_[0] eq '3.0 (quilt)' }
@@ -183,30 +186,6 @@ sub lref () { return "refs/heads/".lbranch(); }
 sub lrref () { return "refs/remotes/$remotename/".server_branch($csuite); }
 sub rrref () { return server_ref($csuite); }
 
-sub lrfetchrefs () { return "refs/dgit-fetch/$csuite"; }
-sub lrfetchref () { return lrfetchrefs.'/'.server_branch($csuite); }
-
-# We fetch some parts of lrfetchrefs/*.  Ideally we delete these
-# locally fetched refs because they have unhelpful names and clutter
-# up gitk etc.  So we track whether we have "used up" head ref (ie,
-# whether we have made another local ref which refers to this object).
-#
-# (If we deleted them unconditionally, then we might end up
-# re-fetching the same git objects each time dgit fetch was run.)
-#
-# So, leach use of lrfetchrefs needs to be accompanied by arrangements
-# in git_fetch_us to fetch the refs in question, and possibly a call
-# to lrfetchref_used.
-
-our (%lrfetchrefs_f, %lrfetchrefs_d);
-# $lrfetchrefs_X{lrfetchrefs."/heads/whatever"} = $objid
-
-sub lrfetchref_used ($) {
-    my ($fullrefname) = @_;
-    my $objid = $lrfetchrefs_f{$fullrefname};
-    $lrfetchrefs_d{$fullrefname} = $objid if defined $objid;
-}
-
 sub stripepoch ($) {
     my ($vsn) = @_;
     $vsn =~ s/^\d+\://;
@@ -586,6 +565,8 @@ sub cmd_help () {
 our $td = $ENV{DGIT_TEST_DUMMY_DIR} || "DGIT_TEST_DUMMY_DIR-unset";
 
 our %defcfg = ('dgit.default.distro' => 'debian',
+              'dgit.default.default-suite' => 'unstable',
+              'dgit.default.old-dsc-distro' => 'debian',
               'dgit-suite.*-security.distro' => 'debian-security',
               'dgit.default.username' => '',
               'dgit.default.archive-query-default-component' => 'main',
@@ -594,6 +575,10 @@ our %defcfg = ('dgit.default.distro' => 'debian',
               'dgit.default.sshpsql-dbname' => 'service=projectb',
               'dgit.default.aptget-components' => 'main',
               'dgit.default.dgit-tag-format' => 'new,old,maint',
+              'dgit.dsc-url-proto-ok.http'    => 'true',
+              'dgit.dsc-url-proto-ok.https'   => 'true',
+              'dgit.dsc-url-proto-ok.git'     => 'true',
+              'dgit.default.dsc-url-proto-ok' => 'false',
               # old means "repo server accepts pushes with old dgit tags"
               # new means "repo server accepts pushes with new dgit tags"
               # maint means "repo server accepts split brain pushes"
@@ -681,7 +666,10 @@ sub git_get_config ($) {
     my ($c) = @_;
     foreach my $src (@gitcfgsources) {
        my $l = $gitcfgs{$src}{$c};
-       printdebug"C $c ".(defined $l ? messagequote "'$l'" : "undef")."\n"
+       croak "$l $c" if $l && !ref $l;
+       printdebug"C $c ".(defined $l ?
+                          join " ", map { messagequote "'$_'" } @$l :
+                          "undef")."\n"
            if $debuglevel >= 4;
        $l or next;
        @$l==1 or badcfg "multiple values for $c".
@@ -694,16 +682,20 @@ sub git_get_config ($) {
 sub cfg {
     foreach my $c (@_) {
        return undef if $c =~ /RETURN-UNDEF/;
+       printdebug "C? $c\n" if $debuglevel >= 5;
        my $v = git_get_config($c);
        return $v if defined $v;
        my $dv = $defcfg{$c};
-       return $dv if defined $dv;
+       if (defined $dv) {
+           printdebug "CD $c $dv\n" if $debuglevel >= 4;
+           return $dv;
+       }
     }
     badcfg "need value for one of: @_\n".
        "$us: distro or suite appears not to be (properly) supported";
 }
 
-sub access_basedistro () {
+sub access_basedistro__noalias () {
     if (defined $idistro) {
        return $idistro;
     } else {   
@@ -723,9 +715,18 @@ sub access_basedistro () {
     }
 }
 
+sub access_basedistro () {
+    my $noalias = access_basedistro__noalias();
+    my $canon = cfg("dgit-distro.$noalias.alias-canon",'RETURN-UNDEF');
+    return $canon // $noalias;
+}
+
 sub access_nomdistro () {
     my $base = access_basedistro();
-    return cfg("dgit-distro.$base.nominal-distro",'RETURN-UNDEF') // $base;
+    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$/)";
+    return $r;
 }
 
 sub access_quirk () {
@@ -789,11 +790,11 @@ sub pushing () {
 Push failed, before we got started.
 You can retry the push, after fixing the problem, if you like.
 END
-    finalise_opts_opts();
+    parseopts_late_defaults();
 }
 
 sub notpushing () {
-    finalise_opts_opts();
+    parseopts_late_defaults();
 }
 
 sub supplementary_message ($) {
@@ -1010,6 +1011,8 @@ our %rmad;
 
 sub archive_query ($;@) {
     my ($method) = shift @_;
+    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'";
     my $proto = $1;
@@ -1162,7 +1165,7 @@ sub aptget_aptcache () { return @aptcache, qw(-c), $aptget_configpath; }
 
 sub aptget_cache_clean {
     runcmd_ordryrun_local qw(sh -ec),
-       'cd "$1"; pwd; find -atime +30 -type f -print0 | xargs -0r echo rm --',
+       'cd "$1"; find -atime +30 -type f -print0 | xargs -0r rm --',
        'x', $aptget_base;
 }
 
@@ -1518,6 +1521,15 @@ sub access_cfg_tagformats () {
     split /\,/, access_cfg('dgit-tag-format');
 }
 
+sub access_cfg_tagformats_can_splitbrain () {
+    my %y = map { $_ => 1 } access_cfg_tagformats;
+    foreach my $needtf (qw(new maint)) {
+       next if $y{$needtf};
+       return 0;
+    }
+    return 1;
+}
+
 sub need_tagformat ($$) {
     my ($fmt, $why) = @_;
     fail "need to use tag format $fmt ($why) but also need".
@@ -1667,6 +1679,7 @@ sub create_remote_git_repo () {
 }
 
 our ($dsc_hash,$lastpush_mergeinput);
+our ($dsc_distro, $dsc_hint_tag, $dsc_hint_url);
 
 our $ud = '.git/dgit/unpack';
 
@@ -1691,7 +1704,13 @@ sub git_write_tree () {
     return $tree;
 }
 
-sub remove_stray_gits () {
+sub git_add_write_tree () {
+    runcmd @git, qw(add -Af .);
+    return git_write_tree();
+}
+
+sub remove_stray_gits ($) {
+    my ($what) = @_;
     my @gitscmd = qw(find -name .git -prune -print0);
     debugcmd "|",@gitscmd;
     open GITS, "-|", @gitscmd or die $!;
@@ -1699,7 +1718,7 @@ sub remove_stray_gits () {
        local $/="\0";
        while (<GITS>) {
            chomp or die;
-           print STDERR "$us: warning: removing from source package: ",
+           print STDERR "$us: warning: removing from $what: ",
                (messagequote $_), "\n";
            rmtree $_;
        }
@@ -1707,8 +1726,8 @@ sub remove_stray_gits () {
     $!=0; $?=0; close GITS or failedcmd @gitscmd;
 }
 
-sub mktree_in_ud_from_only_subdir (;$) {
-    my ($raw) = @_;
+sub mktree_in_ud_from_only_subdir ($;$) {
+    my ($what,$raw) = @_;
 
     # changes into the subdir
     my (@dirs) = <*/.>;
@@ -1717,7 +1736,7 @@ sub mktree_in_ud_from_only_subdir (;$) {
     my $dir = $1;
     changedir $dir;
 
-    remove_stray_gits();
+    remove_stray_gits($what);
     mktree_in_ud_here();
     if (!$raw) {
        my ($format, $fopts) = get_source_format();
@@ -1726,8 +1745,7 @@ sub mktree_in_ud_from_only_subdir (;$) {
        }
     }
 
-    runcmd @git, qw(add -Af);
-    my $tree=git_write_tree();
+    my $tree=git_add_write_tree();
     return ($tree,$dir);
 }
 
@@ -1889,7 +1907,8 @@ END
            push @found_differ, "archive $h->{filename}: ".join "; ", @differ
                if @differ;
        }
-       print "origs $file f.same=$found_same #f._differ=$#found_differ\n";
+       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",
@@ -2120,14 +2139,14 @@ sub generate_commits_from_dsc () {
            $input = $compr_fh;
        }
 
-       rmtree "../unpack-tar";
-       mkdir "../unpack-tar" or die $!;
+       rmtree "_unpack-tar";
+       mkdir "_unpack-tar" or die $!;
        my @tarcmd = qw(tar -x -f -
                        --no-same-owner --no-same-permissions
                        --no-acls --no-xattrs --no-selinux);
        my $tar_pid = fork // die $!;
        if (!$tar_pid) {
-           chdir "../unpack-tar" or die $!;
+           chdir "_unpack-tar" or die $!;
            open STDIN, "<&", $input or die $!;
            exec @tarcmd;
            die "dgit (child): exec $tarcmd[0]: $!";
@@ -2141,11 +2160,21 @@ sub generate_commits_from_dsc () {
        # finally, we have the results in "tarball", but maybe
        # with the wrong permissions
 
-       runcmd qw(chmod -R +rwX ../unpack-tar);
-       changedir "../unpack-tar";
-       my ($tree) = mktree_in_ud_from_only_subdir(1);
-       changedir "../../unpack";
-       rmtree "../unpack-tar";
+       runcmd qw(chmod -R +rwX _unpack-tar);
+       changedir "_unpack-tar";
+       remove_stray_gits($f);
+       mktree_in_ud_here();
+       
+       my ($tree) = git_add_write_tree();
+       my $tentries = cmdoutput @git, qw(ls-tree -z), $tree;
+       if ($tentries =~ m/^\d+ tree (\w+)\t[^\000]+\000$/s) {
+           $tree = $1;
+           printdebug "one subtree $1\n";
+       } else {
+           printdebug "multiple subtrees\n";
+       }
+       changedir "..";
+       rmtree "_unpack-tar";
 
        my $ent = [ $f, $tree ];
        push @tartrees, {
@@ -2184,7 +2213,7 @@ sub generate_commits_from_dsc () {
     push @cmd, qw(-x --), $dscfn;
     runcmd @cmd;
 
-    my ($tree,$dir) = mktree_in_ud_from_only_subdir();
+    my ($tree,$dir) = mktree_in_ud_from_only_subdir("source package");
     if (madformat $dsc->{format}) { 
        check_for_vendor_patches();
     }
@@ -2194,8 +2223,7 @@ sub generate_commits_from_dsc () {
        my @pcmd = qw(dpkg-source --before-build .);
        runcmd shell_cmd 'exec >/dev/null', @pcmd;
        rmtree '.pc';
-       runcmd @git, qw(add -Af);
-       $dappliedtree = git_write_tree();
+       $dappliedtree = git_add_write_tree();
     }
 
     my @clogcmd = qw(dpkg-parsechangelog --format rfc822 --all);
@@ -2340,6 +2368,8 @@ END
        my $path = $ENV{PATH} or die;
 
        foreach my $use_absurd (qw(0 1)) {
+           runcmd @git, qw(checkout -q unpa);
+           runcmd @git, qw(update-ref -d refs/heads/patch-queue/unpa);
            local $ENV{PATH} = $path;
            if ($use_absurd) {
                chomp $@;
@@ -2356,11 +2386,12 @@ END
                die "only absurd git-apply!\n" if !$use_absurd
                    && forceing [qw(import-gitapply-absurd)];
 
-               local $ENV{PATH} = $path if $use_absurd;
+               local $ENV{DGIT_ABSURD_DEBUG} = $debuglevel if $use_absurd;
+               local $ENV{PATH} = $path                    if $use_absurd;
 
                my @showcmd = (gbp_pq, qw(import));
                my @realcmd = shell_cmd
-                   'exec >/dev/null 2>../../gbp-pq-output', @showcmd;
+                   'exec >/dev/null 2>>../../gbp-pq-output', @showcmd;
                debugcmd "+",@realcmd;
                if (system @realcmd) {
                    die +(shellquote @showcmd).
@@ -2472,18 +2503,41 @@ sub ensure_we_have_orig () {
     }
 }
 
-sub git_fetch_us () {
-    # Want to fetch only what we are going to use, unless
-    # deliberately-not-ff, in which case we must fetch everything.
+#---------- git fetch ----------
 
-    my @specs = deliberately_not_fast_forward ? qw(tags/*) :
-       map { "tags/$_" }
-       (quiltmode_splitbrain
-        ? (map { $_->('*',access_nomdistro) }
-           \&debiantag_new, \&debiantag_maintview)
-        : debiantags('*',access_nomdistro));
-    push @specs, server_branch($csuite);
-    push @specs, qw(heads/*) if deliberately_not_fast_forward;
+sub lrfetchrefs () { return "refs/dgit-fetch/".access_basedistro(); }
+sub lrfetchref () { return lrfetchrefs.'/'.server_branch($csuite); }
+
+# We fetch some parts of lrfetchrefs/*.  Ideally we delete these
+# locally fetched refs because they have unhelpful names and clutter
+# up gitk etc.  So we track whether we have "used up" head ref (ie,
+# whether we have made another local ref which refers to this object).
+#
+# (If we deleted them unconditionally, then we might end up
+# re-fetching the same git objects each time dgit fetch was run.)
+#
+# So, leach use of lrfetchrefs needs to be accompanied by arrangements
+# in git_fetch_us to fetch the refs in question, and possibly a call
+# to lrfetchref_used.
+
+our (%lrfetchrefs_f, %lrfetchrefs_d);
+# $lrfetchrefs_X{lrfetchrefs."/heads/whatever"} = $objid
+
+sub lrfetchref_used ($) {
+    my ($fullrefname) = @_;
+    my $objid = $lrfetchrefs_f{$fullrefname};
+    $lrfetchrefs_d{$fullrefname} = $objid if defined $objid;
+}
+
+sub git_lrfetch_sane {
+    my ($supplementary, @specs) = @_;
+    # Make a 'refs/'.lrfetchrefs.'/*' be just like on server,
+    # at least as regards @specs.  Also leave the results in
+    # %lrfetchrefs_f, and arrange for lrfetchref_used to be
+    # able to clean these up.
+    #
+    # With $supplementary==1, @specs must not contain wildcards
+    # and we add to our previous fetches (non-atomically).
 
     # This is rather miserable:
     # When git fetch --prune is passed a fetchspec ending with a *,
@@ -2507,30 +2561,33 @@ sub git_fetch_us () {
     # git fetch to try to generate it.  If we don't manage to generate
     # the target state, we try again.
 
-    printdebug "git_fetch_us specs @specs\n";
+    my $url = access_giturl();
+
+    printdebug "git_lrfetch_sane suppl=$supplementary specs @specs\n";
 
     my $specre = join '|', map {
        my $x = $_;
        $x =~ s/\W/\\$&/g;
-       $x =~ s/\\\*$/.*/;
+       my $wildcard = $x =~ s/\\\*$/.*/;
+       die if $wildcard && $supplementary;
        "(?:refs/$x)";
     } @specs;
-    printdebug "git_fetch_us specre=$specre\n";
+    printdebug "git_lrfetch_sane specre=$specre\n";
     my $wanted_rref = sub {
        local ($_) = @_;
-       return m/^(?:$specre)$/o;
+       return m/^(?:$specre)$/;
     };
 
     my $fetch_iteration = 0;
     FETCH_ITERATION:
     for (;;) {
-       printdebug "git_fetch_us iteration $fetch_iteration\n";
+       printdebug "git_lrfetch_sane iteration $fetch_iteration\n";
         if (++$fetch_iteration > 10) {
            fail "too many iterations trying to get sane fetch!";
        }
 
        my @look = map { "refs/$_" } @specs;
-       my @lcmd = (@git, qw(ls-remote -q --refs), access_giturl(), @look);
+       my @lcmd = (@git, qw(ls-remote -q --refs), $url, @look);
        debugcmd "|",@lcmd;
 
        my %wantr;
@@ -2556,13 +2613,14 @@ END
            "+refs/$_:".lrfetchrefs."/$_";
        } @specs;
 
-       printdebug "git_fetch_us fspecs @fspecs\n";
+       printdebug "git_lrfetch_sane fspecs @fspecs\n";
 
-       my @fcmd = (@git, qw(fetch -p -n -q), access_giturl(), @fspecs);
-       runcmd_ordryrun_local @git, qw(fetch -p -n -q), access_giturl(),
-           @fspecs;
+       my @fcmd = (@git, qw(fetch -p -n -q), $url, @fspecs);
+       runcmd_ordryrun_local @fcmd if @fspecs;
 
-       %lrfetchrefs_f = ();
+       if (!$supplementary) {
+           %lrfetchrefs_f = ();
+       }
        my %objgot;
 
        git_for_each_ref(lrfetchrefs, sub {
@@ -2571,6 +2629,10 @@ END
            $objgot{$objid} = 1;
        });
 
+       if ($supplementary) {
+           last;
+       }
+
        foreach my $lrefname (sort keys %lrfetchrefs_f) {
            my $rrefname = 'refs'.substr($lrefname, length lrfetchrefs);
            if (!exists $wantr{$rrefname}) {
@@ -2612,8 +2674,35 @@ END
        }
        last;
     }
-    printdebug "git_fetch_us: git fetch --no-insane emulation complete\n",
+
+    if (defined $csuite) {
+       printdebug "git_lrfetch_sane: tidying any old suite lrfetchrefs\n";
+       git_for_each_ref("refs/dgit-fetch/$csuite", sub {
+           my ($objid,$objtype,$lrefname,$reftail) = @_;
+           next if $lrfetchrefs_f{$lrefname}; # $csuite eq $distro ?
+           runcmd_ordryrun_local @git, qw(update-ref -d), $lrefname;
+       });
+    }
+
+    printdebug "git_lrfetch_sane: git fetch --no-insane emulation complete\n",
        Dumper(\%lrfetchrefs_f);
+}
+
+sub git_fetch_us () {
+    # Want to fetch only what we are going to use, unless
+    # deliberately-not-ff, in which case we must fetch everything.
+
+    my @specs = deliberately_not_fast_forward ? qw(tags/*) :
+       map { "tags/$_" }
+       (quiltmode_splitbrain
+        ? (map { $_->('*',access_nomdistro) }
+           \&debiantag_new, \&debiantag_maintview)
+        : debiantags('*',access_nomdistro));
+    push @specs, server_branch($csuite);
+    push @specs, $rewritemap;
+    push @specs, qw(heads/*) if deliberately_not_fast_forward;
+
+    git_lrfetch_sane 0, @specs;
 
     my %here;
     my @tagpats = debiantags('*',access_nomdistro);
@@ -2640,6 +2729,8 @@ END
     });
 }
 
+#---------- dsc and archive handling ----------
+
 sub mergeinfo_getclogp ($) {
     # Ensures thit $mi->{Clogp} exists and returns it
     my ($mi) = @_;
@@ -2668,6 +2759,124 @@ sub fetch_from_archive_record_2 ($) {
     }
 }
 
+sub parse_dsc_field ($$) {
+    my ($dsc, $what) = @_;
+    my $f;
+    foreach my $field (@ourdscfield) {
+       $f = $dsc->{$field};
+       last if defined $f;
+    }
+    if (!defined $f) {
+       progress "$what: NO git hash";
+    } 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)";
+       $dsc_hint_tag = [ $dsc_hint_tag ];
+    } elsif ($f =~ m/^\w+\s*$/) {
+       $dsc_hash = $&;
+       $dsc_distro //= cfg qw(dgit.default.old-dsc-distro
+                              dgit.default.distro);
+       $dsc_hint_tag = [ debiantags +(getfield $dsc, 'Version'),
+                         $dsc_distro ];
+       progress "$what: specified git hash";
+    } else {
+       fail "$what: invalid Dgit info";
+    }
+}
+
+sub resolve_dsc_field_commit ($$) {
+    my ($already_distro, $already_mapref) = @_;
+
+    return unless defined $dsc_hash;
+
+    my $mapref =
+       defined $already_mapref &&
+       ($already_distro eq $dsc_distro || !$chase_dsc_distro)
+       ? $already_mapref : undef;
+
+    my $do_fetch;
+    $do_fetch = sub {
+       my ($what, @fetch) = @_;
+
+       local $idistro = $dsc_distro;
+       my $lrf = lrfetchrefs;
+
+       if (!$chase_dsc_distro) {
+           progress
+               "not chasing .dsc distro $dsc_distro: not fetching $what";
+           return 0;
+       }
+
+       progress
+           ".dsc names distro $dsc_distro: fetching $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
+for which we have no configured url and .dsc provides no hint
+END
+           my $proto =
+               $dsc_hint_url =~ m#^([-+0-9a-zA-Z]+):# ? $1 :
+               $dsc_hint_url =~ m#^/# ? 'file' : 'bad-syntax';
+           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
+for which we have no configured url;
+.dsc provices hinted url with protocol $proto which is unsafe.
+(can be overridden by config - consult documentation)
+END
+           $url = $dsc_hint_url;
+       }
+
+       git_lrfetch_sane 1, @fetch;
+
+       return $lrf;
+    };
+
+    my $rewrite_enable = do {
+       local $idistro = $dsc_distro;
+       access_cfg('rewrite-map-enable', 'RETURN-UNDEF');
+    };
+
+    if (parse_cfg_bool 'rewrite-map-enable', 'true', $rewrite_enable) {
+       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
+               "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";
+           } else {
+               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) &&
+           defined git_cat_file $dsc_hash
+           or fail <<END;
+.dsc Dgit metadata requires commit $dsc_hash
+but we could not obtain that object anywhere.
+END
+       foreach my $t (@tags) {
+           my $fullrefname = $lrf.'/'.$t;
+           print STDERR "CHK $t $fullrefname ".Dumper(\%lrfetchrefs_f);
+           next unless $lrfetchrefs_f{$fullrefname};
+           next unless is_fast_fwd "$fullrefname~0", $dsc_hash;
+           lrfetchref_used $fullrefname;
+       }
+    }
+}
+
 sub fetch_from_archive () {
     ensure_setup_existing_tree();
 
@@ -2679,17 +2888,9 @@ sub fetch_from_archive () {
     get_archive_dsc();
 
     if ($dsc) {
-       foreach my $field (@ourdscfield) {
-           $dsc_hash = $dsc->{$field};
-           last if defined $dsc_hash;
-       }
-       if (defined $dsc_hash) {
-           $dsc_hash =~ m/\w+/ or fail "invalid hash in .dsc \`$dsc_hash'";
-           $dsc_hash = $&;
-           progress "last upload to archive specified git hash";
-       } else {
-           progress "last upload to archive has NO git hash";
-       }
+       parse_dsc_field($dsc, 'last upload to archive');
+       resolve_dsc_field_commit access_basedistro,
+           lrfetchrefs."/".$rewritemap
     } else {
        progress "no version available from the archive";
     }
@@ -3072,6 +3273,167 @@ sub setup_new_tree () {
     setup_useremail();
 }
 
+sub multisuite_suite_child ($$$) {
+    my ($tsuite, $merginputs, $fn) = @_;
+    # in child, sets things up, calls $fn->(), and returns undef
+    # in parent, returns canonical suite name for $tsuite
+    my $canonsuitefh = IO::File::new_tmpfile;
+    my $pid = fork // die $!;
+    if (!$pid) {
+       $isuite = $tsuite;
+       $us .= " [$isuite]";
+       $debugprefix .= " ";
+       progress "fetching $tsuite...";
+       canonicalise_suite();
+       print $canonsuitefh $csuite, "\n" or die $!;
+       close $canonsuitefh or die $!;
+       $fn->();
+       return undef;
+    }
+    waitpid $pid,0 == $pid or die $!;
+    fail "failed to obtain $tsuite: ".waitstatusmsg() if $? && $?!=256*4;
+    seek $canonsuitefh,0,0 or die $!;
+    local $csuite = <$canonsuitefh>;
+    die $! unless defined $csuite && chomp $csuite;
+    if ($? == 256*4) {
+       printdebug "multisuite $tsuite missing\n";
+       return $csuite;
+    }
+    printdebug "multisuite $tsuite ok (canon=$csuite)\n";
+    push @$merginputs, {
+        Ref => lrref,
+        Info => $csuite,
+    };
+    return $csuite;
+}
+
+sub fork_for_multisuite ($) {
+    my ($before_fetch_merge) = @_;
+    # if nothing unusual, just returns ''
+    #
+    # if multisuite:
+    # returns 0 to caller in child, to do first of the specified suites
+    # in child, $csuite is not yet set
+    #
+    # returns 1 to caller in parent, to finish up anything needed after
+    # in parent, $csuite is set to canonicalised portmanteau
+
+    my $org_isuite = $isuite;
+    my @suites = split /\,/, $isuite;
+    return '' unless @suites > 1;
+    printdebug "fork_for_multisuite: @suites\n";
+
+    my @mergeinputs;
+
+    my $cbasesuite = multisuite_suite_child($suites[0], \@mergeinputs,
+                                           sub { });
+    return 0 unless defined $cbasesuite;
+
+    fail "package $package missing in (base suite) $cbasesuite"
+       unless @mergeinputs;
+
+    my @csuites = ($cbasesuite);
+
+    $before_fetch_merge->();
+
+    foreach my $tsuite (@suites[1..$#suites]) {
+       my $csubsuite = multisuite_suite_child($tsuite, \@mergeinputs,
+                                              sub {
+            @end = ();
+            fetch();
+           exit 0;
+       });
+       # xxx collecte the ref here
+
+       $csubsuite =~ s/^\Q$cbasesuite\E-/-/;
+       push @csuites, $csubsuite;
+    }
+
+    foreach my $mi (@mergeinputs) {
+       my $ref = git_get_ref $mi->{Ref};
+       die "$mi->{Ref} ?" unless length $ref;
+       $mi->{Commit} = $ref;
+    }
+
+    $csuite = join ",", @csuites;
+
+    my $previous = git_get_ref lrref;
+    if ($previous) {
+       unshift @mergeinputs, {
+            Commit => $previous,
+            Info => "local combined tracking branch",
+            Warning =>
+ "archive seems to have rewound: local tracking branch is ahead!",
+        };
+    }
+
+    foreach my $ix (0..$#mergeinputs) {
+       $mergeinputs[$ix]{Index} = $ix;
+    }
+
+    @mergeinputs = sort {
+       -version_compare(mergeinfo_version $a,
+                        mergeinfo_version $b) # highest version first
+           or
+       $a->{Index} <=> $b->{Index}; # earliest in spec first
+    } @mergeinputs;
+
+    my @needed;
+
+  NEEDED:
+    foreach my $mi (@mergeinputs) {
+       printdebug "multisuite merge check $mi->{Info}\n";
+       foreach my $previous (@needed) {
+           next unless is_fast_fwd $mi->{Commit}, $previous->{Commit};
+           printdebug "multisuite merge un-needed $previous->{Info}\n";
+           next NEEDED;
+       }
+       push @needed, $mi;
+       printdebug "multisuite merge this-needed\n";
+       $mi->{Character} = '+';
+    }
+
+    $needed[0]{Character} = '*';
+
+    my $output = $needed[0]{Commit};
+
+    if (@needed > 1) {
+       printdebug "multisuite merge nontrivial\n";
+       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";
+
+       foreach my $mi (sort { $a->{Index} <=> $b->{Index} } @mergeinputs) {
+           printdebug "multisuite merge include $mi->{Info}\n";
+           $mi->{Character} //= ' ';
+           $commit .= "parent $mi->{Commit}\n";
+           $msg .= sprintf " %s  %-25s %s\n",
+               $mi->{Character},
+               (mergeinfo_version $mi),
+               $mi->{Info};
+       }
+       my $authline = clogp_authline mergeinfo_getclogp $needed[0];
+       $msg .= "\nKey\n".
+           " * marks the highest version branch, which choose to use\n".
+           " + marks each branch which was not already an ancestor\n\n".
+           "[dgit multi-suite $csuite]\n";
+       $commit .=
+           "author $authline\n".
+           "committer $authline\n\n";
+       $output = make_commit_text $commit.$msg;
+       printdebug "multisuite merge generated $output\n";
+    }
+
+    fetch_from_archive_record_1($output);
+    fetch_from_archive_record_2($output);
+
+    progress "calculated combined tracking suite $csuite";
+
+    return 1;
+}
+
 sub clone_set_head () {
     open H, "> .git/HEAD" or die $!;
     print H "ref: ".lref()."\n" or die $!;
@@ -3083,7 +3445,7 @@ sub clone_finish ($) {
     runcmd qw(bash -ec), <<'END';
         set -o pipefail
         git ls-tree -r --name-only -z HEAD | \
-        xargs -0r touch -r . --
+        xargs -0r touch -h -r . --
 END
     printdone "ready for work in $dstdir";
 }
@@ -3091,6 +3453,19 @@ END
 sub clone ($) {
     my ($dstdir) = @_;
     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";
+        changedir $dstdir;
+    });
+    if ($multi_fetched) {
+        printdebug "multi clone after fetch merge\n";
+       clone_set_head();
+       clone_finish($dstdir);
+       exit 0;
+    }
+    printdebug "clone main body\n";
+
     canonicalise_suite();
     my $hasgit = check_for_git();
     mkdir $dstdir or fail "create \`$dstdir': $!";
@@ -3128,7 +3503,9 @@ sub fetch () {
 }
 
 sub pull () {
-    fetch();
+    my $multi_fetched = fork_for_multisuite(sub { });
+    fetch() unless $multi_fetched; # parent
+    return if $multi_fetched eq '0'; # child
     runcmd_ordryrun_local @git, qw(merge -m),"Merge from $csuite [dgit]",
         lrref();
     printdone "fetched to ".lrref()." and merged into HEAD";
@@ -3332,7 +3709,7 @@ tree $tree
 parent $dgitview
 parent $archive_hash
 author $authline
-commiter $authline
+committer $authline
 
 $msg_msg
 
@@ -3473,7 +3850,21 @@ sub push_tagwants ($$$$) {
            TfSuffix => '-maintview',
             View => 'maint',
         };
-    }
+    } elsif ($dodep14tag eq 'no' ? 0
+            : $dodep14tag eq 'want' ? access_cfg_tagformats_can_splitbrain
+            : $dodep14tag eq 'always'
+            ? (access_cfg_tagformats_can_splitbrain or fail <<END)
+--dep14tag-always (or equivalent in config) means server must support
+ both "new" and "maint" tag formats, but config says it doesn't.
+END
+           : die "$dodep14tag ?") {
+       push @tagwants, {
+           TagFn => \&debiantag_maintview,
+           Objid => $dgithead,
+           TfSuffix => '-dgit',
+           View => 'dgit',
+        };
+    };
     foreach my $tw (@tagwants) {
        $tw->{Tag} = $tw->{TagFn}($cversion, access_nomdistro);
        $tw->{Tfn} = sub { $tfbase.$tw->{TfSuffix}.$_[0]; };
@@ -3489,7 +3880,11 @@ sub push_mktags ($$ $$ $) {
 
     die unless $tagwants->[0]{View} eq 'dgit';
 
-    $dsc->{$ourdscfield[0]} = $tagwants->[0]{Objid};
+    my $declaredistro = access_nomdistro();
+    my $reader_giturl = do { local $access_forpush=0; access_giturl(); };
+    $dsc->{$ourdscfield[0]} = join " ",
+       $tagwants->[0]{Objid}, $declaredistro, $tagwants->[0]{Tag},
+       $reader_giturl;
     $dsc->save("$dscfn.tmp") or die $!;
 
     my $changes = parsecontrol($changesfile,$changesfilewhat);
@@ -3506,7 +3901,6 @@ sub push_mktags ($$ $$ $) {
     # to control the "tagger" (b) we can do remote signing
     my $authline = clogp_authline $clogp;
     my $delibs = join(" ", "",@deliberatelies);
-    my $declaredistro = access_nomdistro();
 
     my $mktag = sub {
        my ($tw) = @_;
@@ -3620,7 +4014,7 @@ END
 
     my $dscpath = "$buildproductsdir/$dscfn";
     stat_exists $dscpath or
-       fail "looked for .dsc $dscfn, but $!;".
+       fail "looked for .dsc $dscpath, but $!;".
            " maybe you forgot to build";
 
     responder_send_file('dsc', $dscpath);
@@ -3687,7 +4081,7 @@ END
     progress "checking that $dscfn corresponds to HEAD";
     runcmd qw(dpkg-source -x --),
         $dscpath =~ m#^/# ? $dscpath : "../../../$dscpath";
-    my ($tree,$dir) = mktree_in_ud_from_only_subdir();
+    my ($tree,$dir) = mktree_in_ud_from_only_subdir("source package");
     check_for_vendor_patches() if madformat($dsc->{format});
     changedir '../../../..';
     my @diffcmd = (@git, qw(diff --quiet), $tree, $dgithead);
@@ -3803,8 +4197,12 @@ END
     runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), $dgithead;
 
     supplementary_message(<<'END');
-Push failed, after updating the remote git repository.
-If you want to try again, you must use a new version number.
+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.
+If you need to change the package, you must use a new version number.
 END
     if ($we_are_responder) {
        my $dryrunsuffix = act_local() ? "" : ".tmp";
@@ -3838,7 +4236,6 @@ END
 
 sub cmd_clone {
     parseopts();
-    notpushing();
     my $dstdir;
     badusage "-p is not allowed with clone; specify as argument instead"
        if defined $package;
@@ -3853,8 +4250,9 @@ sub cmd_clone {
     } else {
        badusage "incorrect arguments to dgit clone";
     }
-    $dstdir ||= "$package";
+    notpushing();
 
+    $dstdir ||= "$package";
     if (stat_exists $dstdir) {
        fail "$dstdir already exists";
     }
@@ -3868,6 +4266,7 @@ sub cmd_clone {
                return if $!==&ENOENT;
                die "chdir $cwd_remove: $!";
            }
+           printdebug "clone rmonerror removing $dstdir\n";
            if (stat $dstdir) {
                rmtree($dstdir) or die "remove $dstdir: $!\n";
            } elsif (grep { $! == $_ }
@@ -3892,27 +4291,30 @@ sub branchsuite () {
 }
 
 sub fetchpullargs () {
-    notpushing();
     if (!defined $package) {
        my $sourcep = parsecontrol('debian/control','debian/control');
        $package = getfield $sourcep, 'Source';
     }
     if (@ARGV==0) {
-#      $isuite = branchsuite();  # this doesn't work because dak hates canons
+       $isuite = branchsuite();
        if (!$isuite) {
            my $clogp = parsechangelog();
-           $isuite = getfield $clogp, 'Distribution';
+           my $clogsuite = getfield $clogp, 'Distribution';
+           $isuite= $clogsuite if $clogsuite ne 'UNRELEASED';
        }
     } elsif (@ARGV==1) {
        ($isuite) = @ARGV;
     } else {
        badusage "incorrect arguments to dgit fetch or dgit pull";
     }
+    notpushing();
 }
 
 sub cmd_fetch {
     parseopts();
     fetchpullargs();
+    my $multi_fetched = fork_for_multisuite(sub { });
+    exit 0 if $multi_fetched;
     fetch();
 }
 
@@ -4235,7 +4637,7 @@ END
        local $ENV{'EDITOR'} = cmdoutput qw(realpath --), $0;
        local $ENV{'VISUAL'} = $ENV{'EDITOR'};
        local $ENV{$fakeeditorenv} = cmdoutput qw(realpath --), $descfn;
-       runcmd @dpkgsource, qw(--commit .), $patchname;
+       runcmd @dpkgsource, qw(--commit --include-removal .), $patchname;
     }
 }
 
@@ -4270,17 +4672,21 @@ sub quiltify_trees_differ ($$;$$$) {
 
        if ($unrepres) {
            eval {
-               die "deleted\n" unless $newmode =~ m/[^0]/;
-               die "not a plain file\n" unless $newmode =~ m/^10\d{4}$/;
-               if ($oldmode =~ m/[^0]/) {
+               die "not a plain file\n"
+                   unless $newmode =~ m/^10\d{4}$/ ||
+                          $oldmode =~ m/^10\d{4}$/;
+               if ($oldmode =~ m/[^0]/ &&
+                   $newmode =~ m/[^0]/) {
                    die "mode changed\n" if $oldmode ne $newmode;
                } else {
-                   die "non-default mode\n" unless $newmode =~ m/^100644$/;
+                   die "non-default mode\n"
+                       unless $newmode =~ m/^100644$/ ||
+                              $oldmode =~ m/^100644$/;
                }
            };
            if ($@) {
                local $/="\n"; chomp $@;
-               push @$unrepres, [ $f, $@ ];
+               push @$unrepres, [ $f, "$@ ($oldmode->$newmode)" ];
            }
        }
 
@@ -4713,13 +5119,10 @@ sub build_maybe_quilt_fixup () {
     check_for_vendor_patches();
 
     if (quiltmode_splitbrain) {
-       foreach my $needtf (qw(new maint)) {
-           next if grep { $_ eq $needtf } access_cfg_tagformats;
-           fail <<END
+       fail <<END unless access_cfg_tagformats_can_splitbrain;
 quilt mode $quilt_mode requires split view so server needs to support
  both "new" and "maint" tag formats, but config says it doesn't.
 END
-       }
     }
 
     my $clogp = parsechangelog();
@@ -5008,13 +5411,13 @@ sub quilt_fixup_multipatch ($$$) {
 
     changedir 'fake';
 
-    remove_stray_gits();
+    remove_stray_gits("source package");
     mktree_in_ud_here();
 
     rmtree '.pc';
 
-    runcmd @git, qw(add -Af .);
-    my $unapplied=git_write_tree();
+    runcmd @git, qw(checkout -f), $headref, qw(-- debian);
+    my $unapplied=git_add_write_tree();
     printdebug "fake orig tree object $unapplied\n";
 
     ensuredir '.pc';
@@ -5046,8 +5449,7 @@ END
 
     changedir '../fake';
     rmtree '.pc';
-    runcmd @git, qw(add -Af .);
-    my $oldtiptree=git_write_tree();
+    my $oldtiptree=git_add_write_tree();
     printdebug "fake o+d/p tree object $unapplied\n";
     changedir '../work';
 
@@ -5397,6 +5799,7 @@ sub postbuild_mergechanges_vanilla ($) {
 }
 
 sub cmd_build {
+    build_prep_early();
     my @dbp = (@dpkgbuildpackage, qw(-us -uc), changesopts_initial(), @ARGV);
     my $wantsrc = massage_dbp_args \@dbp;
     if ($wantsrc > 0) {
@@ -5489,6 +5892,7 @@ sub cmd_gbp_build {
 sub cmd_git_build { cmd_gbp_build(); } # compatibility with <= 1.0
 
 sub build_source {
+    build_prep_early();
     my $our_cleanmode = $cleanmode;
     if ($need_split_build_invocation) {
        # Pretend that clean is being done some other way.  This
@@ -5549,6 +5953,7 @@ sub build_source {
 }
 
 sub cmd_build_source {
+    build_prep_early();
     badusage "build-source takes no additional arguments" if @ARGV;
     build_source();
     maybe_unapply_patches_again();
@@ -5577,10 +5982,7 @@ END
 
 sub cmd_quilt_fixup {
     badusage "incorrect arguments to dgit quilt-fixup" if @ARGV;
-    my $clogp = parsechangelog();
-    $version = getfield $clogp, 'Version';
-    $package = getfield $clogp, 'Source';
-    check_not_dirty();
+    build_prep_early();
     clean_tree();
     build_maybe_quilt_fixup();
 }
@@ -5648,30 +6050,35 @@ sub cmd_import_dsc {
 
     parse_dscdata();
 
-    my $dgit_commit = $dsc->{$ourdscfield[0]};
-    if (defined $dgit_commit && 
-       !forceing [qw(import-dsc-with-dgit-field)]) {
-       $dgit_commit =~ m/\w+/ or fail "invalid hash in .dsc";
+    $package = getfield $dsc, 'Source';
+
+    parse_dsc_field($dsc, "Dgit metadata in .dsc")
+       unless forceing [qw(import-dsc-with-dgit-field)];
+
+    if (defined $dsc_hash) {
        progress "dgit: import-dsc of .dsc with Dgit field, using git hash";
+       resolve_dsc_field_commit undef, undef;
+    }
+    if (defined $dsc_hash) {
        my @cmd = (qw(sh -ec),
-                  "echo $dgit_commit | git cat-file --batch-check");
+                  "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 $dgit_commit
+.dsc contains Dgit field referring to object $dsc_hash
 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.
 END
        }
-       if ($oldhash && !is_fast_fwd $oldhash, $dgit_commit) {
+       if ($oldhash && !is_fast_fwd $oldhash, $dsc_hash) {
            if ($force > 0) {
                progress "Not fast forward, forced update.";
            } else {
-               fail "Not fast forward to $dgit_commit";
+               fail "Not fast forward to $dsc_hash";
            }
        }
        @cmd = (@git, qw(update-ref -m), "dgit import-dsc (Dgit): $info",
-               $dstbranch, $dgit_commit);
+               $dstbranch, $dsc_hash);
        runcmd @cmd;
        progress "dgit: import-dsc updated git ref $dstbranch";
        return 0;
@@ -5684,7 +6091,6 @@ Specify  +$specbranch to overwrite, discarding existing history
 END
        if $oldhash && !$force;
 
-    $package = getfield $dsc, 'Source';
     my @dfi = dsc_files_info();
     foreach my $fi (@dfi) {
        my $f = $fi->{Filename};
@@ -5704,7 +6110,7 @@ END
        $there .= "/$f";
        symlink $there, $here or fail "symlink $there to $here: $!";
        progress "made symlink $here -> $there";
-       print STDERR Dumper($fi);
+#      print STDERR Dumper($fi);
     }
     my @mergeinputs = generate_commits_from_dsc();
     die unless @mergeinputs == 1;
@@ -5718,10 +6124,14 @@ END
            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;
 tree $tree
 parent $newhash
 parent $oldhash
+author $authline
+committer $authline
 
 Merge $package ($version) import into $dstbranch
 END
@@ -5749,18 +6159,28 @@ sub cmd_clone_dgit_repos_server {
     badusage "need destination argument" unless @ARGV==1;
     my ($destdir) = @ARGV;
     $package = '_dgit-repos-server';
+    local $access_forpush = 0;
     my @cmd = (@git, qw(clone), access_giturl(), $destdir);
     debugcmd ">",@cmd;
     exec @cmd or fail "exec git clone: $!\n";
 }
 
+sub cmd_print_dgit_repos_server_source_url {
+    badusage "no arguments allowed to dgit print-dgit-repos-server-source-url"
+       if @ARGV;
+    $package = '_dgit-repos-server';
+    local $access_forpush = 0;
+    my $url = access_giturl();
+    print $url, "\n" or die $!;
+}
+
 sub cmd_setup_mergechangelogs {
     badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
     setup_mergechangelogs(1);
 }
 
 sub cmd_setup_useremail {
-    badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
+    badusage "no arguments allowed to dgit setup-useremail" if @ARGV;
     setup_useremail(1);
 }
 
@@ -5886,12 +6306,24 @@ sub parseopts () {
            } elsif (m/^--no-rm-on-error$/s) {
                push @ropts, $_;
                $rmonerror = 0;
+           } elsif (m/^--no-chase-dsc-distro$/s) {
+               push @ropts, $_;
+               $chase_dsc_distro = 0;
            } elsif (m/^--overwrite$/s) {
                push @ropts, $_;
                $overwrite_version = '';
            } elsif (m/^--overwrite=(.+)$/s) {
                push @ropts, $_;
                $overwrite_version = $1;
+           } elsif (m/^--dep14tag$/s) {
+               push @ropts, $_;
+               $dodep14tag= 'want';
+           } elsif (m/^--no-dep14tag$/s) {
+               push @ropts, $_;
+               $dodep14tag= 'no';
+           } elsif (m/^--always-dep14tag$/s) {
+               push @ropts, $_;
+               $dodep14tag= 'always';
            } elsif (m/^--delayed=(\d+)$/s) {
                push @ropts, $_;
                push @dput, $_;
@@ -5922,6 +6354,11 @@ sub parseopts () {
                # undocumented, for testing
                push @ropts, $_;
                $need_split_build_invocation = 1;
+           } elsif (m/^--config-lookup-explode=(.+)$/s) {
+               # undocumented, for testing
+               push @ropts, $_;
+               $gitcfgs{cmdline}{$1} = 'CONFIG-LOOKUP-EXPLODE';
+               # ^ it's supposed to be an array ref
            } elsif (m/^(--[-0-9a-z]+)(=|$)/ && ($oi = $valopts_long{$1})) {
                $val = $2 ? $' : undef; #';
                $valopt->($oi->{Long});
@@ -6010,7 +6447,11 @@ END
 }
 
 
-sub finalise_opts_opts () {
+sub parseopts_late_defaults () {
+    $isuite //= cfg("dgit-distro.$idistro.default-suite", 'RETURN-UNDEF')
+       if defined $idistro;
+    $isuite //= cfg('dgit.default.default-suite');
+
     foreach my $k (keys %opts_opt_map) {
        my $om = $opts_opt_map{$k};
 
@@ -6037,6 +6478,40 @@ sub finalise_opts_opts () {
                     @$om[$insertpos..$#$om] );
        }
     }
+
+    if (!defined $rmchanges) {
+       local $access_forpush;
+       $rmchanges = access_cfg_bool(0, 'rm-old-changes');
+    }
+
+    if (!defined $quilt_mode) {
+       local $access_forpush;
+       $quilt_mode = cfg('dgit.force.quilt-mode', 'RETURN-UNDEF')
+           // access_cfg('quilt-mode', 'RETURN-UNDEF')
+           // 'linear';
+       $quilt_mode =~ m/^($quilt_modes_re)$/ 
+           or badcfg "unknown quilt-mode \`$quilt_mode'";
+       $quilt_mode = $1;
+    }
+
+    if (!defined $dodep14tag) {
+       local $access_forpush;
+       $dodep14tag = access_cfg('dep14tag', 'RETURN-UNDEF') // 'want';
+       $dodep14tag =~ m/^($dodep14tag_re)$/ 
+           or badcfg "unknown dep14tag setting \`$dodep14tag'";
+       $dodep14tag = $1;
+    }
+
+    $need_split_build_invocation ||= quiltmode_splitbrain();
+
+    if (!defined $cleanmode) {
+       local $access_forpush;
+       $cleanmode = access_cfg('clean-mode', 'RETURN-UNDEF');
+       $cleanmode //= 'dpkg-source';
+
+       badcfg "unknown clean-mode \`$cleanmode'" unless
+           $cleanmode =~ m/^($cleanmode_re)$(?!\n)/s;
+    }
 }
 
 if ($ENV{$fakeeditorenv}) {
@@ -6061,32 +6536,6 @@ $cmd =~ y/-/_/;
 my $pre_fn = ${*::}{"pre_$cmd"};
 $pre_fn->() if $pre_fn;
 
-if (!defined $rmchanges) {
-    local $access_forpush;
-    $rmchanges = access_cfg_bool(0, 'rm-old-changes');
-}
-
-if (!defined $quilt_mode) {
-    local $access_forpush;
-    $quilt_mode = cfg('dgit.force.quilt-mode', 'RETURN-UNDEF')
-       // access_cfg('quilt-mode', 'RETURN-UNDEF')
-       // 'linear';
-    $quilt_mode =~ m/^($quilt_modes_re)$/ 
-       or badcfg "unknown quilt-mode \`$quilt_mode'";
-    $quilt_mode = $1;
-}
-
-$need_split_build_invocation ||= quiltmode_splitbrain();
-
-if (!defined $cleanmode) {
-    local $access_forpush;
-    $cleanmode = access_cfg('clean-mode', 'RETURN-UNDEF');
-    $cleanmode //= 'dpkg-source';
-
-    badcfg "unknown clean-mode \`$cleanmode'" unless
-       $cleanmode =~ m/^($cleanmode_re)$(?!\n)/s;
-}
-
 my $fn = ${*::}{"cmd_$cmd"};
 $fn or badusage "unknown operation $cmd";
 $fn->();