chiark / gitweb /
dgit: With sbuild, unapply patches in right dir
[dgit.git] / dgit
diff --git a/dgit b/dgit
index 299c531214cc5a50efd193bc29de9b7d7fea820f..95398497b7c4d9ba77208da8297b33b5c949a851 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -34,12 +34,15 @@ use POSIX;
 use IPC::Open2;
 use Digest::SHA;
 use Digest::MD5;
 use IPC::Open2;
 use Digest::SHA;
 use Digest::MD5;
+use List::Util qw(any);
+use List::MoreUtils qw(pairwise);
+use Carp;
 
 use Debian::Dgit;
 
 our $our_version = 'UNRELEASED'; ###substituted###
 
 
 use Debian::Dgit;
 
 our $our_version = 'UNRELEASED'; ###substituted###
 
-our @rpushprotovsn_support = qw(3 2);
+our @rpushprotovsn_support = qw(4 3 2); # 4 is new tag format
 our $protovsn;
 
 our $isuite = 'unstable';
 our $protovsn;
 
 our $isuite = 'unstable';
@@ -61,15 +64,22 @@ our $cleanmode;
 our $changes_since_version;
 our $rmchanges;
 our $quilt_mode;
 our $changes_since_version;
 our $rmchanges;
 our $quilt_mode;
-our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck';
+our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck|gbp|unapplied';
 our $we_are_responder;
 our $initiator_tempdir;
 our $we_are_responder;
 our $initiator_tempdir;
+our $patches_applied_dirtily = 00;
+our $tagformat_want;
+our $tagformat;
+our $tagformatfn;
 
 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 %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_authline_re = '^([^<>]+) \<(\S+)\> (\d+ [-+]\d+)$';
+our $splitbraincache = 'dgit-intern/quilt-cache';
+
 our (@git) = qw(git);
 our (@dget) = qw(dget);
 our (@curl) = qw(curl -f);
 our (@git) = qw(git);
 our (@dget) = qw(dget);
 our (@curl) = qw(curl -f);
@@ -83,6 +93,7 @@ our (@dpkgbuildpackage) = qw(dpkg-buildpackage -i\.git/ -I.git);
 our (@dpkgsource) = qw(dpkg-source -i\.git/ -I.git);
 our (@dpkggenchanges) = qw(dpkg-genchanges);
 our (@mergechanges) = qw(mergechanges -f);
 our (@dpkgsource) = qw(dpkg-source -i\.git/ -I.git);
 our (@dpkggenchanges) = qw(dpkg-genchanges);
 our (@mergechanges) = qw(mergechanges -f);
+our (@gbp) = qw(gbp);
 our (@changesopts) = ('');
 
 our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
 our (@changesopts) = ('');
 
 our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
@@ -97,6 +108,7 @@ our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
                      'dpkg-source' => \@dpkgsource,
                      'dpkg-buildpackage' => \@dpkgbuildpackage,
                      'dpkg-genchanges' => \@dpkggenchanges,
                      'dpkg-source' => \@dpkgsource,
                      'dpkg-buildpackage' => \@dpkgbuildpackage,
                      'dpkg-genchanges' => \@dpkggenchanges,
+                     'gbp' => \@gbp,
                      'ch' => \@changesopts,
                      'mergechanges' => \@mergechanges);
 
                      'ch' => \@changesopts,
                      'mergechanges' => \@mergechanges);
 
@@ -114,6 +126,7 @@ autoflush STDOUT 1;
 
 our $supplementary_message = '';
 our $need_split_build_invocation = 0;
 
 our $supplementary_message = '';
 our $need_split_build_invocation = 0;
+our $split_brain = 0;
 
 END {
     local ($@, $?);
 
 END {
     local ($@, $?);
@@ -125,6 +138,17 @@ our @ourdscfield = qw(Dgit Vcs-Dgit-Master);
 our $csuite;
 our $instead_distro;
 
 our $csuite;
 our $instead_distro;
 
+sub debiantag ($$) {
+    my ($v,$distro) = @_;
+    return $tagformatfn->($v, $distro);
+}
+
+sub debiantag_maintview ($$) { 
+    my ($v,$distro) = @_;
+    $v =~ y/~:/_%/;
+    return "$distro/$v";
+}
+
 sub lbranch () { return "$branchprefix/$csuite"; }
 my $lbranch_re = '^refs/heads/'.$branchprefix.'/([^/.]+)$';
 sub lref () { return "refs/heads/".lbranch(); }
 sub lbranch () { return "$branchprefix/$csuite"; }
 my $lbranch_re = '^refs/heads/'.$branchprefix.'/([^/.]+)$';
 sub lref () { return "refs/heads/".lbranch(); }
@@ -132,6 +156,28 @@ sub lrref () { return "refs/remotes/$remotename/".server_branch($csuite); }
 sub rrref () { return server_ref($csuite); }
 
 sub lrfetchrefs () { return "refs/dgit-fetch/$csuite"; }
 sub rrref () { return server_ref($csuite); }
 
 sub lrfetchrefs () { return "refs/dgit-fetch/$csuite"; }
+sub lrfetchref () { return lrfetchrefs.'/'.server_branch($csuite); }
+
+# We fetch some parts of lrfetchrefs/*.  Ideally we delete these
+# locally fetched refs because they have unhelpful names and clutter
+# up gitk etc.  So we track whether we have "used up" head ref (ie,
+# whether we have made another local ref which refers to this object).
+#
+# (If we deleted them unconditionally, then we might end up
+# re-fetching the same git objects each time dgit fetch was run.)
+#
+# So, leach use of lrfetchrefs needs to be accompanied by arrangements
+# in git_fetch_us to fetch the refs in question, and possibly a call
+# to lrfetchref_used.
+
+our (%lrfetchrefs_f, %lrfetchrefs_d);
+# $lrfetchrefs_X{lrfetchrefs."/heads/whatever"} = $objid
+
+sub lrfetchref_used ($) {
+    my ($fullrefname) = @_;
+    my $objid = $lrfetchrefs_f{$fullrefname};
+    $lrfetchrefs_d{$fullrefname} = $objid if defined $objid;
+}
 
 sub stripepoch ($) {
     my ($vsn) = @_;
 
 sub stripepoch ($) {
     my ($vsn) = @_;
@@ -173,11 +219,6 @@ sub no_such_package () {
     exit 4;
 }
 
     exit 4;
 }
 
-sub fetchspec () {
-    local $csuite = '*';
-    return  "+".rrref().":".lrref();
-}
-
 sub changedir ($) {
     my ($newdir) = @_;
     printdebug "CD $newdir\n";
 sub changedir ($) {
     my ($newdir) = @_;
     printdebug "CD $newdir\n";
@@ -195,6 +236,10 @@ sub deliberately_not_fast_forward () {
     }
 }
 
     }
 }
 
+sub quiltmode_splitbrain () {
+    $quilt_mode =~ m/gbp|dpm|unapplied/;
+}
+
 #---------- remote protocol support, common ----------
 
 # remote push initiator/responder protocol:
 #---------- remote protocol support, common ----------
 
 # remote push initiator/responder protocol:
@@ -202,6 +247,16 @@ sub deliberately_not_fast_forward () {
 #  where <rargs> is <push-host-dir> <supported-proto-vsn>,... ...
 #  < dgit-remote-push-ready <actual-proto-vsn>
 #
 #  where <rargs> is <push-host-dir> <supported-proto-vsn>,... ...
 #  < dgit-remote-push-ready <actual-proto-vsn>
 #
+# occasionally:
+#
+#  > progress NBYTES
+#  [NBYTES message]
+#
+#  > supplementary-message NBYTES          # $protovsn >= 3
+#  [NBYTES message]
+#
+# main sequence:
+#
 #  > file parsed-changelog
 #  [indicates that output of dpkg-parsechangelog follows]
 #  > data-block NBYTES
 #  > file parsed-changelog
 #  [indicates that output of dpkg-parsechangelog follows]
 #  > data-block NBYTES
@@ -215,7 +270,13 @@ sub deliberately_not_fast_forward () {
 #  > file changes
 #  [etc]
 #
 #  > file changes
 #  [etc]
 #
-#  > param head HEAD
+#  > param head DGIT-VIEW-HEAD
+#  > param csuite SUITE
+#  > param tagformat old|new
+#  > param maint-view MAINT-VIEW-HEAD
+#
+#  > previously REFNAME=OBJNAME       # if --deliberately-not-fast-forward
+#                                     # goes into tag, for replay prevention
 #
 #  > want signed-tag
 #  [indicates that signed tag is wanted]
 #
 #  > want signed-tag
 #  [indicates that signed tag is wanted]
@@ -387,7 +448,7 @@ our ($dscdata,$dscurl,$dsc,$dsc_checked,$skew_warning_vsn);
 
 sub runcmd {
     debugcmd "+",@_;
 
 sub runcmd {
     debugcmd "+",@_;
-    $!=0; $?=0;
+    $!=0; $?=-1;
     failedcmd @_ if system @_;
 }
 
     failedcmd @_ if system @_;
 }
 
@@ -471,10 +532,12 @@ our %defcfg = ('dgit.default.distro' => 'debian',
               'dgit.default.ssh' => 'ssh',
               'dgit.default.archive-query' => 'madison:',
               'dgit.default.sshpsql-dbname' => 'service=projectb',
               'dgit.default.ssh' => 'ssh',
               'dgit.default.archive-query' => 'madison:',
               'dgit.default.sshpsql-dbname' => 'service=projectb',
+              'dgit.default.dgit-tag-format' => 'old,new,maint',
               'dgit-distro.debian.archive-query' => 'ftpmasterapi:',
               'dgit-distro.debian.git-check' => 'url',
               'dgit-distro.debian.git-check-suffix' => '/info/refs',
               'dgit-distro.debian.new-private-pushers' => 't',
               'dgit-distro.debian.archive-query' => 'ftpmasterapi:',
               'dgit-distro.debian.git-check' => 'url',
               'dgit-distro.debian.git-check-suffix' => '/info/refs',
               'dgit-distro.debian.new-private-pushers' => 't',
+              'dgit-distro.debian.dgit-tag-format' => 'old',
               '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-url' => '',
               'dgit-distro.debian/push.git-host' => 'push.dgit.debian.org',
               'dgit-distro.debian/push.git-user-force' => 'dgit',
@@ -522,7 +585,7 @@ sub git_slurp_config () {
     my @cmd = (@git, qw(config -z --get-regexp .*));
     debugcmd "|",@cmd;
 
     my @cmd = (@git, qw(config -z --get-regexp .*));
     debugcmd "|",@cmd;
 
-    open GITS, "-|", @cmd or failedcmd @cmd;
+    open GITS, "-|", @cmd or die $!;
     while (<GITS>) {
        chomp or die;
        printdebug "=> ", (messagequote $_), "\n";
     while (<GITS>) {
        chomp or die;
        printdebug "=> ", (messagequote $_), "\n";
@@ -1096,6 +1159,48 @@ sub archive_query_dummycat ($$) {
     return sort { -version_compare($a->[0],$b->[0]); } @rows;
 }
 
     return sort { -version_compare($a->[0],$b->[0]); } @rows;
 }
 
+#---------- tag format handling ----------
+
+sub access_cfg_tagformats () {
+    split /\,/, access_cfg('dgit-tag-format');
+}
+
+sub need_tagformat ($$) {
+    my ($fmt, $why) = @_;
+    fail "need to use tag format $fmt ($why) but also need".
+       " to use tag format $tagformat_want->[0] ($tagformat_want->[1])".
+       " - no way to proceed"
+       if $tagformat_want && $tagformat_want->[0] ne $fmt;
+    $tagformat_want = [$fmt, $why, $tagformat_want->[2] // 0];
+}
+
+sub select_tagformat () {
+    # sets $tagformatfn
+    return if $tagformatfn && !$tagformat_want;
+    die 'bug' if $tagformatfn && $tagformat_want;
+    # ... $tagformat_want assigned after previous select_tagformat
+
+    my (@supported) = grep { $_ ne 'maint' } access_cfg_tagformats();
+    printdebug "select_tagformat supported @supported\n";
+
+    $tagformat_want //= [ $supported[0], "distro access configuration", 0 ];
+    printdebug "select_tagformat specified @$tagformat_want\n";
+
+    my ($fmt,$why,$override) = @$tagformat_want;
+
+    fail "target distro supports tag formats @supported".
+       " but have to use $fmt ($why)"
+       unless $override
+           or grep { $_ eq $fmt } @supported;
+
+    $tagformat_want = undef;
+    $tagformat = $fmt;
+    $tagformatfn = ${*::}{"debiantag_$fmt"};
+
+    fail "trying to use unknown tag format \`$fmt' ($why) !"
+       unless $tagformatfn;
+}
+
 #---------- archive query entrypoints and rest of program ----------
 
 sub canonicalise_suite () {
 #---------- archive query entrypoints and rest of program ----------
 
 sub canonicalise_suite () {
@@ -1133,9 +1238,11 @@ sub get_archive_dsc () {
        my $fmt = getfield $dsc, 'Format';
        fail "unsupported source format $fmt, sorry" unless $format_ok{$fmt};
        $dsc_checked = !!$digester;
        my $fmt = getfield $dsc, 'Format';
        fail "unsupported source format $fmt, sorry" unless $format_ok{$fmt};
        $dsc_checked = !!$digester;
+       printdebug "get_archive_dsc: Version ".(getfield $dsc, 'Version')."\n";
        return;
     }
     $dsc = undef;
        return;
     }
     $dsc = undef;
+    printdebug "get_archive_dsc: nothing in archive, returning undef\n";
 }
 
 sub check_for_git ();
 }
 
 sub check_for_git ();
@@ -1149,7 +1256,7 @@ sub check_for_git () {
             " set -e; cd ".access_cfg('git-path').";".
             " if test -d $package.git; then echo 1; else echo 0; fi");
        my $r= cmdoutput @cmd;
             " set -e; cd ".access_cfg('git-path').";".
             " if test -d $package.git; then echo 1; else echo 0; fi");
        my $r= cmdoutput @cmd;
-       if ($r =~ m/^divert (\w+)$/) {
+       if (defined $r and $r =~ m/^divert (\w+)$/) {
            my $divert=$1;
            my ($usedistro,) = access_distros();
            # NB that if we are pushing, $usedistro will be $distro/push
            my $divert=$1;
            my ($usedistro,) = access_distros();
            # NB that if we are pushing, $usedistro will be $distro/push
@@ -1158,7 +1265,7 @@ sub check_for_git () {
            progress "diverting to $divert (using config for $instead_distro)";
            return check_for_git();
        }
            progress "diverting to $divert (using config for $instead_distro)";
            return check_for_git();
        }
-       failedcmd @cmd unless $r =~ m/^[01]$/;
+       failedcmd @cmd unless defined $r and $r =~ m/^[01]$/;
        return $r+0;
     } elsif ($how eq 'url') {
        my $prefix = access_cfg('git-check-url','git-url');
        return $r+0;
     } elsif ($how eq 'url') {
        my $prefix = access_cfg('git-check-url','git-url');
@@ -1205,7 +1312,7 @@ sub create_remote_git_repo () {
     }
 }
 
     }
 }
 
-our ($dsc_hash,$lastpush_hash);
+our ($dsc_hash,$lastpush_mergeinput);
 
 our $ud = '.git/dgit/unpack';
 
 
 our $ud = '.git/dgit/unpack';
 
@@ -1232,7 +1339,7 @@ sub git_write_tree () {
 sub remove_stray_gits () {
     my @gitscmd = qw(find -name .git -prune -print0);
     debugcmd "|",@gitscmd;
 sub remove_stray_gits () {
     my @gitscmd = qw(find -name .git -prune -print0);
     debugcmd "|",@gitscmd;
-    open GITS, "-|", @gitscmd or failedcmd @gitscmd;
+    open GITS, "-|", @gitscmd or die $!;
     {
        local $/="\0";
        while (<GITS>) {
     {
        local $/="\0";
        while (<GITS>) {
@@ -1248,7 +1355,7 @@ sub remove_stray_gits () {
 sub mktree_in_ud_from_only_subdir () {
     # changes into the subdir
     my (@dirs) = <*/.>;
 sub mktree_in_ud_from_only_subdir () {
     # changes into the subdir
     my (@dirs) = <*/.>;
-    die unless @dirs==1;
+    die "@dirs ?" unless @dirs==1;
     $dirs[0] =~ m#^([^/]+)/\.$# or die;
     my $dir = $1;
     changedir $dir;
     $dirs[0] =~ m#^([^/]+)/\.$# or die;
     my $dir = $1;
     changedir $dir;
@@ -1314,9 +1421,10 @@ sub clogp_authline ($) {
     $author =~ s#,.*##ms;
     my $date = cmdoutput qw(date), '+%s %z', qw(-d), getfield($clogp,'Date');
     my $authline = "$author $date";
     $author =~ s#,.*##ms;
     my $date = cmdoutput qw(date), '+%s %z', qw(-d), getfield($clogp,'Date');
     my $authline = "$author $date";
-    $authline =~ m/^[^<>]+ \<\S+\> \d+ [-+]\d+$/ or
+    $authline =~ m/$git_authline_re/o or
        fail "unexpected commit author line format \`$authline'".
        " (was generated from changelog Maintainer field)";
        fail "unexpected commit author line format \`$authline'".
        " (was generated from changelog Maintainer field)";
+    return ($1,$2,$3) if wantarray;
     return $authline;
 }
 
     return $authline;
 }
 
@@ -1383,7 +1491,8 @@ sub check_for_vendor_patches () {
                          "distro being accessed");
 }
 
                          "distro being accessed");
 }
 
-sub generate_commit_from_dsc () {
+sub generate_commits_from_dsc () {
+    # See big comment in fetch_from_archive, below.
     prep_ud();
     changedir $ud;
 
     prep_ud();
     changedir $ud;
 
@@ -1432,48 +1541,45 @@ $changes
 # imported from the archive
 END
     close C or die $!;
 # imported from the archive
 END
     close C or die $!;
-    my $outputhash = make_commit qw(../commit.tmp);
+    my $rawimport_hash = make_commit qw(../commit.tmp);
     my $cversion = getfield $clogp, 'Version';
     my $cversion = getfield $clogp, 'Version';
+    my $rawimport_mergeinput = {
+        Commit => $rawimport_hash,
+        Info => "Import of source package",
+    };
+    my @output = ($rawimport_mergeinput);
     progress "synthesised git commit from .dsc $cversion";
     progress "synthesised git commit from .dsc $cversion";
-    if ($lastpush_hash) {
-       runcmd @git, qw(reset --hard), $lastpush_hash;
+    if ($lastpush_mergeinput) {
+       my $lastpush_hash = $lastpush_mergeinput->{Commit};
+       runcmd @git, qw(reset -q --hard), $lastpush_hash;
        runcmd qw(sh -ec), 'dpkg-parsechangelog >>../changelogold.tmp';
        my $oldclogp = parsecontrol('../changelogold.tmp','previous changelog');
        my $oversion = getfield $oldclogp, 'Version';
        my $vcmp =
            version_compare($oversion, $cversion);
        if ($vcmp < 0) {
        runcmd qw(sh -ec), 'dpkg-parsechangelog >>../changelogold.tmp';
        my $oldclogp = parsecontrol('../changelogold.tmp','previous changelog');
        my $oversion = getfield $oldclogp, 'Version';
        my $vcmp =
            version_compare($oversion, $cversion);
        if ($vcmp < 0) {
-           # git upload/ is earlier vsn than archive, use archive
-           open C, ">../commit2.tmp" or die $!;
-           print C <<END or die $!;
-tree $tree
-parent $lastpush_hash
-parent $outputhash
-author $authline
-committer $authline
-
+           @output = ($rawimport_mergeinput, $lastpush_mergeinput,
+               { Message => <<END, ReverseParents => 1 });
 Record $package ($cversion) in archive suite $csuite
 END
 Record $package ($cversion) in archive suite $csuite
 END
-            $outputhash = make_commit qw(../commit2.tmp);
        } elsif ($vcmp > 0) {
            print STDERR <<END or die $!;
 
        } elsif ($vcmp > 0) {
            print STDERR <<END or die $!;
 
-Version actually in archive:    $cversion (older)
-Last allegedly pushed/uploaded: $oversion (newer or same)
+Version actually in archive:   $cversion (older)
+Last version pushed with dgit: $oversion (newer or same)
 $later_warning_msg
 END
 $later_warning_msg
 END
-            $outputhash = $lastpush_hash;
+            @output = $lastpush_mergeinput;
         } else {
         } else {
-           $outputhash = $lastpush_hash;
+           # Same version.  Use what's in the server git branch,
+           # discarding our own import.  (This could happen if the
+           # server automatically imports all packages into git.)
+           @output = $lastpush_mergeinput;
        }
     }
     changedir '../../../..';
        }
     }
     changedir '../../../..';
-    runcmd @git, qw(update-ref -m),"dgit fetch import $cversion",
-            'DGIT_ARCHIVE', $outputhash;
-    cmdoutput @git, qw(log -n2), $outputhash;
-    # ... gives git a chance to complain if our commit is malformed
     rmtree($ud);
     rmtree($ud);
-    return $outputhash;
+    return @output;
 }
 
 sub complete_file_from_dsc ($$) {
 }
 
 sub complete_file_from_dsc ($$) {
@@ -1522,28 +1628,157 @@ sub ensure_we_have_orig () {
 }
 
 sub git_fetch_us () {
 }
 
 sub git_fetch_us () {
-    my @specs = (fetchspec());
-    push @specs,
-        map { "+refs/$_/*:".lrfetchrefs."/$_/*" }
-        qw(tags heads);
-    runcmd_ordryrun_local @git, qw(fetch -p -n -q), access_giturl(), @specs;
+    # Want to fetch only what we are going to use, unless
+    # deliberately-not-ff, in which case we must fetch everything.
+
+    my @specs = deliberately_not_fast_forward ? qw(tags/*) :
+       map { "tags/$_" } debiantags('*',access_basedistro);
+    push @specs, server_branch($csuite);
+    push @specs, qw(heads/*) if deliberately_not_fast_forward;
+
+    # This is rather miserable:
+    # When git-fetch --prune is passed a fetchspec ending with a *,
+    # it does a plausible thing.  If there is no * then:
+    # - it matches subpaths too, even if the supplied refspec
+    #   starts refs, and behaves completely madly if the source
+    #   has refs/refs/something.  (See, for example, Debian #NNNN.)
+    # - if there is no matching remote ref, it bombs out the whole
+    #   fetch.
+    # We want to fetch a fixed ref, and we don't know in advance
+    # if it exists, so this is not suitable.
+    #
+    # Our workaround is to use git-ls-remote.  git-ls-remote has its
+    # own qairks.  Notably, it has the absurd multi-tail-matching
+    # behaviour: git-ls-remote R refs/foo can report refs/foo AND
+    # refs/refs/foo etc.
+    #
+    # Also, we want an idempotent snapshot, but we have to make two
+    # calls to the remote: one to git-ls-remote and to git-fetch.  The
+    # solution is use git-ls-remote to obtain a target state, and
+    # git-fetch to try to generate it.  If we don't manage to generate
+    # the target state, we try again.
+
+    my $specre = join '|', map {
+       my $x = $_;
+       $x =~ s/\W/\\$&/g;
+       $x =~ s/\\\*$/.*/;
+       "(?:refs/$x)";
+    } @specs;
+    printdebug "git_fetch_us specre=$specre\n";
+    my $wanted_rref = sub {
+       local ($_) = @_;
+       return m/^(?:$specre)$/o;
+    };
+
+    my $fetch_iteration = 0;
+    FETCH_ITERATION:
+    for (;;) {
+        if (++$fetch_iteration > 10) {
+           fail "too many iterations trying to get sane fetch!";
+       }
+
+       my @look = map { "refs/$_" } @specs;
+       my @lcmd = (@git, qw(ls-remote -q --refs), access_giturl(), @look);
+       debugcmd "|",@lcmd;
+
+       my %wantr;
+       open GITLS, "-|", @lcmd or die $!;
+       while (<GITLS>) {
+           printdebug "=> ", $_;
+           m/^(\w+)\s+(\S+)\n/ or die "ls-remote $_ ?";
+           my ($objid,$rrefname) = ($1,$2);
+           if (!$wanted_rref->($rrefname)) {
+               print STDERR <<END;
+warning: git-ls-remote @look reported $rrefname; this is silly, ignoring it.
+END
+               next;
+           }
+           $wantr{$rrefname} = $objid;
+       }
+       $!=0; $?=0;
+       close GITLS or failedcmd @lcmd;
+
+       # OK, now %want is exactly what we want for refs in @specs
+       my @fspecs = map {
+           return () if !m/\*$/ && !exists $wantr{"refs/$_"};
+           "+refs/$_:".lrfetchrefs."/$_";
+       } @specs;
+
+       my @fcmd = (@git, qw(fetch -p -n -q), access_giturl(), @fspecs);
+       runcmd_ordryrun_local @git, qw(fetch -p -n -q), access_giturl(),
+           @fspecs;
+
+       %lrfetchrefs_f = ();
+       my %objgot;
+
+       git_for_each_ref(lrfetchrefs, sub {
+           my ($objid,$objtype,$lrefname,$reftail) = @_;
+           $lrfetchrefs_f{$lrefname} = $objid;
+           $objgot{$objid} = 1;
+       });
+
+       foreach my $lrefname (sort keys %lrfetchrefs_f) {
+           my $rrefname = 'refs'.substr($lrefname, length lrfetchrefs);
+           if (!exists $wantr{$rrefname}) {
+               if ($wanted_rref->($rrefname)) {
+                   printdebug <<END;
+git-fetch @fspecs created $lrefname which git-ls-remote @look didn't list.
+END
+               } else {
+                   print STDERR <<END
+warning: git-fetch @fspecs created $lrefname; this is silly, deleting it.
+END
+               }
+               runcmd_ordryrun_local @git, qw(update-ref -d), $lrefname;
+               delete $lrfetchrefs_f{$lrefname};
+               next;
+           }
+       }
+       foreach my $rrefname (sort keys %wantr) {
+           my $lrefname = lrfetchrefs.substr($rrefname, 4);
+           my $got = $lrfetchrefs_f{$lrefname} // '<none>';
+           my $want = $wantr{$rrefname};
+           next if $got eq $want;
+           if (!defined $objgot{$want}) {
+               print STDERR <<END;
+warning: git-ls-remote suggests we want $lrefname
+warning:  and it should refer to $want
+warning:  but git-fetch didn't fetch that object to any relevant ref.
+warning:  This may be due to a race with someone updating the server.
+warning:  Will try again...
+END
+               next FETCH_ITERATION;
+           }
+           printdebug <<END;
+git-fetch @fspecs made $lrefname=$got but want git-ls-remote @look says $want
+END
+           runcmd_ordryrun_local @git, qw(update-ref -m),
+               "dgit fetch git-fetch fixup", $lrefname, $want;
+           $lrfetchrefs_f{$lrefname} = $want;
+       }
+       last;
+    }
+    printdebug "git_fetch_us: git-fetch --no-insane emulation complete\n",
+       Dumper(\%lrfetchrefs_f);
 
     my %here;
 
     my %here;
-    my $tagpat = debiantag('*',access_basedistro);
+    my @tagpats = debiantags('*',access_basedistro);
 
 
-    git_for_each_ref("refs/tags/".$tagpat, sub {
+    git_for_each_ref([map { "refs/tags/$_" } @tagpats], sub {
        my ($objid,$objtype,$fullrefname,$reftail) = @_;
        printdebug "currently $fullrefname=$objid\n";
        $here{$fullrefname} = $objid;
     });
        my ($objid,$objtype,$fullrefname,$reftail) = @_;
        printdebug "currently $fullrefname=$objid\n";
        $here{$fullrefname} = $objid;
     });
-    git_for_each_ref(lrfetchrefs."/tags/".$tagpat, sub {
+    git_for_each_ref([map { lrfetchrefs."/tags/".$_ } @tagpats], sub {
        my ($objid,$objtype,$fullrefname,$reftail) = @_;
        my ($objid,$objtype,$fullrefname,$reftail) = @_;
-       my $lref = "refs".substr($fullrefname, length lrfetchrefs);
+       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;
        printdebug "offered $lref=$objid\n";
        if (!defined $here{$lref}) {
            my @upd = (@git, qw(update-ref), $lref, $objid, '');
            runcmd_ordryrun_local @upd;
+           lrfetchref_used $fullrefname;
        } elsif ($here{$lref} eq $objid) {
        } elsif ($here{$lref} eq $objid) {
+           lrfetchref_used $fullrefname;
        } else {
            print STDERR \
                "Not updateting $lref from $here{$lref} to $objid.\n";
        } else {
            print STDERR \
                "Not updateting $lref from $here{$lref} to $objid.\n";
@@ -1551,6 +1786,21 @@ sub git_fetch_us () {
     });
 }
 
     });
 }
 
+sub mergeinfo_getclogp ($) {
+    my ($mi) = @_;
+    # Ensures thit $mi->{Clogp} exists and returns it
+    return $mi->{Clogp} if $mi->{Clogp};
+    my $mclog = ".git/dgit/clog-$mi->{Commit}";
+    mkpath '.git/dgit';
+    runcmd shell_cmd "exec >$mclog", @git, qw(cat-file blob),
+       "$mi->{Commit}:debian/changelog";
+    $mi->{Clogp} = parsechangelog("-l$mclog");
+}
+
+sub mergeinfo_version ($) {
+    return getfield( (mergeinfo_getclogp $_[0]), 'Version' );
+}
+
 sub fetch_from_archive () {
     # ensures that lrref() is what is actually in the archive,
     #  one way or another
 sub fetch_from_archive () {
     # ensures that lrref() is what is actually in the archive,
     #  one way or another
@@ -1572,35 +1822,164 @@ sub fetch_from_archive () {
        progress "no version available from the archive";
     }
 
        progress "no version available from the archive";
     }
 
-    $lastpush_hash = git_get_ref(lrref());
+    # If the archive's .dsc has a Dgit field, there are three
+    # relevant git commitids we need to choose between and/or merge
+    # together:
+    #   1. $dsc_hash: the Dgit field from the archive
+    #   2. $lastpush_hash: the suite branch on the dgit git server
+    #   3. $lastfetch_hash: our local tracking brach for the suite
+    #
+    # These may all be distinct and need not be in any fast forward
+    # relationship:
+    #
+    # If the dsc was pushed to this suite, then the server suite
+    # branch will have been updated; but it might have been pushed to
+    # a different suite and copied by the archive.  Conversely a more
+    # recent version may have been pushed with dgit but not appeared
+    # in the archive (yet).
+    #
+    # $lastfetch_hash may be awkward because archive imports
+    # (particularly, imports of Dgit-less .dscs) are performed only as
+    # needed on individual clients, so different clients may perform a
+    # different subset of them - and these imports are only made
+    # public during push.  So $lastfetch_hash may represent a set of
+    # imports different to a subsequent upload by a different dgit
+    # client.
+    #
+    # Our approach is as follows:
+    #
+    # As between $dsc_hash and $lastpush_hash: if $lastpush_hash is a
+    # descendant of $dsc_hash, then it was pushed by a dgit user who
+    # had based their work on $dsc_hash, so we should prefer it.
+    # Otherwise, $dsc_hash was installed into this suite in the
+    # archive other than by a dgit push, and (necessarily) after the
+    # last dgit push into that suite (since a dgit push would have
+    # been descended from the dgit server git branch); thus, in that
+    # case, we prefer the archive's version (and produce a
+    # pseudo-merge to overwrite the dgit server git branch).
+    #
+    # (If there is no Dgit field in the archive's .dsc then
+    # generate_commit_from_dsc uses the version numbers to decide
+    # whether the suite branch or the archive is newer.  If the suite
+    # branch is newer it ignores the archive's .dsc; otherwise it
+    # generates an import of the .dsc, and produces a pseudo-merge to
+    # overwrite the suite branch with the archive contents.)
+    #
+    # The outcome of that part of the algorithm is the `public view',
+    # and is same for all dgit clients: it does not depend on any
+    # unpublished history in the local tracking branch.
+    #
+    # As between the public view and the local tracking branch: The
+    # local tracking branch is only updated by dgit fetch, and
+    # whenever dgit fetch runs it includes the public view in the
+    # local tracking branch.  Therefore if the public view is not
+    # descended from the local tracking branch, the local tracking
+    # branch must contain history which was imported from the archive
+    # but never pushed; and, its tip is now out of date.  So, we make
+    # a pseudo-merge to overwrite the old imports and stitch the old
+    # history in.
+    #
+    # Finally: we do not necessarily reify the public view (as
+    # described above).  This is so that we do not end up stacking two
+    # pseudo-merges.  So what we actually do is figure out the inputs
+    # to any public view psuedo-merge and put them in @mergeinputs.
+
+    my @mergeinputs;
+    # $mergeinputs[]{Commit}
+    # $mergeinputs[]{Info}
+    # $mergeinputs[0] is the one whose tree we use
+    # @mergeinputs is in the order we use in the actual commit)
+    #
+    # Also:
+    # $mergeinputs[]{Message} is a commit message to use
+    # $mergeinputs[]{ReverseParents} if def specifies that parent
+    #                                list should be in opposite order
+    # Such an entry has no Commit or Info.  It applies only when found
+    # in the last entry.  (This ugliness is to support making
+    # identical imports to previous dgit versions.)
+
+    my $lastpush_hash = git_get_ref(lrfetchref());
     printdebug "previous reference hash=$lastpush_hash\n";
     printdebug "previous reference hash=$lastpush_hash\n";
-    my $hash;
+    $lastpush_mergeinput = $lastpush_hash && {
+        Commit => $lastpush_hash,
+       Info => "dgit suite branch on dgit git server",
+    };
+
+    my $lastfetch_hash = git_get_ref(lrref());
+    printdebug "fetch_from_archive: lastfetch=$lastfetch_hash\n";
+    my $lastfetch_mergeinput = $lastfetch_hash && {
+       Commit => $lastfetch_hash,
+       Info => "dgit client's archive history view",
+    };
+
+    my $dsc_mergeinput = $dsc_hash && {
+        Commit => $dsc_hash,
+        Info => "Dgit field in .dsc from archive",
+    };
+
+    my $cwd = getcwd();
+    my $del_lrfetchrefs = sub {
+       changedir $cwd;
+       my $gur;
+       printdebug "del_lrfetchrefs\n";
+       foreach my $fullrefname (sort keys %lrfetchrefs_d) {
+           my $objid = $lrfetchrefs_d{$fullrefname};
+           printdebug "del_lrfetchrefs: $fullrefname=$objid.\n";
+           if (!$gur) {
+               $gur ||= new IO::Handle;
+               open $gur, "|-", qw(git update-ref --stdin) or die $!;
+           }
+           printf $gur "delete %s %s\n", $fullrefname, $objid;
+       }
+       if ($gur) {
+           close $gur or failedcmd "git update-ref delete lrfetchrefs";
+       }
+    };
+
     if (defined $dsc_hash) {
        fail "missing remote git history even though dsc has hash -".
     if (defined $dsc_hash) {
        fail "missing remote git history even though dsc has hash -".
-           " could not find ref ".lrref().
-           " (should have been fetched from ".access_giturl()."#".rrref().")"
+           " could not find ref ".rref()." at ".access_giturl()
            unless $lastpush_hash;
            unless $lastpush_hash;
-       $hash = $dsc_hash;
        ensure_we_have_orig();
        if ($dsc_hash eq $lastpush_hash) {
        ensure_we_have_orig();
        if ($dsc_hash eq $lastpush_hash) {
+           @mergeinputs = $dsc_mergeinput
        } elsif (is_fast_fwd($dsc_hash,$lastpush_hash)) {
            print STDERR <<END or die $!;
 
 Git commit in archive is behind the last version allegedly pushed/uploaded.
        } elsif (is_fast_fwd($dsc_hash,$lastpush_hash)) {
            print STDERR <<END or die $!;
 
 Git commit in archive is behind the last version allegedly pushed/uploaded.
-Commit referred to by archive:  $dsc_hash
-Last allegedly pushed/uploaded: $lastpush_hash
+Commit referred to by archive: $dsc_hash
+Last version pushed with dgit: $lastpush_hash
 $later_warning_msg
 END
 $later_warning_msg
 END
-           $hash = $lastpush_hash;
+           @mergeinputs = ($lastpush_mergeinput);
        } else {
        } else {
-           fail "git head (".lrref()."=$lastpush_hash) is not a ".
-               "descendant of archive's .dsc hash ($dsc_hash)";
+           # Archive has .dsc which is not a descendant of the last dgit
+           # push.  This can happen if the archive moves .dscs about.
+           # Just follow its lead.
+           if (is_fast_fwd($lastpush_hash,$dsc_hash)) {
+               progress "archive .dsc names newer git commit";
+               @mergeinputs = ($dsc_mergeinput);
+           } else {
+               progress "archive .dsc names other git commit, fixing up";
+               @mergeinputs = ($dsc_mergeinput, $lastpush_mergeinput);
+           }
        }
     } elsif ($dsc) {
        }
     } elsif ($dsc) {
-       $hash = generate_commit_from_dsc();
+       @mergeinputs = generate_commits_from_dsc();
+       # We have just done an import.  Now, our import algorithm might
+       # have been improved.  But even so we do not want to generate
+       # a new different import of the same package.  So if the
+       # version numbers are the same, just use our existing version.
+       # If the version numbers are different, the archive has changed
+       # (perhaps, rewound).
+       if ($lastfetch_mergeinput &&
+           !version_compare( (mergeinfo_version $lastfetch_mergeinput),
+                             (mergeinfo_version $mergeinputs[0]) )) {
+           @mergeinputs = ($lastfetch_mergeinput);
+       }
     } elsif ($lastpush_hash) {
        # only in git, not in the archive yet
     } elsif ($lastpush_hash) {
        # only in git, not in the archive yet
-       $hash = $lastpush_hash;
+       @mergeinputs = ($lastpush_mergeinput);
        print STDERR <<END or die $!;
 
 Package not found in the archive, but has allegedly been pushed using dgit.
        print STDERR <<END or die $!;
 
 Package not found in the archive, but has allegedly been pushed using dgit.
@@ -1617,14 +1996,126 @@ But we were not able to obtain any version from the archive or git.
 
 END
        }
 
 END
        }
+       unshift @end, $del_lrfetchrefs;
        return 0;
     }
        return 0;
     }
-    printdebug "current hash=$hash\n";
-    if ($lastpush_hash) {
-       fail "not fast forward on last upload branch!".
-           " (archive's version left in DGIT_ARCHIVE)"
-           unless is_fast_fwd($lastpush_hash, $hash);
+
+    if ($lastfetch_hash &&
+       !grep {
+           my $h = $_->{Commit};
+           $h and is_fast_fwd($lastfetch_hash, $h);
+           # If true, one of the existing parents of this commit
+           # is a descendant of the $lastfetch_hash, so we'll
+           # be ff from that automatically.
+       } @mergeinputs
+        ) {
+       # Otherwise:
+       push @mergeinputs, $lastfetch_mergeinput;
+    }
+
+    printdebug "fetch mergeinfos:\n";
+    foreach my $mi (@mergeinputs) {
+       if ($mi->{Info}) {
+           printdebug " commit $mi->{Commit} $mi->{Info}\n";
+       } else {
+           printdebug sprintf " ReverseParents=%d Message=%s",
+               $mi->{ReverseParents}, $mi->{Message};
+       }
     }
     }
+
+    my $compat_info= pop @mergeinputs
+       if $mergeinputs[$#mergeinputs]{Message};
+
+    @mergeinputs = grep { defined $_->{Commit} } @mergeinputs;
+
+    my $hash;
+    if (@mergeinputs > 1) {
+       # here we go, then:
+       my $tree_commit = $mergeinputs[0]{Commit};
+
+       my $tree = cmdoutput @git, qw(cat-file commit), $tree_commit;
+       $tree =~ m/\n\n/;  $tree = $`;
+       $tree =~ m/^tree (\w+)$/m or die "$dsc_hash tree ?";
+       $tree = $1;
+
+       # We use the changelog author of the package in question the
+       # author of this pseudo-merge.  This is (roughly) correct if
+       # this commit is simply representing aa non-dgit upload.
+       # (Roughly because it does not record sponsorship - but we
+       # don't have sponsorship info because that's in the .changes,
+       # which isn't in the archivw.)
+       #
+       # But, it might be that we are representing archive history
+       # updates (including in-archive copies).  These are not really
+       # the responsibility of the person who created the .dsc, but
+       # there is no-one whose name we should better use.  (The
+       # author of the .dsc-named commit is clearly worse.)
+
+       my $useclogp = mergeinfo_getclogp $mergeinputs[0];
+       my $author = clogp_authline $useclogp;
+       my $cversion = getfield $useclogp, 'Version';
+
+       my $mcf = ".git/dgit/mergecommit";
+       open MC, ">", $mcf or die "$mcf $!";
+       print MC <<END or die $!;
+tree $tree
+END
+
+       my @parents = grep { $_->{Commit} } @mergeinputs;
+       @parents = reverse @parents if $compat_info->{ReverseParents};
+       print MC <<END or die $! foreach @parents;
+parent $_->{Commit}
+END
+
+       print MC <<END or die $!;
+author $author
+committer $author
+
+END
+
+       if (defined $compat_info->{Message}) {
+           print MC $compat_info->{Message} or die $!;
+       } else {
+           print MC <<END or die $!;
+Record $package ($cversion) in archive suite $csuite
+
+Record that
+END
+           my $message_add_info = sub {
+               my ($mi) = (@_);
+               my $mversion = mergeinfo_version $mi;
+               printf MC "  %-20s %s\n", $mversion, $mi->{Info}
+                   or die $!;
+           };
+
+           $message_add_info->($mergeinputs[0]);
+           print MC <<END or die $!;
+should be treated as descended from
+END
+           $message_add_info->($_) foreach @mergeinputs[1..$#mergeinputs];
+       }
+
+       close MC or die $!;
+       $hash = make_commit $mcf;
+    } else {
+       $hash = $mergeinputs[0]{Commit};
+    }
+    progress "fetch hash=$hash\n";
+
+    my $chkff = sub {
+       my ($lasth, $what) = @_;
+       return unless $lasth;
+       die "$lasth $hash $what ?" unless is_fast_fwd($lasth, $hash);
+    };
+
+    $chkff->($lastpush_hash, 'dgit repo server tip (last push)');
+    $chkff->($lastfetch_hash, 'local tracking tip (last fetch)');
+
+    runcmd @git, qw(update-ref -m), "dgit fetch $csuite",
+           'DGIT_ARCHIVE', $hash;
+    cmdoutput @git, qw(log -n2), $hash;
+    # ... gives git a chance to complain if our commit is malformed
+
     if (defined $skew_warning_vsn) {
        mkpath '.git/dgit';
        printdebug "SKEW CHECK WANT $skew_warning_vsn\n";
     if (defined $skew_warning_vsn) {
        mkpath '.git/dgit';
        printdebug "SKEW CHECK WANT $skew_warning_vsn\n";
@@ -1644,7 +2135,8 @@ We were able to obtain only   $got_vsn
 END
        }
     }
 END
        }
     }
-    if ($lastpush_hash ne $hash) {
+
+    if ($lastfetch_hash ne $hash) {
        my @upd_cmd = (@git, qw(update-ref -m), 'dgit fetch', lrref(), $hash);
        if (act_local()) {
            cmdoutput @upd_cmd;
        my @upd_cmd = (@git, qw(update-ref -m), 'dgit fetch', lrref(), $hash);
        if (act_local()) {
            cmdoutput @upd_cmd;
@@ -1652,6 +2144,10 @@ END
            dryrun_report @upd_cmd;
        }
     }
            dryrun_report @upd_cmd;
        }
     }
+
+    lrfetchref_used lrfetchref();
+
+    unshift @end, $del_lrfetchrefs;
     return 1;
 }
 
     return 1;
 }
 
@@ -1720,7 +2216,6 @@ sub clone ($) {
     runcmd @git, qw(init -q);
     my $giturl = access_giturl(1);
     if (defined $giturl) {
     runcmd @git, qw(init -q);
     my $giturl = access_giturl(1);
     if (defined $giturl) {
-       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 $!;
        open H, "> .git/HEAD" or die $!;
        print H "ref: ".lref()."\n" or die $!;
        close H or die $!;
@@ -1770,9 +2265,9 @@ sub check_not_dirty () {
 
     my @cmd = (@git, qw(diff --quiet HEAD));
     debugcmd "+",@cmd;
 
     my @cmd = (@git, qw(diff --quiet HEAD));
     debugcmd "+",@cmd;
-    $!=0; $?=0; system @cmd;
-    return if !$! && !$?;
-    if (!$! && $?==256) {
+    $!=0; $?=-1; system @cmd;
+    return if !$?;
+    if ($?==256) {
        fail "working tree is dirty (does not match HEAD)";
     } else {
        failedcmd @cmd;
        fail "working tree is dirty (does not match HEAD)";
     } else {
        failedcmd @cmd;
@@ -1838,11 +2333,15 @@ sub get_source_format () {
 sub madformat ($) {
     my ($format) = @_;
     return 0 unless $format eq '3.0 (quilt)';
 sub madformat ($) {
     my ($format) = @_;
     return 0 unless $format eq '3.0 (quilt)';
+    our $quilt_mode_warned;
     if ($quilt_mode eq 'nocheck') {
     if ($quilt_mode eq 'nocheck') {
-       progress "Not doing any fixup of \`$format' due to --no-quilt-fixup";
+       progress "Not doing any fixup of \`$format' due to".
+           " ----no-quilt-fixup or --quilt=nocheck"
+           unless $quilt_mode_warned++;
        return 0;
     }
        return 0;
     }
-    progress "Format \`$format', checking/updating patch stack";
+    progress "Format \`$format', need to check/update patch stack"
+       unless $quilt_mode_warned++;
     return 1;
 }
 
     return 1;
 }
 
@@ -1859,7 +2358,7 @@ sub push_parse_changelog ($) {
 
     my $dscfn = dscfn($cversion);
 
 
     my $dscfn = dscfn($cversion);
 
-    return ($clogp, $cversion, $tag, $dscfn);
+    return ($clogp, $cversion, $dscfn);
 }
 
 sub push_parse_dsc ($$$) {
 }
 
 sub push_parse_dsc ($$$) {
@@ -1872,13 +2371,38 @@ sub push_parse_dsc ($$$) {
            " but debian/changelog is for $package $cversion";
 }
 
            " but debian/changelog is for $package $cversion";
 }
 
-sub push_mktag ($$$$$$$) {
-    my ($head,$clogp,$tag,
-       $dscfn,
+sub push_tagwants ($$$$) {
+    my ($cversion, $dgithead, $maintviewhead, $tfbase) = @_;
+    my @tagwants;
+    push @tagwants, {
+        TagFn => \&debiantag,
+       Objid => $dgithead,
+        TfSuffix => '',
+        View => 'dgit',
+    };
+    if (defined $maintviewhead) {
+       push @tagwants, {
+            TagFn => \&debiantag_maintview,
+           Objid => $maintviewhead,
+           TfSuffix => '-maintview',
+            View => 'maint',
+        };
+    }
+    foreach my $tw (@tagwants) {
+       $tw->{Tag} = $tw->{TagFn}($cversion, access_basedistro);
+       $tw->{Tfn} = sub { $tfbase.$tw->{TfSuffix}.$_[0]; };
+    }
+    return @tagwants;
+}
+
+sub push_mktags ($$ $$ $) {
+    my ($clogp,$dscfn,
        $changesfile,$changesfilewhat,
        $changesfile,$changesfilewhat,
-       $tfn) = @_;
+        $tagwants) = @_;
 
 
-    $dsc->{$ourdscfield[0]} = $head;
+    die unless $tagwants->[0]{View} eq 'dgit';
+
+    $dsc->{$ourdscfield[0]} = $tagwants->[0]{Objid};
     $dsc->save("$dscfn.tmp") or die $!;
 
     my $changes = parsecontrol($changesfile,$changesfilewhat);
     $dsc->save("$dscfn.tmp") or die $!;
 
     my $changes = parsecontrol($changesfile,$changesfilewhat);
@@ -1896,45 +2420,66 @@ sub push_mktag ($$$$$$$) {
     my $authline = clogp_authline $clogp;
     my $delibs = join(" ", "",@deliberatelies);
     my $declaredistro = access_basedistro();
     my $authline = clogp_authline $clogp;
     my $delibs = join(" ", "",@deliberatelies);
     my $declaredistro = access_basedistro();
-    open TO, '>', $tfn->('.tmp') or die $!;
-    print TO <<END or die $!;
+
+    my $mktag = sub {
+       my ($tw) = @_;
+       my $tfn = $tw->{Tfn};
+       my $head = $tw->{Objid};
+       my $tag = $tw->{Tag};
+
+       open TO, '>', $tfn->('.tmp') or die $!;
+       print TO <<END or die $!;
 object $head
 type commit
 tag $tag
 tagger $authline
 
 object $head
 type commit
 tag $tag
 tagger $authline
 
+END
+       if ($tw->{View} eq 'dgit') {
+           print TO <<END or die $!;
 $package release $cversion for $clogsuite ($csuite) [dgit]
 [dgit distro=$declaredistro$delibs]
 END
 $package release $cversion for $clogsuite ($csuite) [dgit]
 [dgit distro=$declaredistro$delibs]
 END
-    foreach my $ref (sort keys %previously) {
-                   print TO <<END or die $!;
+           foreach my $ref (sort keys %previously) {
+               print TO <<END or die $!;
 [dgit previously:$ref=$previously{$ref}]
 END
 [dgit previously:$ref=$previously{$ref}]
 END
-    }
+           }
+       } elsif ($tw->{View} eq 'maint') {
+           print TO <<END or die $!;
+$package release $cversion for $clogsuite ($csuite)
+(maintainer view tag generated by dgit --quilt=$quilt_mode)
+END
+       } else {
+           die Dumper($tw)."?";
+       }
 
 
-    close TO or die $!;
+       close TO or die $!;
 
 
-    my $tagobjfn = $tfn->('.tmp');
-    if ($sign) {
-       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;
-       push @sign_cmd, $tfn->('.tmp');
-       runcmd_ordryrun @sign_cmd;
-       if (act_scary()) {
-           $tagobjfn = $tfn->('.signed.tmp');
-           runcmd shell_cmd "exec >$tagobjfn", qw(cat --),
-               $tfn->('.tmp'), $tfn->('.tmp.asc');
+       my $tagobjfn = $tfn->('.tmp');
+       if ($sign) {
+           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;
+           push @sign_cmd, $tfn->('.tmp');
+           runcmd_ordryrun @sign_cmd;
+           if (act_scary()) {
+               $tagobjfn = $tfn->('.signed.tmp');
+               runcmd shell_cmd "exec >$tagobjfn", qw(cat --),
+                   $tfn->('.tmp'), $tfn->('.tmp.asc');
+           }
        }
        }
-    }
+       return $tagobjfn;
+    };
 
 
-    return ($tagobjfn);
+    my @r = map { $mktag->($_); } @$tagwants;
+    return @r;
 }
 
 sub sign_changes ($) {
 }
 
 sub sign_changes ($) {
@@ -1955,16 +2500,21 @@ sub dopush ($) {
 Push failed, while preparing your push.
 You can retry the push, after fixing the problem, if you like.
 END
 Push failed, while preparing your push.
 You can retry the push, after fixing the problem, if you like.
 END
+
+    need_tagformat 'new', "quilt mode $quilt_mode"
+        if quiltmode_splitbrain;
+
     prep_ud();
 
     access_giturl(); # check that success is vaguely likely
     prep_ud();
 
     access_giturl(); # check that success is vaguely likely
+    select_tagformat();
 
     my $clogpfn = ".git/dgit/changelog.822.tmp";
     runcmd shell_cmd "exec >$clogpfn", qw(dpkg-parsechangelog);
 
     responder_send_file('parsed-changelog', $clogpfn);
 
 
     my $clogpfn = ".git/dgit/changelog.822.tmp";
     runcmd shell_cmd "exec >$clogpfn", qw(dpkg-parsechangelog);
 
     responder_send_file('parsed-changelog', $clogpfn);
 
-    my ($clogp, $cversion, $tag, $dscfn) =
+    my ($clogp, $cversion, $dscfn) =
        push_parse_changelog("$clogpfn");
 
     my $dscpath = "$buildproductsdir/$dscfn";
        push_parse_changelog("$clogpfn");
 
     my $dscpath = "$buildproductsdir/$dscfn";
@@ -1978,10 +2528,33 @@ END
 
     my $format = getfield $dsc, 'Format';
     printdebug "format $format\n";
 
     my $format = getfield $dsc, 'Format';
     printdebug "format $format\n";
+
+    my $actualhead = git_rev_parse('HEAD');
+    my $dgithead = $actualhead;
+    my $maintviewhead = undef;
+
     if (madformat($format)) {
        # user might have not used dgit build, so maybe do this now:
     if (madformat($format)) {
        # user might have not used dgit build, so maybe do this now:
-       commit_quilty_patch();
+       if (quiltmode_splitbrain()) {
+           my $upstreamversion = $clogp->{Version};
+           $upstreamversion =~ s/-[^-]*$//;
+           changedir $ud;
+           quilt_make_fake_dsc($upstreamversion);
+           my ($dgitview, $cachekey) =
+               quilt_check_splitbrain_cache($actualhead, $upstreamversion);
+           $dgitview or fail
+ "--quilt=$quilt_mode but no cached dgit view:
+ perhaps tree changed since dgit build[-source] ?";
+           $split_brain = 1;
+           $dgithead = $dgitview;
+           $maintviewhead = $actualhead;
+           changedir '../../../..';
+           prep_ud(); # so _only_subdir() works, below
+       } else {
+           commit_quilty_patch();
+       }
     }
     }
+
     check_not_dirty();
     changedir $ud;
     progress "checking that $dscfn corresponds to HEAD";
     check_not_dirty();
     changedir $ud;
     progress "checking that $dscfn corresponds to HEAD";
@@ -1991,9 +2564,9 @@ END
     check_for_vendor_patches() if madformat($dsc->{format});
     changedir '../../../..';
     my $diffopt = $debuglevel>0 ? '--exit-code' : '--quiet';
     check_for_vendor_patches() if madformat($dsc->{format});
     changedir '../../../..';
     my $diffopt = $debuglevel>0 ? '--exit-code' : '--quiet';
-    my @diffcmd = (@git, qw(diff), $diffopt, $tree);
+    my @diffcmd = (@git, qw(diff), $diffopt, $tree, $dgithead);
     debugcmd "+",@diffcmd;
     debugcmd "+",@diffcmd;
-    $!=0; $?=0;
+    $!=0; $?=-1;
     my $r = system @diffcmd;
     if ($r) {
        if ($r==256) {
     my $r = system @diffcmd;
     if ($r) {
        if ($r==256) {
@@ -2005,7 +2578,6 @@ END
            failedcmd @diffcmd;
        }
     }
            failedcmd @diffcmd;
        }
     }
-    my $head = git_rev_parse('HEAD');
     if (!$changesfile) {
        my $pat = changespat $cversion;
        my @cs = glob "$buildproductsdir/$pat";
     if (!$changesfile) {
        my $pat = changespat $cversion;
        my @cs = glob "$buildproductsdir/$pat";
@@ -2019,8 +2591,13 @@ END
     }
 
     responder_send_file('changes',$changesfile);
     }
 
     responder_send_file('changes',$changesfile);
-    responder_send_command("param head $head");
+    responder_send_command("param head $dgithead");
     responder_send_command("param csuite $csuite");
     responder_send_command("param csuite $csuite");
+    responder_send_command("param tagformat $tagformat");
+    if (quiltmode_splitbrain) {
+       die unless ($protovsn//4) >= 4;
+       responder_send_command("param maint-view $maintviewhead");
+    }
 
     if (deliberately_not_fast_forward) {
        git_for_each_ref(lrfetchrefs, sub {
 
     if (deliberately_not_fast_forward) {
        git_for_each_ref(lrfetchrefs, sub {
@@ -2031,8 +2608,9 @@ END
        });
     }
 
        });
     }
 
-    my $tfn = sub { ".git/dgit/tag$_[0]"; };
-    my $tagobjfn;
+    my @tagwants = push_tagwants($cversion, $dgithead, $maintviewhead,
+                                ".git/dgit/tag");
+    my @tagobjfns;
 
     supplementary_message(<<'END');
 Push failed, while signing the tag.
 
     supplementary_message(<<'END');
 Push failed, while signing the tag.
@@ -2040,23 +2618,29 @@ 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) {
 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);
+       @tagobjfns = map { $_->{Tfn}('.signed-tmp') } @tagwants;
+       responder_receive_files('signed-tag', @tagobjfns);
     } else {
     } else {
-       $tagobjfn =
-           push_mktag($head,$clogp,$tag,
-                      $dscpath,
-                      $changesfile,$changesfile,
-                      $tfn);
+       @tagobjfns = push_mktags($clogp,$dscpath,
+                             $changesfile,$changesfile,
+                             \@tagwants);
     }
     supplementary_message(<<'END');
 Push failed, *after* signing the tag.
 If you want to try again, you should use a new version number.
 END
 
     }
     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;
+    pairwise { $a->{TagObjFn} = $b } @tagwants, @tagobjfns;
+
+    foreach my $tw (@tagwants) {
+       my $tag = $tw->{Tag};
+       my $tagobjfn = $tw->{TagObjFn};
+       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;
+    }
 
     supplementary_message(<<'END');
 Push failed, while updating the remote git repository - see messages above.
 
     supplementary_message(<<'END');
 Push failed, while updating the remote git repository - see messages above.
@@ -2065,9 +2649,19 @@ END
     if (!check_for_git()) {
        create_remote_git_repo();
     }
     if (!check_for_git()) {
        create_remote_git_repo();
     }
-    runcmd_ordryrun @git, qw(push),access_giturl(),
-        $forceflag."HEAD:".rrref(), $forceflag."refs/tags/$tag";
-    runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), 'HEAD';
+
+    my @pushrefs = $forceflag.$dgithead.":".rrref();
+    foreach my $tw (@tagwants) {
+       my $view = $tw->{View};
+       next unless $view eq 'dgit'
+           or any { $_ eq $view } access_cfg_tagformats();
+           # ^ $view is "dgit" or "maint" so this looks for "maint"
+           # in archive supported tagformats.
+       push @pushrefs, $forceflag."refs/tags/$tw->{Tag}";
+    }
+
+    runcmd_ordryrun @git, qw(push),access_giturl(), @pushrefs;
+    runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), $dgithead;
 
     supplementary_message(<<'END');
 Push failed, after updating the remote git repository.
 
     supplementary_message(<<'END');
 Push failed, after updating the remote git repository.
@@ -2283,7 +2877,7 @@ sub cmd_remote_push_build_host {
        unless defined $protovsn;
 
     responder_send_command("dgit-remote-push-ready $protovsn");
        unless defined $protovsn;
 
     responder_send_command("dgit-remote-push-ready $protovsn");
-
+    rpush_handle_protovsn_bothends();
     changedir $dir;
     &cmd_push;
 }
     changedir $dir;
     &cmd_push;
 }
@@ -2292,6 +2886,13 @@ sub cmd_remote_push_responder { cmd_remote_push_build_host(); }
 # ... for compatibility with proto vsn.1 dgit (just so that user gets
 #     a good error message)
 
 # ... for compatibility with proto vsn.1 dgit (just so that user gets
 #     a good error message)
 
+sub rpush_handle_protovsn_bothends () {
+    if ($protovsn < 4) {
+       need_tagformat 'old', "rpush negotiated protocol $protovsn";
+    }
+    select_tagformat();
+}
+
 our $i_tmp;
 
 sub i_cleanup {
 our $i_tmp;
 
 sub i_cleanup {
@@ -2350,6 +2951,12 @@ sub cmd_rpush {
     ($protovsn) = initiator_expect { m/^dgit-remote-push-ready (\S+)/ };
     die "$protovsn ?" unless grep { $_ eq $protovsn } @rpushprotovsn_support;
     $supplementary_message = '' unless $protovsn >= 3;
     ($protovsn) = initiator_expect { m/^dgit-remote-push-ready (\S+)/ };
     die "$protovsn ?" unless grep { $_ eq $protovsn } @rpushprotovsn_support;
     $supplementary_message = '' unless $protovsn >= 3;
+
+    fail "rpush negotiated protocol version $protovsn".
+       " which does not support quilt mode $quilt_mode"
+       if quiltmode_splitbrain;
+
+    rpush_handle_protovsn_bothends();
     for (;;) {
        my ($icmd,$iargs) = initiator_expect {
            m/^(\S+)(?: (.*))?$/;
     for (;;) {
        my ($icmd,$iargs) = initiator_expect {
            m/^(\S+)(?: (.*))?$/;
@@ -2421,13 +3028,13 @@ sub i_resp_want ($) {
     print RI "files-end\n" or die $!;
 }
 
     print RI "files-end\n" or die $!;
 }
 
-our ($i_clogp, $i_version, $i_tag, $i_dscfn, $i_changesfn);
+our ($i_clogp, $i_version, $i_dscfn, $i_changesfn);
 
 sub i_localname_parsed_changelog {
     return "remote-changelog.822";
 }
 sub i_file_parsed_changelog {
 
 sub i_localname_parsed_changelog {
     return "remote-changelog.822";
 }
 sub i_file_parsed_changelog {
-    ($i_clogp, $i_version, $i_tag, $i_dscfn) =
+    ($i_clogp, $i_version, $i_dscfn) =
        push_parse_changelog "$i_tmp/remote-changelog.822";
     die if $i_dscfn =~ m#/|^\W#;
 }
        push_parse_changelog "$i_tmp/remote-changelog.822";
     die if $i_dscfn =~ m#/|^\W#;
 }
@@ -2454,17 +3061,26 @@ sub i_want_signed_tag {
     my $head = $i_param{'head'};
     die if $head =~ m/[^0-9a-f]/ || $head !~ m/^../;
 
     my $head = $i_param{'head'};
     die if $head =~ m/[^0-9a-f]/ || $head !~ m/^../;
 
+    my $maintview = $i_param{'maint-view'};
+    die if defined $maintview && $maintview =~ m/[^0-9a-f]/;
+
+    select_tagformat();
+    if ($protovsn >= 4) {
+       my $p = $i_param{'tagformat'} // '<undef>';
+       $p eq $tagformat
+           or badproto \*RO, "tag format mismatch: $p vs. $tagformat";
+    }
+
     die unless $i_param{'csuite'} =~ m/^$suite_re$/;
     $csuite = $&;
     push_parse_dsc $i_dscfn, 'remote dsc', $i_version;
 
     die unless $i_param{'csuite'} =~ m/^$suite_re$/;
     $csuite = $&;
     push_parse_dsc $i_dscfn, 'remote dsc', $i_version;
 
-    my $tagobjfn =
-       push_mktag $head, $i_clogp, $i_tag,
-           $i_dscfn,
-           $i_changesfn, 'remote changes',
-           sub { "tag$_[0]"; };
+    my @tagwants = push_tagwants $i_version, $head, $maintview, "tag";
 
 
-    return $tagobjfn;
+    return
+       push_mktags $i_clogp, $i_dscfn,
+           $i_changesfn, 'remote changes',
+           \@tagwants;
 }
 
 sub i_want_signed_dsc_changes {
 }
 
 sub i_want_signed_dsc_changes {
@@ -2510,17 +3126,28 @@ END
     }
 }
 
     }
 }
 
-sub quiltify_trees_differ ($$) {
-    my ($x,$y) = @_;
-    # returns 1 iff the two tree objects differ other than in debian/
+sub quiltify_trees_differ ($$;$$) {
+    my ($x,$y,$finegrained,$ignorenamesr) = @_;
+    # returns true iff the two tree objects differ other than in debian/
+    # with $finegrained,
+    # returns bitmask 01 - differ in upstream files except .gitignore
+    #                 02 - differ in .gitignore
+    # if $ignorenamesr is defined, $ingorenamesr->{$fn}
+    #  is set for each modified .gitignore filename $fn
     local $/=undef;
     local $/=undef;
-    my @cmd = (@git, qw(diff-tree --name-only -z), $x, $y);
+    my @cmd = (@git, qw(diff-tree --name-only -z));
+    push @cmd, qw(-r) if $finegrained;
+    push @cmd, $x, $y;
     my $diffs= cmdoutput @cmd;
     my $diffs= cmdoutput @cmd;
+    my $r = 0;
     foreach my $f (split /\0/, $diffs) {
     foreach my $f (split /\0/, $diffs) {
-       next if $f eq 'debian';
-       return 1;
+       next if $f =~ m#^debian(?:/.*)?$#s;
+       my $isignore = $f =~ m#^(?:.*/)?.gitignore$#s;
+       $r |= $isignore ? 02 : 01;
+       $ignorenamesr->{$f}=1 if $ignorenamesr && $isignore;
     }
     }
-    return 0;
+    printdebug "quiltify_trees_differ $x $y => $r\n";
+    return $r;
 }
 
 sub quiltify_tree_sentinelfiles ($) {
 }
 
 sub quiltify_tree_sentinelfiles ($) {
@@ -2532,8 +3159,98 @@ sub quiltify_tree_sentinelfiles ($) {
     return $r;
 }
 
     return $r;
 }
 
-sub quiltify ($$$) {
-    my ($clogp,$target,$origtree) = @_;
+sub quiltify_splitbrain_needed () {
+    if (!$split_brain) {
+       progress "dgit view: changes are required...";
+       runcmd @git, qw(checkout -q -b dgit-view);
+       $split_brain = 1;
+    }
+}
+
+sub quiltify_splitbrain ($$$$$$) {
+    my ($clogp, $unapplied, $headref, $diffbits,
+       $editedignores, $cachekey) = @_;
+    if ($quilt_mode !~ m/gbp|dpm/) {
+       # treat .gitignore just like any other upstream file
+       $diffbits = { %$diffbits };
+       $_ = !!$_ foreach values %$diffbits;
+    }
+    # We would like any commits we generate to be reproducible
+    my @authline = clogp_authline($clogp);
+    local $ENV{GIT_COMMITTER_NAME} =  $authline[0];
+    local $ENV{GIT_COMMITTER_EMAIL} = $authline[1];
+    local $ENV{GIT_COMMITTER_DATE} =  $authline[2];
+       
+    if ($quilt_mode =~ m/gbp|unapplied/ &&
+       ($diffbits->{H2O} & 01)) {
+       my $msg =
+ "--quilt=$quilt_mode specified, implying patches-unapplied git tree\n".
+ " but git tree differs from orig in upstream files.";
+       if (!stat_exists "debian/patches") {
+           $msg .=
+ "\n ... debian/patches is missing; perhaps this is a patch queue branch?";
+       }  
+       fail $msg;
+    }
+    if ($quilt_mode =~ m/gbp|unapplied/ &&
+       ($diffbits->{O2A} & 01)) { # some patches
+       quiltify_splitbrain_needed();
+       progress "dgit view: creating patches-applied version using gbp pq";
+       runcmd shell_cmd 'exec >/dev/null', @gbp, qw(pq import);
+       # gbp pq import creates a fresh branch; push back to dgit-view
+       runcmd @git, qw(update-ref refs/heads/dgit-view HEAD);
+       runcmd @git, qw(checkout -q dgit-view);
+    }
+    if (($diffbits->{H2O} & 02) && # user has modified .gitignore
+       !($diffbits->{O2A} & 02)) { # patches do not change .gitignore
+       quiltify_splitbrain_needed();
+       progress "dgit view: creating patch to represent .gitignore changes";
+        ensuredir "debian/patches";
+       my $gipatch = "debian/patches/auto-gitignore";
+       open GIPATCH, ">>", "$gipatch" or die "$gipatch: $!";
+       stat GIPATCH or die "$gipatch: $!";
+       fail "$gipatch already exists; but want to create it".
+           " to record .gitignore changes" if (stat _)[7];
+       print GIPATCH <<END or die "$gipatch: $!";
+Subject: Update .gitignore from Debian packaging branch
+
+The Debian packaging git branch contains these updates to the upstream
+.gitignore file(s).  This patch is autogenerated, to provide these
+updates to users of the official Debian archive view of the package.
+
+[dgit version $our_version]
+---
+END
+        close GIPATCH or die "$gipatch: $!";
+        runcmd shell_cmd "exec >>$gipatch", @git, qw(diff),
+            $unapplied, $headref, "--", sort keys %$editedignores;
+        open SERIES, "+>>", "debian/patches/series" or die $!;
+        defined seek SERIES, -1, 2 or $!==EINVAL or die $!;
+        my $newline;
+        defined read SERIES, $newline, 1 or die $!;
+       print SERIES "\n" or die $! unless $newline eq "\n";
+       print SERIES "auto-gitignore\n" or die $!;
+       close SERIES or die  $!;
+        runcmd @git, qw(add -- debian/patches/series), $gipatch;
+        commit_admin "Commit patch to update .gitignore";
+    }
+
+    my $dgitview = git_rev_parse 'refs/heads/dgit-view';
+
+    changedir '../../../..';
+    ensuredir ".git/logs/refs/dgit-intern";
+    my $makelogfh = new IO::File ".git/logs/refs/$splitbraincache", '>>'
+      or die $!;
+    runcmd @git, qw(update-ref -m), $cachekey, "refs/$splitbraincache",
+       $dgitview;
+
+    progress "dgit view: created (commit id $dgitview)";
+
+    changedir '.git/dgit/unpack/work';
+}
+
+sub quiltify ($$$$) {
+    my ($clogp,$target,$oldtiptree,$failsuggestion) = @_;
 
     # Quilt patchification algorithm
     #
 
     # Quilt patchification algorithm
     #
@@ -2559,12 +3276,6 @@ sub quiltify ($$$) {
     # After traversing PT, we git commit the changes which
     # should be contained within debian/patches.
 
     # After traversing PT, we git commit the changes which
     # should be contained within debian/patches.
 
-    changedir '../fake';
-    rmtree '.pc';
-    runcmd @git, qw(add -Af .);
-    my $oldtiptree=git_write_tree();
-    changedir '../work';
-
     # The search for the path S..T is breadth-first.  We maintain a
     # todo list containing search nodes.  A search node identifies a
     # commit, and looks something like this:
     # The search for the path S..T is breadth-first.  We maintain a
     # todo list containing search nodes.  A search node identifies a
     # commit, and looks something like this:
@@ -2677,6 +3388,7 @@ sub quiltify ($$$) {
            foreach my $notp (@nots) {
                print STDERR "$us:  ", $reportnot->($notp), "\n";
            }
            foreach my $notp (@nots) {
                print STDERR "$us:  ", $reportnot->($notp), "\n";
            }
+           print STDERR "$us: $_\n" foreach @$failsuggestion;
            fail "quilt fixup naive history linearisation failed.\n".
  "Use dpkg-source --commit by hand; or, --quilt=smash for one ugly patch";
        } elsif ($quilt_mode eq 'smash') {
            fail "quilt fixup naive history linearisation failed.\n".
  "Use dpkg-source --commit by hand; or, --quilt=smash for one ugly patch";
        } elsif ($quilt_mode eq 'smash') {
@@ -2686,7 +3398,8 @@ sub quiltify ($$$) {
            die "$quilt_mode ?";
        }
 
            die "$quilt_mode ?";
        }
 
-       my $time = time;
+       my $time = $ENV{'GIT_COMMITTER_DATE'} || time;
+       $time =~ s/\s.*//; # trim timezone from GIT_COMMITTER_DATE
        my $ncommits = 3;
        my $msg = cmdoutput @git, qw(log), "-n$ncommits";
 
        my $ncommits = 3;
        my $msg = cmdoutput @git, qw(log), "-n$ncommits";
 
@@ -2770,6 +3483,8 @@ sub build_maybe_quilt_fixup () {
        quilt_fixup_multipatch($clogp, $headref, $upstreamversion);
     }
 
        quilt_fixup_multipatch($clogp, $headref, $upstreamversion);
     }
 
+    die 'bug' if $split_brain && !$need_split_build_invocation;
+
     changedir '../../../..';
     runcmd_ordryrun_local
         @git, qw(pull --ff-only -q .git/dgit/unpack/work master);
     changedir '../../../..';
     runcmd_ordryrun_local
         @git, qw(pull --ff-only -q .git/dgit/unpack/work master);
@@ -2829,14 +3544,123 @@ sub quilt_fixup_singlepatch ($$$) {
 
     chdir "work";
     commit_quilty_patch();
 
     chdir "work";
     commit_quilty_patch();
+}
+
+sub quilt_make_fake_dsc ($) {
+    my ($upstreamversion) = @_;
+
+    my $fakeversion="$upstreamversion-~~DGITFAKE";
+
+    my $fakedsc=new IO::File 'fake.dsc', '>' or die $!;
+    print $fakedsc <<END or die $!;
+Format: 3.0 (quilt)
+Source: $package
+Version: $fakeversion
+Files:
+END
+
+    my $dscaddfile=sub {
+        my ($b) = @_;
+        
+       my $md = new Digest::MD5;
+
+       my $fh = new IO::File $b, '<' or die "$b $!";
+       stat $fh or die $!;
+       my $size = -s _;
+
+       $md->addfile($fh);
+       print $fakedsc " ".$md->hexdigest." $size $b\n" or die $!;
+    };
+
+    quilt_fixup_linkorigs($upstreamversion, $dscaddfile);
+
+    my @files=qw(debian/source/format debian/rules
+                 debian/control debian/changelog);
+    foreach my $maybe (qw(debian/patches debian/source/options
+                          debian/tests/control)) {
+        next unless stat_exists "../../../$maybe";
+        push @files, $maybe;
+    }
+
+    my $debtar= srcfn $fakeversion,'.debian.tar.gz';
+    runcmd qw(env GZIP=-1n tar -zcf), "./$debtar", qw(-C ../../..), @files;
+
+    $dscaddfile->($debtar);
+    close $fakedsc or die $!;
+}
+
+sub quilt_check_splitbrain_cache ($$) {
+    my ($headref, $upstreamversion) = @_;
+    # Called only if we are in (potentially) split brain mode.
+    # Called in $ud.
+    # Computes the cache key and looks in the cache.
+    # Returns ($dgit_view_commitid, $cachekey) or (undef, $cachekey)
 
 
+    my $splitbrain_cachekey;
     
     
+    progress
+ "dgit: split brain (separate dgit view) may be needed (--quilt=$quilt_mode).";
+    # we look in the reflog of dgit-intern/quilt-cache
+    # we look for an entry whose message is the key for the cache lookup
+    my @cachekey = (qw(dgit), $our_version);
+    push @cachekey, $upstreamversion;
+    push @cachekey, $quilt_mode;
+    push @cachekey, $headref;
+
+    push @cachekey, hashfile('fake.dsc');
+
+    my $srcshash = Digest::SHA->new(256);
+    my %sfs = ( %INC, '$0(dgit)' => $0 );
+    foreach my $sfk (sort keys %sfs) {
+       next unless m/^\$0\b/ || m{^Debian/Dgit\b};
+       $srcshash->add($sfk,"  ");
+       $srcshash->add(hashfile($sfs{$sfk}));
+       $srcshash->add("\n");
+    }
+    push @cachekey, $srcshash->hexdigest();
+    $splitbrain_cachekey = "@cachekey";
+
+    my @cmd = (@git, qw(reflog), '--pretty=format:%H %gs',
+              $splitbraincache);
+    printdebug "splitbrain cachekey $splitbrain_cachekey\n";
+    debugcmd "|(probably)",@cmd;
+    my $child = open GC, "-|";  defined $child or die $!;
+    if (!$child) {
+       chdir '../../..' or die $!;
+       if (!stat ".git/logs/refs/$splitbraincache") {
+           $! == ENOENT or die $!;
+           printdebug ">(no reflog)\n";
+           exit 0;
+       }
+       exec @cmd; die $!;
+    }
+    while (<GC>) {
+       chomp;
+       printdebug ">| ", $_, "\n" if $debuglevel > 1;
+       next unless m/^(\w+) (\S.*\S)$/ && $2 eq $splitbrain_cachekey;
+           
+       my $cachehit = $1;
+       quilt_fixup_mkwork($headref);
+       if ($cachehit ne $headref) {
+           progress "dgit view: found cached (commit id $cachehit)";
+           runcmd @git, qw(checkout -q -b dgit-view), $cachehit;
+           $split_brain = 1;
+           return ($cachehit, $splitbrain_cachekey);
+       }
+       progress "dgit view: found cached, no changes required";
+       return ($headref, $splitbrain_cachekey);
+    }
+    die $! if GC->error;
+    failedcmd unless close GC;
+
+    printdebug "splitbrain cache miss\n";
+    return (undef, $splitbrain_cachekey);
 }
 
 sub quilt_fixup_multipatch ($$$) {
     my ($clogp, $headref, $upstreamversion) = @_;
 
 }
 
 sub quilt_fixup_multipatch ($$$) {
     my ($clogp, $headref, $upstreamversion) = @_;
 
-    progress "starting quiltify (multiple patches, $quilt_mode mode)";
+    progress "examining quilt state (multiple patches, $quilt_mode mode)";
 
     # Our objective is:
     #  - honour any existing .pc in case it has any strangeness
 
     # Our objective is:
     #  - honour any existing .pc in case it has any strangeness
@@ -2883,56 +3707,43 @@ sub quilt_fixup_multipatch ($$$) {
     #     6. Back in the main tree, fast forward to the new HEAD
 
     # Another situation we may have to cope with is gbp-style
     #     6. Back in the main tree, fast forward to the new HEAD
 
     # Another situation we may have to cope with is gbp-style
-    # patches-unapplied trees.  We want to detect these, so we know
-    # to escape into quilt_fixup_gbp.
+    # patches-unapplied trees.
+    #
+    # We would want to detect these, so we know to escape into
+    # quilt_fixup_gbp.  However, this is in general not possible.
+    # Consider a package with a one patch which the dgit user reverts
+    # (with git-revert or the moral equivalent).
+    #
+    # That is indistinguishable in contents from a patches-unapplied
+    # tree.  And looking at the history to distinguish them is not
+    # useful because the user might have made a confusing-looking git
+    # history structure (which ought to produce an error if dgit can't
+    # cope, not a silent reintroduction of an unwanted patch).
     #
     #
-    # A gbp-style tree is one which is not a clean patches-applied
-    # tree, but _is_ a clean patches-unapplied tree.
+    # So gbp users will have to pass an option.  But we can usually
+    # detect their failure to do so: if the tree is not a clean
+    # patches-applied tree, quilt linearisation fails, but the tree
+    # _is_ a clean patches-unapplied tree, we can suggest that maybe
+    # they want --quilt=unapplied.
     #
     # To help detect this, when we are extracting the fake dsc, we
     # first extract it with --skip-patches, and then apply the patches
     # afterwards with dpkg-source --before-build.  That lets us save a
     # tree object corresponding to .origs.
 
     #
     # To help detect this, when we are extracting the fake dsc, we
     # first extract it with --skip-patches, and then apply the patches
     # afterwards with dpkg-source --before-build.  That lets us save a
     # tree object corresponding to .origs.
 
-    my $fakeversion="$upstreamversion-~~DGITFAKE";
-
-    my $fakedsc=new IO::File 'fake.dsc', '>' or die $!;
-    print $fakedsc <<END or die $!;
-Format: 3.0 (quilt)
-Source: $package
-Version: $fakeversion
-Files:
-END
-
-    my $dscaddfile=sub {
-        my ($b) = @_;
-        
-       my $md = new Digest::MD5;
-
-       my $fh = new IO::File $b, '<' or die "$b $!";
-       stat $fh or die $!;
-       my $size = -s _;
+    my $splitbrain_cachekey;
 
 
-       $md->addfile($fh);
-       print $fakedsc " ".$md->hexdigest." $size $b\n" or die $!;
-    };
-
-    quilt_fixup_linkorigs($upstreamversion, $dscaddfile);
+    quilt_make_fake_dsc($upstreamversion);
 
 
-    my @files=qw(debian/source/format debian/rules
-                 debian/control debian/changelog);
-    foreach my $maybe (qw(debian/patches debian/source/options)) {
-        next unless stat_exists "../../../$maybe";
-        push @files, $maybe;
+    if (quiltmode_splitbrain()) {
+       my $cachehit;
+       ($cachehit, $splitbrain_cachekey) =
+           quilt_check_splitbrain_cache($headref, $upstreamversion);
+       return if $cachehit;
     }
 
     }
 
-    my $debtar= srcfn $fakeversion,'.debian.tar.gz';
-    runcmd qw(env GZIP=-1 tar -zcf), "./$debtar", qw(-C ../../..), @files;
-
-    $dscaddfile->($debtar);
-    close $fakedsc or die $!;
-
-    runcmd qw(sh -ec), 'exec dpkg-source --no-check -x fake.dsc >/dev/null';
+    runcmd qw(sh -ec),
+        'exec dpkg-source --no-check --skip-patches -x fake.dsc >/dev/null';
 
     my $fakexdir= $package.'-'.(stripepoch $upstreamversion);
     rename $fakexdir, "fake" or die "$fakexdir $!";
 
     my $fakexdir= $package.'-'.(stripepoch $upstreamversion);
     rename $fakexdir, "fake" or die "$fakexdir $!";
@@ -2942,9 +3753,18 @@ END
     remove_stray_gits();
     mktree_in_ud_here();
 
     remove_stray_gits();
     mktree_in_ud_here();
 
-    changedir '..';
+    rmtree '.pc';
+
+    runcmd @git, qw(add -Af .);
+    my $unapplied=git_write_tree();
+    printdebug "fake orig tree object $unapplied\n";
 
 
-    my $origtree='';
+    ensuredir '.pc';
+
+    runcmd qw(sh -ec),
+        'exec dpkg-source --before-build . >/dev/null';
+
+    changedir '..';
 
     quilt_fixup_mkwork($headref);
 
 
     quilt_fixup_mkwork($headref);
 
@@ -2957,7 +3777,59 @@ END
         rename '../fake/.pc','.pc' or die $!;
     }
 
         rename '../fake/.pc','.pc' or die $!;
     }
 
-    quiltify($clogp,$headref,$origtree);
+    changedir '../fake';
+    rmtree '.pc';
+    runcmd @git, qw(add -Af .);
+    my $oldtiptree=git_write_tree();
+    printdebug "fake o+d/p tree object $unapplied\n";
+    changedir '../work';
+
+
+    # We calculate some guesswork now about what kind of tree this might
+    # be.  This is mostly for error reporting.
+
+    my %editedignores;
+    my $diffbits = {
+        # H = user's HEAD
+        # O = orig, without patches applied
+        # A = "applied", ie orig with H's debian/patches applied
+        H2O => quiltify_trees_differ($headref,  $unapplied, 1,\%editedignores),
+        H2A => quiltify_trees_differ($headref,  $oldtiptree,1),
+        O2A => quiltify_trees_differ($unapplied,$oldtiptree,1),
+    };
+
+    my @dl;
+    foreach my $b (qw(01 02)) {
+        foreach my $v (qw(H2O O2A H2A)) {
+            push @dl, ($diffbits->{$v} & $b) ? '##' : '==';
+        }
+    }
+    printdebug "differences \@dl @dl.\n";
+
+    progress sprintf
+"$us: quilt differences: src:  %s orig %s     gitignores:  %s orig %s\n".
+"$us: quilt differences:      HEAD %s o+d/p               HEAD %s o+d/p",
+                             $dl[0], $dl[1],              $dl[3], $dl[4],
+                                 $dl[2],                     $dl[5];
+
+    my @failsuggestion;
+    if (!($diffbits->{H2O} & $diffbits->{O2A})) {
+        push @failsuggestion, "This might be a patches-unapplied branch.";
+    }  elsif (!($diffbits->{H2A} & $diffbits->{O2A})) {
+        push @failsuggestion, "This might be a patches-applied branch.";
+    }
+    push @failsuggestion, "Maybe you need to specify one of".
+        " --quilt=gbp --quilt=dpm --quilt=unapplied ?";
+
+    if (quiltmode_splitbrain()) {
+       quiltify_splitbrain($clogp, $unapplied, $headref,
+                            $diffbits, \%editedignores,
+                           $splitbrain_cachekey);
+       return;
+    }
+
+    progress "starting quiltify (multiple patches, $quilt_mode mode)";
+    quiltify($clogp,$headref,$oldtiptree,\@failsuggestion);
 
     if (!open P, '>>', ".pc/applied-patches") {
        $!==&ENOENT or die $!;
 
     if (!open P, '>>', ".pc/applied-patches") {
        $!==&ENOENT or die $!;
@@ -2991,15 +3863,45 @@ sub quilt_fixup_editor () {
     exit 0;
 }
 
     exit 0;
 }
 
+sub maybe_apply_patches_dirtily () {
+    return unless $quilt_mode =~ m/gbp|unapplied/;
+    print STDERR <<END or die $!;
+
+dgit: Building, or cleaning with rules target, in patches-unapplied tree.
+dgit: Have to apply the patches - making the tree dirty.
+dgit: (Consider specifying --clean=git and (or) using dgit sbuild.)
+
+END
+    $patches_applied_dirtily = 01;
+    $patches_applied_dirtily |= 02 unless stat_exists '.pc';
+    runcmd qw(dpkg-source --before-build .);
+}
+
+sub maybe_unapply_patches_again () {
+    progress "dgit: Unapplying patches again to tidy up the tree."
+       if $patches_applied_dirtily;
+    runcmd qw(dpkg-source --after-build .)
+       if $patches_applied_dirtily & 01;
+    rmtree '.pc'
+       if $patches_applied_dirtily & 02;
+    $patches_applied_dirtily = 0;
+}
+
 #----- other building -----
 
 #----- other building -----
 
-our $suppress_clean;
+our $clean_using_builder;
+# ^ tree is to be cleaned by dpkg-source's builtin idea that it should
+#   clean the tree before building (perhaps invoked indirectly by
+#   whatever we are using to run the build), rather than separately
+#   and explicitly by us.
 
 sub clean_tree () {
 
 sub clean_tree () {
-    return if $suppress_clean;
+    return if $clean_using_builder;
     if ($cleanmode eq 'dpkg-source') {
     if ($cleanmode eq 'dpkg-source') {
+       maybe_apply_patches_dirtily();
        runcmd_ordryrun_local @dpkgbuildpackage, qw(-T clean);
     } elsif ($cleanmode eq 'dpkg-source-d') {
        runcmd_ordryrun_local @dpkgbuildpackage, qw(-T clean);
     } elsif ($cleanmode eq 'dpkg-source-d') {
+       maybe_apply_patches_dirtily();
        runcmd_ordryrun_local @dpkgbuildpackage, qw(-d -T clean);
     } elsif ($cleanmode eq 'git') {
        runcmd_ordryrun_local @git, qw(clean -xdf);
        runcmd_ordryrun_local @dpkgbuildpackage, qw(-d -T clean);
     } elsif ($cleanmode eq 'git') {
        runcmd_ordryrun_local @git, qw(clean -xdf);
@@ -3021,6 +3923,7 @@ sub cmd_clean () {
     badusage "clean takes no additional arguments" if @ARGV;
     notpushing();
     clean_tree();
     badusage "clean takes no additional arguments" if @ARGV;
     notpushing();
     clean_tree();
+    maybe_unapply_patches_again();
 }
 
 sub build_prep () {
 }
 
 sub build_prep () {
@@ -3101,7 +4004,7 @@ sub massage_dbp_args ($;$) {
     debugcmd '#massaging#', @$cmd if $debuglevel>1;
 #print STDERR "MASS0 ",Dumper($cmd, $xargs, $need_split_build_invocation);
     if ($cleanmode eq 'dpkg-source' && !$need_split_build_invocation) {
     debugcmd '#massaging#', @$cmd if $debuglevel>1;
 #print STDERR "MASS0 ",Dumper($cmd, $xargs, $need_split_build_invocation);
     if ($cleanmode eq 'dpkg-source' && !$need_split_build_invocation) {
-       $suppress_clean = 1;
+       $clean_using_builder = 1;
        return 0;
     }
     # -nc has the side effect of specifying -b if nothing else specified
        return 0;
     }
     # -nc has the side effect of specifying -b if nothing else specified
@@ -3120,11 +4023,13 @@ sub massage_dbp_args ($;$) {
 #print STDERR "MASS1 ",Dumper($cmd, $xargs, $dmode);
     my $r = 0;
     if ($need_split_build_invocation) {
 #print STDERR "MASS1 ",Dumper($cmd, $xargs, $dmode);
     my $r = 0;
     if ($need_split_build_invocation) {
+       printdebug "massage split $dmode.\n";
        $r = $dmode =~ m/[S]/     ? +2 :
             $dmode =~ y/gGF/ABb/ ? +1 :
             $dmode =~ m/[ABb]/   ?  0 :
             die "$dmode ?";
     }
        $r = $dmode =~ m/[S]/     ? +2 :
             $dmode =~ y/gGF/ABb/ ? +1 :
             $dmode =~ m/[ABb]/   ?  0 :
             die "$dmode ?";
     }
+    printdebug "massage done $r $dmode.\n";
     push @$cmd, $dmode;
 #print STDERR "MASS2 ",Dumper($cmd, $xargs, $r);
     return $r;
     push @$cmd, $dmode;
 #print STDERR "MASS2 ",Dumper($cmd, $xargs, $r);
     return $r;
@@ -3140,8 +4045,10 @@ sub cmd_build {
     }
     if ($wantsrc < 2) {
        push @dbp, changesopts_version();
     }
     if ($wantsrc < 2) {
        push @dbp, changesopts_version();
+       maybe_apply_patches_dirtily();
        runcmd_ordryrun_local @dbp;
     }
        runcmd_ordryrun_local @dbp;
     }
+    maybe_unapply_patches_again();
     printdone "build successful\n";
 }
 
     printdone "build successful\n";
 }
 
@@ -3161,11 +4068,12 @@ sub cmd_gbp_build {
     if ($wantsrc > 0) {
        build_source();
     } else {
     if ($wantsrc > 0) {
        build_source();
     } else {
-       if (!$suppress_clean) {
+       if (!$clean_using_builder) {
            push @cmd, '--git-cleaner=true';
        }
        build_prep();
     }
            push @cmd, '--git-cleaner=true';
        }
        build_prep();
     }
+    maybe_unapply_patches_again();
     if ($wantsrc < 2) {
        unless (grep { m/^--git-debian-branch|^--git-ignore-branch/ } @ARGV) {
            canonicalise_suite();
     if ($wantsrc < 2) {
        unless (grep { m/^--git-debian-branch|^--git-ignore-branch/ } @ARGV) {
            canonicalise_suite();
@@ -3179,9 +4087,18 @@ sub cmd_gbp_build {
 sub cmd_git_build { cmd_gbp_build(); } # compatibility with <= 1.0
 
 sub build_source {
 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;
+    my $our_cleanmode = $cleanmode;
+    if ($need_split_build_invocation) {
+       # Pretend that clean is being done some other way.  This
+       # forces us not to try to use dpkg-buildpackage to clean and
+       # build source all in one go; and instead we run dpkg-source
+       # (and build_prep() will do the clean since $clean_using_builder
+       # is false).
+       $our_cleanmode = 'ELSEWHERE';
+    }
+    if ($our_cleanmode =~ m/^dpkg-source/) {
+       # dpkg-source invocation (below) will clean, so build_prep shouldn't
+       $clean_using_builder = 1;
     }
     build_prep();
     $sourcechanges = changespat $version,'source';
     }
     build_prep();
     $sourcechanges = changespat $version,'source';
@@ -3190,18 +4107,38 @@ sub build_source {
            or fail "remove $sourcechanges: $!";
     }
     $dscfn = dscfn($version);
            or fail "remove $sourcechanges: $!";
     }
     $dscfn = dscfn($version);
-    if ($cleanmode eq 'dpkg-source') {
+    if ($our_cleanmode eq 'dpkg-source') {
+       maybe_apply_patches_dirtily();
        runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc -S),
        runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc -S),
-                              changesopts();
-    } elsif ($cleanmode eq 'dpkg-source-d') {
+           changesopts();
+    } elsif ($our_cleanmode eq 'dpkg-source-d') {
+       maybe_apply_patches_dirtily();
        runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc -S -d),
        runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc -S -d),
-                              changesopts();
+           changesopts();
     } else {
     } else {
-       my $pwd = must_getcwd();
-       my $leafdir = basename $pwd;
-       changedir "..";
-       runcmd_ordryrun_local @dpkgsource, qw(-b --), $leafdir;
-       changedir $pwd;
+       my @cmd = (@dpkgsource, qw(-b --));
+       if ($split_brain) {
+           changedir $ud;
+           runcmd_ordryrun_local @cmd, "work";
+           my @udfiles = <${package}_*>;
+           changedir "../../..";
+           foreach my $f (@udfiles) {
+               printdebug "source copy, found $f\n";
+               next unless
+                   $f eq $dscfn or
+                   ($f =~ m/\.debian\.tar(?:\.\w+)$/ &&
+                    $f eq srcfn($version, $&));
+               printdebug "source copy, found $f - renaming\n";
+               rename "$ud/$f", "../$f" or $!==ENOENT
+                   or fail "put in place new source file ($f): $!";
+           }
+       } else {
+           my $pwd = must_getcwd();
+           my $leafdir = basename $pwd;
+           changedir "..";
+           runcmd_ordryrun_local @cmd, $leafdir;
+           changedir $pwd;
+       }
        runcmd_ordryrun_local qw(sh -ec),
            'exec >$1; shift; exec "$@"','x',
            "../$sourcechanges",
        runcmd_ordryrun_local qw(sh -ec),
            'exec >$1; shift; exec "$@"','x',
            "../$sourcechanges",
@@ -3212,6 +4149,7 @@ sub build_source {
 sub cmd_build_source {
     badusage "build-source takes no additional arguments" if @ARGV;
     build_source();
 sub cmd_build_source {
     badusage "build-source takes no additional arguments" if @ARGV;
     build_source();
+    maybe_unapply_patches_again();
     printdone "source built, results in $dscfn and $sourcechanges";
 }
 
     printdone "source built, results in $dscfn and $sourcechanges";
 }
 
@@ -3226,6 +4164,7 @@ sub cmd_sbuild {
            " building would result in ambiguity about the intended results"
            if @unwanted;
     }
            " building would result in ambiguity about the intended results"
            if @unwanted;
     }
+    my $wasdir = must_getcwd();
     changedir "..";
     if (act_local()) {
        stat_exists $dscfn or fail "$dscfn (in parent directory): $!";
     changedir "..";
     if (act_local()) {
        stat_exists $dscfn or fail "$dscfn (in parent directory): $!";
@@ -3254,6 +4193,8 @@ sub cmd_sbuild {
            rename "$cf", "$cf.inmulti" or fail "$cf\{,.inmulti}: $!";
        }
     }
            rename "$cf", "$cf.inmulti" or fail "$cf\{,.inmulti}: $!";
        }
     }
+    changedir $wasdir;
+    maybe_unapply_patches_again();
     printdone "build successful, results in $multichanges\n" or die $!;
 }    
 
     printdone "build successful, results in $multichanges\n" or die $!;
 }    
 
@@ -3420,6 +4361,11 @@ sub parseopts () {
            } elsif (m/^--deliberately-($deliberately_re)$/s) {
                push @ropts, $_;
                push @deliberatelies, $&;
            } elsif (m/^--deliberately-($deliberately_re)$/s) {
                push @ropts, $_;
                push @deliberatelies, $&;
+           } elsif (m/^--dgit-tag-format=(old|new)$/s) {
+               # undocumented, for testing
+               push @ropts, $_;
+               $tagformat_want = [ $1, 'command line', 1 ];
+               # 1 menas overrides distro configuration
            } elsif (m/^--always-split-source-build$/s) {
                # undocumented, for testing
                push @ropts, $_;
            } elsif (m/^--always-split-source-build$/s) {
                # undocumented, for testing
                push @ropts, $_;
@@ -3542,6 +4488,8 @@ if (!defined $quilt_mode) {
     $quilt_mode = $1;
 }
 
     $quilt_mode = $1;
 }
 
+$need_split_build_invocation ||= quiltmode_splitbrain();
+
 if (!defined $cleanmode) {
     local $access_forpush;
     $cleanmode = access_cfg('clean-mode', 'RETURN-UNDEF');
 if (!defined $cleanmode) {
     local $access_forpush;
     $cleanmode = access_cfg('clean-mode', 'RETURN-UNDEF');