chiark / gitweb /
dgit.1: Clarify the --clean= options' documentation. Closes:#800054.
[dgit.git] / dgit
diff --git a/dgit b/dgit
index 451985db3859a10dfc36912d30d9c4192b673659..34152eec985ae6aeb36b1b259a2498262025887d 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -63,7 +63,7 @@ our $existing_package = 'dpkg';
 our $cleanmode;
 our $changes_since_version;
 our $rmchanges;
-our $overwrite_version;
+our $overwrite_version; # undef: not specified; '': check changelog
 our $quilt_mode;
 our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck|gbp|dpm|unapplied';
 our $we_are_responder;
@@ -77,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';
@@ -94,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
@@ -109,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);
 
@@ -150,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(); }
@@ -241,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:
@@ -534,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',
@@ -867,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 $!;
@@ -1194,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 ];
@@ -1367,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);
@@ -1417,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 ($) {
@@ -1430,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';
@@ -1508,10 +1566,12 @@ sub check_for_vendor_patches () {
 
 sub generate_commits_from_dsc () {
     # See big comment in fetch_from_archive, below.
+    # See also README.dsc-import.
     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$#;
 
@@ -1522,48 +1582,310 @@ 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 $oldclogp = mergeinfo_getclogp($lastpush_mergeinput);
        my $oversion = getfield $oldclogp, 'Version';
@@ -1631,9 +1953,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;
     }
@@ -1813,6 +2136,8 @@ sub mergeinfo_version ($) {
 }
 
 sub fetch_from_archive () {
+    ensure_setup_existing_tree();
+
     # Ensures that lrref() is what is actually in the archive, one way
     # or another, according to us - ie this client's
     # appropritaely-updated archive view.  Also returns the commit id.
@@ -2114,7 +2439,7 @@ END
     } else {
        $hash = $mergeinputs[0]{Commit};
     }
-    progress "fetch hash=$hash\n";
+    printdebug "fetch hash=$hash\n";
 
     my $chkff = sub {
        my ($lasth, $what) = @_;
@@ -2212,6 +2537,13 @@ sub setup_useremail (;$) {
     $setup->('name', 'DEBFULLNAME');
 }
 
+sub ensure_setup_existing_tree () {
+    my $k = "remote.$remotename.skipdefaultupdate";
+    my $c = git_get_config $k;
+    return if defined $c;
+    set_local_git_config $k, 'true';
+}
+
 sub setup_new_tree () {
     setup_mergechangelogs();
     setup_useremail();
@@ -2307,7 +2639,11 @@ sub commit_quilty_patch () {
     }
     my @adds = map { s/[][*?\\]/\\$&/g; $_; } sort keys %adds;
     runcmd_ordryrun_local @git, qw(add -f), @adds;
-    commit_admin "Commit Debian 3.0 (quilt) metadata";
+    commit_admin <<END
+Commit Debian 3.0 (quilt) metadata
+
+[dgit ($our_version) quilt-fixup]
+END
 }
 
 sub get_source_format () {
@@ -2341,7 +2677,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;
@@ -2356,6 +2692,108 @@ 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
@@ -2372,97 +2810,73 @@ sub splitbrain_pseudomerge ($$$$) {
     #   this:                                   $dgitview'
     #
 
-    # We work with tuples [ $thing, $what ]
-    # (often $thing is a commit hash; $what is a description)
-
-    my $tag_lookup = sub {
-       my ($tagname, $what) = @_;
-       printdebug "splitbrain_pseudomerge tag_lookup $what\n";
-       my $lrefname = lrfetchrefs."/tags/$tagname";
-       my $tagobj = $lrfetchrefs_f{$lrefname};
-       defined $tagobj or fail <<END;
-Wanted tag $tagname ($what) on dgit server, but not found
-END
-       printdebug "splitbrain_pseudomerge tag_lookup $tagobj $what\n";
-       return [ git_rev_parse($tagobj), $what ];
-    };
-
-    my $cond_equal = sub {
-       my ($x,$y) = @_;
-       $x->[0] eq $y->[0] or fail <<END;
-$x->[1] ($x->[0]) not equal to $y->[1] ($y->[0])
-END
-    };
-    my $cond_ff = sub {
-       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
-    };
+    printdebug "splitbrain_pseudomerge...\n";
 
-    my $arch_clogp = commit_getclogp $archive_hash;
-    my $i_arch_v = [ (getfield $arch_clogp, 'Version'),
-                    'version currently in archive' ];
-    
-    printdebug "splitbrain_pseudomerge i_arch_v @$i_arch_v\n";
+    my $i_arch_v = pseudomerge_version_check($clogp, $archive_hash);
 
     return $dgitview unless defined $archive_hash;
 
-    if (defined $overwrite_version) {
-       progress "Declaring that HEAD inciudes all changes in archive...";
-       progress "Checking that $overwrite_version does so...";
-       $cond_equal->([ $overwrite_version, '--overwrite= version' ],
-                     $i_arch_v);
-    } else {
+    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 = $tag_lookup->($t_dep14, "maintainer view tag");
+    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 = $tag_lookup->($t_dgit, "dgit view tag");
+    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";
 
-    $cond_equal->($i_dgit, $i_archive);
-    $cond_ff->($i_dep14, $i_dgit);
-    $overwrite_version // $cond_ff->($i_dep14, [ $maintview, 'HEAD' ]);
+    infopair_cond_equal($i_dgit, $i_archive);
+    infopair_cond_ff($i_dep14, $i_dgit);
+    $overwrite_version // infopair_cond_ff($i_dep14, [ $maintview, 'HEAD' ]);
 
-    my $tree = cmdoutput qw(git rev-parse), "${dgitview}:";
-    my $authline = clogp_authline $clogp;
+    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
 
-    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
+    progress "Made pseudo-merge of $i_arch_v->[0] into dgit view.";
+    return $r;
+}      
 
-END
-    if (defined $overwrite_version) {
-       print MC <<END;
-Declare fast forward from $overwrite_version
+sub plain_overwrite_pseudomerge ($$$) {
+    my ($clogp, $head, $archive_hash) = @_;
 
-[dgit --quilt=$quilt_mode --overwrite-version=$overwrite_version]
-END
-    } else {
-       print MC <<END;
-Make fast forward from $i_arch_v->[0]
+    printdebug "plain_overwrite_pseudomerge...";
 
-[dgit --quilt=$quilt_mode]
-END
-    }
-    close MC or die $!;
+    my $i_arch_v = pseudomerge_version_check($clogp, $archive_hash);
 
-    progress "Making pseudo-merge of $i_arch_v->[0] into dgit view.";
-    return make_commit($pmf);
-}      
+    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) = @_;
@@ -2667,7 +3081,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};
@@ -2691,6 +3105,12 @@ END
        }
     }
 
+    if (defined $overwrite_version && !defined $maintviewhead) {
+       $dgithead = plain_overwrite_pseudomerge($clogp,
+                                               $dgithead,
+                                               $archive_hash);
+    }
+
     check_not_dirty();
 
     my $forceflag = '';
@@ -2702,10 +3122,10 @@ END
        } 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.";
        }
     }
 
@@ -2810,7 +3230,8 @@ END
        push @pushrefs, $forceflag."refs/tags/$tw->{Tag}";
     }
 
-    runcmd_ordryrun @git, qw(push),access_giturl(), @pushrefs;
+    runcmd_ordryrun @git,
+       qw(-c push.followTags=false push), access_giturl(), @pushrefs;
     runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), $dgithead;
 
     supplementary_message(<<'END');
@@ -3230,13 +3651,10 @@ sub quiltify_dpkg_commit ($$$;$) {
     mkpath '.git/dgit';
     my $descfn = ".git/dgit/quilt-description.tmp";
     open O, '>', $descfn or die "$descfn: $!";
-    $msg =~ s/\s+$//g;
-    $msg =~ s/\n/\n /g;
-    $msg =~ s/^\s+$/ ./mg;
+    $msg =~ s/\n+/\n\n/;
     print O <<END or die $!;
-Description: $msg
-Author: $author
-$xinfo
+From: $author
+${xinfo}Subject: $msg
 ---
 
 END
@@ -3304,7 +3722,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 =
@@ -3327,7 +3748,7 @@ END
        ($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);
@@ -3357,7 +3778,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: $!";
@@ -3371,7 +3792,11 @@ END
        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";
+        commit_admin <<END
+Commit patch to update .gitignore
+
+[dgit ($our_version) update-gitignore-quilt-fixup]
+END
     }
 
     my $dgitview = git_rev_parse 'refs/heads/dgit-view';
@@ -3566,15 +3991,65 @@ sub quiltify ($$$$) {
        $commitdata =~ m/^author (.*) \d+ [-+0-9]+$/m or die "$cc ?";
        my $author = $1;
 
+       my $commitdate = cmdoutput
+           @git, qw(log -n1 --pretty=format:%aD), $cc;
+
        $msg =~ s/^(.*)\n*/$1\n/ or die "$cc $msg ?";
 
+       my $strip_nls = sub { $msg =~ s/\n+$//; $msg .= "\n"; };
+       $strip_nls->();
+
        my $title = $1;
-       my $patchname = $title;
-       $patchname =~ s/[.:]$//;
-       $patchname =~ y/ A-Z/-a-z/;
-       $patchname =~ y/-a-z0-9_.+=~//cd;
-       $patchname =~ s/^\W/x-$&/;
-       $patchname = substr($patchname,0,40);
+       my $patchname;
+       my $patchdir;
+
+       my $gbp_check_suitable = sub {
+           $_ = shift;
+           my ($what) = @_;
+
+           eval {
+               die "contains unexpected slashes\n" if m{//} || m{/$};
+               die "contains leading punctuation\n" if m{^\W} || m{/\W};
+               die "contains bad character(s)\n" if m{[^-a-z0-9_.+=~/]}i;
+               die "too long" if length > 200;
+           };
+           return $_ unless $@;
+           print STDERR "quiltifying commit $cc:".
+               " ignoring/dropping Gbp-Pq $what: $@";
+           return undef;
+       };
+
+       if ($msg =~ s/^ (?: gbp(?:-pq)? : \s* name \s+ |
+                          gbp-pq-name: \s* )
+                      (\S+) \s* \n //ixm) {
+           $patchname = $gbp_check_suitable->($1, 'Name');
+       }
+       if ($msg =~ s/^ (?: gbp(?:-pq)? : \s* topic \s+ |
+                          gbp-pq-topic: \s* )
+                      (\S+) \s* \n //ixm) {
+           $patchdir = $gbp_check_suitable->($1, 'Topic');
+       }
+
+       $strip_nls->();
+
+       if (!defined $patchname) {
+           $patchname = $title;
+           $patchname =~ s/[.:]$//;
+           $patchname =~ y/ A-Z/-a-z/;
+           $patchname =~ y/-a-z0-9_.+=~//cd;
+           $patchname =~ s/^\W/x-$&/;
+           $patchname = substr($patchname,0,40);
+       }
+       if (!defined $patchdir) {
+           $patchdir = '';
+       }
+       if (length $patchdir) {
+           $patchname = "$patchdir/$patchname";
+       }
+       if ($patchname =~ m{^(.*)/}) {
+           mkpath "debian/patches/$1";
+       }
+
        my $index;
        for ($index='';
             stat "debian/patches/$patchname$index";
@@ -3592,6 +4067,7 @@ sub quiltify ($$$$) {
        runcmd @git, qw(checkout -q), $target, qw(debian/changelog);
 
        quiltify_dpkg_commit "$patchname$index", $author, $msg,
+           "Date: $commitdate\n".
            "X-Dgit-Generated: $clogp->{Version} $cc\n";
 
        runcmd @git, qw(checkout -q), $cc, qw(debian/changelog);
@@ -3602,11 +4078,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');
 
@@ -3648,7 +4134,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);
@@ -3657,7 +4143,11 @@ sub quilt_fixup_linkorigs ($$) {
 
 sub quilt_fixup_delete_pc () {
     runcmd @git, qw(rm -rqf .pc);
-    commit_admin "Commit removal of .pc (quilt series tracking data)";
+    commit_admin <<END
+Commit removal of .pc (quilt series tracking data)
+
+[dgit ($our_version) upgrade quilt-remove-pc]
+END
 }
 
 sub quilt_fixup_singlepatch ($$$) {
@@ -3676,12 +4166,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();
 }
 
@@ -4191,17 +4681,24 @@ sub cmd_build {
     printdone "build successful\n";
 }
 
+sub pre_gbp_build {
+    $quilt_mode //= 'gbp';
+}
+
 sub cmd_gbp_build {
     my @dbp = @dpkgbuildpackage;
 
     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) {
@@ -4494,6 +4991,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;
@@ -4612,6 +5115,9 @@ if (!@ARGV) {
 my $cmd = shift @ARGV;
 $cmd =~ y/-/_/;
 
+my $pre_fn = ${*::}{"pre_$cmd"};
+$pre_fn->() if $pre_fn;
+
 if (!defined $rmchanges) {
     local $access_forpush;
     $rmchanges = access_cfg_bool(0, 'rm-old-changes');