chiark / gitweb /
Split brain: Make prep_ud be able to make a different dir (nfc)
[dgit.git] / dgit
diff --git a/dgit b/dgit
index c652e5db10113279fc22aa6d20ece200a641342f..2069298b906741e9d6f2a93f5afe793ccf5146c4 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -57,8 +57,9 @@ our $rmonerror = 1;
 our @deliberatelies;
 our %previously;
 our $existing_package = 'dpkg';
 our @deliberatelies;
 our %previously;
 our $existing_package = 'dpkg';
-our $cleanmode = 'dpkg-source';
+our $cleanmode;
 our $changes_since_version;
 our $changes_since_version;
+our $rmchanges;
 our $quilt_mode;
 our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck';
 our $we_are_responder;
 our $quilt_mode;
 our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck';
 our $we_are_responder;
@@ -67,6 +68,7 @@ our $initiator_tempdir;
 our %format_ok = map { $_=>1 } ("1.0","3.0 (native)","3.0 (quilt)");
 
 our $suite_re = '[-+.0-9a-z]+';
 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);
 
 our (@git) = qw(git);
 our (@dget) = qw(dget);
@@ -74,7 +76,7 @@ our (@curl) = qw(curl -f);
 our (@dput) = qw(dput);
 our (@debsign) = qw(debsign);
 our (@gpg) = qw(gpg);
 our (@dput) = qw(dput);
 our (@debsign) = qw(debsign);
 our (@gpg) = qw(gpg);
-our (@sbuild) = qw(sbuild -A);
+our (@sbuild) = qw(sbuild);
 our (@ssh) = 'ssh';
 our (@dgit) = qw(dgit);
 our (@dpkgbuildpackage) = qw(dpkg-buildpackage -i\.git/ -I.git);
 our (@ssh) = 'ssh';
 our (@dgit) = qw(dgit);
 our (@dpkgbuildpackage) = qw(dpkg-buildpackage -i\.git/ -I.git);
@@ -91,19 +93,27 @@ our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
                      'sbuild' => \@sbuild,
                      'ssh' => \@ssh,
                      'dgit' => \@dgit,
                      'sbuild' => \@sbuild,
                      'ssh' => \@ssh,
                      'dgit' => \@dgit,
+                     'git' => \@git,
                      'dpkg-source' => \@dpkgsource,
                      'dpkg-buildpackage' => \@dpkgbuildpackage,
                      'dpkg-genchanges' => \@dpkggenchanges,
                      'ch' => \@changesopts,
                      'mergechanges' => \@mergechanges);
 
                      '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 = '';
 
 our $keyid;
 
 autoflush STDOUT 1;
 
 our $supplementary_message = '';
+our $need_split_build_invocation = 0;
 
 END {
     local ($@, $?);
 
 END {
     local ($@, $?);
@@ -139,6 +149,11 @@ sub dscfn ($) {
     return srcfn($vsn,".dsc");
 }
 
     return srcfn($vsn,".dsc");
 }
 
+sub changespat ($;$) {
+    my ($vsn, $arch) = @_;
+    return "${package}_".(stripepoch $vsn)."_".($arch//'*').".changes";
+}
+
 our $us = 'dgit';
 initdebug('');
 
 our $us = 'dgit';
 initdebug('');
 
@@ -147,7 +162,7 @@ END {
     local ($?);
     foreach my $f (@end) {
        eval { $f->(); };
     local ($?);
     foreach my $f (@end) {
        eval { $f->(); };
-       warn "$us: cleanup: $@" if length $@;
+       print STDERR "$us: cleanup: $@" if length $@;
     }
 };
 
     }
 };
 
@@ -416,7 +431,8 @@ our $helpmsg = <<END;
 main usages:
   dgit [dgit-opts] clone [dgit-opts] package [suite] [./dir|/dir]
   dgit [dgit-opts] fetch|pull [dgit-opts] [suite]
 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:
   dgit [dgit-opts] push [dgit-opts] [suite]
   dgit [dgit-opts] rpush build-host:build-dir ...
 important dgit options:
@@ -497,28 +513,36 @@ our %defcfg = ('dgit.default.distro' => 'debian',
               'dgit-distro.test-dummy.upload-host' => 'test-dummy',
                );
 
               'dgit-distro.test-dummy.upload-host' => 'test-dummy',
                );
 
-sub git_get_config ($) {
-    my ($c) = @_;
+our %gitcfg;
 
 
-    our %git_get_config_memo;
-    if (exists $git_get_config_memo{$c}) {
-       return $git_get_config_memo{$c};
-    }
+sub git_slurp_config () {
+    local ($debuglevel) = $debuglevel-2;
+    local $/="\0";
 
 
-    my $v;
-    my @cmd = (@git, qw(config --), $c);
-    {
-       local ($debuglevel) = $debuglevel-2;
-       $v = cmdoutput_errok @cmd;
-    };
-    if ($?==0) {
-    } elsif ($?==256) {
-       $v = undef;
-    } else {
-       failedcmd @cmd;
+    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{$`} }, $'; #';
     }
     }
-    $git_get_config_memo{$c} = $v;
-    return $v;
+    $!=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 {
 }
 
 sub cfg {
@@ -603,6 +627,11 @@ sub pushing () {
 Push failed, before we got started.
 You can retry the push, after fixing the problem, if you like.
 END
 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 ($) {
 }
 
 sub supplementary_message ($) {
@@ -638,7 +667,7 @@ sub access_distros () {
     @l;
 }
 
     @l;
 }
 
-sub access_cfg (@) {
+sub access_cfg_cfgs (@) {
     my (@keys) = @_;
     my @cfgs;
     # The nesting of these loops determines the search order.  We put
     my (@keys) = @_;
     my @cfgs;
     # The nesting of these loops determines the search order.  We put
@@ -665,10 +694,21 @@ sub access_cfg (@) {
     }
     push @cfgs, map { "dgit.default.$_" } @realkeys;
     push @cfgs, @rundef;
     }
     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;
 }
 
     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/) {
 sub string_to_ssh ($) {
     my ($spec) = @_;
     if ($spec =~ m/\s/) {
@@ -959,7 +999,7 @@ sub sshpsql ($$$) {
     open P, "-|", @cmd or die $!;
     while (<P>) {
        chomp or die;
     open P, "-|", @cmd or die $!;
     while (<P>) {
        chomp or die;
-       printdebug("$debugprefix>|$_|\n");
+       printdebug(">|$_|\n");
        push @rows, $_;
     }
     $!=0; $?=0; close P or failedcmd @cmd;
        push @rows, $_;
     }
     $!=0; $?=0; close P or failedcmd @cmd;
@@ -1127,6 +1167,9 @@ sub check_for_git () {
        my $url = "$prefix/$package$suffix";
        my @cmd = (qw(curl -sS -I), $url);
        my $result = cmdoutput @cmd;
        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);
        $result =~ m/^\S+ (404|200) /s or
            fail "unexpected results from git check query - ".
                Dumper($prefix, $result);
@@ -1166,10 +1209,12 @@ our ($dsc_hash,$lastpush_hash);
 
 our $ud = '.git/dgit/unpack';
 
 
 our $ud = '.git/dgit/unpack';
 
-sub prep_ud () {
-    rmtree($ud);
+sub prep_ud (;$) {
+    my ($d) = @_;
+    $d //= $ud;
+    rmtree($d);
     mkpath '.git/dgit';
     mkpath '.git/dgit';
-    mkdir $ud or die $!;
+    mkdir $d or die $!;
 }
 
 sub mktree_in_ud_here () {
 }
 
 sub mktree_in_ud_here () {
@@ -1184,14 +1229,7 @@ sub git_write_tree () {
     return $tree;
 }
 
     return $tree;
 }
 
-sub mktree_in_ud_from_only_subdir () {
-    # changes into the subdir
-    my (@dirs) = <*/.>;
-    die unless @dirs==1;
-    $dirs[0] =~ m#^([^/]+)/\.$# or die;
-    my $dir = $1;
-    changedir $dir;
-
+sub remove_stray_gits () {
     my @gitscmd = qw(find -name .git -prune -print0);
     debugcmd "|",@gitscmd;
     open GITS, "-|", @gitscmd or failedcmd @gitscmd;
     my @gitscmd = qw(find -name .git -prune -print0);
     debugcmd "|",@gitscmd;
     open GITS, "-|", @gitscmd or failedcmd @gitscmd;
@@ -1205,9 +1243,19 @@ sub mktree_in_ud_from_only_subdir () {
        }
     }
     $!=0; $?=0; close GITS or failedcmd @gitscmd;
        }
     }
     $!=0; $?=0; close GITS or failedcmd @gitscmd;
+}
 
 
+sub mktree_in_ud_from_only_subdir () {
+    # changes into the subdir
+    my (@dirs) = <*/.>;
+    die unless @dirs==1;
+    $dirs[0] =~ m#^([^/]+)/\.$# or die;
+    my $dir = $1;
+    changedir $dir;
+
+    remove_stray_gits();
     mktree_in_ud_here();
     mktree_in_ud_here();
-    my $format=get_source_format();
+    my ($format, $fopts) = get_source_format();
     if (madformat($format)) {
        rmtree '.pc';
     }
     if (madformat($format)) {
        rmtree '.pc';
     }
@@ -1343,11 +1391,12 @@ sub generate_commit_from_dsc () {
        my $f = $fi->{Filename};
        die "$f ?" if $f =~ m#/|^\.|\.dsc$|\.tmp$#;
 
        my $f = $fi->{Filename};
        die "$f ?" if $f =~ m#/|^\.|\.dsc$|\.tmp$#;
 
-       link "../../../$f", $f
+       link_ltarget "../../../$f", $f
            or $!==&ENOENT
            or die "$f $!";
 
            or $!==&ENOENT
            or die "$f $!";
 
-       complete_file_from_dsc('.', $fi);
+       complete_file_from_dsc('.', $fi)
+           or next;
 
        if (is_orig_file($f)) {
            link $f, "../../../../$f"
 
        if (is_orig_file($f)) {
            link $f, "../../../../$f"
@@ -1442,10 +1491,10 @@ sub complete_file_from_dsc ($$) {
        my $furl = $dscurl;
        $furl =~ s{/[^/]+$}{};
        $furl .= "/$f";
        my $furl = $dscurl;
        $furl =~ s{/[^/]+$}{};
        $furl .= "/$f";
-       die "$f ?" unless $f =~ m/^${package}_/;
+       die "$f ?" unless $f =~ m/^\Q${package}\E_/;
        die "$f ?" if $f =~ m#/#;
        runcmd_ordryrun_local @curl,qw(-o),$tf,'--',"$furl";
        die "$f ?" if $f =~ m#/#;
        runcmd_ordryrun_local @curl,qw(-o),$tf,'--',"$furl";
-       next if !act_local();
+       return 0 if !act_local();
        $downloaded = 1;
     }
 
        $downloaded = 1;
     }
 
@@ -1459,13 +1508,16 @@ sub complete_file_from_dsc ($$) {
            " demands hash $fi->{Hash} ".
            ($downloaded ? "(got wrong file from archive!)"
             : "(perhaps you should delete this file?)");
            " demands hash $fi->{Hash} ".
            ($downloaded ? "(got wrong file from archive!)"
             : "(perhaps you should delete this file?)");
+
+    return 1;
 }
 
 sub ensure_we_have_orig () {
     foreach my $fi (dsc_files_info()) {
        my $f = $fi->{Filename};
        next unless is_orig_file($f);
 }
 
 sub ensure_we_have_orig () {
     foreach my $fi (dsc_files_info()) {
        my $f = $fi->{Filename};
        next unless is_orig_file($f);
-       complete_file_from_dsc('..', $fi);
+       complete_file_from_dsc('..', $fi)
+           or next;
     }
 }
 
     }
 }
 
@@ -1608,7 +1660,10 @@ sub set_local_git_config ($$) {
     runcmd @git, qw(config), $k, $v;
 }
 
     runcmd @git, qw(config), $k, $v;
 }
 
-sub setup_mergechangelogs () {
+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';
     my $driver = 'dpkg-mergechangelogs';
     my $cb = "merge.$driver";
     my $attrs = '.git/info/attributes';
@@ -1635,12 +1690,32 @@ sub setup_mergechangelogs () {
     rename "$attrs.new", "$attrs" or die "$attrs: $!";
 }
 
     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();
     badusage "dry run makes no sense with clone" unless act_local();
     my $hasgit = check_for_git();
 sub clone ($) {
     my ($dstdir) = @_;
     canonicalise_suite();
     badusage "dry run makes no sense with clone" unless act_local();
     my $hasgit = check_for_git();
-    mkdir $dstdir or die "$dstdir $!";
+    mkdir $dstdir or fail "create \`$dstdir': $!";
     changedir $dstdir;
     runcmd @git, qw(init -q);
     my $giturl = access_giturl(1);
     changedir $dstdir;
     runcmd @git, qw(init -q);
     my $giturl = access_giturl(1);
@@ -1664,7 +1739,7 @@ sub clone ($) {
        $vcsgiturl =~ s/\s+-b\s+\S+//g;
        runcmd @git, qw(remote add vcs-git), $vcsgiturl;
     }
        $vcsgiturl =~ s/\s+-b\s+\S+//g;
        runcmd @git, qw(remote add vcs-git), $vcsgiturl;
     }
-    setup_mergechangelogs();
+    setup_new_tree();
     runcmd @git, qw(reset --hard), lrref();
     printdone "ready for work in $dstdir";
 }
     runcmd @git, qw(reset --hard), lrref();
     printdone "ready for work in $dstdir";
 }
@@ -1685,7 +1760,14 @@ sub pull () {
 }
 
 sub check_not_dirty () {
 }
 
 sub check_not_dirty () {
+    foreach my $f (qw(local-options local-patch-header)) {
+       if (stat_exists "debian/source/$f") {
+           fail "git tree contains debian/source/$f";
+       }
+    }
+
     return if $ignoredirty;
     return if $ignoredirty;
+
     my @cmd = (@git, qw(diff --quiet HEAD));
     debugcmd "+",@cmd;
     $!=0; $?=0; system @cmd;
     my @cmd = (@git, qw(diff --quiet HEAD));
     debugcmd "+",@cmd;
     $!=0; $?=0; system @cmd;
@@ -1717,11 +1799,32 @@ sub commit_quilty_patch () {
        progress "nothing quilty to commit, ok.";
        return;
     }
        progress "nothing quilty to commit, ok.";
        return;
     }
-    runcmd_ordryrun_local @git, qw(add), sort keys %adds;
+    my @adds = map { s/[][*?\\]/\\$&/g; $_; } sort keys %adds;
+    runcmd_ordryrun_local @git, qw(add -f), @adds;
     commit_admin "Commit Debian 3.0 (quilt) metadata";
 }
 
 sub get_source_format () {
     commit_admin "Commit Debian 3.0 (quilt) metadata";
 }
 
 sub get_source_format () {
+    my %options;
+    if (open F, "debian/source/options") {
+       while (<F>) {
+           next if m/^\s*\#/;
+           next unless m/\S/;
+           s/\s+$//; # ignore missing final newline
+           if (m/\s*\#\s*/) {
+               my ($k, $v) = ($`, $'); #');
+               $v =~ s/^"(.*)"$/$1/;
+               $options{$k} = $v;
+           } else {
+               $options{$_} = 1;
+           }
+       }
+       F->error and die $!;
+       close F;
+    } else {
+       die $! unless $!==&ENOENT;
+    }
+
     if (!open F, "debian/source/format") {
        die $! unless $!==&ENOENT;
        return '';
     if (!open F, "debian/source/format") {
        die $! unless $!==&ENOENT;
        return '';
@@ -1729,7 +1832,7 @@ sub get_source_format () {
     $_ = <F>;
     F->error and die $!;
     chomp;
     $_ = <F>;
     F->error and die $!;
     chomp;
-    return $_;
+    return ($_, \%options);
 }
 
 sub madformat ($) {
 }
 
 sub madformat ($) {
@@ -1816,6 +1919,9 @@ END
        if (!defined $keyid) {
            $keyid = access_cfg('keyid','RETURN-UNDEF');
        }
        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;
        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;
@@ -1873,6 +1979,7 @@ END
     my $format = getfield $dsc, 'Format';
     printdebug "format $format\n";
     if (madformat($format)) {
     my $format = getfield $dsc, 'Format';
     printdebug "format $format\n";
     if (madformat($format)) {
+       # user might have not used dgit build, so maybe do this now:
        commit_quilty_patch();
     }
     check_not_dirty();
        commit_quilty_patch();
     }
     check_not_dirty();
@@ -1900,19 +2007,13 @@ END
     }
     my $head = git_rev_parse('HEAD');
     if (!$changesfile) {
     }
     my $head = git_rev_parse('HEAD');
     if (!$changesfile) {
-       my $multi = "$buildproductsdir/".
-           "${package}_".(stripepoch $cversion)."_multi.changes";
-       if (stat_exists "$multi") {
-           $changesfile = $multi;
-       } else {
-           my $pat = "${package}_".(stripepoch $cversion)."_*.changes";
-           my @cs = glob "$buildproductsdir/$pat";
-           fail "failed to find unique changes file".
-               " (looked for $pat in $buildproductsdir, or $multi);".
-               " perhaps you need to use dgit -C"
-               unless @cs==1;
-           ($changesfile) = @cs;
-       }
+       my $pat = changespat $cversion;
+       my @cs = glob "$buildproductsdir/$pat";
+       fail "failed to find unique changes file".
+           " (looked for $pat in $buildproductsdir);".
+           " perhaps you need to use dgit -C"
+           unless @cs==1;
+       ($changesfile) = @cs;
     } else {
        $changesfile = "$buildproductsdir/$changesfile";
     }
     } else {
        $changesfile = "$buildproductsdir/$changesfile";
     }
@@ -1986,7 +2087,7 @@ END
        sign_changes $changesfile;
     }
 
        sign_changes $changesfile;
     }
 
-    supplementary_message(<<'END');
+    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
 Push failed, while uploading package(s) to the archive server.
 You can retry the upload of exactly these same files with dput of:
   $changesfile
@@ -2004,6 +2105,7 @@ END
 
 sub cmd_clone {
     parseopts();
 
 sub cmd_clone {
     parseopts();
+    notpushing();
     my $dstdir;
     badusage "-p is not allowed with clone; specify as argument instead"
        if defined $package;
     my $dstdir;
     badusage "-p is not allowed with clone; specify as argument instead"
        if defined $package;
@@ -2033,7 +2135,13 @@ sub cmd_clone {
                return if $!==&ENOENT;
                die "chdir $cwd_remove: $!";
            }
                return if $!==&ENOENT;
                die "chdir $cwd_remove: $!";
            }
-           rmtree($dstdir) or die "remove $dstdir: $!\n";
+           if (stat $dstdir) {
+               rmtree($dstdir) or die "remove $dstdir: $!\n";
+           } elsif (!grep { $! == $_ }
+                    (ENOENT, ENOTDIR, EACCES, EPERM, ELOOP)) {
+           } else {
+               print STDERR "check whether to remove $dstdir: $!\n";
+           }
        };
     }
 
        };
     }
 
@@ -2051,6 +2159,7 @@ sub branchsuite () {
 }
 
 sub fetchpullargs () {
 }
 
 sub fetchpullargs () {
+    notpushing();
     if (!defined $package) {
        my $sourcep = parsecontrol('debian/control','debian/control');
        $package = getfield $sourcep, 'Source';
     if (!defined $package) {
        my $sourcep = parsecontrol('debian/control','debian/control');
        $package = getfield $sourcep, 'Source';
@@ -2084,8 +2193,8 @@ sub cmd_pull {
 }
 
 sub cmd_push {
 }
 
 sub cmd_push {
-    pushing();
     parseopts();
     parseopts();
+    pushing();
     badusage "-p is not allowed with dgit push" if defined $package;
     check_not_dirty();
     my $clogp = parsechangelog();
     badusage "-p is not allowed with dgit push" if defined $package;
     check_not_dirty();
     my $clogp = parsechangelog();
@@ -2397,7 +2506,7 @@ END
        local $ENV{'EDITOR'} = cmdoutput qw(realpath --), $0;
        local $ENV{'VISUAL'} = $ENV{'EDITOR'};
        local $ENV{$fakeeditorenv} = cmdoutput qw(realpath --), $descfn;
        local $ENV{'EDITOR'} = cmdoutput qw(realpath --), $0;
        local $ENV{'VISUAL'} = $ENV{'EDITOR'};
        local $ENV{$fakeeditorenv} = cmdoutput qw(realpath --), $descfn;
-       runcmd_ordryrun_local @dpkgsource, qw(--commit .), $patchname;
+       runcmd @dpkgsource, qw(--commit .), $patchname;
     }
 }
 
     }
 }
 
@@ -2451,9 +2560,10 @@ sub quiltify ($$) {
     # should be contained within debian/patches.
 
     changedir '../fake';
     # should be contained within debian/patches.
 
     changedir '../fake';
+    remove_stray_gits();
     mktree_in_ud_here();
     rmtree '.pc';
     mktree_in_ud_here();
     rmtree '.pc';
-    runcmd @git, 'add', '.';
+    runcmd @git, qw(add -Af .);
     my $oldtiptree=git_write_tree();
     changedir '../work';
 
     my $oldtiptree=git_write_tree();
     changedir '../work';
 
@@ -2554,7 +2664,7 @@ sub quiltify ($$) {
        my $abbrev = sub {
            my $x = $_[0]{Commit};
            $x =~ s/(.*?[0-9a-z]{8})[0-9a-z]*$/$1/;
        my $abbrev = sub {
            my $x = $_[0]{Commit};
            $x =~ s/(.*?[0-9a-z]{8})[0-9a-z]*$/$1/;
-           return $;
+           return $x;
        };
        my $reportnot = sub {
            my ($notp) = @_;
        };
        my $reportnot = sub {
            my ($notp) = @_;
@@ -2641,12 +2751,95 @@ sub quiltify ($$) {
 }
 
 sub build_maybe_quilt_fixup () {
 }
 
 sub build_maybe_quilt_fixup () {
-    my $format=get_source_format;
+    my ($format,$fopts) = get_source_format;
     return unless madformat $format;
     # sigh
 
     check_for_vendor_patches();
 
     return unless madformat $format;
     # sigh
 
     check_for_vendor_patches();
 
+    my $clogp = parsechangelog();
+    my $headref = git_rev_parse('HEAD');
+
+    prep_ud();
+    changedir $ud;
+
+    my $upstreamversion=$version;
+    $upstreamversion =~ s/-[^-]*$//;
+
+    if ($fopts->{'single-debian-patch'}) {
+       quilt_fixup_singlepatch($clogp, $headref, $upstreamversion);
+    } else {
+       quilt_fixup_multipatch($clogp, $headref, $upstreamversion);
+    }
+
+    changedir '../../../..';
+    runcmd_ordryrun_local
+        @git, qw(pull --ff-only -q .git/dgit/unpack/work master);
+}
+
+sub quilt_fixup_mkwork ($) {
+    my ($headref) = @_;
+
+    mkdir "work" or die $!;
+    changedir "work";
+    mktree_in_ud_here();
+    runcmd @git, qw(reset -q --hard), $headref;
+}
+
+sub quilt_fixup_linkorigs ($$) {
+    my ($upstreamversion, $fn) = @_;
+    # calls $fn->($leafname);
+
+    foreach my $f (<../../../../*>) { #/){
+       my $b=$f; $b =~ s{.*/}{};
+       {
+           local ($debuglevel) = $debuglevel-1;
+           printdebug "QF linkorigs $b, $f ?\n";
+       }
+       next unless is_orig_file $b, srcfn $upstreamversion,'';
+       printdebug "QF linkorigs $b, $f Y\n";
+       link_ltarget $f, $b or die "$b $!";
+        $fn->($b);
+    }
+}
+
+sub quilt_fixup_delete_pc () {
+    runcmd @git, qw(rm -rqf .pc);
+    commit_admin "Commit removal of .pc (quilt series tracking data)";
+}
+
+sub quilt_fixup_singlepatch ($$$) {
+    my ($clogp, $headref, $upstreamversion) = @_;
+
+    progress "starting quiltify (single-debian-patch)";
+
+    # dpkg-source --commit generates new patches even if
+    # single-debian-patch is in debian/source/options.  In order to
+    # get it to generate debian/patches/debian-changes, it is
+    # necessary to build the source package.
+
+    quilt_fixup_linkorigs($upstreamversion, sub { });
+    quilt_fixup_mkwork($headref);
+
+    rmtree("debian/patches");
+
+    runcmd @dpkgsource, qw(-b .);
+    chdir "..";
+    runcmd @dpkgsource, qw(-x), (srcfn $version, ".dsc");
+    rename srcfn("$upstreamversion", "/debian/patches"), 
+           "work/debian/patches";
+
+    chdir "work";
+    commit_quilty_patch();
+
+    
+}
+
+sub quilt_fixup_multipatch ($$$) {
+    my ($clogp, $headref, $upstreamversion) = @_;
+
+    progress "starting quiltify (multiple patches, $quilt_mode mode)";
+
     # Our objective is:
     #  - honour any existing .pc in case it has any strangeness
     #  - determine the git commit corresponding to the tip of
     # Our objective is:
     #  - honour any existing .pc in case it has any strangeness
     #  - determine the git commit corresponding to the tip of
@@ -2670,7 +2863,7 @@ sub build_maybe_quilt_fixup () {
     # can work.  We do this as follows:
     #     1. Collect all relevant .orig from parent directory
     #     2. Generate a debian.tar.gz out of
     # can work.  We do this as follows:
     #     1. Collect all relevant .orig from parent directory
     #     2. Generate a debian.tar.gz out of
-    #         debian/{patches,rules,source/format}
+    #         debian/{patches,rules,source/format,source/options}
     #     3. Generate a fake .dsc containing just these fields:
     #          Format Source Version Files
     #     4. Extract the fake .dsc
     #     3. Generate a fake .dsc containing just these fields:
     #          Format Source Version Files
     #     4. Extract the fake .dsc
@@ -2691,15 +2884,6 @@ sub build_maybe_quilt_fixup () {
     #     5. If we had a .pc in-tree, delete it, and git-commit
     #     6. Back in the main tree, fast forward to the new HEAD
 
     #     5. If we had a .pc in-tree, delete it, and git-commit
     #     6. Back in the main tree, fast forward to the new HEAD
 
-    my $clogp = parsechangelog();
-    my $headref = git_rev_parse('HEAD');
-
-    prep_ud();
-    changedir $ud;
-
-    my $upstreamversion=$version;
-    $upstreamversion =~ s/-[^-]*$//;
-
     my $fakeversion="$upstreamversion-~~DGITFAKE";
 
     my $fakedsc=new IO::File 'fake.dsc', '>' or die $!;
     my $fakeversion="$upstreamversion-~~DGITFAKE";
 
     my $fakedsc=new IO::File 'fake.dsc', '>' or die $!;
@@ -2723,16 +2907,12 @@ END
        print $fakedsc " ".$md->hexdigest." $size $b\n" or die $!;
     };
 
        print $fakedsc " ".$md->hexdigest." $size $b\n" or die $!;
     };
 
-    foreach my $f (<../../../../*>) { #/){
-       my $b=$f; $b =~ s{.*/}{};
-       next unless is_orig_file $b, srcfn $upstreamversion,'';
-       link $f, $b or die "$b $!";
-        $dscaddfile->($b);
-    }
+    quilt_fixup_linkorigs($upstreamversion, $dscaddfile);
 
     my @files=qw(debian/source/format debian/rules);
 
     my @files=qw(debian/source/format debian/rules);
-    if (stat_exists '../../../debian/patches') {
-        push @files, 'debian/patches';
+    foreach my $maybe (qw(debian/patches debian/source/options)) {
+        next unless stat_exists "../../../$maybe";
+        push @files, $maybe;
     }
 
     my $debtar= srcfn $fakeversion,'.debian.tar.gz';
     }
 
     my $debtar= srcfn $fakeversion,'.debian.tar.gz';
@@ -2746,10 +2926,7 @@ END
     my $fakexdir= $package.'-'.(stripepoch $upstreamversion);
     rename $fakexdir, "fake" or die "$fakexdir $!";
 
     my $fakexdir= $package.'-'.(stripepoch $upstreamversion);
     rename $fakexdir, "fake" or die "$fakexdir $!";
 
-    mkdir "work" or die $!;
-    changedir "work";
-    mktree_in_ud_here();
-    runcmd @git, qw(reset --hard), $headref;
+    quilt_fixup_mkwork($headref);
 
     my $mustdeletepc=0;
     if (stat_exists ".pc") {
 
     my $mustdeletepc=0;
     if (stat_exists ".pc") {
@@ -2771,12 +2948,8 @@ END
     commit_quilty_patch();
 
     if ($mustdeletepc) {
     commit_quilty_patch();
 
     if ($mustdeletepc) {
-        runcmd @git, qw(rm -rqf .pc);
-        commit_admin "Commit removal of .pc (quilt series tracking data)";
+        quilt_fixup_delete_pc();
     }
     }
-
-    changedir '../../../..';
-    runcmd @git, qw(pull --ff-only -q .git/dgit/unpack/work master);
 }
 
 sub quilt_fixup_editor () {
 }
 
 sub quilt_fixup_editor () {
@@ -2800,7 +2973,10 @@ sub quilt_fixup_editor () {
 
 #----- other building -----
 
 
 #----- other building -----
 
+our $suppress_clean;
+
 sub clean_tree () {
 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') {
     if ($cleanmode eq 'dpkg-source') {
        runcmd_ordryrun_local @dpkgbuildpackage, qw(-T clean);
     } elsif ($cleanmode eq 'dpkg-source-d') {
@@ -2823,10 +2999,12 @@ sub clean_tree () {
 
 sub cmd_clean () {
     badusage "clean takes no additional arguments" if @ARGV;
 
 sub cmd_clean () {
     badusage "clean takes no additional arguments" if @ARGV;
+    notpushing();
     clean_tree();
 }
 
 sub build_prep () {
     clean_tree();
 }
 
 sub build_prep () {
+    notpushing();
     badusage "-p is not allowed when building" if defined $package;
     check_not_dirty();
     clean_tree();
     badusage "-p is not allowed when building" if defined $package;
     check_not_dirty();
     clean_tree();
@@ -2835,10 +3013,23 @@ sub build_prep () {
     $package = getfield $clogp, 'Source';
     $version = getfield $clogp, 'Version';
     build_maybe_quilt_fixup();
     $package = getfield $clogp, 'Source';
     $version = getfield $clogp, 'Version';
     build_maybe_quilt_fixup();
+    if ($rmchanges) {
+       my $pat = changespat $version;
+       foreach my $f (glob "$buildproductsdir/$pat") {
+           if (act_local()) {
+               unlink $f or fail "remove old changes file $f: $!";
+           } else {
+               progress "would remove $f";
+           }
+       }
+    }
 }
 
 }
 
-sub changesopts () {
+sub changesopts_initial () {
     my @opts =@changesopts[1..$#changesopts];
     my @opts =@changesopts[1..$#changesopts];
+}
+
+sub changesopts_version () {
     if (!defined $changes_since_version) {
        my @vsns = archive_query('archive_query');
        my @quirk = access_quirk();
     if (!defined $changes_since_version) {
        my @vsns = archive_query('archive_query');
        my @quirk = access_quirk();
@@ -2859,59 +3050,132 @@ sub changesopts () {
        }
     }
     if ($changes_since_version ne '_') {
        }
     }
     if ($changes_since_version ne '_') {
-       unshift @opts, "-v$changes_since_version";
+       return ("-v$changes_since_version");
+    } else {
+       return ();
     }
     }
-    return @opts;
 }
 
 }
 
-sub massage_dbp_args ($) {
-    my ($cmd) = @_;
-    return unless $cleanmode =~ m/git|none/;
+sub changesopts () {
+    return (changesopts_initial(), changesopts_version());
+}
+
+sub massage_dbp_args ($;$) {
+    my ($cmd,$xargs) = @_;
+    # We need to:
+    #
+    #  - if we're going to split the source build out so we can
+    #    do strange things to it, massage the arguments to dpkg-buildpackage
+    #    so that the main build doessn't build source (or add an argument
+    #    to stop it building source by default).
+    #
+    #  - add -nc to stop dpkg-source cleaning the source tree,
+    #    unless we're not doing a split build and want dpkg-source
+    #    as cleanmode, in which case we can do nothing
+    #
+    # return values:
+    #    0 - source will NOT need to be built separately by caller
+    #   +1 - source will need to be built separately by caller
+    #   +2 - source will need to be built separately by caller AND
+    #        dpkg-buildpackage should not in fact be run at all!
     debugcmd '#massaging#', @$cmd if $debuglevel>1;
     debugcmd '#massaging#', @$cmd if $debuglevel>1;
-    my @newcmd = shift @$cmd;
+#print STDERR "MASS0 ",Dumper($cmd, $xargs, $need_split_build_invocation);
+    if ($cleanmode eq 'dpkg-source' && !$need_split_build_invocation) {
+       $suppress_clean = 1;
+       return 0;
+    }
     # -nc has the side effect of specifying -b if nothing else specified
     # -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
     # and some combinations of -S, -b, et al, are errors, rather than
-    # later simply overriding earlier
-    push @newcmd, '-F' unless grep { m/^-[bBASF]$/ } @$cmd;
-    push @newcmd, @$cmd;
-    @$cmd = @newcmd;
+    # later simply overriding earlie.  So we need to:
+    #  - search the command line for these options
+    #  - pick the last one
+    #  - perhaps add our own as a default
+    #  - perhaps adjust it to the corresponding non-source-building version
+    my $dmode = '-F';
+    foreach my $l ($cmd, $xargs) {
+       next unless $l;
+       @$l = grep { !(m/^-[SgGFABb]$/s and $dmode=$_) } @$l;
+    }
+    push @$cmd, '-nc';
+#print STDERR "MASS1 ",Dumper($cmd, $xargs, $dmode);
+    my $r = 0;
+    if ($need_split_build_invocation) {
+       $r = $dmode =~ m/[S]/     ? +2 :
+            $dmode =~ y/gGF/ABb/ ? +1 :
+            $dmode =~ m/[ABb]/   ?  0 :
+            die "$dmode ?";
+    }
+    push @$cmd, $dmode;
+#print STDERR "MASS2 ",Dumper($cmd, $xargs, $r);
+    return $r;
 }
 
 sub cmd_build {
 }
 
 sub cmd_build {
-    build_prep();
-    my @dbp = (@dpkgbuildpackage, qw(-us -uc), changesopts(), @ARGV);
-    massage_dbp_args \@dbp;
-    runcmd_ordryrun_local @dbp;
+    my @dbp = (@dpkgbuildpackage, qw(-us -uc), changesopts_initial(), @ARGV);
+    my $wantsrc = massage_dbp_args \@dbp;
+    if ($wantsrc > 0) {
+       build_source();
+    } else {
+       build_prep();
+    }
+    if ($wantsrc < 2) {
+       push @dbp, changesopts_version();
+       runcmd_ordryrun_local @dbp;
+    }
     printdone "build successful\n";
 }
 
     printdone "build successful\n";
 }
 
-sub cmd_git_build {
-    build_prep();
+sub cmd_gbp_build {
     my @dbp = @dpkgbuildpackage;
     my @dbp = @dpkgbuildpackage;
-    massage_dbp_args \@dbp;
-    my @cmd =
-       (qw(git-buildpackage -us -uc --git-no-sign-tags),
-        "--git-builder=@dbp");
-    unless (grep { m/^--git-debian-branch|^--git-ignore-branch/ } @ARGV) {
-       canonicalise_suite();
-       push @cmd, "--git-debian-branch=".lbranch();
+
+    my $wantsrc = 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 ($wantsrc > 0) {
+       build_source();
+    } else {
+       if (!$suppress_clean) {
+           push @cmd, '--git-cleaner=true';
+       }
+       build_prep();
+    }
+    if ($wantsrc < 2) {
+       unless (grep { m/^--git-debian-branch|^--git-ignore-branch/ } @ARGV) {
+           canonicalise_suite();
+           push @cmd, "--git-debian-branch=".lbranch();
+       }
+       push @cmd, changesopts();
+       runcmd_ordryrun_local @cmd, @ARGV;
     }
     }
-    push @cmd, changesopts();
-    runcmd_ordryrun_local @cmd, @ARGV;
     printdone "build successful\n";
 }
     printdone "build successful\n";
 }
+sub cmd_git_build { cmd_gbp_build(); } # compatibility with <= 1.0
 
 sub build_source {
 
 sub build_source {
+    if ($cleanmode =~ m/^dpkg-source/) {
+       # dpkg-source will clean, so we shouldn't
+       $suppress_clean = 1;
+    }
     build_prep();
     build_prep();
-    $sourcechanges = "${package}_".(stripepoch $version)."_source.changes";
+    $sourcechanges = changespat $version,'source';
+    if (act_local()) {
+       unlink "../$sourcechanges" or $!==ENOENT
+           or fail "remove $sourcechanges: $!";
+    }
     $dscfn = dscfn($version);
     if ($cleanmode eq 'dpkg-source') {
     $dscfn = dscfn($version);
     if ($cleanmode eq 'dpkg-source') {
-       runcmd_ordryrun_local (@dpkgbuildpackage, qw(-us -uc -S)),
-           changesopts();
+       runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc -S),
+                              changesopts();
     } elsif ($cleanmode eq 'dpkg-source-d') {
     } elsif ($cleanmode eq 'dpkg-source-d') {
-       runcmd_ordryrun_local (@dpkgbuildpackage, qw(-us -uc -S -d)),
-           changesopts();
+       runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc -S -d),
+                              changesopts();
     } else {
        my $pwd = must_getcwd();
        my $leafdir = basename $pwd;
     } else {
        my $pwd = must_getcwd();
        my $leafdir = basename $pwd;
@@ -2933,29 +3197,42 @@ sub cmd_build_source {
 
 sub cmd_sbuild {
     build_source();
 
 sub cmd_sbuild {
     build_source();
+    my $pat = changespat $version;
+    if (!$rmchanges) {
+       my @unwanted = map { s#^\.\./##; $_; } glob "../$pat";
+       @unwanted = grep { $_ ne changespat $version,'source' } @unwanted;
+       fail "changes files other than source matching $pat".
+           " already present (@unwanted);".
+           " building would result in ambiguity about the intended results"
+           if @unwanted;
+    }
     changedir "..";
     changedir "..";
-    my $pat = "${package}_".(stripepoch $version)."_*.changes";
     if (act_local()) {
        stat_exists $dscfn or fail "$dscfn (in parent directory): $!";
        stat_exists $sourcechanges
            or fail "$sourcechanges (in parent directory): $!";
     if (act_local()) {
        stat_exists $dscfn or fail "$dscfn (in parent directory): $!";
        stat_exists $sourcechanges
            or fail "$sourcechanges (in parent directory): $!";
-       foreach my $cf (glob $pat) {
-           next if $cf eq $sourcechanges;
-           unlink $cf or fail "remove $cf: $!";
-       }
     }
     }
-    runcmd_ordryrun_local @sbuild, @ARGV, qw(-d), $isuite, $dscfn;
+    runcmd_ordryrun_local @sbuild, qw(-d), $isuite, @ARGV, $dscfn;
     my @changesfiles = glob $pat;
     @changesfiles = sort {
        ($b =~ m/_source\.changes$/ <=> $a =~ m/_source\.changes$/)
            or $a cmp $b
     } @changesfiles;
     fail "wrong number of different changes files (@changesfiles)"
     my @changesfiles = glob $pat;
     @changesfiles = sort {
        ($b =~ m/_source\.changes$/ <=> $a =~ m/_source\.changes$/)
            or $a cmp $b
     } @changesfiles;
     fail "wrong number of different changes files (@changesfiles)"
-       unless @changesfiles;
+       unless @changesfiles==2;
+    my $binchanges = parsecontrol($changesfiles[1], "binary changes file");
+    foreach my $l (split /\n/, getfield $binchanges, 'Files') {
+       fail "$l found in binaries changes file $binchanges"
+           if $l =~ m/\.dsc$/;
+    }
     runcmd_ordryrun_local @mergechanges, @changesfiles;
     runcmd_ordryrun_local @mergechanges, @changesfiles;
-    my $multichanges = "${package}_".(stripepoch $version)."_multi.changes";
+    my $multichanges = changespat $version,'multi';
     if (act_local()) {
        stat_exists $multichanges or fail "$multichanges: $!";
     if (act_local()) {
        stat_exists $multichanges or fail "$multichanges: $!";
+       foreach my $cf (glob $pat) {
+           next if $cf eq $multichanges;
+           rename "$cf", "$cf.inmulti" or fail "$cf\{,.inmulti}: $!";
+       }
     }
     printdone "build successful, results in $multichanges\n" or die $!;
 }    
     }
     printdone "build successful, results in $multichanges\n" or die $!;
 }    
@@ -2965,6 +3242,8 @@ sub cmd_quilt_fixup {
     my $clogp = parsechangelog();
     $version = getfield $clogp, 'Version';
     $package = getfield $clogp, 'Source';
     my $clogp = parsechangelog();
     $version = getfield $clogp, 'Version';
     $package = getfield $clogp, 'Source';
+    check_not_dirty();
+    clean_tree();
     build_maybe_quilt_fixup();
 }
 
     build_maybe_quilt_fixup();
 }
 
@@ -2987,7 +3266,17 @@ sub cmd_clone_dgit_repos_server {
 
 sub cmd_setup_mergechangelogs {
     badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
 
 sub cmd_setup_mergechangelogs {
     badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
-    setup_mergechangelogs();
+    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 ----------
 }
 
 #---------- argument parsing and main program ----------
@@ -2997,6 +3286,44 @@ sub cmd_version {
     exit 0;
 }
 
     exit 0;
 }
 
+our (%valopts_long, %valopts_short);
+our @rvalopts;
+
+sub defvalopt ($$$$) {
+    my ($long,$short,$val_re,$how) = @_;
+    my $oi = { Long => $long, Short => $short, Re => $val_re, How => $how };
+    $valopts_long{$long} = $oi;
+    $valopts_short{$short} = $oi;
+    # $how subref should:
+    #   do whatever assignemnt or thing it likes with $_[0]
+    #   if the option should not be passed on to remote, @rvalopts=()
+    # or $how can be a scalar ref, meaning simply assign the value
+}
+
+defvalopt '--since-version', '-v', '[^_]+|_', \$changes_since_version;
+defvalopt '--distro',        '-d', '.+',      \$idistro;
+defvalopt '',                '-k', '.+',      \$keyid;
+defvalopt '--existing-package','', '.*',      \$existing_package;
+defvalopt '--build-products-dir','','.*',     \$buildproductsdir;
+defvalopt '--clean',       '', $cleanmode_re, \$cleanmode;
+defvalopt '--quilt',     '', $quilt_modes_re, \$quilt_mode;
+
+defvalopt '', '-c', '.*=.*', sub { push @git, '-c', @_; };
+
+defvalopt '', '-C', '.+', sub {
+    ($changesfile) = (@_);
+    if ($changesfile =~ s#^(.*)/##) {
+       $buildproductsdir = $1;
+    }
+};
+
+defvalopt '--initiator-tempdir','','.*', sub {
+    ($initiator_tempdir) = (@_);
+    $initiator_tempdir =~ m#^/# or
+       badusage "--initiator-tempdir must be used specify an".
+       " absolute, not relative, directory."
+};
+
 sub parseopts () {
     my $om;
 
 sub parseopts () {
     my $om;
 
@@ -3006,6 +3333,27 @@ sub parseopts () {
        @ssh = ($ENV{'GIT_SSH'});
     }
 
        @ssh = ($ENV{'GIT_SSH'});
     }
 
+    my $oi;
+    my $val;
+    my $valopt = sub {
+       my ($what) = @_;
+       @rvalopts = ($_);
+       if (!defined $val) {
+           badusage "$what needs a value" unless @ARGV;
+           $val = shift @ARGV;
+           push @rvalopts, $val;
+       }
+       badusage "bad value \`$val' for $what" unless
+           $val =~ m/^$oi->{Re}$(?!\n)/s;
+       my $how = $oi->{How};
+       if (ref($how) eq 'SCALAR') {
+           $$how = $val;
+       } else {
+           $how->($val);
+       }
+       push @ropts, @rvalopts;
+    };
+
     while (@ARGV) {
        last unless $ARGV[0] =~ m/^-/;
        $_ = shift @ARGV;
     while (@ARGV) {
        last unless $ARGV[0] =~ m/^-/;
        $_ = shift @ARGV;
@@ -3027,10 +3375,7 @@ sub parseopts () {
            } elsif (m/^--new$/) {
                push @ropts, $_;
                $new_package=1;
            } 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, $_;
                     ($om = $opts_opt_map{$1}) &&
                     length $om->[0]) {
                push @ropts, $_;
@@ -3040,30 +3385,6 @@ sub parseopts () {
                     ($om = $opts_opt_map{$1})) {
                push @ropts, $_;
                push @$om, $2;
                     ($om = $opts_opt_map{$1})) {
                push @ropts, $_;
                push @$om, $2;
-           } elsif (m/^--existing-package=(.*)/s) {
-               push @ropts, $_;
-               $existing_package = $1;
-           } elsif (m/^--initiator-tempdir=(.*)/s) {
-               $initiator_tempdir = $1;
-               $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(?:-d)?|git|git-ff|check|none)$/s) {
-               push @ropts, $_;
-               $cleanmode = $1;
-           } elsif (m/^--clean=(.*)$/s) {
-               badusage "unknown cleaning mode \`$1'";
-           } elsif (m/^--quilt=($quilt_modes_re)$/s) {
-               push @ropts, $_;
-               $quilt_mode = $1;
-           } elsif (m/^--quilt=(.*)$/s) {
-               badusage "unknown quilt fixup mode \`$1'";
            } elsif (m/^--ignore-dirty$/s) {
                push @ropts, $_;
                $ignoredirty = 1;
            } elsif (m/^--ignore-dirty$/s) {
                push @ropts, $_;
                $ignoredirty = 1;
@@ -3073,9 +3394,19 @@ sub parseopts () {
            } elsif (m/^--no-rm-on-error$/s) {
                push @ropts, $_;
                $rmonerror = 0;
            } elsif (m/^--no-rm-on-error$/s) {
                push @ropts, $_;
                $rmonerror = 0;
+           } elsif (m/^--(no-)?rm-old-changes$/s) {
+               push @ropts, $_;
+               $rmchanges = !$1;
            } elsif (m/^--deliberately-($deliberately_re)$/s) {
                push @ropts, $_;
                push @deliberatelies, $&;
            } elsif (m/^--deliberately-($deliberately_re)$/s) {
                push @ropts, $_;
                push @deliberatelies, $&;
+           } elsif (m/^--always-split-source-build$/s) {
+               # undocumented, for testing
+               push @ropts, $_;
+               $need_split_build_invocation = 1;
+           } elsif (m/^(--[-0-9a-z]+)(=|$)/ && ($oi = $valopts_long{$1})) {
+               $val = $2 ? $' : undef; #';
+               $valopt->($oi->{Long});
            } else {
                badusage "unknown long option \`$_'";
            }
            } else {
                badusage "unknown long option \`$_'";
            }
@@ -3096,30 +3427,10 @@ sub parseopts () {
                } elsif (s/^-N/-/) {
                    push @ropts, $&;
                    $new_package=1;
                } 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, $_;
                    $_ = '';
                } elsif (m/^-m/) {
                    push @ropts, $&;
                    push @changesopts, $_;
                    $_ = '';
-               } 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;
-                   if ($changesfile =~ s#^(.*)/##) {
-                       $buildproductsdir = $1;
-                   }
-               } elsif (s/^-k(.+)//s) {
-                   $keyid=$1;
-               } elsif (m/^-[vdCk]$/) {
-                   badusage
- "option \`$_' requires an argument (and no space before the argument)";
                } elsif (s/^-wn$//s) {
                    push @ropts, $&;
                    $cleanmode = 'none';
                } elsif (s/^-wn$//s) {
                    push @ropts, $&;
                    $cleanmode = 'none';
@@ -3138,6 +3449,11 @@ sub parseopts () {
                } elsif (s/^-wc$//s) {
                    push @ropts, $&;
                    $cleanmode = 'check';
                } 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 \`$_'";
                }
                } else {
                    badusage "unknown short option \`$_'";
                }
@@ -3146,11 +3462,40 @@ 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}) {
 if ($ENV{$fakeeditorenv}) {
+    git_slurp_config();
     quilt_fixup_editor();
 }
 
 parseopts();
     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"
 
 print STDERR "DRY RUN ONLY\n" if $dryrun_level > 1;
 print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n"
@@ -3162,6 +3507,11 @@ if (!@ARGV) {
 my $cmd = shift @ARGV;
 $cmd =~ y/-/_/;
 
 my $cmd = shift @ARGV;
 $cmd =~ y/-/_/;
 
+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')
 if (!defined $quilt_mode) {
     local $access_forpush;
     $quilt_mode = cfg('dgit.force.quilt-mode', 'RETURN-UNDEF')
@@ -3172,6 +3522,15 @@ if (!defined $quilt_mode) {
     $quilt_mode = $1;
 }
 
     $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->();
 my $fn = ${*::}{"cmd_$cmd"};
 $fn or badusage "unknown operation $cmd";
 $fn->();