chiark / gitweb /
NOTES: Remove obsolete file
[dgit.git] / dgit
diff --git a/dgit b/dgit
index 90b1fde67a5cb9887e42dec2e3be18c2c2183347..630c9f7441e707b37ffaea0021ba91c6a7a21652 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -63,8 +63,9 @@ our $existing_package = 'dpkg';
 our $cleanmode;
 our $changes_since_version;
 our $rmchanges;
+our $overwrite_version; # undef: not specified; '': check changelog
 our $quilt_mode;
-our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck|gbp|unapplied';
+our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck|gbp|dpm|unapplied';
 our $we_are_responder;
 our $initiator_tempdir;
 our $patches_applied_dirtily = 00;
@@ -76,6 +77,9 @@ 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 $orig_f_comp_re = 'orig(?:-[-0-9a-z]+)?';
+our $orig_f_sig_re = '\\.(?:asc|gpg|pgp)';
+our $orig_f_tail_re = "$orig_f_comp_re\\.tar(?:\\.\\w+)?(?:$orig_f_sig_re)?";
 
 our $git_authline_re = '^([^<>]+) \<(\S+)\> (\d+ [-+]\d+)$';
 our $splitbraincache = 'dgit-intern/quilt-cache';
@@ -93,7 +97,8 @@ 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 (@gbp) = qw(gbp);
+our (@gbp_build) = ('');
+our (@gbp_pq) = ('gbp pq');
 our (@changesopts) = ('');
 
 our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
@@ -108,7 +113,8 @@ our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
                      'dpkg-source' => \@dpkgsource,
                      'dpkg-buildpackage' => \@dpkgbuildpackage,
                      'dpkg-genchanges' => \@dpkggenchanges,
-                     'gbp' => \@gbp,
+                     'gbp-build' => \@gbp_build,
+                     'gbp-pq' => \@gbp_pq,
                      'ch' => \@changesopts,
                      'mergechanges' => \@mergechanges);
 
@@ -149,6 +155,8 @@ sub debiantag_maintview ($$) {
     return "$distro/$v";
 }
 
+sub madformat ($) { $_[0] eq '3.0 (quilt)' }
+
 sub lbranch () { return "$branchprefix/$csuite"; }
 my $lbranch_re = '^refs/heads/'.$branchprefix.'/([^/.]+)$';
 sub lref () { return "refs/heads/".lbranch(); }
@@ -240,6 +248,17 @@ sub quiltmode_splitbrain () {
     $quilt_mode =~ m/gbp|dpm|unapplied/;
 }
 
+sub opts_opt_multi_cmd {
+    my @cmd;
+    push @cmd, split /\s+/, shift @_;
+    push @cmd, @_;
+    @cmd;
+}
+
+sub gbp_pq {
+    return opts_opt_multi_cmd @gbp_pq;
+}
+
 #---------- remote protocol support, common ----------
 
 # remote push initiator/responder protocol:
@@ -533,11 +552,16 @@ our %defcfg = ('dgit.default.distro' => 'debian',
               'dgit.default.archive-query' => 'madison:',
               'dgit.default.sshpsql-dbname' => 'service=projectb',
               'dgit.default.dgit-tag-format' => 'old,new,maint',
+              # old means "repo server accepts pushes with old dgit tags"
+              # new means "repo server accepts pushes with new dgit tags"
+              # maint means "repo server accepts split brain pushes"
+              # hist means "repo server may have old pushes without new tag"
+              #   ("hist" is implied by "old")
               '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.dgit-tag-format' => 'new',
               'dgit-distro.debian/push.git-url' => '',
               'dgit-distro.debian/push.git-host' => 'push.dgit.debian.org',
               'dgit-distro.debian/push.git-user-force' => 'dgit',
@@ -866,11 +890,11 @@ sub getfield ($$) {
     my ($dctrl,$field) = @_;
     my $v = $dctrl->{$field};
     return $v if defined $v;
-    fail "missing field $field in ".$v->get_option('name');
+    fail "missing field $field in ".$dctrl->get_option('name');
 }
 
 sub parsechangelog {
-    my $c = Dpkg::Control::Hash->new();
+    my $c = Dpkg::Control::Hash->new(name => 'parsed changelog');
     my $p = new IO::Handle;
     my @cmd = (qw(dpkg-parsechangelog), @_);
     open $p, '-|', @cmd or die $!;
@@ -879,6 +903,19 @@ sub parsechangelog {
     return $c;
 }
 
+sub commit_getclogp ($) {
+    # Returns the parsed changelog hashref for a particular commit
+    my ($objid) = @_;
+    our %commit_getclogp_memo;
+    my $memo = $commit_getclogp_memo{$objid};
+    return $memo if $memo;
+    mkpath '.git/dgit';
+    my $mclog = ".git/dgit/clog-$objid";
+    runcmd shell_cmd "exec >$mclog", @git, qw(cat-file blob),
+       "$objid:debian/changelog";
+    $commit_getclogp_memo{$objid} = parsechangelog("-l$mclog");
+}
+
 sub must_getcwd () {
     my $d = getcwd();
     defined $d or fail "getcwd failed: $!";
@@ -1180,7 +1217,7 @@ sub select_tagformat () {
     die 'bug' if $tagformatfn && $tagformat_want;
     # ... $tagformat_want assigned after previous select_tagformat
 
-    my (@supported) = grep { $_ ne 'maint' } access_cfg_tagformats();
+    my (@supported) = grep { $_ =~ m/^(?:old|new)$/ } access_cfg_tagformats();
     printdebug "select_tagformat supported @supported\n";
 
     $tagformat_want //= [ $supported[0], "distro access configuration", 0 ];
@@ -1326,6 +1363,7 @@ sub prep_ud (;$) {
 
 sub mktree_in_ud_here () {
     runcmd qw(git init -q);
+    runcmd qw(git config gc.auto 0);
     rmtree('.git/objects');
     symlink '../../../../objects','.git/objects' or die $!;
 }
@@ -1352,20 +1390,25 @@ sub remove_stray_gits () {
     $!=0; $?=0; close GITS or failedcmd @gitscmd;
 }
 
-sub mktree_in_ud_from_only_subdir () {
+sub mktree_in_ud_from_only_subdir (;$) {
+    my ($raw) = @_;
+
     # changes into the subdir
     my (@dirs) = <*/.>;
-    die "@dirs ?" unless @dirs==1;
+    die "expected one subdir but found @dirs ?" unless @dirs==1;
     $dirs[0] =~ m#^([^/]+)/\.$# or die;
     my $dir = $1;
     changedir $dir;
 
     remove_stray_gits();
     mktree_in_ud_here();
-    my ($format, $fopts) = get_source_format();
-    if (madformat($format)) {
-       rmtree '.pc';
+    if (!$raw) {
+       my ($format, $fopts) = get_source_format();
+       if (madformat($format)) {
+           rmtree '.pc';
+       }
     }
+
     runcmd @git, qw(add -Af);
     my $tree=git_write_tree();
     return ($tree,$dir);
@@ -1402,12 +1445,20 @@ sub dsc_files () {
     map { $_->{Filename} } dsc_files_info();
 }
 
-sub is_orig_file ($;$) {
-    local ($_) = $_[0];
-    my $base = $_[1];
-    m/\.orig(?:-\w+)?\.tar\.\w+$/ or return 0;
-    defined $base or return 1;
-    return $` eq $base;
+sub is_orig_file_in_dsc ($$) {
+    my ($f, $dsc_files_info) = @_;
+    return 0 if @$dsc_files_info <= 1;
+    # One file means no origs, and the filename doesn't have a "what
+    # part of dsc" component.  (Consider versions ending `.orig'.)
+    return 0 unless $f =~ m/\.$orig_f_tail_re$/o;
+    return 1;
+}
+
+sub is_orig_file_of_vsn ($$) {
+    my ($f, $upstreamvsn) = @_;
+    my $base = srcfn $upstreamvsn, '';
+    return 0 unless $f =~ m/^\Q$base\E\.$orig_f_tail_re$/;
+    return 1;
 }
 
 sub make_commit ($) {
@@ -1415,6 +1466,28 @@ sub make_commit ($) {
     return cmdoutput @git, qw(hash-object -w -t commit), $file;
 }
 
+sub make_commit_text ($) {
+    my ($text) = @_;
+    my ($out, $in);
+    my @cmd = (@git, qw(hash-object -w -t commit --stdin));
+    debugcmd "|",@cmd;
+    print Dumper($text) if $debuglevel > 1;
+    my $child = open2($out, $in, @cmd) or die $!;
+    my $h;
+    eval {
+       print $in $text or die $!;
+       close $in or die $!;
+       $h = <$out>;
+       $h =~ m/^\w+$/ or die;
+       $h = $&;
+       printdebug "=> $h\n";
+    };
+    close $out;
+    waitpid $child, 0 == $child or die "$child $!";
+    $? and failedcmd @cmd;
+    return $h;
+}
+
 sub clogp_authline ($) {
     my ($clogp) = @_;
     my $author = getfield $clogp, 'Maintainer';
@@ -1496,7 +1569,8 @@ sub generate_commits_from_dsc () {
     prep_ud();
     changedir $ud;
 
-    foreach my $fi (dsc_files_info()) {
+    my @dfi = dsc_files_info();
+    foreach my $fi (@dfi) {
        my $f = $fi->{Filename};
        die "$f ?" if $f =~ m#/|^\.|\.dsc$|\.tmp$#;
 
@@ -1507,53 +1581,312 @@ sub generate_commits_from_dsc () {
        complete_file_from_dsc('.', $fi)
            or next;
 
-       if (is_orig_file($f)) {
+       if (is_orig_file_in_dsc($f, \@dfi)) {
            link $f, "../../../../$f"
                or $!==&EEXIST
                or die "$f $!";
        }
     }
 
+    # We unpack and record the orig tarballs first, so that we only
+    # need disk space for one private copy of the unpacked source.
+    # But we can't make them into commits until we have the metadata
+    # from the debian/changelog, so we record the tree objects now and
+    # make them into commits later.
+    my @tartrees;
+    my $upstreamv = $dsc->{version};
+    $upstreamv =~ s/-[^-]+$//;
+    my $orig_f_base = srcfn $upstreamv, '';
+
+    foreach my $fi (@dfi) {
+       # We actually import, and record as a commit, every tarball
+       # (unless there is only one file, in which case there seems
+       # little point.
+
+       my $f = $fi->{Filename};
+       printdebug "import considering $f ";
+       (printdebug "only one dfi\n"), next if @dfi == 1;
+       (printdebug "not tar\n"), next unless $f =~ m/\.tar(\.\w+)?$/;
+       (printdebug "signature\n"), next if $f =~ m/$orig_f_sig_re$/o;
+       my $compr_ext = $1;
+
+       my ($orig_f_part) =
+           $f =~ m/^\Q$orig_f_base\E\.([^._]+)?\.tar(?:\.\w+)?$/;
+
+       printdebug "Y ", (join ' ', map { $_//"(none)" }
+                         $compr_ext, $orig_f_part
+                        ), "\n";
+
+       my $input = new IO::File $f, '<' or die "$f $!";
+       my $compr_pid;
+       my @compr_cmd;
+
+       if (defined $compr_ext) {
+           my $cname =
+               Dpkg::Compression::compression_guess_from_filename $f;
+           fail "Dpkg::Compression cannot handle file $f in source package"
+               if defined $compr_ext && !defined $cname;
+           my $compr_proc =
+               new Dpkg::Compression::Process compression => $cname;
+           my @compr_cmd = $compr_proc->get_uncompress_cmdline();
+           my $compr_fh = new IO::Handle;
+           my $compr_pid = open $compr_fh, "-|" // die $!;
+           if (!$compr_pid) {
+               open STDIN, "<&", $input or die $!;
+               exec @compr_cmd;
+               die "dgit (child): exec $compr_cmd[0]: $!\n";
+           }
+           $input = $compr_fh;
+       }
+
+       rmtree "../unpack-tar";
+       mkdir "../unpack-tar" or die $!;
+       my @tarcmd = qw(tar -x -f -
+                       --no-same-owner --no-same-permissions
+                       --no-acls --no-xattrs --no-selinux);
+       my $tar_pid = fork // die $!;
+       if (!$tar_pid) {
+           chdir "../unpack-tar" or die $!;
+           open STDIN, "<&", $input or die $!;
+           exec @tarcmd;
+           die "dgit (child): exec $tarcmd[0]: $!";
+       }
+       $!=0; (waitpid $tar_pid, 0) == $tar_pid or die $!;
+       !$? or failedcmd @tarcmd;
+
+       close $input or
+           (@compr_cmd ? failedcmd @compr_cmd
+            : die $!);
+       # finally, we have the results in "tarball", but maybe
+       # with the wrong permissions
+
+       runcmd qw(chmod -R +rwX ../unpack-tar);
+       changedir "../unpack-tar";
+       my ($tree) = mktree_in_ud_from_only_subdir(1);
+       changedir "../../unpack";
+       rmtree "../unpack-tar";
+
+       my $ent = [ $f, $tree ];
+       push @tartrees, {
+            Orig => !!$orig_f_part,
+            Sort => (!$orig_f_part         ? 2 :
+                    $orig_f_part =~ m/-/g ? 1 :
+                                            0),
+            F => $f,
+            Tree => $tree,
+        };
+    }
+
+    @tartrees = sort {
+       # put any without "_" first (spec is not clear whether files
+       # are always in the usual order).  Tarballs without "_" are
+       # the main orig or the debian tarball.
+       $a->{Sort} <=> $b->{Sort} or
+       $a->{F}    cmp $b->{F}
+    } @tartrees;
+
+    my $any_orig = grep { $_->{Orig} } @tartrees;
+
     my $dscfn = "$package.dsc";
 
+    my $treeimporthow = 'package';
+
     open D, ">", $dscfn or die "$dscfn: $!";
     print D $dscdata or die "$dscfn: $!";
     close D or die "$dscfn: $!";
     my @cmd = qw(dpkg-source);
     push @cmd, '--no-check' if $dsc_checked;
+    if (madformat $dsc->{format}) {
+       push @cmd, '--skip-patches';
+       $treeimporthow = 'unpatched';
+    }
     push @cmd, qw(-x --), $dscfn;
     runcmd @cmd;
 
     my ($tree,$dir) = mktree_in_ud_from_only_subdir();
-    check_for_vendor_patches() if madformat($dsc->{format});
-    runcmd qw(sh -ec), 'dpkg-parsechangelog >../changelog.tmp';
-    my $clogp = parsecontrol('../changelog.tmp',"commit's changelog");
+    if (madformat $dsc->{format}) { 
+       check_for_vendor_patches();
+    }
+
+    my $dappliedtree;
+    if (madformat $dsc->{format}) {
+       my @pcmd = qw(dpkg-source --before-build .);
+       runcmd shell_cmd 'exec >/dev/null', @pcmd;
+       rmtree '.pc';
+       runcmd @git, qw(add -Af);
+       $dappliedtree = git_write_tree();
+    }
+
+    my @clogcmd = qw(dpkg-parsechangelog --format rfc822 --all);
+    debugcmd "|",@clogcmd;
+    open CLOGS, "-|", @clogcmd or die $!;
+
+    my $clogp;
+    my $r1clogp;
+
+    printdebug "import clog search...\n";
+
+    for (;;) {
+       my $stanzatext = do { local $/=""; <CLOGS>; };
+       printdebug "import clogp ".Dumper($stanzatext) if $debuglevel>1;
+       last if !defined $stanzatext;
+
+       my $desc = "package changelog, entry no.$.";
+       open my $stanzafh, "<", \$stanzatext or die;
+       my $thisstanza = parsecontrolfh $stanzafh, $desc, 1;
+       $clogp //= $thisstanza;
+
+       printdebug "import clog $thisstanza->{version} $desc...\n";
+
+       last if !$any_orig; # we don't need $r1clogp
+
+       # We look for the first (most recent) changelog entry whose
+       # version number is lower than the upstream version of this
+       # package.  Then the last (least recent) previous changelog
+       # entry is treated as the one which introduced this upstream
+       # version and used for the synthetic commits for the upstream
+       # tarballs.
+
+       # One might think that a more sophisticated algorithm would be
+       # necessary.  But: we do not want to scan the whole changelog
+       # file.  Stopping when we see an earlier version, which
+       # necessarily then is an earlier upstream version, is the only
+       # realistic way to do that.  Then, either the earliest
+       # changelog entry we have seen so far is indeed the earliest
+       # upload of this upstream version; or there are only changelog
+       # entries relating to later upstream versions (which is not
+       # possible unless the changelog and .dsc disagree about the
+       # version).  Then it remains to choose between the physically
+       # last entry in the file, and the one with the lowest version
+       # number.  If these are not the same, we guess that the
+       # versions were created in a non-monotic order rather than
+       # that the changelog entries have been misordered.
+
+       printdebug "import clog $thisstanza->{version} vs $upstreamv...\n";
+
+       last if version_compare($thisstanza->{version}, $upstreamv) < 0;
+       $r1clogp = $thisstanza;
+
+       printdebug "import clog $r1clogp->{version} becomes r1\n";
+    }
+    die $! if CLOGS->error;
+    close CLOGS or $?==(SIGPIPE<<8) or failedcmd @clogcmd;
+
+    $clogp or fail "package changelog has no entries!";
+
     my $authline = clogp_authline $clogp;
     my $changes = getfield $clogp, 'Changes';
+    my $cversion = getfield $clogp, 'Version';
+
+    if (@tartrees) {
+       $r1clogp //= $clogp; # maybe there's only one entry;
+       my $r1authline = clogp_authline $r1clogp;
+       # Strictly, r1authline might now be wrong if it's going to be
+       # unused because !$any_orig.  Whatever.
+
+       printdebug "import tartrees authline   $authline\n";
+       printdebug "import tartrees r1authline $r1authline\n";
+
+       foreach my $tt (@tartrees) {
+           printdebug "import tartree $tt->{F} $tt->{Tree}\n";
+
+           $tt->{Commit} = make_commit_text($tt->{Orig} ? <<END_O : <<END_T);
+tree $tt->{Tree}
+author $r1authline
+committer $r1authline
+
+Import $tt->{F}
+
+[dgit import orig $tt->{F}]
+END_O
+tree $tt->{Tree}
+author $authline
+committer $authline
+
+Import $tt->{F}
+
+[dgit import tarball $package $cversion $tt->{F}]
+END_T
+       }
+    }
+
+    printdebug "import main commit\n";
+
     open C, ">../commit.tmp" or die $!;
     print C <<END or die $!;
 tree $tree
+END
+    print C <<END or die $! foreach @tartrees;
+parent $_->{Commit}
+END
+    print C <<END or die $!;
 author $authline
 committer $authline
 
 $changes
 
-# imported from the archive
+[dgit import $treeimporthow $package $cversion]
 END
+
     close C or die $!;
     my $rawimport_hash = make_commit qw(../commit.tmp);
-    my $cversion = getfield $clogp, 'Version';
+
+    if (madformat $dsc->{format}) {
+       printdebug "import apply patches...\n";
+
+       # regularise the state of the working tree so that
+       # the checkout of $rawimport_hash works nicely.
+       my $dappliedcommit = make_commit_text(<<END);
+tree $dappliedtree
+author $authline
+committer $authline
+
+[dgit dummy commit]
+END
+       runcmd @git, qw(checkout -q -b dapplied), $dappliedcommit;
+
+       runcmd @git, qw(checkout -q -b unpa), $rawimport_hash;
+
+       # We need the answers 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];
+       local $ENV{GIT_AUTHOR_NAME} =  $authline[0];
+       local $ENV{GIT_AUTHOR_EMAIL} = $authline[1];
+       local $ENV{GIT_AUTHOR_DATE} =  $authline[2];
+
+       eval {
+           runcmd shell_cmd 'exec >/dev/null 2>../../gbp-pq-output',
+               gbp_pq, qw(import);
+       };
+       if ($@) {
+           { local $@; eval { runcmd qw(cat ../../gbp-pq-output); }; }
+           die $@;
+       }
+
+       my $gapplied = git_rev_parse('HEAD');
+       my $gappliedtree = cmdoutput @git, qw(rev-parse HEAD:);
+       $gappliedtree eq $dappliedtree or
+           fail <<END;
+gbp-pq import and dpkg-source disagree!
+ gbp-pq import gave commit $gapplied
+ gbp-pq import gave tree $gappliedtree
+ dpkg-source --before-build gave tree $dappliedtree
+END
+       $rawimport_hash = $gapplied;
+    }
+
+    progress "synthesised git commit from .dsc $cversion";
+
     my $rawimport_mergeinput = {
         Commit => $rawimport_hash,
         Info => "Import of source package",
     };
     my @output = ($rawimport_mergeinput);
-    progress "synthesised git commit from .dsc $cversion";
+
     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 $oldclogp = mergeinfo_getclogp($lastpush_mergeinput);
        my $oversion = getfield $oldclogp, 'Version';
        my $vcmp =
            version_compare($oversion, $cversion);
@@ -1619,9 +1952,10 @@ sub complete_file_from_dsc ($$) {
 }
 
 sub ensure_we_have_orig () {
-    foreach my $fi (dsc_files_info()) {
+    my @dfi = dsc_files_info();
+    foreach my $fi (@dfi) {
        my $f = $fi->{Filename};
-       next unless is_orig_file($f);
+       next unless is_orig_file_in_dsc($f, \@dfi);
        complete_file_from_dsc('..', $fi)
            or next;
     }
@@ -1632,7 +1966,11 @@ sub git_fetch_us () {
     # deliberately-not-ff, in which case we must fetch everything.
 
     my @specs = deliberately_not_fast_forward ? qw(tags/*) :
-       map { "tags/$_" } debiantags('*',access_basedistro);
+       map { "tags/$_" }
+       (quiltmode_splitbrain
+        ? (map { $_->('*',access_basedistro) }
+           \&debiantag_new, \&debiantag_maintview)
+        : debiantags('*',access_basedistro));
     push @specs, server_branch($csuite);
     push @specs, qw(heads/*) if deliberately_not_fast_forward;
 
@@ -1787,14 +2125,9 @@ END
 }
 
 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");
+    my ($mi) = @_;
+    $mi->{Clogp} = commit_getclogp($mi->{Commit});
 }
 
 sub mergeinfo_version ($) {
@@ -1885,7 +2218,7 @@ sub fetch_from_archive () {
     # 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.
+    # to any public view pseudo-merge and put them in @mergeinputs.
 
     my @mergeinputs;
     # $mergeinputs[]{Commit}
@@ -1924,10 +2257,10 @@ sub fetch_from_archive () {
     my $del_lrfetchrefs = sub {
        changedir $cwd;
        my $gur;
-       printdebug "del_lrfetchrefs\n";
+       printdebug "del_lrfetchrefs...\n";
        foreach my $fullrefname (sort keys %lrfetchrefs_d) {
            my $objid = $lrfetchrefs_d{$fullrefname};
-           printdebug "del_lrfetchrefs: $fullrefname=$objid.\n";
+           printdebug "del_lrfetchrefs: $objid $fullrefname\n";
            if (!$gur) {
                $gur ||= new IO::Handle;
                open $gur, "|-", qw(git update-ref --stdin) or die $!;
@@ -2103,7 +2436,7 @@ END
     } else {
        $hash = $mergeinputs[0]{Commit};
     }
-    progress "fetch hash=$hash\n";
+    printdebug "fetch hash=$hash\n";
 
     my $chkff = sub {
        my ($lasth, $what) = @_;
@@ -2122,10 +2455,7 @@ END
     if (defined $skew_warning_vsn) {
        mkpath '.git/dgit';
        printdebug "SKEW CHECK WANT $skew_warning_vsn\n";
-       my $clogf = ".git/dgit/changelog.tmp";
-       runcmd shell_cmd "exec >$clogf",
-           @git, qw(cat-file blob), "$hash:debian/changelog";
-       my $gotclogp = parsechangelog("-l$clogf");
+       my $gotclogp = commit_getclogp($hash);
        my $got_vsn = getfield $gotclogp, 'Version';
        printdebug "SKEW CHECK GOT $got_vsn\n";
        if (version_compare($got_vsn, $skew_warning_vsn) < 0) {
@@ -2333,7 +2663,7 @@ sub get_source_format () {
     return ($_, \%options);
 }
 
-sub madformat ($) {
+sub madformat_wantfixup ($) {
     my ($format) = @_;
     return 0 unless $format eq '3.0 (quilt)';
     our $quilt_mode_warned;
@@ -2348,6 +2678,192 @@ sub madformat ($) {
     return 1;
 }
 
+# An "infopair" is a tuple [ $thing, $what ]
+# (often $thing is a commit hash; $what is a description)
+
+sub infopair_cond_equal ($$) {
+    my ($x,$y) = @_;
+    $x->[0] eq $y->[0] or fail <<END;
+$x->[1] ($x->[0]) not equal to $y->[1] ($y->[0])
+END
+};
+
+sub infopair_lrf_tag_lookup ($$) {
+    my ($tagnames, $what) = @_;
+    # $tagname may be an array ref
+    my @tagnames = ref $tagnames ? @$tagnames : ($tagnames);
+    printdebug "infopair_lrfetchref_tag_lookup $what @tagnames\n";
+    foreach my $tagname (@tagnames) {
+       my $lrefname = lrfetchrefs."/tags/$tagname";
+       my $tagobj = $lrfetchrefs_f{$lrefname};
+       next unless defined $tagobj;
+       printdebug "infopair_lrfetchref_tag_lookup $tagobj $tagname $what\n";
+       return [ git_rev_parse($tagobj), $what ];
+    }
+    fail @tagnames==1 ? <<END : <<END;
+Wanted tag $what (@tagnames) on dgit server, but not found
+END
+Wanted tag $what (one of: @tagnames) on dgit server, but not found
+END
+}
+
+sub infopair_cond_ff ($$) {
+    my ($anc,$desc) = @_;
+    is_fast_fwd($anc->[0], $desc->[0]) or fail <<END;
+$anc->[1] ($anc->[0]) .. $desc->[1] ($desc->[0]) is not fast forward
+END
+};
+
+sub pseudomerge_version_check ($$) {
+    my ($clogp, $archive_hash) = @_;
+
+    my $arch_clogp = commit_getclogp $archive_hash;
+    my $i_arch_v = [ (getfield $arch_clogp, 'Version'),
+                    'version currently in archive' ];
+    if (defined $overwrite_version) {
+       if (length $overwrite_version) {
+           infopair_cond_equal([ $overwrite_version,
+                                 '--overwrite= version' ],
+                               $i_arch_v);
+       } else {
+           my $v = $i_arch_v->[0];
+           progress "Checking package changelog for archive version $v ...";
+           eval {
+               my @xa = ("-f$v", "-t$v");
+               my $vclogp = parsechangelog @xa;
+               my $cv = [ (getfield $vclogp, 'Version'),
+                          "Version field from dpkg-parsechangelog @xa" ];
+               infopair_cond_equal($i_arch_v, $cv);
+           };
+           if ($@) {
+               $@ =~ s/^dgit: //gm;
+               fail "$@".
+                   "Perhaps debian/changelog does not mention $v ?";
+           }
+       }
+    }
+    
+    printdebug "pseudomerge_version_check i_arch_v @$i_arch_v\n";
+    return $i_arch_v;
+}
+
+sub pseudomerge_make_commit ($$$$ $$) {
+    my ($clogp, $dgitview, $archive_hash, $i_arch_v,
+       $msg_cmd, $msg_msg) = @_;
+    progress "Declaring that HEAD inciudes all changes in $i_arch_v->[0]...";
+
+    my $tree = cmdoutput qw(git rev-parse), "${dgitview}:";
+    my $authline = clogp_authline $clogp;
+
+    chomp $msg_msg;
+    $msg_cmd .=
+       !defined $overwrite_version ? ""
+       : !length  $overwrite_version ? " --overwrite"
+       : " --overwrite=".$overwrite_version;
+
+    mkpath '.git/dgit';
+    my $pmf = ".git/dgit/pseudomerge";
+    open MC, ">", $pmf or die "$pmf $!";
+    print MC <<END or die $!;
+tree $tree
+parent $dgitview
+parent $archive_hash
+author $authline
+commiter $authline
+
+$msg_msg
+
+[$msg_cmd]
+END
+    close MC or die $!;
+
+    return make_commit($pmf);
+}
+
+sub splitbrain_pseudomerge ($$$$) {
+    my ($clogp, $maintview, $dgitview, $archive_hash) = @_;
+    # => $merged_dgitview
+    printdebug "splitbrain_pseudomerge...\n";
+    #
+    #     We:      debian/PREVIOUS    HEAD($maintview)
+    # expect:          o ----------------- o
+    #                    \                   \
+    #                     o                   o
+    #                 a/d/PREVIOUS        $dgitview
+    #                $archive_hash              \
+    #  If so,                \                   \
+    #  we do:                 `------------------ o
+    #   this:                                   $dgitview'
+    #
+
+    printdebug "splitbrain_pseudomerge...\n";
+
+    my $i_arch_v = pseudomerge_version_check($clogp, $archive_hash);
+
+    return $dgitview unless defined $archive_hash;
+
+    if (!defined $overwrite_version) {
+       progress "Checking that HEAD inciudes all changes in archive...";
+    }
+
+    return $dgitview if is_fast_fwd $archive_hash, $dgitview;
+
+    my $t_dep14 = debiantag_maintview $i_arch_v->[0], access_basedistro;
+    my $i_dep14 = infopair_lrf_tag_lookup($t_dep14, "maintainer view tag");
+    my $t_dgit = debiantag_new $i_arch_v->[0], access_basedistro;
+    my $i_dgit = infopair_lrf_tag_lookup($t_dgit, "dgit view tag");
+    my $i_archive = [ $archive_hash, "current archive contents" ];
+
+    printdebug "splitbrain_pseudomerge i_archive @$i_archive\n";
+
+    infopair_cond_equal($i_dgit, $i_archive);
+    infopair_cond_ff($i_dep14, $i_dgit);
+    $overwrite_version // infopair_cond_ff($i_dep14, [ $maintview, 'HEAD' ]);
+
+    my $r = pseudomerge_make_commit
+       $clogp, $dgitview, $archive_hash, $i_arch_v,
+       "dgit --quilt=$quilt_mode",
+       (defined $overwrite_version ? <<END_OVERWR : <<END_MAKEFF);
+Declare fast forward from $overwrite_version
+END_OVERWR
+Make fast forward from $i_arch_v->[0]
+END_MAKEFF
+
+    progress "Made pseudo-merge of $i_arch_v->[0] into dgit view.";
+    return $r;
+}      
+
+sub plain_overwrite_pseudomerge ($$$) {
+    my ($clogp, $head, $archive_hash) = @_;
+
+    printdebug "plain_overwrite_pseudomerge...";
+
+    my $i_arch_v = pseudomerge_version_check($clogp, $archive_hash);
+
+    my @tagformats = access_cfg_tagformats();
+    my @t_overwr =
+       map { $_->($i_arch_v->[0], access_basedistro) }
+       (grep { m/^(?:old|hist)$/ } @tagformats)
+       ? \&debiantags : \&debiantag_new;
+    my $i_overwr = infopair_lrf_tag_lookup \@t_overwr, "previous version tag";
+    my $i_archive = [ $archive_hash, "current archive contents" ];
+
+    infopair_cond_equal($i_overwr, $i_archive);
+
+    return $head if is_fast_fwd $archive_hash, $head;
+
+    my $m = "Declare fast forward from $i_arch_v->[0]";
+
+    my $r = pseudomerge_make_commit
+       $clogp, $head, $archive_hash, $i_arch_v,
+       "dgit", $m;
+
+    runcmd @git, qw(update-ref -m), $m, 'HEAD', $r, $head;
+
+    progress "Make pseudo-merge of $i_arch_v->[0] into your HEAD.";
+    return $r;
+}
+
 sub push_parse_changelog ($) {
     my ($clogpfn) = @_;
 
@@ -2395,6 +2911,7 @@ sub push_tagwants ($$$$) {
        $tw->{Tag} = $tw->{TagFn}($cversion, access_basedistro);
        $tw->{Tfn} = sub { $tfbase.$tw->{TfSuffix}.$_[0]; };
     }
+    printdebug 'push_tagwants: ', Dumper(\@_, \@tagwants);
     return @tagwants;
 }
 
@@ -2550,7 +3067,7 @@ END
     my $dgithead = $actualhead;
     my $maintviewhead = undef;
 
-    if (madformat($format)) {
+    if (madformat_wantfixup($format)) {
        # user might have not used dgit build, so maybe do this now:
        if (quiltmode_splitbrain()) {
            my $upstreamversion = $clogp->{Version};
@@ -2563,7 +3080,9 @@ END
  "--quilt=$quilt_mode but no cached dgit view:
  perhaps tree changed since dgit build[-source] ?";
            $split_brain = 1;
-           $dgithead = $dgitview;
+           $dgithead = splitbrain_pseudomerge($clogp,
+                                              $actualhead, $dgitview,
+                                              $archive_hash);
            $maintviewhead = $actualhead;
            changedir '../../../..';
            prep_ud(); # so _only_subdir() works, below
@@ -2572,21 +3091,27 @@ END
        }
     }
 
+    if (defined $overwrite_version && !defined $maintviewhead) {
+       $dgithead = plain_overwrite_pseudomerge($clogp,
+                                               $dgithead,
+                                               $archive_hash);
+    }
+
     check_not_dirty();
 
     my $forceflag = '';
     if ($archive_hash) {
-       if (is_fast_fwd($archive_hash, 'HEAD')) {
+       if (is_fast_fwd($archive_hash, $dgithead)) {
            # ok
        } elsif (deliberately_not_fast_forward) {
            $forceflag = '+';
        } else {
            fail "dgit push: HEAD is not a descendant".
                " of the archive's version.\n".
-               "dgit: To overwrite its contents,".
-               " use git merge -s ours ".lrref().".\n".
-               "dgit: To rewind history, if permitted by the archive,".
-               " use --deliberately-not-fast-forward";
+               "To overwrite the archive's contents,".
+               " pass --overwrite[=VERSION].\n".
+               "To rewind history, if permitted by the archive,".
+               " use --deliberately-not-fast-forward.";
        }
     }
 
@@ -2630,7 +3155,7 @@ END
     responder_send_command("param head $dgithead");
     responder_send_command("param csuite $csuite");
     responder_send_command("param tagformat $tagformat");
-    if (quiltmode_splitbrain) {
+    if (defined $maintviewhead) {
        die unless ($protovsn//4) >= 4;
        responder_send_command("param maint-view $maintviewhead");
     }
@@ -2688,11 +3213,6 @@ END
 
     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}";
     }
 
@@ -3190,7 +3710,10 @@ sub quiltify_splitbrain ($$$$$$) {
     local $ENV{GIT_COMMITTER_NAME} =  $authline[0];
     local $ENV{GIT_COMMITTER_EMAIL} = $authline[1];
     local $ENV{GIT_COMMITTER_DATE} =  $authline[2];
-       
+    local $ENV{GIT_AUTHOR_NAME} =  $authline[0];
+    local $ENV{GIT_AUTHOR_EMAIL} = $authline[1];
+    local $ENV{GIT_AUTHOR_DATE} =  $authline[2];
+
     if ($quilt_mode =~ m/gbp|unapplied/ &&
        ($diffbits->{H2O} & 01)) {
        my $msg =
@@ -3202,15 +3725,30 @@ sub quiltify_splitbrain ($$$$$$) {
        }  
        fail $msg;
     }
+    if ($quilt_mode =~ m/dpm/ &&
+       ($diffbits->{H2A} & 01)) {
+       fail <<END;
+--quilt=$quilt_mode specified, implying patches-applied git tree
+ but git tree differs from result of applying debian/patches to upstream
+END
+    }
     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);
+       runcmd shell_cmd 'exec >/dev/null', gbp_pq, qw(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 ($quilt_mode =~ m/gbp|dpm/ &&
+       ($diffbits->{O2A} & 02)) {
+       fail <<END
+--quilt=$quilt_mode specified, implying that HEAD is for use with a
+ tool which does not create patches for changes to upstream
+ .gitignores: but, such patches exist in debian/patches.
+END
+    }
     if (($diffbits->{H2O} & 02) && # user has modified .gitignore
        !($diffbits->{O2A} & 02)) { # patches do not change .gitignore
        quiltify_splitbrain_needed();
@@ -3228,7 +3766,7 @@ 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]
+[dgit ($our_version) update-gitignore]
 ---
 END
         close GIPATCH or die "$gipatch: $!";
@@ -3473,11 +4011,21 @@ sub quiltify ($$$$) {
 
 sub build_maybe_quilt_fixup () {
     my ($format,$fopts) = get_source_format;
-    return unless madformat $format;
+    return unless madformat_wantfixup $format;
     # sigh
 
     check_for_vendor_patches();
 
+    if (quiltmode_splitbrain) {
+       foreach my $needtf (qw(new maint)) {
+           next if grep { $_ eq $needtf } access_cfg_tagformats;
+           fail <<END
+quilt mode $quilt_mode requires split view so server needs to support
+ both "new" and "maint" tag formats, but config says it doesn't.
+END
+       }
+    }
+
     my $clogp = parsechangelog();
     my $headref = git_rev_parse('HEAD');
 
@@ -3519,7 +4067,7 @@ sub quilt_fixup_linkorigs ($$) {
            local ($debuglevel) = $debuglevel-1;
            printdebug "QF linkorigs $b, $f ?\n";
        }
-       next unless is_orig_file $b, srcfn $upstreamversion,'';
+       next unless is_orig_file_of_vsn $b, $upstreamversion;
        printdebug "QF linkorigs $b, $f Y\n";
        link_ltarget $f, $b or die "$b $!";
         $fn->($b);
@@ -3547,12 +4095,12 @@ sub quilt_fixup_singlepatch ($$$) {
     rmtree("debian/patches");
 
     runcmd @dpkgsource, qw(-b .);
-    chdir "..";
+    changedir "..";
     runcmd @dpkgsource, qw(-x), (srcfn $version, ".dsc");
     rename srcfn("$upstreamversion", "/debian/patches"), 
            "work/debian/patches";
 
-    chdir "work";
+    changedir "work";
     commit_quilty_patch();
 }
 
@@ -4067,12 +4615,15 @@ sub cmd_gbp_build {
 
     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);
+    if (!length $gbp_build[0]) {
+       if (length executable_on_path('git-buildpackage')) {
+           $gbp_build[0] = qw(git-buildpackage);
+       } else {
+           $gbp_build[0] = 'gbp buildpackage';
+       }
     }
+    my @cmd = opts_opt_multi_cmd @gbp_build;
+
     push @cmd, (qw(-us -uc --git-no-sign-tags), "--git-builder=@dbp");
 
     if ($wantsrc > 0) {
@@ -4365,6 +4916,12 @@ sub parseopts () {
            } elsif (m/^--no-rm-on-error$/s) {
                push @ropts, $_;
                $rmonerror = 0;
+           } elsif (m/^--overwrite$/s) {
+               push @ropts, $_;
+               $overwrite_version = '';
+           } elsif (m/^--overwrite=(.+)$/s) {
+               push @ropts, $_;
+               $overwrite_version = $1;
            } elsif (m/^--(no-)?rm-old-changes$/s) {
                push @ropts, $_;
                $rmchanges = !$1;