chiark / gitweb /
Convert to defvalopt: --distro, -d
[dgit.git] / dgit
diff --git a/dgit b/dgit
index c315c7f1606846ae74dde955052254b3ba5b8f76..89633444a5979c570bd0b4bf2514bd7e33f2d313 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -2,7 +2,7 @@
 # dgit
 # Integration between git and Debian-style archives
 #
-# Copyright (C)2013 Ian Jackson
+# Copyright (C)2013-2015 Ian Jackson
 #
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -18,7 +18,9 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 use strict;
-$SIG{__WARN__} = sub { die $_[0]; };
+
+use Debian::Dgit;
+setup_sigwarn();
 
 use IO::Handle;
 use Data::Dumper;
@@ -37,7 +39,8 @@ use Debian::Dgit;
 
 our $our_version = 'UNRELEASED'; ###substituted###
 
-our $rpushprotovsn = 2;
+our @rpushprotovsn_support = qw(3 2);
+our $protovsn;
 
 our $isuite = 'unstable';
 our $idistro;
@@ -52,9 +55,9 @@ our $new_package = 0;
 our $ignoredirty = 0;
 our $rmonerror = 1;
 our @deliberatelies;
-our %supersedes;
+our %previously;
 our $existing_package = 'dpkg';
-our $cleanmode = 'dpkg-source';
+our $cleanmode;
 our $changes_since_version;
 our $quilt_mode;
 our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck';
@@ -64,6 +67,7 @@ our $initiator_tempdir;
 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 (@git) = qw(git);
 our (@dget) = qw(dget);
@@ -88,18 +92,32 @@ our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
                      'sbuild' => \@sbuild,
                      'ssh' => \@ssh,
                      'dgit' => \@dgit,
+                     'git' => \@git,
                      'dpkg-source' => \@dpkgsource,
                      'dpkg-buildpackage' => \@dpkgbuildpackage,
                      'dpkg-genchanges' => \@dpkggenchanges,
                      'ch' => \@changesopts,
                      'mergechanges' => \@mergechanges);
 
-our %opts_opt_cmdonly = ('gpg' => 1);
+our %opts_opt_cmdonly = ('gpg' => 1, 'git' => 1);
+our %opts_cfg_insertpos = map {
+    $_,
+    scalar @{ $opts_opt_map{$_} }
+} keys %opts_opt_map;
+
+sub finalise_opts_opts();
 
 our $keyid;
 
 autoflush STDOUT 1;
 
+our $supplementary_message = '';
+
+END {
+    local ($@, $?);
+    print STDERR "! $_\n" foreach $supplementary_message =~ m/^.+$/mg;
+}
+
 our $remotename = 'dgit';
 our @ourdscfield = qw(Dgit Vcs-Dgit-Master);
 our $csuite;
@@ -111,7 +129,7 @@ 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/$isuite"; }
+sub lrfetchrefs () { return "refs/dgit-fetch/$csuite"; }
 
 sub stripepoch ($) {
     my ($vsn) = @_;
@@ -165,14 +183,17 @@ sub deliberately ($) {
 }
 
 sub deliberately_not_fast_forward () {
-    deliberately('not-fast-forward') ||
-       deliberately('TEST-not-fast-forward-dgit-only');
+    foreach (qw(not-fast-forward fresh-repo)) {
+       return 1 if deliberately($_) || deliberately("TEST-dgit-only-$_");
+    }
 }
 
 #---------- remote protocol support, common ----------
 
 # remote push initiator/responder protocol:
-#  < dgit-remote-push-ready [optional extra info ignored by old initiators]
+#  $ dgit remote-push-build-host <n-rargs> <rargs>... <push-args>...
+#  where <rargs> is <push-host-dir> <supported-proto-vsn>,... ...
+#  < dgit-remote-push-ready <actual-proto-vsn>
 #
 #  > file parsed-changelog
 #  [indicates that output of dpkg-parsechangelog follows]
@@ -267,7 +288,7 @@ sub protocol_send_file ($$) {
 
 sub protocol_read_bytes ($$) {
     my ($fh, $nbytes) = @_;
-    $nbytes =~ m/^[1-9]\d{0,5}$/ 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";
@@ -403,7 +424,8 @@ our $helpmsg = <<END;
 main usages:
   dgit [dgit-opts] clone [dgit-opts] package [suite] [./dir|/dir]
   dgit [dgit-opts] fetch|pull [dgit-opts] [suite]
-  dgit [dgit-opts] build [git-buildpackage-opts|dpkg-buildpackage-opts]
+  dgit [dgit-opts] build [dpkg-buildpackage-opts]
+  dgit [dgit-opts] sbuild [sbuild-opts]
   dgit [dgit-opts] push [dgit-opts] [suite]
   dgit [dgit-opts] rpush build-host:build-dir ...
 important dgit options:
@@ -443,20 +465,28 @@ our %defcfg = ('dgit.default.distro' => 'debian',
               'dgit.default.archive-query' => 'madison:',
               'dgit.default.sshpsql-dbname' => 'service=projectb',
               'dgit-distro.debian.archive-query' => 'ftpmasterapi:',
-              'dgit-distro.debian.git-host' => 'dgit-git.debian.net',
-              'dgit-distro.debian.git-user-force' => 'dgit',
-              'dgit-distro.debian.git-proto' => 'git+ssh://',
-              'dgit-distro.debian.git-path' => '/dgit/debian/repos',
-              'dgit-distro.debian.git-check' => 'ssh-cmd',
+              'dgit-distro.debian.git-check' => 'url',
+              'dgit-distro.debian.git-check-suffix' => '/info/refs',
+              'dgit-distro.debian.new-private-pushers' => 't',
+              'dgit-distro.debian/push.git-url' => '',
+              'dgit-distro.debian/push.git-host' => 'push.dgit.debian.org',
+              'dgit-distro.debian/push.git-user-force' => 'dgit',
+              'dgit-distro.debian/push.git-proto' => 'git+ssh://',
+              'dgit-distro.debian/push.git-path' => '/dgit/debian/repos',
+              'dgit-distro.debian/push.git-create' => 'true',
+              'dgit-distro.debian/push.git-check' => 'ssh-cmd',
  'dgit-distro.debian.archive-query-url', 'https://api.ftp-master.debian.org/',
- 'dgit-distro.debian.archive-query-tls-key',
-    '/etc/ssl/certs/%HOST%.pem:/etc/dgit/%HOST%.pem',
-              'dgit-distro.debian.diverts.alioth' => '/alioth',
-              'dgit-distro.debian/alioth.git-host' => 'git.debian.org',
-              'dgit-distro.debian/alioth.git-user-force' => '',
-              'dgit-distro.debian/alioth.git-proto' => 'git+ssh://',
-              'dgit-distro.debian/alioth.git-path' => '/git/dgit-repos/repos',
-              'dgit-distro.debian/alioth.git-create' => 'ssh-cmd',
+# 'dgit-distro.debian.archive-query-tls-key',
+#    '/etc/ssl/certs/%HOST%.pem:/etc/dgit/%HOST%.pem',
+# ^ this does not work because curl is broken nowadays
+# Fixing #790093 properly will involve providing providing the key
+# in some pacagke and maybe updating these paths.
+#
+# 'dgit-distro.debian.archive-query-tls-curl-args',
+#   '--ca-path=/etc/ssl/ca-debian',
+# ^ this is a workaround but works (only) on DSA-administered machines
+              'dgit-distro.debian.git-url' => 'https://git.dgit.debian.org',
+              'dgit-distro.debian.git-url-suffix' => '',
               'dgit-distro.debian.upload-host' => 'ftp-master', # for dput
               'dgit-distro.debian.mirror' => 'http://ftp.debian.org/debian/',
  'dgit-distro.debian.backports-quirk' => '(squeeze)-backports*',
@@ -476,20 +506,43 @@ our %defcfg = ('dgit.default.distro' => 'debian',
               'dgit-distro.test-dummy.upload-host' => 'test-dummy',
                );
 
+our %gitcfg;
+
+sub git_slurp_config () {
+    local ($debuglevel) = $debuglevel-2;
+    local $/="\0";
+
+    my @cmd = (@git, qw(config -z --get-regexp .*));
+    debugcmd "|",@cmd;
+
+    open GITS, "-|", @cmd or failedcmd @cmd;
+    while (<GITS>) {
+       chomp or die;
+       printdebug "=> ", (messagequote $_), "\n";
+       m/\n/ or die "$_ ?";
+       push @{ $gitcfg{$`} }, $'; #';
+    }
+    $!=0; $?=0;
+    close GITS
+       or ($!==0 && $?==256)
+       or failedcmd @cmd;
+}
+
+sub git_get_config ($) {
+    my ($c) = @_;
+    my $l = $gitcfg{$c};
+    printdebug"C $c ".(defined $l ? messagequote "'$l'" : "undef")."\n"
+       if $debuglevel >= 4;
+    $l or return undef;
+    @$l==1 or badcfg "multiple values for $c" if @$l > 1;
+    return $l->[0];
+}
+
 sub cfg {
     foreach my $c (@_) {
        return undef if $c =~ /RETURN-UNDEF/;
-       my @cmd = (@git, qw(config --), $c);
-       my $v;
-       {
-           local ($debuglevel) = $debuglevel-2;
-           $v = cmdoutput_errok @cmd;
-       };
-       if ($?==0) {
-           return $v;
-       } elsif ($?!=256) {
-           failedcmd @cmd;
-       }
+       my $v = git_get_config($c);
+       return $v if defined $v;
        my $dv = $defcfg{$c};
        return $dv if defined $dv;
     }
@@ -524,6 +577,68 @@ sub access_quirk () {
     return ('none',undef);
 }
 
+our $access_forpush;
+
+sub parse_cfg_bool ($$$) {
+    my ($what,$def,$v) = @_;
+    $v //= $def;
+    return
+       $v =~ m/^[ty1]/ ? 1 :
+       $v =~ m/^[fn0]/ ? 0 :
+       badcfg "$what needs t (true, y, 1) or f (false, n, 0) not \`$v'";
+}      
+
+sub access_forpush_config () {
+    my $d = access_basedistro();
+
+    return 1 if
+       $new_package &&
+       parse_cfg_bool('new-private-pushers', 0,
+                      cfg("dgit-distro.$d.new-private-pushers",
+                          'RETURN-UNDEF'));
+
+    my $v = cfg("dgit-distro.$d.readonly", 'RETURN-UNDEF');
+    $v //= 'a';
+    return
+       $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)";
+}
+
+sub access_forpush () {
+    $access_forpush //= access_forpush_config();
+    return $access_forpush;
+}
+
+sub pushing () {
+    die "$access_forpush ?" if ($access_forpush // 1) ne 1;
+    badcfg "pushing but distro is configured readonly"
+       if access_forpush_config() eq '0';
+    $access_forpush = 1;
+    $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
+    finalise_opts_opts();
+}
+
+sub notpushing () {
+    finalise_opts_opts();
+}
+
+sub supplementary_message ($) {
+    my ($msg) = @_;
+    if (!$we_are_responder) {
+       $supplementary_message = $msg;
+       return;
+    } elsif ($protovsn >= 3) {
+       responder_send_command "supplementary-message ".length($msg)
+           or die $!;
+       print PO $msg or die $!;
+    }
+}
+
 sub access_distros () {
     # Returns list of distros to try, in order
     #
@@ -537,10 +652,15 @@ sub access_distros () {
     my (undef,$quirkdistro) = access_quirk();
     unshift @l, $quirkdistro;
     unshift @l, $instead_distro;
-    return grep { defined } @l;
+    @l = grep { defined } @l;
+
+    if (access_forpush()) {
+       @l = map { ("$_/push", $_) } @l;
+    }
+    @l;
 }
 
-sub access_cfg (@) {
+sub access_cfg_cfgs (@) {
     my (@keys) = @_;
     my @cfgs;
     # The nesting of these loops determines the search order.  We put
@@ -567,10 +687,21 @@ sub access_cfg (@) {
     }
     push @cfgs, map { "dgit.default.$_" } @realkeys;
     push @cfgs, @rundef;
+    return @cfgs;
+}
+
+sub access_cfg (@) {
+    my (@keys) = @_;
+    my (@cfgs) = access_cfg_cfgs(@keys);
     my $value = cfg(@cfgs);
     return $value;
 }
 
+sub access_cfg_bool ($$) {
+    my ($def, @keys) = @_;
+    parse_cfg_bool($keys[0], $def, access_cfg(@keys, 'RETURN-UNDEF'));
+}
+
 sub string_to_ssh ($) {
     my ($spec) = @_;
     if ($spec =~ m/\s/) {
@@ -610,15 +741,19 @@ sub access_gituserhost () {
 sub access_giturl (;$) {
     my ($optional) = @_;
     my $url = access_cfg('git-url','RETURN-UNDEF');
-    if (!defined $url) {
+    my $suffix;
+    if (!length $url) {
        my $proto = access_cfg('git-proto', 'RETURN-UNDEF');
        return undef unless defined $proto;
        $url =
            $proto.
            access_gituserhost().
            access_cfg('git-path');
+    } else {
+       $suffix = access_cfg('git-url-suffix','RETURN-UNDEF');
     }
-    return "$url/$package.git";
+    $suffix //= '.git';
+    return "$url/$package$suffix";
 }             
 
 sub parsecontrolfh ($$;$) {
@@ -705,16 +840,25 @@ sub archive_api_query_cmd ($) {
     my $url = access_cfg('archive-query-url');
     if ($url =~ m#^https://([-.0-9a-z]+)/#) {
        my $host = $1;
-       my $keys = access_cfg('archive-query-tls-key','RETURN-UNDEF');
+       my $keys = access_cfg('archive-query-tls-key','RETURN-UNDEF') //'';
        foreach my $key (split /\:/, $keys) {
            $key =~ s/\%HOST\%/$host/g;
            if (!stat $key) {
                fail "for $url: stat $key: $!" unless $!==ENOENT;
                next;
            }
-           push @cmd, "--ca-certificate=$key", "--ca-directory=/dev/enoent";
+           fail "config requested specific TLS key but do not know".
+               " how to get curl to use exactly that EE key ($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
+#           # new curl options.
            last;
        }
+       # Fixing #790093 properly will involve providing a value
+       # for this on clients.
+       my $kargs = access_cfg('archive-query-tls-curl-ca-args','RETURN-UNDEF');
+       push @cmd, split / /, $kargs if defined $kargs;
     }
     push @cmd, $url.$subpath;
     return @cmd;
@@ -848,7 +992,7 @@ sub sshpsql ($$$) {
     open P, "-|", @cmd or die $!;
     while (<P>) {
        chomp or die;
-       printdebug("$debugprefix>|$_|\n");
+       printdebug(">|$_|\n");
        push @rows, $_;
     }
     $!=0; $?=0; close P or failedcmd @cmd;
@@ -1001,13 +1145,35 @@ sub check_for_git () {
        if ($r =~ m/^divert (\w+)$/) {
            my $divert=$1;
            my ($usedistro,) = access_distros();
+           # 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;
-           printdebug "diverting $divert so using distro $instead_distro\n";
+           progress "diverting to $divert (using config for $instead_distro)";
            return check_for_git();
        }
        failedcmd @cmd unless $r =~ m/^[01]$/;
        return $r+0;
+    } elsif ($how eq 'url') {
+       my $prefix = access_cfg('git-check-url','git-url');
+       my $suffix = access_cfg('git-check-suffix','git-suffix',
+                               'RETURN-UNDEF') // '.git';
+       my $url = "$prefix/$package$suffix";
+       my @cmd = (qw(curl -sS -I), $url);
+       my $result = cmdoutput @cmd;
+       $result =~ s/^\S+ 200 .*\n\r?\n//;
+       # 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 - ".
+               Dumper($prefix, $result);
+       my $code = $1;
+       if ($code eq '404') {
+           return 0;
+       } elsif ($code eq '200') {
+           return 1;
+       } else {
+           die;
+       }
     } elsif ($how eq 'true') {
        return 1;
     } elsif ($how eq 'false') {
@@ -1054,6 +1220,22 @@ sub git_write_tree () {
     return $tree;
 }
 
+sub remove_stray_gits () {
+    my @gitscmd = qw(find -name .git -prune -print0);
+    debugcmd "|",@gitscmd;
+    open GITS, "-|", @gitscmd or failedcmd @gitscmd;
+    {
+       local $/="\0";
+       while (<GITS>) {
+           chomp or die;
+           print STDERR "$us: warning: removing from source package: ",
+               (messagequote $_), "\n";
+           rmtree $_;
+       }
+    }
+    $!=0; $?=0; close GITS or failedcmd @gitscmd;
+}
+
 sub mktree_in_ud_from_only_subdir () {
     # changes into the subdir
     my (@dirs) = <*/.>;
@@ -1061,7 +1243,8 @@ sub mktree_in_ud_from_only_subdir () {
     $dirs[0] =~ m#^([^/]+)/\.$# or die;
     my $dir = $1;
     changedir $dir;
-    fail "source package contains .git directory" if stat_exists '.git';
+
+    remove_stray_gits();
     mktree_in_ud_here();
     my $format=get_source_format();
     if (madformat($format)) {
@@ -1128,6 +1311,69 @@ sub clogp_authline ($) {
     return $authline;
 }
 
+sub vendor_patches_distro ($$) {
+    my ($checkdistro, $what) = @_;
+    return unless defined $checkdistro;
+
+    my $series = "debian/patches/\L$checkdistro\E.series";
+    printdebug "checking for vendor-specific $series ($what)\n";
+
+    if (!open SERIES, "<", $series) {
+       die "$series $!" unless $!==ENOENT;
+       return;
+    }
+    while (<SERIES>) {
+       next unless m/\S/;
+       next if m/^\s+\#/;
+
+       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
+distros.  dgit cannot safely operate on such packages on affected
+distros, because the meaning of source packages is not stable.
+
+Please ask the distro/maintainer to remove the distro-specific series
+files and use a different technique (if necessary, uploading actually
+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";
+    }
+    die "$series $!" if SERIES->error;
+    close SERIES;
+}
+
+sub check_for_vendor_patches () {
+    # This dpkg-source feature doesn't seem to be documented anywhere!
+    # But it can be found in the changelog (reformatted):
+
+    #   commit  4fa01b70df1dc4458daee306cfa1f987b69da58c
+    #   Author: Raphael Hertzog <hertzog@debian.org>
+    #   Date: Sun  Oct  3  09:36:48  2010 +0200
+
+    #   dpkg-source: correctly create .pc/.quilt_series with alternate
+    #   series files
+    #   
+    #   If you have debian/patches/ubuntu.series and you were
+    #   unpacking the source package on ubuntu, quilt was still
+    #   directed to debian/patches/series instead of
+    #   debian/patches/ubuntu.series.
+    #   
+    #   debian/changelog                        |    3 +++
+    #   scripts/Dpkg/Source/Package/V3/quilt.pm |    4 +++-
+    #   2 files changed, 6 insertions(+), 1 deletion(-)
+
+    use Dpkg::Vendor;
+    vendor_patches_distro($ENV{DEB_VENDOR}, "DEB_VENDOR");
+    vendor_patches_distro(Dpkg::Vendor::get_current_vendor(),
+                        "Dpkg::Vendor \`current vendor'");
+    vendor_patches_distro(access_basedistro(),
+                         "distro being accessed");
+}
+
 sub generate_commit_from_dsc () {
     prep_ud();
     changedir $ud;
@@ -1160,6 +1406,7 @@ sub generate_commit_from_dsc () {
     runcmd @cmd;
 
     my ($tree,$dir) = mktree_in_ud_from_only_subdir();
+    check_for_vendor_patches() if madformat($dsc->{format});
     runcmd qw(sh -ec), 'dpkg-parsechangelog >../changelog.tmp';
     my $clogp = parsecontrol('../changelog.tmp',"commit's changelog");
     my $authline = clogp_authline $clogp;
@@ -1262,12 +1509,33 @@ sub ensure_we_have_orig () {
 }
 
 sub git_fetch_us () {
-    runcmd_ordryrun_local @git, qw(fetch),access_giturl(),fetchspec();
-    if (deliberately_not_fast_forward) {
-       runcmd_ordryrun_local @git, qw(fetch -p), access_giturl(),
-           map { "+refs/$_/*:".lrfetchrefs."/$_/*" }
-           qw(tags heads);
-    }
+    my @specs = (fetchspec());
+    push @specs,
+        map { "+refs/$_/*:".lrfetchrefs."/$_/*" }
+        qw(tags heads);
+    runcmd_ordryrun_local @git, qw(fetch -p -n -q), access_giturl(), @specs;
+
+    my %here;
+    my $tagpat = debiantag('*',access_basedistro);
+
+    git_for_each_ref("refs/tags/".$tagpat, sub {
+       my ($objid,$objtype,$fullrefname,$reftail) = @_;
+       printdebug "currently $fullrefname=$objid\n";
+       $here{$fullrefname} = $objid;
+    });
+    git_for_each_ref(lrfetchrefs."/tags/".$tagpat, sub {
+       my ($objid,$objtype,$fullrefname,$reftail) = @_;
+       my $lref = "refs".substr($fullrefname, length lrfetchrefs);
+       printdebug "offered $lref=$objid\n";
+       if (!defined $here{$lref}) {
+           my @upd = (@git, qw(update-ref), $lref, $objid, '');
+           runcmd_ordryrun_local @upd;
+       } elsif ($here{$lref} eq $objid) {
+       } else {
+           print STDERR \
+               "Not updateting $lref from $here{$lref} to $objid.\n";
+       }
+    });
 }
 
 sub fetch_from_archive () {
@@ -1374,6 +1642,61 @@ END
     return 1;
 }
 
+sub set_local_git_config ($$) {
+    my ($k, $v) = @_;
+    runcmd @git, qw(config), $k, $v;
+}
+
+sub setup_mergechangelogs (;$) {
+    my ($always) = @_;
+    return unless $always || access_cfg_bool(1, 'setup-mergechangelogs');
+
+    my $driver = 'dpkg-mergechangelogs';
+    my $cb = "merge.$driver";
+    my $attrs = '.git/info/attributes';
+    ensuredir '.git/info';
+
+    open NATTRS, ">", "$attrs.new" or die "$attrs.new $!";
+    if (!open ATTRS, "<", $attrs) {
+       $!==ENOENT or die "$attrs: $!";
+    } else {
+       while (<ATTRS>) {
+           chomp;
+           next if m{^debian/changelog\s};
+           print NATTRS $_, "\n" or die $!;
+       }
+       ATTRS->error and die $!;
+       close ATTRS;
+    }
+    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.driver", 'dpkg-mergechangelogs -m %O %A %B %A';
+
+    rename "$attrs.new", "$attrs" or die "$attrs: $!";
+}
+
+sub setup_useremail (;$) {
+    my ($always) = @_;
+    return unless $always || access_cfg_bool(1, 'setup-useremail');
+
+    my $setup = sub {
+       my ($k, $envvar) = @_;
+       my $v = access_cfg("user-$k", 'RETURN-UNDEF') // $ENV{$envvar};
+       return unless defined $v;
+       set_local_git_config "user.$k", $v;
+    };
+
+    $setup->('email', 'DEBEMAIL');
+    $setup->('name', 'DEBFULLNAME');
+}
+
+sub setup_new_tree () {
+    setup_mergechangelogs();
+    setup_useremail();
+}
+
 sub clone ($) {
     my ($dstdir) = @_;
     canonicalise_suite();
@@ -1384,7 +1707,7 @@ sub clone ($) {
     runcmd @git, qw(init -q);
     my $giturl = access_giturl(1);
     if (defined $giturl) {
-       runcmd @git, qw(config), "remote.$remotename.fetch", fetchspec();
+       set_local_git_config "remote.$remotename.fetch", fetchspec();
        open H, "> .git/HEAD" or die $!;
        print H "ref: ".lref()."\n" or die $!;
        close H or die $!;
@@ -1403,6 +1726,7 @@ sub clone ($) {
        $vcsgiturl =~ s/\s+-b\s+\S+//g;
        runcmd @git, qw(remote add vcs-git), $vcsgiturl;
     }
+    setup_new_tree();
     runcmd @git, qw(reset --hard), lrref();
     printdone "ready for work in $dstdir";
 }
@@ -1489,7 +1813,7 @@ sub push_parse_changelog ($) {
 
     $package = getfield $clogp, 'Source';
     my $cversion = getfield $clogp, 'Version';
-    my $tag = debiantag($cversion);
+    my $tag = debiantag($cversion, access_basedistro);
     runcmd @git, qw(check-ref-format), $tag;
 
     my $dscfn = dscfn($cversion);
@@ -1541,9 +1865,9 @@ tagger $authline
 $package release $cversion for $clogsuite ($csuite) [dgit]
 [dgit distro=$declaredistro$delibs]
 END
-    foreach my $ref (sort keys %supersedes) {
+    foreach my $ref (sort keys %previously) {
                    print TO <<END or die $!;
-[dgit supersede:$ref=$supersedes{$ref}]
+[dgit previously:$ref=$previously{$ref}]
 END
     }
 
@@ -1554,6 +1878,9 @@ END
        if (!defined $keyid) {
            $keyid = access_cfg('keyid','RETURN-UNDEF');
        }
+        if (!defined $keyid) {
+           $keyid = getfield $clogp, 'Maintainer';
+        }
        unlink $tfn->('.tmp.asc') or $!==&ENOENT or die $!;
        my @sign_cmd = (@gpg, qw(--detach-sign --armor));
        push @sign_cmd, qw(-u),$keyid if defined $keyid;
@@ -1583,6 +1910,10 @@ sub sign_changes ($) {
 sub dopush ($) {
     my ($forceflag) = @_;
     printdebug "actually entering push\n";
+    supplementary_message(<<'END');
+Push failed, while preparing your push.
+You can retry the push, after fixing the problem, if you like.
+END
     prep_ud();
 
     access_giturl(); # check that success is vaguely likely
@@ -1615,6 +1946,7 @@ sub dopush ($) {
     runcmd qw(dpkg-source -x --),
         $dscpath =~ m#^/# ? $dscpath : "../../../$dscpath";
     my ($tree,$dir) = mktree_in_ud_from_only_subdir();
+    check_for_vendor_patches() if madformat($dsc->{format});
     changedir '../../../..';
     my $diffopt = $debuglevel>0 ? '--exit-code' : '--quiet';
     my @diffcmd = (@git, qw(diff), $diffopt, $tree);
@@ -1631,12 +1963,6 @@ sub dopush ($) {
            failedcmd @diffcmd;
        }
     }
-#fetch from alioth
-#do fast forward check and maybe fake merge
-#    if (!is_fast_fwd(mainbranch
-#    runcmd @git, qw(fetch -p ), "$alioth_git/$package.git",
-#        map { lref($_).":".rref($_) }
-#        (uploadbranch());
     my $head = git_rev_parse('HEAD');
     if (!$changesfile) {
        my $multi = "$buildproductsdir/".
@@ -1660,18 +1986,23 @@ sub dopush ($) {
     responder_send_command("param head $head");
     responder_send_command("param csuite $csuite");
 
-    if ($forceflag) {
+    if (deliberately_not_fast_forward) {
        git_for_each_ref(lrfetchrefs, sub {
            my ($objid,$objtype,$lrfetchrefname,$reftail) = @_;
            my $rrefname= substr($lrfetchrefname, length(lrfetchrefs) + 1);
-           responder_send_command("supersedes $rrefname=$objid");
-           $supersedes{$rrefname} = $objid;
+           responder_send_command("previously $rrefname=$objid");
+           $previously{$rrefname} = $objid;
        });
     }
 
     my $tfn = sub { ".git/dgit/tag$_[0]"; };
     my $tagobjfn;
 
+    supplementary_message(<<'END');
+Push failed, while signing the tag.
+You can retry the push, after fixing the problem, if you like.
+END
+    # If we manage to sign but fail to record it anywhere, it's fine.
     if ($we_are_responder) {
        $tagobjfn = $tfn->('.signed.tmp');
        responder_receive_files('signed-tag', $tagobjfn);
@@ -1682,19 +2013,30 @@ sub dopush ($) {
                       $changesfile,$changesfile,
                       $tfn);
     }
+    supplementary_message(<<'END');
+Push failed, *after* signing the tag.
+If you want to try again, you should use a new version number.
+END
 
     my $tag_obj_hash = cmdoutput @git, qw(hash-object -w -t tag), $tagobjfn;
     runcmd_ordryrun @git, qw(verify-tag), $tag_obj_hash;
     runcmd_ordryrun_local @git, qw(update-ref), "refs/tags/$tag", $tag_obj_hash;
-    runcmd_ordryrun @git, qw(tag -v --), $tag;
 
+    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
     if (!check_for_git()) {
        create_remote_git_repo();
     }
     runcmd_ordryrun @git, qw(push),access_giturl(),
-        $forceflag."HEAD:".rrref(), "refs/tags/$tag";
+        $forceflag."HEAD:".rrref(), $forceflag."refs/tags/$tag";
     runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), 'HEAD';
 
+    supplementary_message(<<'END');
+Push failed, after updating the remote git repository.
+If you want to try again, you must use a new version number.
+END
     if ($we_are_responder) {
        my $dryrunsuffix = act_local() ? "" : ".tmp";
        responder_receive_files('signed-dsc-changes',
@@ -1709,16 +2051,25 @@ sub dopush ($) {
        sign_changes $changesfile;
     }
 
+    supplementary_message(<<'END');
+Push failed, while uploading package(s) to the archive server.
+You can retry the upload of exactly these same files with dput of:
+  $changesfile
+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";
 
+    supplementary_message('');
     responder_send_command("complete");
 }
 
 sub cmd_clone {
     parseopts();
+    notpushing();
     my $dstdir;
     badusage "-p is not allowed with clone; specify as argument instead"
        if defined $package;
@@ -1766,6 +2117,7 @@ sub branchsuite () {
 }
 
 sub fetchpullargs () {
+    notpushing();
     if (!defined $package) {
        my $sourcep = parsecontrol('debian/control','debian/control');
        $package = getfield $sourcep, 'Source';
@@ -1800,6 +2152,7 @@ sub cmd_pull {
 
 sub cmd_push {
     parseopts();
+    pushing();
     badusage "-p is not allowed with dgit push" if defined $package;
     check_not_dirty();
     my $clogp = parsechangelog();
@@ -1815,13 +2168,19 @@ sub cmd_push {
     if ($new_package) {
        local ($package) = $existing_package; # this is a hack
        canonicalise_suite();
-    }
-    if (defined $specsuite && $specsuite ne $isuite) {
+    } else {
        canonicalise_suite();
-       $csuite eq $specsuite or
+    }
+    if (defined $specsuite &&
+       $specsuite ne $isuite &&
+       $specsuite ne $csuite) {
            fail "dgit push: changelog specifies $isuite ($csuite)".
                " but command line specifies $specsuite";
     }
+    supplementary_message(<<'END');
+Push failed, while checking state of the archive.
+You can retry the push, after fixing the problem, if you like.
+END
     if (check_for_git()) {
        git_fetch_us();
     }
@@ -1862,6 +2221,8 @@ sub cmd_remote_push_build_host {
     $we_are_responder = 1;
     $us .= " (build host)";
 
+    pushing();
+
     open PI, "<&STDIN" or die $!;
     open STDIN, "/dev/null" or die $!;
     open PO, ">&STDOUT" or die $!;
@@ -1870,11 +2231,16 @@ sub cmd_remote_push_build_host {
     autoflush STDOUT 1;
 
     $vsnwant //= 1;
-    fail "build host has dgit rpush protocol version".
-       " $rpushprotovsn but invocation host has $vsnwant"
-       unless grep { $rpushprotovsn eq $_ } split /,/, $vsnwant;
+    ($protovsn) = grep {
+       $vsnwant =~ m{^(?:.*,)?$_(?:,.*)?$}
+    } @rpushprotovsn_support;
+
+    fail "build host has dgit rpush protocol versions ".
+       (join ",", @rpushprotovsn_support).
+        " but invocation host has $vsnwant"
+       unless defined $protovsn;
 
-    responder_send_command("dgit-remote-push-ready $rpushprotovsn");
+    responder_send_command("dgit-remote-push-ready $protovsn");
 
     changedir $dir;
     &cmd_push;
@@ -1910,6 +2276,7 @@ sub i_method {
 }
 
 sub cmd_rpush {
+    pushing();
     my $host = nextarg;
     my $dir;
     if ($host =~ m/^((?:[^][]|\[[^][]*\])*)\:/) {
@@ -1919,7 +2286,8 @@ sub cmd_rpush {
        $dir = nextarg;
     }
     $dir =~ s{^-}{./-};
-    my @rargs = ($dir,$rpushprotovsn);
+    my @rargs = ($dir);
+    push @rargs, join ",", @rpushprotovsn_support;
     my @rdgit;
     push @rdgit, @dgit;
     push @rdgit, @ropts;
@@ -1937,7 +2305,9 @@ sub cmd_rpush {
     }
     $i_child_pid = open2(\*RO, \*RI, @cmd);
     changedir $i_tmp;
-    initiator_expect { m/^dgit-remote-push-ready/ };
+    ($protovsn) = initiator_expect { m/^dgit-remote-push-ready (\S+)/ };
+    die "$protovsn ?" unless grep { $_ eq $protovsn } @rpushprotovsn_support;
+    $supplementary_message = '' unless $protovsn >= 3;
     for (;;) {
        my ($icmd,$iargs) = initiator_expect {
            m/^(\S+)(?: (.*))?$/;
@@ -1953,6 +2323,11 @@ sub i_resp_progress ($) {
     progress $msg;
 }
 
+sub i_resp_supplementary_message ($) {
+    my ($rhs) = @_;
+    $supplementary_message = protocol_read_bytes \*RO, $rhs;
+}
+
 sub i_resp_complete {
     my $pid = $i_child_pid;
     $i_child_pid = undef; # prevents killing some other process with same pid
@@ -1983,12 +2358,12 @@ sub i_resp_param ($) {
     $i_param{$1} = $2;
 }
 
-sub i_resp_supersedes ($) {
+sub i_resp_previously ($) {
     $_[0] =~ m#^(refs/tags/\S+)=(\w+)$#
-       or badproto \*RO, "bad supersedes spec";
+       or badproto \*RO, "bad previously spec";
     my $r = system qw(git check-ref-format), $1;
-    die "bad supersedes ref spec ($r)" if $r;
-    $supersedes{$1} = $2;
+    die "bad previously ref spec ($r)" if $r;
+    $previously{$1} = $2;
 }
 
 our %i_wanted;
@@ -2143,6 +2518,7 @@ sub quiltify ($$) {
     # should be contained within debian/patches.
 
     changedir '../fake';
+    remove_stray_gits();
     mktree_in_ud_here();
     rmtree '.pc';
     runcmd @git, 'add', '.';
@@ -2253,7 +2629,7 @@ sub quiltify ($$) {
            my $s = $abbrev->($notp);
            my $c = $notp->{Child};
            $s .= "..".$abbrev->($c) if $c;
-           $s .= ": ".$c->{Whynot};
+           $s .= ": ".$notp->{Whynot};
            return $s;
        };
        if ($quilt_mode eq 'linear') {
@@ -2337,6 +2713,8 @@ sub build_maybe_quilt_fixup () {
     return unless madformat $format;
     # sigh
 
+    check_for_vendor_patches();
+
     # Our objective is:
     #  - honour any existing .pc in case it has any strangeness
     #  - determine the git commit corresponding to the tip of
@@ -2461,7 +2839,7 @@ END
     commit_quilty_patch();
 
     if ($mustdeletepc) {
-        runcmd @git, qw(rm -rq .pc);
+        runcmd @git, qw(rm -rqf .pc);
         commit_admin "Commit removal of .pc (quilt series tracking data)";
     }
 
@@ -2490,11 +2868,24 @@ sub quilt_fixup_editor () {
 
 #----- other building -----
 
+our $suppress_clean;
+
 sub clean_tree () {
+    return if $suppress_clean;
     if ($cleanmode eq 'dpkg-source') {
        runcmd_ordryrun_local @dpkgbuildpackage, qw(-T clean);
+    } elsif ($cleanmode eq 'dpkg-source-d') {
+       runcmd_ordryrun_local @dpkgbuildpackage, qw(-d -T clean);
     } elsif ($cleanmode eq 'git') {
        runcmd_ordryrun_local @git, qw(clean -xdf);
+    } elsif ($cleanmode eq 'git-ff') {
+       runcmd_ordryrun_local @git, qw(clean -xdff);
+    } elsif ($cleanmode eq 'check') {
+       my $leftovers = cmdoutput @git, qw(clean -xdn);
+       if (length $leftovers) {
+           print STDERR $leftovers, "\n" or die $!;
+           fail "tree contains uncommitted files and --clean=check specified";
+       }
     } elsif ($cleanmode eq 'none') {
     } else {
        die "$cleanmode ?";
@@ -2503,10 +2894,12 @@ sub clean_tree () {
 
 sub cmd_clean () {
     badusage "clean takes no additional arguments" if @ARGV;
+    notpushing();
     clean_tree();
 }
 
 sub build_prep () {
+    notpushing();
     badusage "-p is not allowed when building" if defined $package;
     check_not_dirty();
     clean_tree();
@@ -2517,8 +2910,11 @@ sub build_prep () {
     build_maybe_quilt_fixup();
 }
 
-sub changesopts () {
+sub changesopts_initial () {
     my @opts =@changesopts[1..$#changesopts];
+}
+
+sub changesopts_version () {
     if (!defined $changes_since_version) {
        my @vsns = archive_query('archive_query');
        my @quirk = access_quirk();
@@ -2539,22 +2935,60 @@ sub changesopts () {
        }
     }
     if ($changes_since_version ne '_') {
-       unshift @opts, "-v$changes_since_version";
+       return ("-v$changes_since_version");
+    } else {
+       return ();
     }
-    return @opts;
+}
+
+sub changesopts () {
+    return (changesopts_initial(), changesopts_version());
+}
+
+sub massage_dbp_args ($;$) {
+    my ($cmd,$xargs) = @_;
+    if ($cleanmode eq 'dpkg-source') {
+       $suppress_clean = 1;
+       return;
+    }
+    debugcmd '#massaging#', @$cmd if $debuglevel>1;
+    my @newcmd = shift @$cmd;
+    # -nc has the side effect of specifying -b if nothing else specified
+    push @newcmd, '-nc';
+    # and some combinations of -S, -b, et al, are errors, rather than
+    # later simply overriding earlier
+    push @newcmd, '-F' unless grep { m/^-[bBASF]$/ } (@$cmd, @$xargs);
+    push @newcmd, @$cmd;
+    @$cmd = @newcmd;
 }
 
 sub cmd_build {
+    my @dbp = (@dpkgbuildpackage, qw(-us -uc), changesopts_initial(), @ARGV);
+    massage_dbp_args \@dbp;
     build_prep();
-    runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc), changesopts(), @ARGV;
+    push @dbp, changesopts_version();
+    runcmd_ordryrun_local @dbp;
     printdone "build successful\n";
 }
 
-sub cmd_git_build {
+sub cmd_gbp_build {
+    my @dbp = @dpkgbuildpackage;
+    massage_dbp_args \@dbp, \@ARGV;
+
+    my @cmd;
+    if (length executable_on_path('git-buildpackage')) {
+       @cmd = qw(git-buildpackage);
+    } else {
+       @cmd = qw(gbp buildpackage);
+    }
+    push @cmd, (qw(-us -uc --git-no-sign-tags), "--git-builder=@dbp");
+
+    if ($cleanmode eq 'dpkg-source') {
+       $suppress_clean = 1;
+    } else {
+       push @cmd, '--git-cleaner=true';
+    }
     build_prep();
-    my @cmd =
-       (qw(git-buildpackage -us -uc --git-no-sign-tags),
-        "--git-builder=@dpkgbuildpackage");
     unless (grep { m/^--git-debian-branch|^--git-ignore-branch/ } @ARGV) {
        canonicalise_suite();
        push @cmd, "--git-debian-branch=".lbranch();
@@ -2563,14 +2997,22 @@ sub cmd_git_build {
     runcmd_ordryrun_local @cmd, @ARGV;
     printdone "build successful\n";
 }
+sub cmd_git_build { cmd_gbp_build(); } # compatibility with <= 1.0
 
 sub build_source {
+    if ($cleanmode =~ m/^dpkg-source/) {
+       # dpkg-source will clean, so we shouldn't
+       $suppress_clean = 1;
+    }
     build_prep();
     $sourcechanges = "${package}_".(stripepoch $version)."_source.changes";
     $dscfn = dscfn($version);
     if ($cleanmode eq 'dpkg-source') {
        runcmd_ordryrun_local (@dpkgbuildpackage, qw(-us -uc -S)),
            changesopts();
+    } elsif ($cleanmode eq 'dpkg-source-d') {
+       runcmd_ordryrun_local (@dpkgbuildpackage, qw(-us -uc -S -d)),
+           changesopts();
     } else {
        my $pwd = must_getcwd();
        my $leafdir = basename $pwd;
@@ -2595,7 +3037,7 @@ sub cmd_sbuild {
     changedir "..";
     my $pat = "${package}_".(stripepoch $version)."_*.changes";
     if (act_local()) {
-       stat_exist $dscfn or fail "$dscfn (in parent directory): $!";
+       stat_exists $dscfn or fail "$dscfn (in parent directory): $!";
        stat_exists $sourcechanges
            or fail "$sourcechanges (in parent directory): $!";
        foreach my $cf (glob $pat) {
@@ -2635,6 +3077,30 @@ sub cmd_archive_api_query {
     exec @cmd or fail "exec curl: $!\n";
 }
 
+sub cmd_clone_dgit_repos_server {
+    badusage "need destination argument" unless @ARGV==1;
+    my ($destdir) = @ARGV;
+    $package = '_dgit-repos-server';
+    my @cmd = (@git, qw(clone), access_giturl(), $destdir);
+    debugcmd ">",@cmd;
+    exec @cmd or fail "exec git clone: $!\n";
+}
+
+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;
+    setup_useremail(1);
+}
+
+sub cmd_setup_new_tree {
+    badusage "no arguments allowed to dgit setup-tree" if @ARGV;
+    setup_new_tree();
+}
+
 #---------- argument parsing and main program ----------
 
 sub cmd_version {
@@ -2642,6 +3108,24 @@ sub cmd_version {
     exit 0;
 }
 
+our (%valopts_long, %valopts_short);
+our @rvalopts;
+
+sub defvalopt ($$$&) {
+    my ($long,$short,$val_re,$fn) = @_;
+    my $oi = { Long => $long, Short => $short, Re => $val_re, Fn => $fn };
+    $valopts_long{$long} = $oi;
+    $valopts_short{$short} = $oi;
+    # $fn subref should:
+    #   do whatever assignemnt or thing it likes with $_[0]
+    #   if the option should not be passed on to remote, @rvalopts=()
+}
+
+defvalopt '--since-version', '-v', '[^_]+|_', sub {
+    ($changes_since_version) = @_;
+};
+defvalopt '--distro', '-d', '.+', sub { ($idistro) = (@_); };
+
 sub parseopts () {
     my $om;
 
@@ -2651,6 +3135,22 @@ sub parseopts () {
        @ssh = ($ENV{'GIT_SSH'});
     }
 
+    my $oi;
+    my $val;
+    my $valopt = sub {
+       my ($what) = @_;
+       @rvalopts = ($_);
+       if (!defined $val) {
+           badusage "$what needs a value" unless length @ARGV;
+           $val = shift @ARGV;
+           push @rvalopts, $val;
+       }
+       badusage "bad value \`$val' for $what" unless
+           $val =~ m/^$oi->{Re}$(?!\n)/s;
+       $oi->{Fn}($val);
+       push @ropts, @rvalopts;
+    };
+
     while (@ARGV) {
        last unless $ARGV[0] =~ m/^-/;
        $_ = shift @ARGV;
@@ -2672,10 +3172,7 @@ sub parseopts () {
            } elsif (m/^--new$/) {
                push @ropts, $_;
                $new_package=1;
-           } elsif (m/^--since-version=([^_]+|_)$/) {
-               push @ropts, $_;
-               $changes_since_version = $1;
-           } elsif (m/^--([-0-9a-z]+)=(.*)/s &&
+           } elsif (m/^--([-0-9a-z]+)=(.+)/s &&
                     ($om = $opts_opt_map{$1}) &&
                     length $om->[0]) {
                push @ropts, $_;
@@ -2693,13 +3190,10 @@ sub parseopts () {
                $initiator_tempdir =~ m#^/# or
                    badusage "--initiator-tempdir must be used specify an".
                        " absolute, not relative, directory."
-           } elsif (m/^--distro=(.*)/s) {
-               push @ropts, $_;
-               $idistro = $1;
            } elsif (m/^--build-products-dir=(.*)/s) {
                push @ropts, $_;
                $buildproductsdir = $1;
-           } elsif (m/^--clean=(dpkg-source|git|none)$/s) {
+           } elsif (m/^--clean=($cleanmode_re)$/os) {
                push @ropts, $_;
                $cleanmode = $1;
            } elsif (m/^--clean=(.*)$/s) {
@@ -2721,6 +3215,9 @@ sub parseopts () {
            } elsif (m/^--deliberately-($deliberately_re)$/s) {
                push @ropts, $_;
                push @deliberatelies, $&;
+           } elsif (m/^(--[-0-9a-z]+)(=|$)/ && ($oi = $valopts_long{$1})) {
+               $val = $2 ? $' : undef; #';
+               $valopt->($oi->{Long});
            } else {
                badusage "unknown long option \`$_'";
            }
@@ -2741,9 +3238,6 @@ sub parseopts () {
                } elsif (s/^-N/-/) {
                    push @ropts, $&;
                    $new_package=1;
-               } elsif (s/^-v([^_]+|_)$//s) {
-                   push @ropts, $&;
-                   $changes_since_version = $1;
                } elsif (m/^-m/) {
                    push @ropts, $&;
                    push @changesopts, $_;
@@ -2751,9 +3245,6 @@ sub parseopts () {
                } elsif (s/^-c(.*=.*)//s) {
                    push @ropts, $&;
                    push @git, '-c', $1;
-               } elsif (s/^-d(.+)//s) {
-                   push @ropts, $&;
-                   $idistro = $1;
                } elsif (s/^-C(.+)//s) {
                    push @ropts, $&;
                    $changesfile = $1;
@@ -2762,7 +3253,7 @@ sub parseopts () {
                    }
                } elsif (s/^-k(.+)//s) {
                    $keyid=$1;
-               } elsif (m/^-[vdCk]$/) {
+               } elsif (m/^-[dCk]$/) {
                    badusage
  "option \`$_' requires an argument (and no space before the argument)";
                } elsif (s/^-wn$//s) {
@@ -2771,9 +3262,23 @@ sub parseopts () {
                } elsif (s/^-wg$//s) {
                    push @ropts, $&;
                    $cleanmode = 'git';
+               } elsif (s/^-wgf$//s) {
+                   push @ropts, $&;
+                   $cleanmode = 'git-ff';
                } elsif (s/^-wd$//s) {
                    push @ropts, $&;
                    $cleanmode = 'dpkg-source';
+               } elsif (s/^-wdd$//s) {
+                   push @ropts, $&;
+                   $cleanmode = 'dpkg-source-d';
+               } elsif (s/^-wc$//s) {
+                   push @ropts, $&;
+                   $cleanmode = 'check';
+               } elsif (m/^-[a-zA-Z]/ && ($oi = $valopts_short{$&})) {
+                   $val = $'; #';
+                   $val = undef unless length $val;
+                   $valopt->($oi->{Short});
+                   $_ = '';
                } else {
                    badusage "unknown short option \`$_'";
                }
@@ -2782,11 +3287,41 @@ sub parseopts () {
     }
 }
 
+sub finalise_opts_opts () {
+    foreach my $k (keys %opts_opt_map) {
+       my $om = $opts_opt_map{$k};
+
+       my $v = access_cfg("cmd-$k", 'RETURN-UNDEF');
+       if (defined $v) {
+           badcfg "cannot set command for $k"
+               unless length $om->[0];
+           $om->[0] = $v;
+       }
+
+       foreach my $c (access_cfg_cfgs("opts-$k")) {
+           my $vl = $gitcfg{$c};
+           printdebug "CL $c ",
+               ($vl ? join " ", map { shellquote } @$vl : ""),
+               "\n" if $debuglevel >= 4;
+           next unless $vl;
+           badcfg "cannot configure options for $k"
+               if $opts_opt_cmdonly{$k};
+           my $insertpos = $opts_cfg_insertpos{$k};
+           @$om = ( @$om[0..$insertpos-1],
+                    @$vl,
+                    @$om[$insertpos..$#$om] );
+       }
+    }
+}
+
 if ($ENV{$fakeeditorenv}) {
+    git_slurp_config();
     quilt_fixup_editor();
 }
 
 parseopts();
+git_slurp_config();
+
 print STDERR "DRY RUN ONLY\n" if $dryrun_level > 1;
 print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n"
     if $dryrun_level == 1;
@@ -2798,6 +3333,7 @@ my $cmd = shift @ARGV;
 $cmd =~ y/-/_/;
 
 if (!defined $quilt_mode) {
+    local $access_forpush;
     $quilt_mode = cfg('dgit.force.quilt-mode', 'RETURN-UNDEF')
        // access_cfg('quilt-mode', 'RETURN-UNDEF')
        // 'linear';
@@ -2806,6 +3342,15 @@ if (!defined $quilt_mode) {
     $quilt_mode = $1;
 }
 
+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->();