chiark / gitweb /
badcommit-fixup: Merge from a filtered view of my personal playground
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Fri, 6 Jan 2017 16:09:39 +0000 (16:09 +0000)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Fri, 6 Jan 2017 16:09:39 +0000 (16:09 +0000)
Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
152 files changed:
.gitignore [new file with mode: 0644]
DEVELOPER-CERTIFICATE [new file with mode: 0644]
Debian/Dgit.pm [new file with mode: 0644]
Debian/Dgit/Infra.pm [new file with mode: 0644]
Debian/Dgit/Policy/Debian.pm [new file with mode: 0644]
Makefile [new file with mode: 0644]
README.dsc-import [new file with mode: 0644]
absurd/git [new file with mode: 0755]
debian/changelog [new file with mode: 0644]
debian/compat [new file with mode: 0644]
debian/control [new file with mode: 0644]
debian/copyright [new file with mode: 0644]
debian/rules [new file with mode: 0755]
debian/tests/control [new file with mode: 0644]
debian/tests/control.in [new file with mode: 0644]
dgit [new file with mode: 0755]
dgit-maint-gbp.7.pod [new file with mode: 0644]
dgit-maint-merge.7.pod [new file with mode: 0644]
dgit-maint-native.7.pod [new file with mode: 0644]
dgit-nmu-simple.7.pod [new file with mode: 0644]
dgit-sponsorship.7.pod [new file with mode: 0644]
dgit-user.7.pod [new file with mode: 0644]
dgit.1 [new file with mode: 0644]
dgit.7 [new file with mode: 0644]
infra/cgit-regen-config [new file with mode: 0755]
infra/dgit-mirror-rsync [new file with mode: 0755]
infra/dgit-repos-admin-debian [new file with mode: 0755]
infra/dgit-repos-policy-debian [new file with mode: 0755]
infra/dgit-repos-policy-trusting [new file with mode: 0755]
infra/dgit-repos-server [new file with mode: 0755]
infra/dgit-ssh-dispatch [new file with mode: 0755]
infra/drs-cron-wrap [new file with mode: 0755]
infra/get-dm-txt [new file with mode: 0755]
infra/get-suites [new file with mode: 0755]
infra/ssh-wrap [new file with mode: 0755]
local-pod-man [new file with mode: 0755]
tests/Makefile [new file with mode: 0644]
tests/adhoc [new file with mode: 0755]
tests/drs-git-ext [new file with mode: 0755]
tests/dsd-ssh [new file with mode: 0755]
tests/enumerate-tests [new file with mode: 0755]
tests/git-srcs/pari-extra_3-1.git.tar [new file with mode: 0644]
tests/git-template.tar [new file with mode: 0644]
tests/gnupg/dd.gpg [new file with mode: 0644]
tests/gnupg/dm.gpg [new file with mode: 0644]
tests/gnupg/dm.txt [new file with mode: 0644]
tests/gnupg/pubring.gpg [new file with mode: 0644]
tests/gnupg/random_seed [new file with mode: 0644]
tests/gnupg/secring.gpg [new file with mode: 0644]
tests/gnupg/trustdb.gpg [new file with mode: 0644]
tests/lib [new file with mode: 0644]
tests/lib-build-modes [new file with mode: 0644]
tests/lib-core [new file with mode: 0644]
tests/lib-import-chk [new file with mode: 0644]
tests/lib-mirror [new file with mode: 0644]
tests/lib-orig-include-exclude [new file with mode: 0644]
tests/lib-reprepro [new file with mode: 0644]
tests/lib-restricts [new file with mode: 0644]
tests/pkg-srcs/example_1.0-1+absurd.debian.tar.xz [new file with mode: 0644]
tests/pkg-srcs/example_1.0-1+absurd.dsc [new file with mode: 0644]
tests/pkg-srcs/example_1.0-1.100.debian.tar.xz [new file with mode: 0644]
tests/pkg-srcs/example_1.0-1.100.dsc [new file with mode: 0644]
tests/pkg-srcs/example_1.0-1.debian.tar.xz [new file with mode: 0644]
tests/pkg-srcs/example_1.0-1.dsc [new file with mode: 0644]
tests/pkg-srcs/example_1.0.orig-docs.tar.gz [new file with mode: 0644]
tests/pkg-srcs/example_1.0.orig.tar.gz [new file with mode: 0644]
tests/pkg-srcs/pari-extra_3-1.diff.gz [new file with mode: 0644]
tests/pkg-srcs/pari-extra_3-1.dsc [new file with mode: 0644]
tests/pkg-srcs/pari-extra_3-2~dummy1.diff.gz [new file with mode: 0644]
tests/pkg-srcs/pari-extra_3-2~dummy1.dsc [new file with mode: 0644]
tests/pkg-srcs/pari-extra_3.orig.tar.gz [new file with mode: 0644]
tests/pkg-srcs/ruby-rails-3.2_3.2.6-1.debian.tar.gz [new file with mode: 0644]
tests/pkg-srcs/ruby-rails-3.2_3.2.6-1.dsc [new file with mode: 0644]
tests/pkg-srcs/ruby-rails-3.2_3.2.6.orig.tar.gz [new file with mode: 0644]
tests/pkg-srcs/sunxi-tools_1.2-2.~~dgittest.debian.tar.gz [new file with mode: 0644]
tests/pkg-srcs/sunxi-tools_1.2-2.~~dgittest.dsc [new file with mode: 0644]
tests/pkg-srcs/sunxi-tools_1.2.orig.tar.gz [new file with mode: 0644]
tests/run-all [new file with mode: 0755]
tests/setup/examplegit [new file with mode: 0755]
tests/setup/gnupg [new file with mode: 0755]
tests/ssh [new file with mode: 0755]
tests/suites [new file with mode: 0644]
tests/tartree-edit [new file with mode: 0755]
tests/tests/absurd-gitapply [new file with mode: 0755]
tests/tests/build-modes [new file with mode: 0755]
tests/tests/build-modes-asplit [new file with mode: 0755]
tests/tests/build-modes-gbp [new file with mode: 0755]
tests/tests/build-modes-gbp-asplit [new file with mode: 0755]
tests/tests/build-modes-sbuild [new file with mode: 0755]
tests/tests/clone-clogsigpipe [new file with mode: 0755]
tests/tests/clone-gitnosuite [new file with mode: 0755]
tests/tests/clone-nogit [new file with mode: 0755]
tests/tests/clone-reprepro [new file with mode: 0755]
tests/tests/debpolicy-dbretry [new file with mode: 0755]
tests/tests/debpolicy-newreject [new file with mode: 0755]
tests/tests/debpolicy-quilt-gbp [new file with mode: 0755]
tests/tests/distropatches-reject [new file with mode: 0755]
tests/tests/drs-clone-nogit [new file with mode: 0755]
tests/tests/drs-push-masterupdate [new file with mode: 0755]
tests/tests/drs-push-rejects [new file with mode: 0755]
tests/tests/dsd-clone-drs [new file with mode: 0755]
tests/tests/dsd-clone-nogit [new file with mode: 0755]
tests/tests/dsd-divert [new file with mode: 0755]
tests/tests/fetch-localgitonly [new file with mode: 0755]
tests/tests/fetch-somegit-notlast [new file with mode: 0755]
tests/tests/gbp-orig [new file with mode: 0755]
tests/tests/gitconfig [new file with mode: 0755]
tests/tests/import-dsc [new file with mode: 0755]
tests/tests/import-native [new file with mode: 0755]
tests/tests/import-nonnative [new file with mode: 0755]
tests/tests/import-tarbomb [new file with mode: 0755]
tests/tests/inarchivecopy [new file with mode: 0755]
tests/tests/mirror [new file with mode: 0755]
tests/tests/mirror-debnewgit [new file with mode: 0755]
tests/tests/mirror-private [new file with mode: 0755]
tests/tests/mismatches-contents [new file with mode: 0755]
tests/tests/mismatches-dscchanges [new file with mode: 0755]
tests/tests/multisuite [new file with mode: 0755]
tests/tests/newtag-clone-nogit [new file with mode: 0755]
tests/tests/oldnewtagalt [new file with mode: 0755]
tests/tests/oldtag-clone-nogit [new file with mode: 0755]
tests/tests/orig-include-exclude [new file with mode: 0755]
tests/tests/orig-include-exclude-chkquery [new file with mode: 0755]
tests/tests/overwrite-chkclog [new file with mode: 0755]
tests/tests/overwrite-junk [new file with mode: 0755]
tests/tests/overwrite-splitbrains [new file with mode: 0755]
tests/tests/overwrite-version [new file with mode: 0755]
tests/tests/push-buildproductsdir [new file with mode: 0755]
tests/tests/push-newpackage [new file with mode: 0755]
tests/tests/push-nextdgit [new file with mode: 0755]
tests/tests/quilt [new file with mode: 0755]
tests/tests/quilt-gbp [new file with mode: 0755]
tests/tests/quilt-gbp-build-modes [new file with mode: 0755]
tests/tests/quilt-gbp-build-modes-sbuild [new file with mode: 0755]
tests/tests/quilt-singlepatch [new file with mode: 0755]
tests/tests/quilt-splitbrains [new file with mode: 0755]
tests/tests/rpush [new file with mode: 0755]
tests/tests/spelling [new file with mode: 0755]
tests/tests/tag-updates [new file with mode: 0755]
tests/tests/test-list-uptodate [new file with mode: 0755]
tests/tests/trustingpolicy-replay [new file with mode: 0755]
tests/tests/unrepresentable [new file with mode: 0755]
tests/tests/version-opt [new file with mode: 0755]
tests/tstunt/Dpkg/Changelog/Parse.pm [new file with mode: 0644]
tests/tstunt/debuild [new file with mode: 0755]
tests/tstunt/dpkg-parsechangelog [new file with mode: 0755]
tests/tstunt/lintian [new file with mode: 0755]
tests/using-intree [new file with mode: 0755]
tests/worktrees/example_1.0.tar [new file with mode: 0644]
tests/worktrees/pari-extra_3-1.tar [new file with mode: 0644]
tests/worktrees/pari-extra_drs.tar [new file with mode: 0644]
tests/worktrees/ruby-rails-3.2_test.tar [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..13e2c4b
--- /dev/null
@@ -0,0 +1,15 @@
+*~
+tests/tmp
+debian/dgit
+debian/dgit-infrastructure
+debian/files
+debian/*.substvars
+debian/*.log
+debian/debhelper-build-stamp
+dgit-user.7
+dgit-nmu-simple.7
+dgit-maint-native.7
+dgit-maint-merge.7
+dgit-maint-gbp.7
+dgit-sponsorship.7
+substituted
diff --git a/DEVELOPER-CERTIFICATE b/DEVELOPER-CERTIFICATE
new file mode 100644 (file)
index 0000000..912d22e
--- /dev/null
@@ -0,0 +1,38 @@
+Developer Certificate of Origin
+Version 1.1
+
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+1 Letterman Drive
+Suite D4700
+San Francisco, CA, 94129
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+    have the right to submit it under the open source license
+    indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+    of my knowledge, is covered under an appropriate open source
+    license and I have the right under that license to submit that
+    work with modifications, whether created in whole or in part
+    by me, under the same open source license (unless I am
+    permitted to submit under a different license), as indicated
+    in the file; or
+
+(c) The contribution was provided directly to me by some other
+    person who certified (a), (b) or (c) and I have not modified
+    it.
+
+(d) I understand and agree that this project and the contribution
+    are public and that a record of the contribution (including all
+    personal information I submit with it, including my sign-off) is
+    maintained indefinitely and may be redistributed consistent with
+    this project or the open source license(s) involved.
+
diff --git a/Debian/Dgit.pm b/Debian/Dgit.pm
new file mode 100644 (file)
index 0000000..e9921d6
--- /dev/null
@@ -0,0 +1,368 @@
+# -*- perl -*-
+# dgit
+# Debian::Dgit: functions common to dgit and its helpers and servers
+#
+# Copyright (C) 2015-2016  Ian Jackson
+#
+#    This program is free software; you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation; either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package Debian::Dgit;
+
+use strict;
+use warnings;
+
+use Carp;
+use POSIX;
+use IO::Handle;
+use Config;
+use Digest::SHA;
+use Data::Dumper;
+
+BEGIN {
+    use Exporter   ();
+    our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
+
+    $VERSION     = 1.00;
+    @ISA         = qw(Exporter);
+    @EXPORT      = qw(setup_sigwarn
+                     dep14_version_mangle
+                      debiantags debiantag_old debiantag_new
+                     server_branch server_ref
+                      stat_exists link_ltarget
+                     hashfile
+                      fail ensuredir executable_on_path
+                      waitstatusmsg failedcmd_waitstatus
+                     failedcmd_report_cmd failedcmd
+                      cmdoutput cmdoutput_errok
+                      git_rev_parse git_get_ref git_for_each_ref
+                      git_for_each_tag_referring is_fast_fwd
+                      $package_re $component_re $deliberately_re
+                      $branchprefix
+                      initdebug enabledebug enabledebuglevel
+                      printdebug debugcmd
+                      $debugprefix *debuglevel *DEBUG
+                      shellquote printcmd messagequote);
+    # implicitly uses $main::us
+    %EXPORT_TAGS = ( policyflags => [qw(NOFFCHECK FRESHREPO NOCOMMITCHECK)] );
+    @EXPORT_OK   = @{ $EXPORT_TAGS{policyflags} };
+}
+
+our @EXPORT_OK;
+
+our $package_re = '[0-9a-z][-+.0-9a-z]*';
+our $component_re = '[0-9a-zA-Z][-+.0-9a-zA-Z]*';
+our $deliberately_re = "(?:TEST-)?$package_re";
+our $branchprefix = 'dgit';
+
+# policy hook exit status bits
+# see dgit-repos-server head comment for documentation
+# 1 is reserved in case something fails with `exit 1' and to spot
+# dynamic loader, runtime, etc., failures, which report 127 or 255
+sub NOFFCHECK () { return 0x2; }
+sub FRESHREPO () { return 0x4; }
+sub NOCOMMITCHECK () { return 0x8; }
+
+our $debugprefix;
+our $debuglevel = 0;
+
+sub setup_sigwarn () {
+    our $sigwarn_mainprocess = $$;
+    $SIG{__WARN__} = sub { 
+       die $_[0] unless getppid == $sigwarn_mainprocess;
+    };
+}
+
+sub initdebug ($) { 
+    ($debugprefix) = @_;
+    open DEBUG, ">/dev/null" or die $!;
+}
+
+sub enabledebug () {
+    open DEBUG, ">&STDERR" or die $!;
+    DEBUG->autoflush(1);
+    $debuglevel ||= 1;
+}
+    
+sub enabledebuglevel ($) {
+    my ($newlevel) = @_; # may be undef (eg from env var)
+    die if $debuglevel;
+    $newlevel //= 0;
+    $newlevel += 0;
+    return unless $newlevel;
+    $debuglevel = $newlevel;
+    enabledebug();
+}
+    
+sub printdebug {
+    print DEBUG $debugprefix, @_ or die $! if $debuglevel>0;
+}
+
+sub messagequote ($) {
+    local ($_) = @_;
+    s{\\}{\\\\}g;
+    s{\n}{\\n}g;
+    s{\x08}{\\b}g;
+    s{\t}{\\t}g;
+    s{[\000-\037\177]}{ sprintf "\\x%02x", ord $& }ge;
+    $_;
+}
+
+sub shellquote {
+    my @out;
+    local $_;
+    foreach my $a (@_) {
+       $_ = $a;
+       if (!length || m{[^-=_./:0-9a-z]}i) {
+           s{['\\]}{'\\$&'}g;
+           push @out, "'$_'";
+       } else {
+           push @out, $_;
+       }
+    }
+    return join ' ', @out;
+}
+
+sub printcmd {
+    my $fh = shift @_;
+    my $intro = shift @_;
+    print $fh $intro," " or die $!;
+    print $fh shellquote @_ or die $!;
+    print $fh "\n" or die $!;
+}
+
+sub debugcmd {
+    my $extraprefix = shift @_;
+    printcmd(\*DEBUG,$debugprefix.$extraprefix,@_) if $debuglevel>0;
+}
+
+sub dep14_version_mangle ($) {
+    my ($v) = @_;
+    # DEP-14 patch proposed 2016-11-09  "Version Mangling"
+    $v =~ y/~:/_%/;
+    $v =~ s/\.(?=\.|$|lock$)/.#/g;
+    return $v;
+}
+
+sub debiantag_old ($$) { 
+    my ($v,$distro) = @_;
+    return "$distro/". dep14_version_mangle $v;
+}
+
+sub debiantag_new ($$) { 
+    my ($v,$distro) = @_;
+    return "archive/$distro/".dep14_version_mangle $v;
+}
+
+sub debiantags ($$) {
+    my ($version,$distro) = @_;
+    map { $_->($version, $distro) } (\&debiantag_new, \&debiantag_old);
+}
+
+sub server_branch ($) { return "$branchprefix/$_[0]"; }
+sub server_ref ($) { return "refs/".server_branch($_[0]); }
+
+sub stat_exists ($) {
+    my ($f) = @_;
+    return 1 if stat $f;
+    return 0 if $!==&ENOENT;
+    die "stat $f: $!";
+}
+
+sub _us () {
+    $::us // ($0 =~ m#[^/]*$#, $&);
+}
+
+sub fail { 
+    my $s = "@_\n";
+    $s =~ s/\n\n$/\n/;
+    my $prefix = _us().": ";
+    $s =~ s/^/$prefix/gm;
+    die $s;
+}
+
+sub ensuredir ($) {
+    my ($dir) = @_; # does not create parents
+    return if mkdir $dir;
+    return if $! == EEXIST;
+    die "mkdir $dir: $!";
+}
+
+sub executable_on_path ($) {
+    my ($program) = @_;
+    return 1 if $program =~ m{/};
+    my @path = split /:/, ($ENV{PATH} // "/usr/local/bin:/bin:/usr/bin");
+    foreach my $pe (@path) {
+       my $here = "$pe/$program";
+       return $here if stat_exists $here && -x _;
+    }
+    return undef;
+}
+
+our @signames = split / /, $Config{sig_name};
+
+sub waitstatusmsg () {
+    if (!$?) {
+       return "terminated, reporting successful completion";
+    } elsif (!($? & 255)) {
+       return "failed with error exit status ".WEXITSTATUS($?);
+    } elsif (WIFSIGNALED($?)) {
+       my $signum=WTERMSIG($?);
+       return "died due to fatal signal ".
+           ($signames[$signum] // "number $signum").
+           ($? & 128 ? " (core dumped)" : ""); # POSIX(3pm) has no WCOREDUMP
+    } else {
+       return "failed with unknown wait status ".$?;
+    }
+}
+
+sub failedcmd_report_cmd {
+    my $intro = shift @_;
+    $intro //= "failed command";
+    { local ($!); printcmd \*STDERR, _us().": $intro:", @_ or die $!; };
+}
+
+sub failedcmd_waitstatus {
+    if ($? < 0) {
+       return "failed to fork/exec: $!";
+    } elsif ($?) {
+       return "subprocess ".waitstatusmsg();
+    } else {
+       return "subprocess produced invalid output";
+    }
+}
+
+sub failedcmd {
+    # Expects $!,$? as set by close - see below.
+    # To use with system(), set $?=-1 first.
+    #
+    # Actual behaviour of perl operations:
+    #   success              $!==0       $?==0       close of piped open
+    #   program failed       $!==0       $? >0       close of piped open
+    #   syscall failure      $! >0       $?=-1       close of piped open
+    #   failure              $! >0       unchanged   close of something else
+    #   success              trashed     $?==0       system
+    #   program failed       trashed     $? >0       system
+    #   syscall failure      $! >0       unchanged   system
+    failedcmd_report_cmd undef, @_;
+    fail failedcmd_waitstatus();
+}
+
+sub cmdoutput_errok {
+    confess Dumper(\@_)." ?" if grep { !defined } @_;
+    debugcmd "|",@_;
+    open P, "-|", @_ or die "$_[0] $!";
+    my $d;
+    $!=0; $?=0;
+    { local $/ = undef; $d = <P>; }
+    die $! if P->error;
+    if (!close P) { printdebug "=>!$?\n"; return undef; }
+    chomp $d;
+    if ($debuglevel > 0) {
+       $d =~ m/^.*/;
+       my $dd = $&;
+       my $more = (length $' ? '...' : ''); #');
+       $dd =~ s{[^\n -~]|\\}{ sprintf "\\x%02x", ord $& }ge;
+       printdebug "=> \`$dd'",$more,"\n";
+    }
+    return $d;
+}
+
+sub cmdoutput {
+    my $d = cmdoutput_errok @_;
+    defined $d or failedcmd @_;
+    return $d;
+}
+
+sub link_ltarget ($$) {
+    my ($old,$new) = @_;
+    lstat $old or return undef;
+    if (-l _) {
+       $old = cmdoutput qw(realpath  --), $old;
+    }
+    my $r = link $old, $new;
+    $r = symlink $old, $new if !$r && $!==EXDEV;
+    $r or die "(sym)link $old $new: $!";
+}
+
+sub hashfile ($) {
+    my ($fn) = @_;
+    my $h = Digest::SHA->new(256);
+    $h->addfile($fn);
+    return $h->hexdigest();
+}
+
+sub git_rev_parse ($) {
+    return cmdoutput qw(git rev-parse), "$_[0]~0";
+}
+
+sub git_for_each_ref ($$;$) {
+    my ($pattern,$func,$gitdir) = @_;
+    # calls $func->($objid,$objtype,$fullrefname,$reftail);
+    # $reftail is RHS of ref after refs/[^/]+/
+    # breaks if $pattern matches any ref `refs/blah' where blah has no `/'
+    # $pattern may be an array ref to mean multiple patterns
+    $pattern = [ $pattern ] unless ref $pattern;
+    my @cmd = (qw(git for-each-ref), @$pattern);
+    if (defined $gitdir) {
+       @cmd = ('sh','-ec','cd "$1"; shift; exec "$@"','x', $gitdir, @cmd);
+    }
+    open GFER, "-|", @cmd or die $!;
+    debugcmd "|", @cmd;
+    while (<GFER>) {
+       chomp or die "$_ ?";
+       printdebug "|> ", $_, "\n";
+       m#^(\w+)\s+(\w+)\s+(refs/[^/]+/(\S+))$# or die "$_ ?";
+       $func->($1,$2,$3,$4);
+    }
+    $!=0; $?=0; close GFER or die "$pattern $? $!";
+}
+
+sub git_get_ref ($) {
+    # => '' if no such ref
+    my ($refname) = @_;
+    local $_ = $refname;
+    s{^refs/}{[r]efs/} or die "$refname $_ ?";
+    return cmdoutput qw(git for-each-ref --format=%(objectname)), $_;
+}
+
+sub git_for_each_tag_referring ($$) {
+    my ($objreferring, $func) = @_;
+    # calls $func->($tagobjid,$refobjid,$fullrefname,$tagname);
+    printdebug "git_for_each_tag_referring ",
+        ($objreferring // 'UNDEF'),"\n";
+    git_for_each_ref('refs/tags', sub {
+       my ($tagobjid,$objtype,$fullrefname,$tagname) = @_;
+       return unless $objtype eq 'tag';
+       my $refobjid = git_rev_parse $tagobjid;
+       return unless
+           !defined $objreferring # caller wants them all
+           or $tagobjid eq $objreferring
+           or $refobjid eq $objreferring;
+       $func->($tagobjid,$refobjid,$fullrefname,$tagname);
+    });
+}
+
+sub is_fast_fwd ($$) {
+    my ($ancestor,$child) = @_;
+    my @cmd = (qw(git merge-base), $ancestor, $child);
+    my $mb = cmdoutput_errok @cmd;
+    if (defined $mb) {
+       return git_rev_parse($mb) eq git_rev_parse($ancestor);
+    } else {
+       $?==256 or failedcmd @cmd;
+       return 0;
+    }
+}
+
+1;
diff --git a/Debian/Dgit/Infra.pm b/Debian/Dgit/Infra.pm
new file mode 100644 (file)
index 0000000..eff460b
--- /dev/null
@@ -0,0 +1,17 @@
+# -*- perl -*-
+
+package Debian::Dgit::Infra;
+
+use strict;
+use warnings;
+
+# Scripts and programs which are going to `use Debian::Dgit' but which
+# live in dgit-infrastructure (ie are installed with install-infra)
+# should `use Debian::Dgit::Infra' first.  All this module does is
+# adjust @INC so that the script gets the version of the script from
+# the dgit-infrastructure package (which is installed in a different
+# location and may be a different version).
+
+# unshift @INC, q{/usr/share/dgit/infra/perl5}; ###substituted###
+
+1;
diff --git a/Debian/Dgit/Policy/Debian.pm b/Debian/Dgit/Policy/Debian.pm
new file mode 100644 (file)
index 0000000..12f1ee1
--- /dev/null
@@ -0,0 +1,42 @@
+# -*- perl -*-
+
+package Debian::Dgit::Policy::Debian;
+
+use strict;
+use warnings;
+
+use POSIX;
+
+BEGIN {
+    use Exporter   ();
+    our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
+
+    $VERSION     = 1.00;
+    @ISA         = qw(Exporter);
+    @EXPORT      = qw(poldb_path poldb_setup $poldbh);
+    %EXPORT_TAGS = ( );
+    @EXPORT_OK   = qw();
+}
+
+our @EXPORT_OK;
+
+our $poldbh;
+
+sub poldb_path ($) {
+    my ($repos) = @_;
+    return "$repos/policy.sqlite3";
+}
+
+sub poldb_setup ($;$) {
+    my ($policydb, $hook) = @_;
+
+    $poldbh ||= DBI->connect("dbi:SQLite:$policydb",'','', {
+       RaiseError=>1, PrintError=>1, AutoCommit=>0
+                          });
+
+    $hook->() if $hook;
+
+    $poldbh->do("PRAGMA foreign_keys = ON");
+}
+
+1;
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..68c1a02
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,106 @@
+# dgit
+# Integration between git and Debian-style archives
+#
+# Copyright (C)2013-2016 Ian Jackson
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+INSTALL=install
+INSTALL_DIR=$(INSTALL) -d
+INSTALL_PROGRAM=$(INSTALL) -m 755
+INSTALL_DATA=$(INSTALL) -m 644
+
+prefix?=/usr/local
+
+bindir=$(prefix)/bin
+mandir=$(prefix)/share/man
+perldir=$(prefix)/share/perl5
+man1dir=$(mandir)/man1
+man7dir=$(mandir)/man7
+infraexamplesdir=$(prefix)/share/doc/dgit-infrastructure/examples
+txtdocdir=$(prefix)/share/doc/dgit
+absurddir=$(prefix)/share/dgit/absurd
+
+PROGRAMS=dgit
+MAN1PAGES=dgit.1
+
+MAN7PAGES=dgit.7                               \
+       dgit-user.7 dgit-nmu-simple.7           \
+       dgit-maint-native.7                     \
+       dgit-maint-merge.7 dgit-maint-gbp.7     \
+       dgit-sponsorship.7
+
+TXTDOCS=README.dsc-import
+PERLMODULES=Debian/Dgit.pm
+ABSURDITIES=git
+
+INFRA_PROGRAMS=dgit-repos-server dgit-ssh-dispatch \
+       dgit-repos-policy-debian dgit-repos-admin-debian \
+       dgit-repos-policy-trusting dgit-mirror-rsync
+INFRA_EXAMPLES=get-dm-txt ssh-wrap drs-cron-wrap get-suites
+INFRA_PERLMODULES= \
+       Debian/Dgit.pm \
+       Debian/Dgit/Infra.pm \
+       Debian/Dgit/Policy/Debian.pm
+
+all:   $(MAN7PAGES) $(addprefix substituted/,$(PROGRAMS))
+
+substituted/%: %
+       mkdir -p substituted
+       perl -pe 's{\bundef\b}{'\''$(absurddir)'\''} if m/###substituted###/' \
+               <$< >$@
+
+install:       installdirs all
+       $(INSTALL_PROGRAM) $(addprefix substituted/,$(PROGRAMS)) \
+               $(DESTDIR)$(bindir)
+       $(INSTALL_PROGRAM) $(addprefix absurd/,$(ABSURDITIES)) \
+               $(DESTDIR)$(absurddir)
+       $(INSTALL_DATA) $(MAN1PAGES) $(DESTDIR)$(man1dir)
+       $(INSTALL_DATA) $(MAN7PAGES) $(DESTDIR)$(man7dir)
+       $(INSTALL_DATA) $(TXTDOCS) $(DESTDIR)$(txtdocdir)
+       set -e; for m in $(PERLMODULES); do \
+               $(INSTALL_DATA) $$m $(DESTDIR)$(perldir)/$${m%/*}; \
+       done
+
+installdirs:
+       $(INSTALL_DIR) $(DESTDIR)$(bindir) \
+               $(DESTDIR)$(man1dir) $(DESTDIR)$(man7dir) \
+               $(DESTDIR)$(txtdocdir) $(DESTDIR)$(absurddir) \
+               $(addprefix $(DESTDIR)$(perldir)/, $(dir $(PERLMODULES)))
+
+install-infra: installdirs-infra
+       $(INSTALL_PROGRAM) $(addprefix infra/, $(INFRA_PROGRAMS)) \
+               $(DESTDIR)$(bindir)
+       $(INSTALL_PROGRAM) $(addprefix infra/, $(INFRA_EXAMPLES)) \
+               $(DESTDIR)$(infraexamplesdir)
+       set -e; for m in $(INFRA_PERLMODULES); do \
+               $(INSTALL_DATA) $$m $(DESTDIR)$(perldir)/$${m%/*}; \
+       done
+
+installdirs-infra:
+       $(INSTALL_DIR) $(DESTDIR)$(bindir) $(DESTDIR)$(infraexamplesdir) \
+               $(addprefix $(DESTDIR)$(perldir)/, $(dir $(INFRA_PERLMODULES)))
+
+check installcheck:
+
+clean distclean mostlyclean maintainer-clean:
+       rm -rf tests/tmp substituted
+       set -e; for m in $(MAN7PAGES); do \
+               test -e $$m.pod && rm -f $$m; \
+       done
+
+%.7: %.7.pod
+       pod2man --section=7 --date="Debian Project" --center="dgit" \
+               --name=$(subst .7,,$@) \
+               $^ $@
diff --git a/README.dsc-import b/README.dsc-import
new file mode 100644 (file)
index 0000000..1ec53b0
--- /dev/null
@@ -0,0 +1,106 @@
+We would like to: represent the input tarballs as a commit each (which
+all get merged together as if by git merge -s subtree), and for quilt
+packages, each patch as a commit.  But w want to avoid (as much as
+possible) reimplementing the package extraction algorithm in
+dpkg-source.
+
+dpkg-source does not currently provide interfaces that look like they
+are intended for what dgit wants to do.  And dgit wants to work with
+old versions of dpkg, so I have implemented the following algorithm
+rather than wait for such interfaces added (even supposing that a sane
+interface could be designed, which is doubtful):
+
+* dgit will untar each input tarball.
+
+  This will be done by scanning the .dsc for things whose names look
+  like (compressed) tarballs, and using the interfaces provided by
+  Dpkg::Compression to get at the tarball.
+
+  Each input tarball unpack will be done separately, and will be
+  followed by git add and git write-tree, to obtain a git tree object
+  corresponding to the tarball contents.
+
+  That tree object will be made into a commit object with no parents.
+  (The package changelog will be searched for the earliest version
+  with the right upstream version component, and the information found
+  there used for the commit object's metadata.)
+
+* For `3.0 (quilt), dgit will run
+    dpkg-source -x --skip-patches
+
+  git plumbing will be used to make the result into a tree and a
+  commit.  The commit will have as parents all the tarballs previously
+  mentioned.  The main orig tarball will be the leftmost parent and
+  the debian tarball the rightmost parent.  The metadata will come
+  from the .dsc and/or the final changelog entry.
+
+  dgit will then dpkg-source --before-build and record the resulting
+  tree, too.
+
+  Then, dgit will switch back to the patches-unapplied version and use
+  `gbp pq import' (in the private working area) to turn the
+  patches-unapplied tree into a patches-applied one.
+
+  Finally dgit will check that the gbp pq generated patches-applied
+  version has the same git tree object as the one generated by
+  dpkg-source --before-build.
+
+* For source formats other than `3.0 (quilt)', dgit will do simply
+    dpkg-source -x.
+
+  Again, it will make that into a tree and a commit.
+
+* For source formats with only single file entry in the .dsc, the
+  (one) tarball is not imported separately (since its tree object
+  would be the same as the extracted object), and the commit of the
+  dpkg-source -x output has no parents.
+
+* As currently, there will be a final no-change-to-the-tree
+  pseudomerge commit which stitches the package into the relevant dgit
+  suite branch.  (By `pseudomerge' we mean something that looks as if
+  it was made with git merge -s ours.)
+
+* As currently, dgit will take steps so that none of the git trees
+  discussed above contain a .pc directory.
+
+
+This has the following properties:
+
+* Each input tarball is represented by a different commit; in usual
+  cases these commits will be the same for every upload of the same
+  upstream version.
+
+* For `3.0 (quilt)' each patch's changes to the upstream files appears
+  as a single git commit (as is the effect of the debian tarball);
+  also, there is a commit object whose tree is just the debian/
+  directory, which might well be the same as certain debian-only git
+  workflow trees.
+
+* For `1.0' non-native, the effect of the diff is represented as a
+  commit.  So eg `git blame' will show synthetic commits corresponding
+  to the correct parts of the input source package.
+
+* It is possible to `git cherry-pick' etc. commits representing `3.0
+  (quilt)' patches.  It is even possible fish out the patch stack as
+  git branch and rebase it elsewhere etc., since the patch stack is
+  represented as a contiguous series of commits which make only the
+  relevant upstream changes.
+
+* Every orig tarball in the source package is decompressed twice, but
+  disk space for only one extra copy of its unpacked contents is
+  needed.  (The converse would be possible in principle but would be
+  very hard to arrange with the current interfaces provided by the
+  various tools.)
+
+* No back doors into the innards of dpkg-source (nor changes to
+  dpkg-dev) are required.
+
+* dgit does grow a dependency on git-buildpackage.
+
+* Knowledge of the source format embedded in dgit is is restricted to
+  some relatively straightforward processing of filenames found in
+  .dsc files.
+
+* dgit now depends on dpkg-source -x --skip-patches followed by
+  dpkg-source --before-build being the same as dpkg-source -x
+  (for `3.0 (quilt)').
diff --git a/absurd/git b/absurd/git
new file mode 100755 (executable)
index 0000000..0f562b5
--- /dev/null
@@ -0,0 +1,115 @@
+#!/bin/sh
+set -e
+
+case "$DGIT_ABSURD_DEBUG" in
+''|0)  exec 3>/dev/null ;;
+1)     exec 3>>../../gbp-pq-output ;;
+*)     exec 3>>../../gbp-pq-output 2>&3 ;;
+esac
+
+log () {
+       echo >&3 "DGIT ABSURD GIT APPLY (DEBUG)  $*"
+       echo >&2 "DGIT ABSURD GIT APPLY (STDERR) $*"
+}
+
+fail () {
+       log "FAILED: $*"
+       exit 127
+}
+
+self=${0%/*}
+npath=${PATH#$self:}
+if test "x$PATH" = "x$npath"; then
+       fail "PATH FILTER FAIL ($0 $self $PATH)"
+fi
+
+bypass=true
+for arg in "$@"; do
+       case "$arg" in
+       apply)  bypass=false; break     ;;
+       -*)                             ;;
+       *)      bypass=true; break      ;;
+       esac
+done
+
+if $bypass; then
+       PATH=$npath
+       echo >&3 "DGIT ABSURD GIT APPLY - BYPASS: $*"
+       exec git "$@"
+fi
+
+log "NO BYPASS: $*"
+
+case "$DGIT_ABSURD_DEBUG" in
+''|0|1) ;;
+*)     set -x ;;
+esac
+
+#exec >/dev/tty 2>&1
+
+index=0
+noo=0
+
+for arg in "$@"; do
+       case "$noo.$arg" in
+       1.--index)
+               index=1
+               continue
+               ;;
+       1.--whitespace=fix)
+               continue
+               ;;
+       ?.-*)
+               fail "UNKNOWN OPTION $arg ($*)"
+               ;;
+       0.apply)
+               ;;
+       1.*)    patch="$arg"
+               ;;
+       *)
+               fail "BAD USAGE $arg ($noo $*)"
+       esac
+       noo=$(( $noo + 1 ))
+done
+
+if [ $noo != 2 ]; then
+       fail "NO PATCH ($*)"
+fi
+
+pwd=`pwd`
+patch=${patch#$pwd/debian/patches/}
+rm -f debian/patches/series
+
+# Work around #848611.
+# We need a stunt filename which the source package must not
+# contain.  A trick is to use the commit hash of HEAD, whose
+# hash value cannot appear in any file in its own tree.
+omgwtf="dgit-omg-wtf-$(git rev-parse HEAD)"
+cat <<END >debian/patches/$omgwtf
+---
+--- a/$omgwtf  2016-10-31 23:28:47.314155919 +0000
++++ b/$omgwtf  2016-12-18 22:40:01.870058270 +0000
+@@ -0,0 +1 @@
++:
+END
+printf "%s\n" "$omgwtf" >debian/patches/series
+printf "%s\n" "$patch" >>debian/patches/series
+
+# Just in case some joker tries to patch .git/something
+mv .git ../.git
+set +e
+dpkg-source --before-build .
+rc=$?
+set -e
+rm -rf .git
+mv ../.git .
+test $rc = 0
+
+rm -f $omgwtf debian/patches/$omgwtf
+
+rm -rf .pc
+git checkout debian/patches/series
+git add -Af .
+
+log "APPLIED $patch"
+#printf 'APPLIED '; date --iso-8601=ns
diff --git a/debian/changelog b/debian/changelog
new file mode 100644 (file)
index 0000000..be2c165
--- /dev/null
@@ -0,0 +1,1100 @@
+dgit (2.16~) unstable; urgency=medium
+
+  * 
+
+ --
+
+dgit (2.15) UNRELEASED; urgency=high
+
+  Infastructure:
+  * Prevent introduction of new commits which lack `committer'
+    information.  Ie, prevent the reception of new commits afflicted by
+    #849041.  Existing commits are tolerated.
+
+  Test suite:
+  * Be much stricter about messages from git-fsck.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Thu, 05 Jan 2017 18:20:23 +0000
+
+dgit (2.14) unstable; urgency=critical
+
+  CRITICAL BUGFIX:
+  * Do not generate bogus commits with --overwrite or import-dsc.
+    Closes:#849041.
+
+  Test suite:
+  * Run a lot of git-fsck.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Wed, 04 Jan 2017 22:52:55 +0000
+
+dgit (2.13) unstable; urgency=high
+
+  Changed behaviour:
+  * quilt fixup: Permit creation of patches which delete files, by psssing
+    --include-removal to dpkg-source, and tolerating it when we do our
+    quilt fixup analysis.  dpkg-source has supported this since at least
+    stretch.  Closes:#848901.
+
+  Error messages:
+  * Improve "cannot represent change" message: print the git old and new
+    modes too.
+
+  Bugfix:
+  * Import: Switch back to unpa branch on patch import iterations.
+    In particular, do not fail utterly if dpkg-source and gbp disagree.
+    Closes:#848843.
+
+  Documentation [Sean Whitton]:
+  * dgit-maint-gbp(7): Remove reference to closed bug.  Closes:#848725.
+  * dgit-sponsorship(7): Update in light of fixed #844129.  Closes:#848789.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Wed, 21 Dec 2016 01:32:41 +0000
+
+dgit (2.12) unstable; urgency=high
+
+  Changed behaviours:
+  * By default, generate a DEP-14 tag as well as a dgit archive/*
+    tag, even in non-split-view quilt modes.  Closes:#844129.
+  * Version tags mangling: Protect dots, as per proposed update to DEP-14.
+
+  Documentation:
+  * dgit-maint-merge(7): Explain how to change to this workflow
+    from an existing git workflow.  [Sean Whitton]  Closes:#847807.
+  * dgit-maint-native(7): Clarify that we mean native source format.
+    [Phil Hands]  Closes:#847987.
+
+  Error messages:
+  * Slightly better message when .dsc not found.  Apropos of #844128.
+  * Give better advice if .dsc/.changes signing fails: if no changes
+    are needed to the package, user may indeed just debsign and dput.
+    Closes:#844131.
+  * Produce better error reporting when absurd git wrapper fails
+    on a patch during .dsc import.  Apropos of #848391.
+
+  Bugfixes:
+  * If we cannot hardlink origs into our extraction area, use symlinks
+    instead.  Closes:#844570.
+  * Suppress some leftover debugging output from import-dsc.
+    Closes:#847658.
+  * Do not fail when cloning a package containing dangling symlinks.
+    Closes:#848512.
+  * Do not fail to import a .dsc containing patches which patch files
+    multiple times, due to #848611.  Closes:#848391.
+  * Do not fail to import a .dsc containing patches to .git/ (!)
+  * infra: dgit-repos-policy-debian which broke due to recent git setting
+    GIT_ALTERNATE_OBJECT_DIRECTORIES in the pre-receive-hook.
+    (fixes test suite regression in stretch).
+
+  Test suite:
+  * Provide and use stunt lintian and debuild, to avoid lintian
+    complaining about our stupid test packages.
+    (fixes test suite regression in stretch).
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Mon, 19 Dec 2016 17:35:18 +0000
+
+dgit (2.11) unstable; urgency=medium
+
+  Documentation:
+  * dgit-user(7): Better explanation of combined suites (comma syntax).
+    Thanks to Sean Whitton for review and suggestions.
+  * dgit(1), dgit(7): Better reference docs for combined suites.
+  * dgit(1): Improve formatting of rpush section.
+
+  Test suite:
+  * Replace make in Test-Depends with build-essential.  Most of the tests
+    do in fact run dpkg-buildpackage which bombs out if build-essential is
+    missing.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Tue, 08 Nov 2016 22:41:29 +0000
+
+dgit (2.10) unstable; urgency=medium
+
+  New features:
+  * Support the Debian *-security suites.
+  * New comma-separated multiple-suite merging facility (readonly),
+    so that users can easily track "jessie, or jessie-security".
+  * dgit-user(7): Suggest `dgit clone P jessie,-security'.
+
+  Bugfixes:
+  * Cope when an orig tarball is a tarbomb.  Ie, if it contains
+    other than one single directory toplevel.  Closes:#843422.
+  * Actually honour the branch name, if we are on dgit branch, to specify
+    the suite, as documented in the manpage.
+  * When cloning a distro which has no git server, correctly leave
+    the user on the local dgit branch, not on `master'.
+  * Fix an unconditional print that was supposed to be a printdebug:
+      origs <blah>.orig.tar.gz f.same=1 #f._differ=-1
+  * Print a slightly better message if .git found in orig tarball(s).
+
+  Test suite:
+  * Test suite: Add fakeroot and make to Test-Depends.  These aren't
+    necessarily pulled in by anything else.  (dpkg-dev Recommends
+    build-essential.  But we don't actually need build-essential.)
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Tue, 08 Nov 2016 01:08:51 +0000
+
+dgit (2.9) unstable; urgency=medium
+
+  New features:
+  * During push, automatically calculate which .origs are required,
+    so user never needs [--ch:]-sa or [--ch:]-sd.  Closes:#829116.
+  * New import-dsc feature.
+  * New option --dgit-view-save= for split view quilt modes.
+    In particular, means that the output of a split view quilt-fixup
+    is left somewhere useful.
+  * dgit clone: Set timestamps in cloned tree to a single unified time.
+    This makes it less likely that the user will trip over any
+    timestamp-dependent FTBFS bugs (eg #842452).
+  * Support dgit --delayed= push (with a warning in the manpage
+    about possible skew).
+  * dgit gbp-build will arrange to let gbp buildpackage generate
+    .orig tarballs if it seems applicable.  Closes:#841094.
+
+  Documentation improvements:
+  * dgit-*(7).  Many new tutorial manpages, several written and many
+    improved by Sean Whitton.
+  * dgit(7): Substantial updates, including documenting split view.
+  * dgit(1): Better cross-references.
+  * dgit(1): Remove obsolete workflow information.
+  * dgit(1): Improved BUGS section.
+  * Fix changelog entry for SIGPIPE to correctly mention
+    Closes:#841090.
+
+  Bugfixes:
+  * Split brain mode: Fix --new.  Closes:#842577.
+  * Properly look for .origs etc. in .., fetching them less often.
+    Closes:#842386.
+  * Reject `dgit pull' in split view quilt modes, to avoid
+    creating unfortunate wreckage on non-dgit-view branches.
+    Closes:#842608.
+  * Cope when cloning suite which doesn't receive uploads,
+    like testing.  Closes:#842621.
+  * Properly fetch all archive dgit view tags, as we intended.
+  * Actually provide a -p (--package=) option (!)
+
+  Test suite fixes:
+  * Test suite: Explicitly configure user.name and user.email, so
+    that tests work when environment doesn't have defaults.
+    Closes:#842279 (I hope).
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Mon, 31 Oct 2016 12:47:18 +0000
+
+dgit (2.8) unstable; urgency=medium
+
+  * When in split build mode for `gbp-build' or `build', run
+    mergechanges as is required.  Closes:#841990.
+  * Test suite: build-mode-*: Check that right .changes comes out
+    (detects #841990).
+  * Defend against debian/patches/series being an unusual object, in case
+    dpkg-source doesn't, in absurd git-apply fallback.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Tue, 25 Oct 2016 17:29:23 +0100
+
+dgit (2.7) unstable; urgency=medium
+
+  Absurd bugfix for serious bug:
+  * Work around `git-apply' problems (eg #841865, #829067) exposed by
+    `gbp pq import' (#841866) by sometimes falling back to an emulation of
+    git-apply in terms of dpkg-source --before-build.  Closes:#841867.
+
+  Minor changes:
+  * dgit(1): Reorder the options, moving more important ones earlier.
+  * dgit(1): Some more info about --deliberately.
+  * Provide various --force-something options.  Please don't use them.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Mon, 24 Oct 2016 02:37:28 +0100
+
+dgit (2.6) unstable; urgency=medium
+
+  Fixes to HTTP handling:
+  * Check for non-2xx HTTP status codes from ftpmaster api server.
+  * Always honour --curl= and --curl:.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sun, 23 Oct 2016 14:57:22 +0100
+
+dgit (2.5) unstable; urgency=low
+
+  Substantive changes:
+  * Do not crash in split brain quilt modes when the two brains are
+    actually identical.  (Eg --quilt=gbp with no patches.)  Closes:#841770.
+  * Switch to new archive/ tag format by default, even in
+    non-split-brain mode.
+  * Provide --gbp and --dpm as aliases for --quilt=gbp and --quilt=dpm.
+
+  Documentation:
+  * dgit-maint-merge(7): New tutorial manpage from Sean Whitton.
+
+  Test suite:
+  * Introduce setup/gnupg, to help work around gnupg2 bug #841143
+    and improve performance by amortising gnupg migration cost.
+  * Various bugfixes.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sun, 23 Oct 2016 13:20:23 +0100
+
+dgit (2.4) unstable; urgency=low
+
+  Bugfixes:
+  * split brain cache: Fix a wrong implicit reference to $_.
+    Closes:#841383.
+  * split brain cache: Make sure to write reflog entries for cache updates
+    even if the eventual tree (and therefore commit) is the same.
+    Otherwise, after updating dgit, the cache might have the right answer
+    but not be refreshed even by a build.
+  * dgit gbp-build: No longer invent a --git-debian-branch option.
+    Usually the user is a maintainer using split brain, and we should rely
+    on their own gbp configuration to specify the right check.
+    Closes:#841100.
+
+  Minor docs fix:
+  * dgit(1): Document which --ch: options are a good idea.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Thu, 20 Oct 2016 16:31:54 +0100
+
+dgit (2.3) unstable; urgency=low
+
+  * With --overwrite, do not check all sorts of tags (which may
+    not exist, or might contain wrong things).  Closes:#841101.
+  * When generating pseudomerge in quilt split brain mode due to
+    --overwrite, actually include the version number in the commit
+    message.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Tue, 18 Oct 2016 01:58:05 +0100
+
+dgit (2.2) unstable; urgency=low
+
+  * Fix config relating to Debian to actually make split brain mode
+    work.  Closes:#841085.
+  * Detect SIGPIPE (and SIGCHLD) being blocked or ignored.
+    Closes:#841090.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Mon, 17 Oct 2016 17:31:18 +0100
+
+dgit (2.1) unstable; urgency=low
+
+  * Do not crash due in clone to failure to handle dpkg-parsechangelog
+    SIGPIPE.  Closes:#840989.  Avoids:
+       dgit: failed command: dpkg-parsechangelog --format rfc822 --all
+       dgit: subprocess died due to fatal signal PIPE
+  * git- prefixes: Fix some occurrences of `git-foo' in infrastructure,
+    messages, and test suite.  Filter out .../git-core from PATH in
+    test suite so that we catch future occurrences.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sun, 16 Oct 2016 19:05:14 +0100
+
+dgit (2.0) unstable; urgency=low
+
+  Incompatible change:
+  * dgit sbuild: does not pass -A to sbuild.  Consequently the default
+    build is now simply sbuild's default.  With older sbuilds it was
+    possible to override dgit's -A by passing another option.  But this
+    has been changed recently and now this default setting is very awkward
+    to change for the dgit user.
+  * dgit gbp-build: Make --quilt=gbp the default.  (See below.)
+  * New tag format (for dgit view) archive/debian/VERSION.
+
+  Major new feature:
+  * --quilt=gbp, --quilt=dpm, --quilt=unpacked: Introduce facility for
+    split view (dgit/mainiainer view), to improve compatibility with some
+    workflow tools.
+
+  New checks and improved behaviours in dgit:
+  * When running dpkg-buildpackage, cope if user specified -g or -G.
+  * dgit sbuild: check that the set of .changes files found is as we
+    expect, before calling mergechanges.  Re:#800060.
+  * dgit sbuild: Rename the used-up .changes files to `.inmulti' to
+    avoid accidental use of the wrong one (by software, or by users).
+  * dgit sbuild: Check that the binary .changes file doesn't contain a
+    .dsc.
+  * Introduce --rm-old-changes to delete previous builds' changes files.
+  * Remove any pre-existing _source.changes file before building source,
+    as a safety check.
+  * No longer tolerate a multitude of .changes files when doing push.
+    Instead, insist on a single one.  Closes:#800110.
+  * dgit sbuild no longer deletes extranious .changes files; instead
+    we rely on --rm-old-changes, or failing that, fail early.
+  * When doing quilt linearisation, treat upstream .gitignores not
+    in the toplevel the same way we treat ones in the toplevel.
+  * When automatically generating quilt patch, honour GIT_COMMITTER_DATE
+    for filename creation (makes filename deterministic in test suite).
+  * New --overwrite option, replaces need to for user to use
+    git merge -s ours.  Closes:#838718.
+  * When generating quilt patches from git commits, make patches that
+    look quite like git-format-patch output (rather than strange things
+    based on an obselete interpretation of DEP-3).
+  * When generating quilt patches from git commits, honour (and strip)
+    any Gbp-Pq headers (that we understand).
+  * Several dgit-generated commits now have slightly better annotations
+    from dgit about what it was doing.
+  * Before committing to push, check that .dsc and .changes correspond.
+    Closes:#800060.
+  * Better error message if non-split-brain patch stack no longer
+    applies (due to new upstream version, or user messing with it).
+    Closes:#833025.
+  * Better error message if HEAD contains changes unrepresentable
+    by `3.0 (quilt)'.  Closes:#834618.
+  * Much better error message when HEAD and .dsc do not match.
+    Closes:#809516.
+
+  Infrastructure:
+  * dgit-repos-policy-debian: Better error handling.
+  * dgit-repos-policy-debian.: fix git-cat-file-handling with multiple
+    taints in db (!).
+  * dgit-infrastructure has, and uses, its own copies of the perl modules.
+    This avoids introducing a versioned dependency between dgit and
+    dgit-infrastructure (and also makes it easier to test cross-version
+    compatibility).
+
+  Documentation:
+  * Document the dgit-distro.DISTRO.quilt-mode config setting.
+  * Clarify the --clean= options' documentation.  Closes:#800054.
+  * Discourage use of the --PROGRAM:OPTION escape hatch.  (Apropos
+    of various bug reports including #800060 and #833025.)
+  * Document the expected form of HEAD for each --quilt= mode.
+
+  Bugfixes:
+  * When cleaning up after failed clone, stat the to-be-cleaned-up
+    directory before running rmtree on it.  Closes:#796773.
+  * Do not call "warn" on failure of cleanup handler in END block
+    (since warn has been made fatal and aborts the cleanup chain).
+  * Print better error message (with `fail' rather than `die') if
+    `dgit clone' cannot create the destination directory.
+  * Properly substitute $changesfile in one of the `You can retry'
+    messages.  Closes:#800078.
+  * Pass --ch:* and -v options to dpkg-buildpackage when building
+    source.  Fixes bad Perl poetry syntax.  Closes:#829121.
+  * When synthesing a commit from a .dsc from the archive, stop
+    internal git reset from printing a confusing message about HEAD.
+  * Turn off git gc in the private working areas.
+  * Do not fail to do some important quilt processing in some
+    --quilt modes.
+  * Fix two calls to chdir without proper error checking.
+  * Fix a couple of bugs in error reporting.
+  * Fix several bugs in .orig detection/recognition.
+  * Tidy up refs/dgit-fetch/ after dgit fetch (if successful).
+  * Fix handling of in-archive copies.
+  * Don't break if user has push.followTags=true.  Closes:#827878.
+  * Arrange for the special dgit remote to be skipped by git fetch --all
+    etc.  And no longer configure a fetch spec, since it won't work
+    anyway.  Closes:#827892.
+  * Allow local git config options to override user-global ones,
+    as is proper.  Closes:#835858.
+  * When generating patch filenames from titles, first transliterate
+    them (lossily) to ascii.  Closes:#834807.
+
+  Test suite:
+  * When sbuild fails, do not crash due to sed not finding the log
+    file.  Instead, simply tolerate the absence of the log file.
+  * Put --no-arch-all in build-modes-sbuild act, not only its real_act.
+    Cosmetic change only.
+  * Set GIT_COMMITTER_DATE and GIT_AUTHOR_DATE and increment them
+    explicitly in drs-push-rejects test.  This avoids date dependencies
+    which can cause that test to fail on fast computers.
+  * Remove some spurios .debs from the example_1.0.tar.
+  * Increase sqlite_busy_timeout in debpolicy-dbretry, because old
+    zealot is very slow and we need to give the other processes time
+    to rollback and release the lock.
+  * Test quilt single-debian-patch.
+  * Provide `tartree-edit gitfetchinfo' etc. to help with comparing
+    different test case git working tree tarballs.
+  * Test dgit-repos-policy-debian with multiple (identical, as it happens)
+    existing taints.
+  * Provide better log output for certain failures.
+  * Many new tests (especially for new functionality).
+  * Add missing debhelper (>=8) to test suite's global Depends.
+  * tstunt arrangements: Fix mishandling of PERLLIB, etc.
+  * tstunt-parsechangelog: Produce Timestamp field (like official one
+    does, now).
+  * Do not fail when git requires --allow-unrelated-histories.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sun, 16 Oct 2016 12:12:50 +0100
+
+dgit (1.4) unstable; urgency=high
+
+  Bugfixes:
+  * Unbreak --dry-run (`exiting subroutine via next', broken in
+    ac221d67, bug released in 0.22).
+  * When running git-add in commit-quilty-patch, properly escape
+    filenames (which git-add treats as glob patterns).
+  * When running git-add in commit-quilty-patch, use -f and sometimes -A,
+    so as to avoid being broken by any .gitignore, etc.
+  * When quilt linearisation fails, print the right information in
+    the error message.  (This has been broken forever.)
+  * Cope properly with `3.0 (quilt)' with single-debian-patch.
+    Closes:#796016.  (Still does not work with wheezy's dpkg-source, so
+    no test case yet.)
+  * With dgit sbuild, pass our -d before the user's arguments, so that
+    the user can override it.  Closes:#796019.
+
+  New checks and improved behaviours:
+  * Detect and reject git trees containing debian/source/local-options
+    or debian/source/local-patch-header.
+  * In --dry-run mode, _do_ actually run dpkg-source --commit so that we
+    actually do construct the quilt fixup commit; instead, honour
+    --dry-run by avoiding pulling it back to your HEAD.
+  * quilt-fixup checks that the git tree is clean, as for build-prep.
+
+  Documentation:
+  * In dgit(7), discuss binaries and documentation present in upstream but
+    removed by rules clean.
+
+  Test suite:
+  * Run quilt-fixup with -wgf in distropatches-reject,
+    so that we don't need build-depends.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sat, 22 Aug 2015 15:31:02 +0100
+
+dgit (1.3) unstable; urgency=high
+
+  Important bugfixes:
+  * In option parser test `@ARGV' not `length @ARGV'.  Closes:#795710.
+  * Properly quote package name when constructing regexp in
+    complete_file_from_dsc.  Closes:#795736.  Also, grep the code for
+    likely similar problems elsewhere and improve a (harmless) instance in
+    dgit-repos-server.
+
+  Other improvements:
+  * If a .orig in .. is a symlink, hardlink the link target into our
+    private unpack directory, rather than the link itself (since latter
+    won't work if the symlink is relative).  Closes:#795665.
+  * Test suite: Fix t-restriction-x-dgit-schroot-build in non-adt mode.
+  * Infrastructure: Improve an error message in dgit-repos-policy-debian.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sun, 16 Aug 2015 17:51:02 +0100
+
+dgit (1.2) unstable; urgency=high
+
+  Improvements:
+  * Honour *.clean-mode configuration setting for --clean= mode.
+  * No longer require option values to be cuddled: support `--opt val' and
+    `-o val'.  Closes:#763332.
+
+  Manpages:
+  * Fix typos.
+  * Document that tags are in DEP-14 format, and that they
+    are used for authenticating pushes.
+  * Correct cross-reference to point to browse.d.d.o.
+  * Move dgit.default.* to main CONFIGURATION section.
+
+  Administrivia:
+  * Add missing close of #793060 to changelog for version 1.1.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Fri, 14 Aug 2015 18:27:20 +0100
+
+dgit (1.1) unstable; urgency=medium
+
+  Bugfixes:
+  * When source package contains things called .git (even files, and even
+    in subdirectories), remove them.  Closes:#793671.
+  * Work around curl -sS -I printing `HTTP/1.0 200 Connection established'
+    before the actual header, so dgit works with https_proxy set (!)
+  * --new is needed for read access to packages in NEW, too.  Document
+    this, and make it work properly.
+  * Work around #793471 (madness with $SIG{__WARN__} and Perl's system
+    builtin): move $SIG{} setting into setup_sigwarn in Dgit.pm, and
+    check getppid.
+  * When invoking git-buildpackage via dgit gbp-build, consider our
+    command line arguments when massaging the dpkg-buildpackage arguments,
+    so that we don't end up giving dpkg-buildpackage contradictory
+    instructions.
+  * Cope with new git-buildpackage which provides gbp, rather than the
+    eponymous command, on PATH.
+
+  Configurability:
+  * Honour dgit-distros.DISTRO.cmd-CMD and .opts-CMD.  Closes:#793427.
+  * Make configuration able to prevent dpkg-mergechangelogs setup.
+  * Provide dgit setup-new-tree (like dpkg-setup-mergechangelogs
+    but only does it if not disabled in config).
+  * Set up git user.email and user.name from distro access config
+    or DEBEMAIL/DEBFULLNAME.  Closes:#793410.
+  * When key to use not specified any other way, use the debian/changelog
+    trailer line.  Closes:#793423.
+  * Honour --git= (mostly).
+
+  Documentation:
+  * Fix some manpage typos.  [ Richard Hartmann ]
+  * Manpage said that --clean=check was -wn but that is --clean=none;
+    correctly document that --clean=check is actually -wc.
+  * Document that up to -DDDD (not just -DD) is meaningfully different.
+  * Document that -cname=value applies only for this run.
+  * Improve manpage comment about defining a new distro.
+  * Document that --quilt=linear is the default for Debian.
+  * Fix a formatting problem in --build-products-dir= doc.
+  * In manpage, do not seem to imply that NMU should be of only one
+    new commit.
+  * Qualify to Debian the manpage comment about how to do NMU.
+  * In discussion on how to start using dgit when already using git, do
+    not imply/assume that existing git history will have identical trees
+    to dgit history.
+  * Remove stray sentence in config section of manpage.
+  * Manpage: Clarify wording of readonly config.
+  * Manpage: Better cross-references for -k and keyid.
+  * dgit(7): No longer say that dgit-repos lives on Alioth.
+
+  Improvements:
+  * Introduce more sophisticated protocol negotiation for rpush.
+  * Do not quote `:' in shellquote.
+  * Print a supplementary message when push fails, giving advice to
+    the user about how to retry.  Closes:#793144.
+  * Slurp in entire git config, for better performance.
+  * Rename `git-build' operation to `gbp-build' to make it clearer what
+    it's for.  Keep the old name as an alias.
+  * Show `dgit sbuild' in usage message.
+  * When we are using dpkg-buildpackage to clean before using it to also
+    do the build, let it do its cleaning thing as part of its run, rather
+    than running it twice.  When we are _not_ supposed to be using
+    dpkg-buildpackage to clean, but we are running it to do the build,
+    pass -nc.  Closes:#793060.
+  * Also suppress spurious runs of the clean target when building using
+    git-buildpackage.
+  * When exec fails, always print the program name in the error message.
+
+  Infrastructure:
+  * Infrastructure: Get mirroring right for fresh repos of existing
+    packages (!)
+
+  Packaging, cleanups, debugging and test suite:
+  * Fix Vcs-Git and Vcs-Browse to refer to chiark.  (The dgit-repos on
+    alioth aren't suitable right now because the master there can
+    currently only be updated with an actual upload, ie dgit push.)
+  * Make warnings fatal in dpkg-repos-admin-debian, dgit-ssh-dispatch
+    (using setup_sigwarn).
+  * tstunt/dpkg-parsechangelog: Make warnings fatal (directly).
+  * tstunt/dpkg-parsechangelog: Do not complain if PERLLIB is empty.
+  * Test suite: Honour DGIT_TEST_DEBUG=''.
+  * With -DDDD, print out all gitcfg references (copious!)
+  * Fix a debug message in the obsolete sshpsql archive access driver.
+  * Test suite: More automatic enumeration of tests.
+  * Test suite: Provide tests which check that all our various build
+    operations run the right targets as expected (ie, that we are massaging
+    the arguments to dpkg-buildpackage, and suppressing our clean target,
+    etc., correctly).
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Mon, 27 Jul 2015 16:34:31 +0100
+
+dgit (1.0) unstable; urgency=medium
+
+  Improvements:
+  * Switch to new production git repositories for reading.
+    (this can no longer divert to alioth).  Public readonly access
+    now works.  Closes:#791447.
+  * Memoise git config lookups (big speedup!)
+  * Provide -wdd aka --clean=dpkg-source-d.  Closes:#792433.
+  * Provide -wc aka --clean=check.
+
+  Manpage updates:
+  * Remove some obsolete caveats from BUGS.
+  * Reorganise and complete the configuration section.
+  * Remove obselete comment about DMs not being able to push.
+    We have, for now, a way to add keys manually.  Closes:#720173.
+
+  Access machinery:
+  * Remove configuration relating to alioth.
+  * Provide for different access mechanisms when pushing.
+  * Provide for configurable git url suffix.
+  * Allow git-url to be '' to force fallback to git-proto etc.
+  * Provide for checking git presence via http[s].
+  * Do some quoting on debug output (needed if the server might not
+    be trustworthy and might send us bad stuff).
+  * Talk to push.dgit.debian.org, rather than the .debian.net alias.
+
+  Infrastructure:
+  * Provide for mirroring git updates to a different server.
+  * Provide cgit-regen-config command for cgi-grnet-01.
+  * Make dgit-ssh-dispatch not spew (harmless) warnings if caller
+    tries for a shell session (ie SSH_ORIGINAL_COMMAND not set).
+
+  Cleanups:
+  * Remove an obsolete comment from the code.
+  * Improve an error message from dgit-repos-policy-debian.
+  * Test suite: Break out t-make-hook-link.
+  * Fix a manpage typo.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sun, 19 Jul 2015 22:15:53 +0100
+
+dgit (0.30) unstable; urgency=high
+
+  INCOMPATIBLE CHANGES:
+
+  * Client uses new infrastructure:
+    - Check for new dgit git service on dgit-git.debian.net (ie
+      gideon.debian.org), with transition plan based on diversion feature.
+      Closes:#720172.
+    - Old versions of dgit will stop working when the server-side handle is
+      pulled.
+
+  * dgit git trees no longer contain .pc for format `3.0 (quilt)' source
+    packages.  Closes:#764606.
+    - It is deleted whenever we find it.
+    - Older versions of dgit will choke on trees without .pc.
+    - (When doing quilt fixup, we recreate a suitable .pc in a temporary
+      directory so that we can do dpkg-source --comit.)
+
+  * All users are urged to upgrade ASAP.
+
+  Other significant improvements:
+
+  * When generating quilt patches, try to linearise the git history into a
+    series of individual new patches for debian/patches.  Closes:#770710.
+
+  * When receiving a push with dgit-repos-server, update the server's
+    refs/heads/master if we are pushing to what the distro regards as a
+    relevant branch, and the push would ff master.  Closes:#728209.
+
+  * For non-Debian distros, distro version release tags contain distro
+    name a la DEP-14 (rather than hardcoding `debian/').
+
+  * Set up a merge driver for debian/changelog.  Closes:#769291.
+
+  * --clean=git and --clean=none cause dgit to pass -nc to
+    dpkg-buildpackage, suppressing calls to the package's clean target.
+    Also, expand the documentation in this area slightly.  Closes:#768590.
+
+  * Provide --clean=git-ff (aka -wgf), which is useful for dgit itself (!)
+
+  Minor improvements:
+
+  * Reduce some noise output and improve the clarity of some messages.
+  * Be more careful about tag updates during fetch: only update tags
+    referring to uploads to distro we are trying to fetch from.
+  * Change realpath dependency to `coreutils (>= 8.23-1~) | realpath'
+    (Closes:#786955.)
+
+  Bugfixes:
+
+  * Fix handling of rmadison-based and gitless distros (e.g., Ubuntu).
+  * Add missing `gpgv' to test dependencies in debian/tests/control.
+  * Strip `-b <branch>' from contents of Vcs-Git header, when setting up
+    the vcs-git remote.  Closes:#759374.
+  * Do not offer wget as an alternative dependency to curl.  We always
+    unconditionally invoke curl and have no code to use wget.
+    Closes:#760805.
+  * Complain about lack of cuddled values for value-taking single-letter
+    options, rather than thinking the user meat an empty value.
+    Closes:#763332.
+  * Reject (rather than ignoring) further options merged witth -wn, -wg,
+    -wd.
+  * Fix inaccurate error message when archive's git hash is not an
+    ancestor of git repo's git hash.
+  * Detect and bomb out on vendor-specific `3.0 (quilt)' patch series.
+  * Fix the rules clean target to remove test results and output.
+
+  Documentation improvements:
+
+  * Break out dgit(7) from dgit(1).
+  * Provide example workflow for dgit rpush.  Closes:#763334.
+    (Also part of the fix for #768470.)
+  * Document that dgit repos are cloneable with git, in dgit(1)
+    section MODEL.  [Andreas Barth.]  Closes:#768470.
+  * Better documentation for quilt series handling.
+  * Document under `dgit push' that it is best to build with dgit too.
+    Closes:#763333.
+  * Other minor clarifications and improvements.
+
+  Behind-the-scenes work:
+
+  * Use ftpmasterapi archive query method.
+    (Closes:#727702.  Also partly obsoletes #768470.)
+  * New dgit-infrastructure binary package containing dgit-repos-server et
+    al.  Client users probably don't want this stuff.  Also, it provides a
+    convenient way to publish the dependencies.
+  * Many many bugfixes to the server side (dpkg-repos-server et al.).
+  * Add :..; prefix to ssh remote commands, for the benefit of future
+    forced command wrappers.  Implicitly, this defines a new ssh-based
+    command protocol.  Closes:#720174, #720175.
+  * Distro access configuration handling changes (should not be noticeable
+    to most users).
+  * In places, significant restructuring or tidying up.
+  * Turn all perl warnings into errors using $SIG{__WARN__}.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sun, 05 Jul 2015 01:34:55 +0100
+
+dgit (0.22.1) unstable; urgency=high
+
+  * Use Dpkg::Version::version_compare everywhere, not
+    Dpkg::Version::version_compare_string.  The latter is entirely wrong,
+    meaning that dgit would get many version comparisons wrong.
+    Closes:#768038.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Tue, 04 Nov 2014 12:46:40 +0000
+
+dgit (0.22) unstable; urgency=medium
+
+  Bugfixes:
+  * Clone removes destination directory on error.  Closes:#736153.
+  * Work with wheezy-backports (and keep squeeze-backports working too).
+    Closes:#736524.
+  * Work in read-only no-git-history mode with Ubuntu.  You still have
+    to pass -dubuntu.  Closes:#751781.
+  * Use mirror.ftp-master.debian.org DNS alias rather than coccia.
+    Closes:#752602.
+  * Check hashes of files ourselves rather than running dget to
+    re-retreive the .dsc.
+  * Check SHA-256 of .dsc against hash from archive_query (ie projectb)
+    rather than letting dpkg-source do a signature verification.
+    Closes:#737619.  Closes:#737625.
+  * Treat .dsc as bytes, just like everything else, rather than letting
+    HTTP::Message convert it to a Perl unicode string which the rest of
+    the program mishandles.  Closes:#738536.
+
+  Minor improvements:
+  * Include canonicalised suite name in signed tag message.
+  * Mention cross-version dgit rpush incompatibility in manpage.
+  * Check for rpush protocol version incompatibility and crash early
+    if incompatible.
+  * New script tests/using-intree for running tests on the source tree.
+  * Do not spew diff output to terminal (by default).  Print sensible
+    message instead.  Closes:#736526.
+  * Print better message for lack of configuration settings.
+  * Document that dgit rpush needs gnupg and your public key on the build
+    host.  Closes:#736529.
+  * Fix a manpage reference to `--dget=' where `--dgit=' was intended.
+  * Provide t-archive-process-incoming and t-archive-query subroutines for
+    regression test scripts to use.
+  * Print better message for unknown operations.
+  * Provide `dgit clean'.  Closes:#736527.
+  * When cloning, set up a remote `vcs-git' from the package's Vcs-Git
+    (and put an appropriate caveat in the manpage).  Closes:#740687.
+    Closes:#740721.
+  * Improve error message for .dsc having already been signed (iff
+    using libdpkg-perl 1.17.x).  Closes:#731635.
+  * Improve error message for .dsc parsing failures more generally.
+  * Better reporting of child exit statuses (esp. deaths due to signals).
+  * In rpush, on protocol error talking to build host, check if the
+    subprocess died and report differently if so.  Closes:#736528.
+  * Fixed a manpage typo.
+  * When tests invoke dgit, use --dgit= so that subprocesses use our
+    dgit rather than system one.
+  * Add a test for dgit rpush.
+
+  Major new feature, currently stalled awaiting server infrastructure:
+  * dgit-repos-server: New program for receiving signed-tag-based
+    pushes.  Corresponding support in dgit itself, but not currently
+    used by default for any distro.
+  * Bring forward push of the version tag ref so it happens alongside
+    the push of the suite branch ref.
+  * New git-check and git-create methods "true" which are no-ops.
+  * test-dummy-drs `distro': for testing dgit and dgit-repos-server.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Tue, 19 Aug 2014 11:24:02 +0100
+
+dgit (0.21) unstable; urgency=medium
+
+  Bugfixes relating to unclean trees:
+  * Run a clean (of the specified type) before any build operation; do
+    this with `dpkg-buildpackage -T' clean if necessary, so -wd now works
+    with all the building methods.
+  * Refuse to do quilt fixup (explicitly requested, or as a result of
+    build) if the tree contains ignored files.  Closes:#731632.
+
+  Error message improvements:
+  * Use failedcmd to report errors when ssh psql fails.  Closes:#734281.
+  * failedcmd prints $us, not $_[0] - ie, dgit doesn't pretend,
+    in the error message, to be its child.
+  * Do not report the (irrelevant) $? when madison parsing fails.
+
+  Better workflow flexibility:
+  * Provide --build-products-dir option (and corresponding semantics
+    for -C) to specify where to find the files to upload.  Closes:#731633.
+
+  Support for Debian backports suites:
+  * New quirks infrastructure in configuration and internals,
+    for suites (or perhaps distros) which are a bit like others.
+  * Use correct default archive location.
+  * Compute "-v" option default value correctly.
+  * Closes:#733954.
+
+  Packaging improvement:
+  * Add `Testsuite: autopkgtest' to debian/control.  (This will only have
+    the right effect with recent enought dpkg; it will generate a harmless
+    warning with earlier versions of dpkg.)
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sun, 19 Jan 2014 02:14:25 +0000
+
+dgit (0.20) unstable; urgency=high
+
+  * Use newest (not oldest) version currently in suite when calculating
+    what value to use for -v<version> by default.  Closes:#732781.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sat, 21 Dec 2013 19:13:56 +0000
+
+dgit (0.19) unstable; urgency=low
+
+  Testing facilities:
+  * Provide "test-dummy" distro with "dummycat" access method.
+  * Provide a selection of autopkgtest (DEP-8) tests.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Wed, 27 Nov 2013 18:27:17 +0000
+
+dgit (0.18.2) unstable; urgency=high
+
+  Bump archive upload urgency to high.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sun, 24 Nov 2013 17:42:57 +0000
+dgit (0.18.1) unstable; urgency=low
+
+  Bugfixes:
+  * sshpsql archive query method passes LANG=C.  Closes:#729788.
+  * Subcommand program or argument options containing hyphens work.
+    (Eg, --dpkg-buildpackage:blah was previously incorrectly rejected.)
+
+  Packaging fixes:
+  * Depend on dput.
+  * Depend on curl | wget, as dget needs one of those.  (The dput package,
+    which contains dget, doesn't require them because dput itself works
+    without.)
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sun, 24 Nov 2013 17:36:03 +0000
+
+dgit (0.18) unstable; urgency=low
+
+  Major new feature:
+  * Remote push (dgit rpush), a la debsign -r.  Closes:#721185.
+
+  Improved behaviours:
+  * dgit build, by default, uses the archive to find out what the correct
+    -v<version> option is to pass to dpkg-genchanges.  Closes:#727200.
+  * Abolish the sshdakls method and replace it with sshpsql: that is, ssh
+    (to coccia, by default) and run sql commands on the ftpmaster
+    database.  This is faster and has fewer bugs but is vulnerable to db
+    schema changes.  Closes:#726955.  Closes:#720170.  Closes:#720176.
+  * When generating git tags, quote the (uncanonicalised) changelog's
+    Distribution field as the suite.
+  * Command execution reports from --dry-run go to stderr.
+
+  Other new features:
+  * Support --gpg=... to provide a replacement command for gpg.
+  * Support --ssh=... and --ssh:... to affect how we run ssh.
+
+  Bugfixes:
+  * When using sbuild, pass the arguments to mergechanges in the right
+    order so that we use the correct Description (the _source one,
+    not the one from sbuild which didn't get e.g. -v<version>).
+  * push actually takes an optional suite, like it says in the synopsis.
+    Closes:#727125.
+  * Fix dgit --damp-run sbuild to actually work.
+  * Fix the "shellquote" internal subroutine.  The bugs in it ought not to
+    have caused any real trouble in previous versions of dgit.
+
+  Documentation and message fixes:
+  * manpage: Clarify comments about orig tarballs.  Closes: #723605.
+  * manpage: Remove comment in BUGS about lack of policy docs
+    for Dgit field, which is specified now.  Closes:#720201.
+  * manpage: Make discussion of --existing-package less scary.  The
+    default archive access method no longer needs it.  Closes:#720171.
+  * Mention "git merge", not "git-merge", in helpful message.
+    Closes:#725632.
+
+  Internal and debugging improvements:
+  * Report chdir actions in debugging output.
+  * Improvements to implementation of --dry-run and --damp-run.
+  * Some code motion and cleanups.
+
+  Note: changelog entries for the following versions, which were uploaded
+  to Debian experimental, have been collapsed into this single entry:
+    0.18~experimental2 0.18~experimental1
+    0.17~experimental7 0.17~experimental6 0.17~experimental5
+    0.17~experimental4 0.17~experimental3 0.17~experimental2
+    0.17~experimental1
+    0.16~experimental3 0.16~experimental2 0.16~experimental1
+  We do describe here all the changes since 0.17.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sat, 09 Nov 2013 10:12:13 +0000
+
+dgit (0.17) unstable; urgency=high
+
+  * Do not grobble around in .git/refs/; instead, use git-show-ref.
+    This avoids breaking when git makes packed refs.  Closes:728893.
+  * Clarify error message for missing refs/remotes/dgit/dgit/<suite>.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Thu, 07 Nov 2013 00:02:47 +0000
+
+dgit (0.16) unstable; urgency=high
+
+  * Format `(3.0) quilt' fixup does not mind extraneous other files
+    in the build tree (e.g., build products and logs).  Closes: #727053.
+  * Set autoflush on stdout, to get better ordering of debugging
+    etc. output when stdout is redirected.
+  * New --damp-run mode, for more convenient and fuller testing etc.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Tue, 22 Oct 2013 13:06:54 +0100
+
+dgit (0.15) unstable; urgency=low
+
+  * Better handling of packages pushed using dgit and stuck in NEW.
+    (And, use of `--new' is not needed with fetch.)  Closes: #722199.
+  * More comprehensive warnings in many cases of archive skew.
+  * Implement `dgit help' as well as `--help'.  Closes: #721661.
+  * Provide `dgit version' and `--version'.  Closes: #721654.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Thu, 12 Sep 2013 00:14:05 +0100
+
+dgit (0.14) unstable; urgency=low
+
+  * Include package name in tag message.
+  * Create directory .git/dgit when needed during build.  Closes: #721428.
+  * Add Vcs-Git and Vcs-Browser [Richard Hartmann].  Closes: #721404.
+    These fields refer to the development branch, "master", on alioth,
+    not to the dgit suite refs (which are not accessible to git clone).
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sun, 01 Sep 2013 18:30:44 +0100
+
+dgit (0.13) unstable; urgency=low
+
+  * Reuse already-downloaded .orig files after checking their hashes.
+    Closes: #720526.  (This introduces a dependency on Digest::SHA.)
+  * Do not always pointlessly fetch the .dsc twice.  (That code was
+    erroneously duplicated during editing, apparently.)
+  * Remove DGET_UNPACK from the environment in case the user has set it.
+  * Remove scary warning from Description.
+  * When uploading to Debian, tell dput to upload to "ftp-master".  This
+    avoids problems with derivatives whose dput has a different default.
+    Closes: #720958.
+  * Fix some bugs in dgit fetch --dry-run which made dgit push
+    --dry-run often not work at all.
+  * Update the local tracking branch for the dgit remote, when pushing.
+    Closes: #720956.
+  * Fix references in manpage to old Vcs-Dgit-Master field name.
+  * Reorganise manpage sections to be in a more conventional order.
+  * New manpage section on FILES IN THE SOURCE PACKAGE BUT NOT IN GIT.
+    Closes: #721186.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Thu, 29 Aug 2013 00:27:23 +0100
+
+dgit (0.12) unstable; urgency=low
+
+  * Cope with packages with epoch.  Closes: #720897.
+  * Improve error message for non-fast-forward push.  Closes: #720896.
+  * New --ignore-dirty option to skip noncritical check.  Closes: #720895.
+  * New --no-quilt-fixup option to suppress quilt fixup.  RTFM.
+  * Add Closes line for #720595 to changelog entry for 0.11.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Mon, 26 Aug 2013 16:50:39 +0100
+
+dgit (0.11) unstable; urgency=low
+
+  * Location of dgit-repos is now git.debian.org:/git/dgit-repos/repos.
+    Closes: #720525.  The rename on the server side will break older
+    versions of dgit.
+  * Fix bug which would make quilt patch fixup fail if git status
+    produced "M" lines.
+  * Autogenerated quilt patch fixup patch Description contains several
+    recent git commits, rather than implying that the patch corresponds
+    exactly to the top git commit.
+  * Use "ftp.debian.org" not "http.debian.net" as the default Debian
+    archive.  (http.debian.net tends to defeat certain kinds of cacheing,
+    and can also have more skew.)
+  * dgit build uses dpkg-buildpackage; there is a dgit git-build
+    for using git-buildpackage.  Closes: #720595.
+  * Better error message for use of UNRELEASED suite.  Closes: #720523.
+  * Do not canonicalise suite more than once.  Related to: #720526.
+  * Fix a badly open-coded copy of check_not_dirty.  Closes: #720524.
+  * Fix some bugs in building (eg build-source would fail to do the quilt
+    fixup; the --clean check in build was wrong).
+  * Add missing dependency on realpath.
+  * git-build (git-buildpackage wrapper) does not bother canonicalising
+    the suite if --git-ignore-branch is used.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sun, 25 Aug 2013 17:00:43 +0100
+
+dgit (0.10) unstable; urgency=low
+
+  * Create .pc/applied-patches - do not empty it (!)
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sun, 25 Aug 2013 00:51:50 +0100
+
+dgit (0.9) unstable; urgency=low
+
+  * New cleaning arrangements.
+  * More comprehensive workaround for `3.0 (quilt)'.
+  * In push, double-check the .changes against the changelog.
+  * Better error when source package contains .git.  Closes: #720555.
+  * Change our .dsc field name to `Dgit'.  Relevant to #720201.
+  * Fix bug handling our synthetic merges when we see them in
+    the remote suite branch.
+  * `3.0 (quilt)' fixup creates .pc/applied-patches since modern
+    dpkg-source creates it even though old ones didn't always.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sat, 24 Aug 2013 18:49:02 +0100
+
+dgit (0.8) unstable; urgency=low
+
+  * Fix comparison of archive's .dsc's hash and git branch head
+    to DTRT.
+  * When creating repos in dgit-repos (using the ssh-cmd method),
+    copy _template rather than using mkdir and git init.
+    Closes: #720522.
+  * In push, do git fetch as well as archive fetch, or archive
+    fetch can fail.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Fri, 23 Aug 2013 12:24:09 +0100
+
+dgit (0.7) unstable; urgency=low
+
+  * If dak ls, or rmadison, reports multiple versions, look for them
+    all, and pick the newest .dsc that doesn't give 404.
+  * Manpage formatting fix.
+  * Name the local remote tracking branch remotes/dgit/dgit/<suite>
+    so that we avoid a warning from git about ambiguous branch names.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Thu, 22 Aug 2013 18:29:10 +0100
+
+dgit (0.6) unstable; urgency=low
+
+  * Allow fetching when archive has out-of-date git hash in .dsc.
+    Closes: #720490.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Thu, 22 Aug 2013 16:02:10 +0100
+
+dgit (0.5) unstable; urgency=low
+
+  * Upload to unstable, as this version mostly works.  (All the RC
+    bugs of which I'm aware are now properly represented in the BTS.)
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Thu, 22 Aug 2013 15:38:00 +0100
+
+dgit (0.4~pre2) experimental; urgency=low
+
+  * Mangle debian/<version> tags the way git-buildpackage does
+    (as of git-buildpackage 0.5.5, 3c6bbd0f4992f8da).
+  * Support dgit-distro.<distro>.keyid config option.
+  * Revert change to ssh to alioth CNAME, as the recommended CNAME
+    is to something with no write access to the fs and the new CNAME
+    has not yet been set up.  This reintroduces #720172 :-/.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Thu, 22 Aug 2013 15:31:17 +0100
+
+dgit (0.4~pre1) experimental; urgency=low
+
+  * Use dgit.debian.net vhost on alioth.  Closes:#720172.
+  * Usage message.  Closes:#720085.
+  * Provide "dgit sbuild".
+  * Assorted manpage fixes and improvements.
+  * Fail if a required config item is missing.
+  * Much better error messages.
+  * Better error checking when parsing RFC822-style control data.
+  * Better checking that the supplied .dsc and debian/changes correspond.
+  * Ordering improvement in push: don't add dsc field until git push done.
+  * New --distro option (helps with unknown suites).
+  * Bugfixes.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Thu, 22 Aug 2013 13:36:44 +0100
+
+dgit (0.3) experimental; urgency=low
+
+  * New version which appears to be able to sort of work at least
+    some of the time.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Sat, 17 Aug 2013 09:18:04 +0100
+
+dgit (0.2) experimental; urgency=low
+
+  * New version which might actually work but probably won't.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Fri, 16 Aug 2013 16:52:17 +0100
+
+dgit (0.1) experimental; urgency=low
+
+  * Initial experimental (partial) version.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Thu, 15 Aug 2013 12:09:01 +0100
+
diff --git a/debian/compat b/debian/compat
new file mode 100644 (file)
index 0000000..ec63514
--- /dev/null
@@ -0,0 +1 @@
+9
diff --git a/debian/control b/debian/control
new file mode 100644 (file)
index 0000000..9402fd2
--- /dev/null
@@ -0,0 +1,39 @@
+Source: dgit
+Section: devel
+Priority: optional
+Maintainer: Ian Jackson <ijackson@chiark.greenend.org.uk>
+Standards-Version: 3.9.4.0
+Build-Depends: debhelper (>= 9)
+Testsuite: autopkgtest
+Vcs-Git: git://git.chiark.greenend.org.uk/~ianmdlvl/dgit.git
+Vcs-Browser: http://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git/dgit.git/
+
+Package: dgit
+Depends: perl, libwww-perl, libdpkg-perl, git-core, devscripts, dpkg-dev,
+         ${misc:Depends}, git-buildpackage, liblist-moreutils-perl,
+         coreutils (>= 8.23-1~) | realpath,
+         libdigest-sha-perl, dput, curl, apt,
+         libjson-perl, ca-certificates,
+         libtext-iconv-perl, libtext-glob-perl
+Recommends: ssh-client, libtext-iconv-perl
+Suggests: sbuild
+Architecture: all
+Description: git interoperability with the Debian archive
+ dgit (with the associated infrastructure) makes it possible to
+ treat the Debian archive as a git repository.
+ .
+ dgit push constructs uploads from git commits
+ .
+ dgit clone and dgit fetch construct git commits from uploads.
+
+Package: dgit-infrastructure
+Depends: ${misc:Depends}, perl, git-core, gpgv, chiark-utils-bin,
+         libjson-perl, libdigest-sha-perl, libdbd-sqlite3-perl, sqlite3,
+         libwww-perl, libdpkg-perl
+Recommends: dgit
+Architecture: all
+Priority: extra
+Description: dgit server backend infrastructure
+ This package contains tools which are useful for setting up a dgit
+ git repository server.  You probably want dgit, the client package,
+ instead of dgit-infrastructure.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644 (file)
index 0000000..a5b8245
--- /dev/null
@@ -0,0 +1,72 @@
+dgit
+Integration between git and Debian-style archives
+
+Copyright (C)2013-2016 Ian Jackson
+Copyright (C)2016 Sean Whitton
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+A copy of the GNU General Public License v3 can be found in
+/usr/share/common-licenses/GPL-3.
+
+
+The tests/ directory contains a complete copy of the source code for
+the pari-extra 3-1 package.  This is a dummy package containing only
+Debian metadata, by Bill Alombert, with a licence statement saying
+it's GPL (implicitly GPLv3 compatible).
+
+
+Contributions are accepted upstram under the same terms; please sign
+off your patches (by writing an approprite Signed-Off-By tag in your
+commit message or patch submission) to indicate your attestation that
+the Developer Certificate of Origin (version 1.1) applies.
+
+
+-8<-
+
+Developer Certificate of Origin
+Version 1.1
+
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+1 Letterman Drive
+Suite D4700
+San Francisco, CA, 94129
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+    have the right to submit it under the open source license
+    indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+    of my knowledge, is covered under an appropriate open source
+    license and I have the right under that license to submit that
+    work with modifications, whether created in whole or in part
+    by me, under the same open source license (unless I am
+    permitted to submit under a different license), as indicated
+    in the file; or
+
+(c) The contribution was provided directly to me by some other
+    person who certified (a), (b) or (c) and I have not modified
+    it.
+
+(d) I understand and agree that this project and the contribution
+    are public and that a record of the contribution (including all
+    personal information I submit with it, including my sign-off) is
+    maintained indefinitely and may be redistributed consistent with
+    this project or the open source license(s) involved.
+
diff --git a/debian/rules b/debian/rules
new file mode 100755 (executable)
index 0000000..9249f88
--- /dev/null
@@ -0,0 +1,59 @@
+#!/usr/bin/make -f
+
+# dgit
+# Integration between git and Debian-style archives
+#
+# Copyright (C)2013-2016 Ian Jackson
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+export prefix=/usr
+
+%:
+       dh $@
+
+override_dh_gencontrol:
+       dh_gencontrol
+       set -e; \
+        cd debian/dgit; \
+        v=$$(perl -ne 'print if s/^version:\s+//i' DEBIAN/control); \
+        perl -i -pe "s/UNRELEASED/$$v/g if m/###substituted###/" usr/bin/dgit
+
+globalperl=/usr/share/perl5
+infraperl=/usr/share/dgit/infra/perl5
+
+override_dh_auto_install:
+       make install prefix=/usr DESTDIR=debian/dgit
+       make install-infra prefix=/usr DESTDIR=debian/dgit-infrastructure \
+               perldir=$(infraperl)
+#      # Most of the Perl modules in dgit-infrastructure live in
+#      # $(infraperl).  The exception is Debian::Dgit::Infra, which
+#      # lives in $(globalperl) and adds $(infraperl) to @INC.
+       set -ex; \
+        base=debian/dgit-infrastructure; \
+        mod=Debian/Dgit/Infra.pm; \
+        src=$${base}$(infraperl)/$${mod}; \
+        dst=$${base}$(globalperl)/$${mod}; \
+        mkdir -p $${dst%/*}; \
+        mv -f $$src $$dst; \
+        perl -i -p -e 'next unless m/###substituted###/;' \
+               -e 'next unless s/^# (?=unshift \@INC,)//;' \
+               -e 'die unless s{q\{\S+\}}{q{$(infraperl)}};' \
+                $$dst
+
+debian/tests/control: tests/enumerate-tests debian/tests/control.in
+       $< gencontrol >$@.new && mv -f $@.new $@
+
+debian/tests/control: tests/lib-core tests/lib-restricts
+debian/tests/control: tests/tests $(wildcard tests/tests/*[^~#])
diff --git a/debian/tests/control b/debian/tests/control
new file mode 100644 (file)
index 0000000..7bd2a13
--- /dev/null
@@ -0,0 +1,31 @@
+Tests: build-modes-gbp
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, git-buildpackage
+
+Tests: clone-reprepro
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, reprepro
+
+Tests: dsd-clone-drs
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential
+Restrictions: x-dgit-intree-only x-dgit-git-only
+
+Tests: mirror mirror-debnewgit mirror-private
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, rsync
+
+Tests: build-modes-sbuild quilt-gbp-build-modes-sbuild
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, sbuild
+Restrictions: x-dgit-schroot-build
+
+Tests: spelling
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential
+Restrictions: x-dgit-git-only
+
+Tests: absurd-gitapply build-modes build-modes-asplit build-modes-gbp-asplit clone-clogsigpipe clone-gitnosuite clone-nogit debpolicy-dbretry debpolicy-newreject debpolicy-quilt-gbp distropatches-reject drs-clone-nogit drs-push-masterupdate drs-push-rejects dsd-clone-nogit dsd-divert fetch-localgitonly fetch-somegit-notlast gbp-orig gitconfig import-dsc import-native import-nonnative import-tarbomb inarchivecopy mismatches-contents mismatches-dscchanges multisuite newtag-clone-nogit oldnewtagalt oldtag-clone-nogit orig-include-exclude orig-include-exclude-chkquery overwrite-chkclog overwrite-junk overwrite-splitbrains overwrite-version push-buildproductsdir push-newpackage push-nextdgit quilt quilt-gbp quilt-gbp-build-modes quilt-singlepatch quilt-splitbrains rpush tag-updates test-list-uptodate trustingpolicy-replay unrepresentable version-opt
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential
+
diff --git a/debian/tests/control.in b/debian/tests/control.in
new file mode 100644 (file)
index 0000000..c032ba8
--- /dev/null
@@ -0,0 +1,2 @@
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential
diff --git a/dgit b/dgit
new file mode 100755 (executable)
index 0000000..9d3584f
--- /dev/null
+++ b/dgit
@@ -0,0 +1,6339 @@
+#!/usr/bin/perl -w
+# dgit
+# Integration between git and Debian-style archives
+#
+# Copyright (C)2013-2016 Ian Jackson
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+use strict;
+
+use Debian::Dgit;
+setup_sigwarn();
+
+use IO::Handle;
+use Data::Dumper;
+use LWP::UserAgent;
+use Dpkg::Control::Hash;
+use File::Path;
+use File::Temp qw(tempdir);
+use File::Basename;
+use Dpkg::Version;
+use POSIX;
+use IPC::Open2;
+use Digest::SHA;
+use Digest::MD5;
+use List::Util qw(any);
+use List::MoreUtils qw(pairwise);
+use Text::Glob qw(match_glob);
+use Fcntl qw(:DEFAULT :flock);
+use Carp;
+
+use Debian::Dgit;
+
+our $our_version = 'UNRELEASED'; ###substituted###
+our $absurdity = undef; ###substituted###
+
+our @rpushprotovsn_support = qw(4 3 2); # 4 is new tag format
+our $protovsn;
+
+our $isuite = 'unstable';
+our $idistro;
+our $package;
+our @ropts;
+
+our $sign = 1;
+our $dryrun_level = 0;
+our $changesfile;
+our $buildproductsdir = '..';
+our $new_package = 0;
+our $ignoredirty = 0;
+our $rmonerror = 1;
+our @deliberatelies;
+our %previously;
+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|dpm|unapplied';
+our $dodep14tag;
+our $dodep14tag_re = 'want|no|always';
+our $split_brain_save;
+our $we_are_responder;
+our $initiator_tempdir;
+our $patches_applied_dirtily = 00;
+our $tagformat_want;
+our $tagformat;
+our $tagformatfn;
+
+our %forceopts = map { $_=>0 }
+    qw(unrepresentable unsupported-source-format
+       dsc-changes-mismatch changes-origs-exactly
+       import-gitapply-absurd
+       import-gitapply-no-absurd
+       import-dsc-with-dgit-field);
+
+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';
+
+our (@git) = qw(git);
+our (@dget) = qw(dget);
+our (@curl) = qw(curl);
+our (@dput) = qw(dput);
+our (@debsign) = qw(debsign);
+our (@gpg) = qw(gpg);
+our (@sbuild) = qw(sbuild);
+our (@ssh) = 'ssh';
+our (@dgit) = qw(dgit);
+our (@aptget) = qw(apt-get);
+our (@aptcache) = qw(apt-cache);
+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_build) = ('');
+our (@gbp_pq) = ('gbp pq');
+our (@changesopts) = ('');
+
+our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
+                    'curl' => \@curl,
+                    'dput' => \@dput,
+                    'debsign' => \@debsign,
+                     'gpg' => \@gpg,
+                     'sbuild' => \@sbuild,
+                     'ssh' => \@ssh,
+                     'dgit' => \@dgit,
+                     'git' => \@git,
+                     'apt-get' => \@aptget,
+                     'apt-cache' => \@aptcache,
+                     'dpkg-source' => \@dpkgsource,
+                     'dpkg-buildpackage' => \@dpkgbuildpackage,
+                     'dpkg-genchanges' => \@dpkggenchanges,
+                     'gbp-build' => \@gbp_build,
+                     'gbp-pq' => \@gbp_pq,
+                     'ch' => \@changesopts,
+                     'mergechanges' => \@mergechanges);
+
+our %opts_opt_cmdonly = ('gpg' => 1, 'git' => 1);
+our %opts_cfg_insertpos = map {
+    $_,
+    scalar @{ $opts_opt_map{$_} }
+} keys %opts_opt_map;
+
+sub finalise_opts_opts();
+
+our $keyid;
+
+autoflush STDOUT 1;
+
+our $supplementary_message = '';
+our $need_split_build_invocation = 0;
+our $split_brain = 0;
+
+END {
+    local ($@, $?);
+    print STDERR "! $_\n" foreach $supplementary_message =~ m/^.+$/mg;
+}
+
+our $remotename = 'dgit';
+our @ourdscfield = qw(Dgit Vcs-Dgit-Master);
+our $csuite;
+our $instead_distro;
+
+if (!defined $absurdity) {
+    $absurdity = $0;
+    $absurdity =~ s{/[^/]+$}{/absurd} or die;
+}
+
+sub debiantag ($$) {
+    my ($v,$distro) = @_;
+    return $tagformatfn->($v, $distro);
+}
+
+sub debiantag_maintview ($$) { 
+    my ($v,$distro) = @_;
+    return "$distro/".dep14_version_mangle $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(); }
+sub lrref () { return "refs/remotes/$remotename/".server_branch($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) = @_;
+    $vsn =~ s/^\d+\://;
+    return $vsn;
+}
+
+sub srcfn ($$) {
+    my ($vsn,$sfx) = @_;
+    return "${package}_".(stripepoch $vsn).$sfx
+}
+
+sub dscfn ($) {
+    my ($vsn) = @_;
+    return srcfn($vsn,".dsc");
+}
+
+sub changespat ($;$) {
+    my ($vsn, $arch) = @_;
+    return "${package}_".(stripepoch $vsn)."_".($arch//'*').".changes";
+}
+
+sub upstreamversion ($) {
+    my ($vsn) = @_;
+    $vsn =~ s/-[^-]+$//;
+    return $vsn;
+}
+
+our $us = 'dgit';
+initdebug('');
+
+our @end;
+END { 
+    local ($?);
+    foreach my $f (@end) {
+       eval { $f->(); };
+       print STDERR "$us: cleanup: $@" if length $@;
+    }
+};
+
+sub badcfg { print STDERR "$us: invalid configuration: @_\n"; exit 12; }
+
+sub forceable_fail ($$) {
+    my ($forceoptsl, $msg) = @_;
+    fail $msg unless grep { $forceopts{$_} } @$forceoptsl;
+    print STDERR "warning: overriding problem due to --force:\n". $msg;
+}
+
+sub forceing ($) {
+    my ($forceoptsl) = @_;
+    my @got = grep { $forceopts{$_} } @$forceoptsl;
+    return 0 unless @got;
+    print STDERR
+ "warning: skipping checks or functionality due to --force-$got[0]\n";
+}
+
+sub no_such_package () {
+    print STDERR "$us: package $package does not exist in suite $isuite\n";
+    exit 4;
+}
+
+sub changedir ($) {
+    my ($newdir) = @_;
+    printdebug "CD $newdir\n";
+    chdir $newdir or confess "chdir: $newdir: $!";
+}
+
+sub deliberately ($) {
+    my ($enquiry) = @_;
+    return !!grep { $_ eq "--deliberately-$enquiry" } @deliberatelies;
+}
+
+sub deliberately_not_fast_forward () {
+    foreach (qw(not-fast-forward fresh-repo)) {
+       return 1 if deliberately($_) || deliberately("TEST-dgit-only-$_");
+    }
+}
+
+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:
+#  $ dgit remote-push-build-host <n-rargs> <rargs>... <push-args>...
+#  where <rargs> is <push-host-dir> <supported-proto-vsn>,... ...
+#  < dgit-remote-push-ready <actual-proto-vsn>
+#
+# 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
+#  > [NBYTES bytes of data (no newline)]
+#  [maybe some more blocks]
+#  > data-end
+#
+#  > file dsc
+#  [etc]
+#
+#  > file changes
+#  [etc]
+#
+#  > 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]
+#  < data-block NBYTES
+#  < [NBYTES bytes of data (no newline)]
+#  [maybe some more blocks]
+#  < data-end
+#  < files-end
+#
+#  > want signed-dsc-changes
+#  < data-block NBYTES    [transfer of signed dsc]
+#  [etc]
+#  < data-block NBYTES    [transfer of signed changes]
+#  [etc]
+#  < files-end
+#
+#  > complete
+
+our $i_child_pid;
+
+sub i_child_report () {
+    # Sees if our child has died, and reap it if so.  Returns a string
+    # describing how it died if it failed, or undef otherwise.
+    return undef unless $i_child_pid;
+    my $got = waitpid $i_child_pid, WNOHANG;
+    return undef if $got <= 0;
+    die unless $got == $i_child_pid;
+    $i_child_pid = undef;
+    return undef unless $?;
+    return "build host child ".waitstatusmsg();
+}
+
+sub badproto ($$) {
+    my ($fh, $m) = @_;
+    fail "connection lost: $!" if $fh->error;
+    fail "protocol violation; $m not expected";
+}
+
+sub badproto_badread ($$) {
+    my ($fh, $wh) = @_;
+    fail "connection lost: $!" if $!;
+    my $report = i_child_report();
+    fail $report if defined $report;
+    badproto $fh, "eof (reading $wh)";
+}
+
+sub protocol_expect (&$) {
+    my ($match, $fh) = @_;
+    local $_;
+    $_ = <$fh>;
+    defined && chomp or badproto_badread $fh, "protocol message";
+    if (wantarray) {
+       my @r = &$match;
+       return @r if @r;
+    } else {
+       my $r = &$match;
+       return $r if $r;
+    }
+    badproto $fh, "\`$_'";
+}
+
+sub protocol_send_file ($$) {
+    my ($fh, $ourfn) = @_;
+    open PF, "<", $ourfn or die "$ourfn: $!";
+    for (;;) {
+       my $d;
+       my $got = read PF, $d, 65536;
+       die "$ourfn: $!" unless defined $got;
+       last if !$got;
+       print $fh "data-block ".length($d)."\n" or die $!;
+       print $fh $d or die $!;
+    }
+    PF->error and die "$ourfn $!";
+    print $fh "data-end\n" or die $!;
+    close PF;
+}
+
+sub protocol_read_bytes ($$) {
+    my ($fh, $nbytes) = @_;
+    $nbytes =~ m/^[1-9]\d{0,5}$|^0$/ or badproto \*RO, "bad byte count";
+    my $d;
+    my $got = read $fh, $d, $nbytes;
+    $got==$nbytes or badproto_badread $fh, "data block";
+    return $d;
+}
+
+sub protocol_receive_file ($$) {
+    my ($fh, $ourfn) = @_;
+    printdebug "() $ourfn\n";
+    open PF, ">", $ourfn or die "$ourfn: $!";
+    for (;;) {
+       my ($y,$l) = protocol_expect {
+           m/^data-block (.*)$/ ? (1,$1) :
+           m/^data-end$/ ? (0,) :
+           ();
+       } $fh;
+       last unless $y;
+       my $d = protocol_read_bytes $fh, $l;
+       print PF $d or die $!;
+    }
+    close PF or die $!;
+}
+
+#---------- remote protocol support, responder ----------
+
+sub responder_send_command ($) {
+    my ($command) = @_;
+    return unless $we_are_responder;
+    # called even without $we_are_responder
+    printdebug ">> $command\n";
+    print PO $command, "\n" or die $!;
+}    
+
+sub responder_send_file ($$) {
+    my ($keyword, $ourfn) = @_;
+    return unless $we_are_responder;
+    printdebug "]] $keyword $ourfn\n";
+    responder_send_command "file $keyword";
+    protocol_send_file \*PO, $ourfn;
+}
+
+sub responder_receive_files ($@) {
+    my ($keyword, @ourfns) = @_;
+    die unless $we_are_responder;
+    printdebug "[[ $keyword @ourfns\n";
+    responder_send_command "want $keyword";
+    foreach my $fn (@ourfns) {
+       protocol_receive_file \*PI, $fn;
+    }
+    printdebug "[[\$\n";
+    protocol_expect { m/^files-end$/ } \*PI;
+}
+
+#---------- remote protocol support, initiator ----------
+
+sub initiator_expect (&) {
+    my ($match) = @_;
+    protocol_expect { &$match } \*RO;
+}
+
+#---------- end remote code ----------
+
+sub progress {
+    if ($we_are_responder) {
+       my $m = join '', @_;
+       responder_send_command "progress ".length($m) or die $!;
+       print PO $m or die $!;
+    } else {
+       print @_, "\n";
+    }
+}
+
+our $ua;
+
+sub url_get {
+    if (!$ua) {
+       $ua = LWP::UserAgent->new();
+       $ua->env_proxy;
+    }
+    my $what = $_[$#_];
+    progress "downloading $what...";
+    my $r = $ua->get(@_) or die $!;
+    return undef if $r->code == 404;
+    $r->is_success or fail "failed to fetch $what: ".$r->status_line;
+    return $r->decoded_content(charset => 'none');
+}
+
+our ($dscdata,$dscurl,$dsc,$dsc_checked,$skew_warning_vsn);
+
+sub runcmd {
+    debugcmd "+",@_;
+    $!=0; $?=-1;
+    failedcmd @_ if system @_;
+}
+
+sub act_local () { return $dryrun_level <= 1; }
+sub act_scary () { return !$dryrun_level; }
+
+sub printdone {
+    if (!$dryrun_level) {
+       progress "$us ok: @_";
+    } else {
+       progress "would be ok: @_ (but dry run only)";
+    }
+}
+
+sub dryrun_report {
+    printcmd(\*STDERR,$debugprefix."#",@_);
+}
+
+sub runcmd_ordryrun {
+    if (act_scary()) {
+       runcmd @_;
+    } else {
+       dryrun_report @_;
+    }
+}
+
+sub runcmd_ordryrun_local {
+    if (act_local()) {
+       runcmd @_;
+    } else {
+       dryrun_report @_;
+    }
+}
+
+sub shell_cmd {
+    my ($first_shell, @cmd) = @_;
+    return qw(sh -ec), $first_shell.'; exec "$@"', 'x', @cmd;
+}
+
+our $helpmsg = <<END;
+main usages:
+  dgit [dgit-opts] clone [dgit-opts] package [suite] [./dir|/dir]
+  dgit [dgit-opts] fetch|pull [dgit-opts] [suite]
+  dgit [dgit-opts] build [dpkg-buildpackage-opts]
+  dgit [dgit-opts] sbuild [sbuild-opts]
+  dgit [dgit-opts] push [dgit-opts] [suite]
+  dgit [dgit-opts] rpush build-host:build-dir ...
+important dgit options:
+  -k<keyid>           sign tag and package with <keyid> instead of default
+  --dry-run -n        do not change anything, but go through the motions
+  --damp-run -L       like --dry-run but make local changes, without signing
+  --new -N            allow introducing a new package
+  --debug -D          increase debug level
+  -c<name>=<value>    set git config option (used directly by dgit too)
+END
+
+our $later_warning_msg = <<END;
+Perhaps the upload is stuck in incoming.  Using the version from git.
+END
+
+sub badusage {
+    print STDERR "$us: @_\n", $helpmsg or die $!;
+    exit 8;
+}
+
+sub nextarg {
+    @ARGV or badusage "too few arguments";
+    return scalar shift @ARGV;
+}
+
+sub cmd_help () {
+    print $helpmsg or die $!;
+    exit 0;
+}
+
+our $td = $ENV{DGIT_TEST_DUMMY_DIR} || "DGIT_TEST_DUMMY_DIR-unset";
+
+our %defcfg = ('dgit.default.distro' => 'debian',
+              'dgit-suite.*-security.distro' => 'debian-security',
+              'dgit.default.username' => '',
+              'dgit.default.archive-query-default-component' => 'main',
+              'dgit.default.ssh' => 'ssh',
+              'dgit.default.archive-query' => 'madison:',
+              'dgit.default.sshpsql-dbname' => 'service=projectb',
+              'dgit.default.aptget-components' => 'main',
+              'dgit.default.dgit-tag-format' => 'new,old,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/push.git-url' => '',
+              'dgit-distro.debian/push.git-host' => 'push.dgit.debian.org',
+              'dgit-distro.debian/push.git-user-force' => 'dgit',
+              'dgit-distro.debian/push.git-proto' => 'git+ssh://',
+              'dgit-distro.debian/push.git-path' => '/dgit/debian/repos',
+              'dgit-distro.debian/push.git-create' => 'true',
+              'dgit-distro.debian/push.git-check' => 'ssh-cmd',
+ 'dgit-distro.debian.archive-query-url', 'https://api.ftp-master.debian.org/',
+# 'dgit-distro.debian.archive-query-tls-key',
+#    '/etc/ssl/certs/%HOST%.pem:/etc/dgit/%HOST%.pem',
+# ^ this does not work because curl is broken nowadays
+# Fixing #790093 properly will involve providing providing the key
+# in some pacagke and maybe updating these paths.
+#
+# 'dgit-distro.debian.archive-query-tls-curl-args',
+#   '--ca-path=/etc/ssl/ca-debian',
+# ^ this is a workaround but works (only) on DSA-administered machines
+              'dgit-distro.debian.git-url' => 'https://git.dgit.debian.org',
+              'dgit-distro.debian.git-url-suffix' => '',
+              'dgit-distro.debian.upload-host' => 'ftp-master', # for dput
+              'dgit-distro.debian.mirror' => 'http://ftp.debian.org/debian/',
+ 'dgit-distro.debian-security.archive-query' => 'aptget:',
+ 'dgit-distro.debian-security.mirror' => 'http://security.debian.org/debian-security/',
+ 'dgit-distro.debian-security.aptget-suite-map' => 's#-security$#/updates#',
+ 'dgit-distro.debian-security.aptget-suite-rmap' => 's#$#-security#',
+ 'dgit-distro.debian-security.nominal-distro' => 'debian',
+ 'dgit-distro.debian.backports-quirk' => '(squeeze)-backports*',
+ 'dgit-distro.debian-backports.mirror' => 'http://backports.debian.org/debian-backports/',
+              'dgit-distro.ubuntu.git-check' => 'false',
+ 'dgit-distro.ubuntu.mirror' => 'http://archive.ubuntu.com/ubuntu',
+              'dgit-distro.test-dummy.ssh' => "$td/ssh",
+              'dgit-distro.test-dummy.username' => "alice",
+              'dgit-distro.test-dummy.git-check' => "ssh-cmd",
+              'dgit-distro.test-dummy.git-create' => "ssh-cmd",
+              'dgit-distro.test-dummy.git-url' => "$td/git",
+              'dgit-distro.test-dummy.git-host' => "git",
+              'dgit-distro.test-dummy.git-path' => "$td/git",
+              'dgit-distro.test-dummy.archive-query' => "dummycatapi:",
+              'dgit-distro.test-dummy.archive-query-url' => "file://$td/aq/",
+              'dgit-distro.test-dummy.mirror' => "file://$td/mirror/",
+              'dgit-distro.test-dummy.upload-host' => 'test-dummy',
+               );
+
+our %gitcfgs;
+our @gitcfgsources = qw(cmdline local global system);
+
+sub git_slurp_config () {
+    local ($debuglevel) = $debuglevel-2;
+    local $/="\0";
+
+    # This algoritm is a bit subtle, but this is needed so that for
+    # options which we want to be single-valued, we allow the
+    # different config sources to override properly.  See #835858.
+    foreach my $src (@gitcfgsources) {
+       next if $src eq 'cmdline';
+       # we do this ourselves since git doesn't handle it
+       
+       my @cmd = (@git, qw(config -z --get-regexp), "--$src", qw(.*));
+       debugcmd "|",@cmd;
+
+       open GITS, "-|", @cmd or die $!;
+       while (<GITS>) {
+           chomp or die;
+           printdebug "=> ", (messagequote $_), "\n";
+           m/\n/ or die "$_ ?";
+           push @{ $gitcfgs{$src}{$`} }, $'; #';
+       }
+       $!=0; $?=0;
+       close GITS
+           or ($!==0 && $?==256)
+           or failedcmd @cmd;
+    }
+}
+
+sub git_get_config ($) {
+    my ($c) = @_;
+    foreach my $src (@gitcfgsources) {
+       my $l = $gitcfgs{$src}{$c};
+       printdebug"C $c ".(defined $l ? messagequote "'$l'" : "undef")."\n"
+           if $debuglevel >= 4;
+       $l or next;
+       @$l==1 or badcfg "multiple values for $c".
+           " (in $src git config)" if @$l > 1;
+       return $l->[0];
+    }
+    return undef;
+}
+
+sub cfg {
+    foreach my $c (@_) {
+       return undef if $c =~ /RETURN-UNDEF/;
+       my $v = git_get_config($c);
+       return $v if defined $v;
+       my $dv = $defcfg{$c};
+       return $dv if defined $dv;
+    }
+    badcfg "need value for one of: @_\n".
+       "$us: distro or suite appears not to be (properly) supported";
+}
+
+sub access_basedistro () {
+    if (defined $idistro) {
+       return $idistro;
+    } else {   
+       my $def = cfg("dgit-suite.$isuite.distro", 'RETURN-UNDEF');
+       return $def if defined $def;
+       foreach my $src (@gitcfgsources, 'internal') {
+           my $kl = $src eq 'internal' ? \%defcfg : $gitcfgs{$src};
+           next unless $kl;
+           foreach my $k (keys %$kl) {
+               next unless $k =~ m#^dgit-suite\.(.*)\.distro$#;
+               my $dpat = $1;
+               next unless match_glob $dpat, $isuite;
+               return $kl->{$k};
+           }
+       }
+       return cfg("dgit.default.distro");
+    }
+}
+
+sub access_nomdistro () {
+    my $base = access_basedistro();
+    return cfg("dgit-distro.$base.nominal-distro",'RETURN-UNDEF') // $base;
+}
+
+sub access_quirk () {
+    # returns (quirk name, distro to use instead or undef, quirk-specific info)
+    my $basedistro = access_basedistro();
+    my $backports_quirk = cfg("dgit-distro.$basedistro.backports-quirk",
+                             'RETURN-UNDEF');
+    if (defined $backports_quirk) {
+       my $re = $backports_quirk;
+       $re =~ s/[^-0-9a-z_\%*()]/\\$&/ig;
+       $re =~ s/\*/.*/g;
+       $re =~ s/\%/([-0-9a-z_]+)/
+           or $re =~ m/[()]/ or badcfg "backports-quirk needs \% or ( )";
+       if ($isuite =~ m/^$re$/) {
+           return ('backports',"$basedistro-backports",$1);
+       }
+    }
+    return ('none',undef);
+}
+
+our $access_forpush;
+
+sub parse_cfg_bool ($$$) {
+    my ($what,$def,$v) = @_;
+    $v //= $def;
+    return
+       $v =~ m/^[ty1]/ ? 1 :
+       $v =~ m/^[fn0]/ ? 0 :
+       badcfg "$what needs t (true, y, 1) or f (false, n, 0) not \`$v'";
+}      
+
+sub access_forpush_config () {
+    my $d = access_basedistro();
+
+    return 1 if
+       $new_package &&
+       parse_cfg_bool('new-private-pushers', 0,
+                      cfg("dgit-distro.$d.new-private-pushers",
+                          'RETURN-UNDEF'));
+
+    my $v = cfg("dgit-distro.$d.readonly", 'RETURN-UNDEF');
+    $v //= 'a';
+    return
+       $v =~ m/^[ty1]/ ? 0 : # force readonly,    forpush = 0
+       $v =~ m/^[fn0]/ ? 1 : # force nonreadonly, forpush = 1
+       $v =~ m/^[a]/  ? '' : # auto,              forpush = ''
+       badcfg "readonly needs t (true, y, 1) or f (false, n, 0) or a (auto)";
+}
+
+sub access_forpush () {
+    $access_forpush //= access_forpush_config();
+    return $access_forpush;
+}
+
+sub pushing () {
+    die "$access_forpush ?" if ($access_forpush // 1) ne 1;
+    badcfg "pushing but distro is configured readonly"
+       if access_forpush_config() eq '0';
+    $access_forpush = 1;
+    $supplementary_message = <<'END' unless $we_are_responder;
+Push failed, before we got started.
+You can retry the push, after fixing the problem, if you like.
+END
+    finalise_opts_opts();
+}
+
+sub notpushing () {
+    finalise_opts_opts();
+}
+
+sub supplementary_message ($) {
+    my ($msg) = @_;
+    if (!$we_are_responder) {
+       $supplementary_message = $msg;
+       return;
+    } elsif ($protovsn >= 3) {
+       responder_send_command "supplementary-message ".length($msg)
+           or die $!;
+       print PO $msg or die $!;
+    }
+}
+
+sub access_distros () {
+    # Returns list of distros to try, in order
+    #
+    # We want to try:
+    #    0. `instead of' distro name(s) we have been pointed to
+    #    1. the access_quirk distro, if any
+    #    2a. the user's specified distro, or failing that  } basedistro
+    #    2b. the distro calculated from the suite          }
+    my @l = access_basedistro();
+
+    my (undef,$quirkdistro) = access_quirk();
+    unshift @l, $quirkdistro;
+    unshift @l, $instead_distro;
+    @l = grep { defined } @l;
+
+    push @l, access_nomdistro();
+
+    if (access_forpush()) {
+       @l = map { ("$_/push", $_) } @l;
+    }
+    @l;
+}
+
+sub access_cfg_cfgs (@) {
+    my (@keys) = @_;
+    my @cfgs;
+    # The nesting of these loops determines the search order.  We put
+    # the key loop on the outside so that we search all the distros
+    # for each key, before going on to the next key.  That means that
+    # if access_cfg is called with a more specific, and then a less
+    # specific, key, an earlier distro can override the less specific
+    # without necessarily overriding any more specific keys.  (If the
+    # distro wants to override the more specific keys it can simply do
+    # so; whereas if we did the loop the other way around, it would be
+    # impossible to for an earlier distro to override a less specific
+    # key but not the more specific ones without restating the unknown
+    # values of the more specific keys.
+    my @realkeys;
+    my @rundef;
+    # We have to deal with RETURN-UNDEF specially, so that we don't
+    # terminate the search prematurely.
+    foreach (@keys) {
+       if (m/RETURN-UNDEF/) { push @rundef, $_; last; }
+       push @realkeys, $_
+    }
+    foreach my $d (access_distros()) {
+       push @cfgs, map { "dgit-distro.$d.$_" } @realkeys;
+    }
+    push @cfgs, map { "dgit.default.$_" } @realkeys;
+    push @cfgs, @rundef;
+    return @cfgs;
+}
+
+sub access_cfg (@) {
+    my (@keys) = @_;
+    my (@cfgs) = access_cfg_cfgs(@keys);
+    my $value = cfg(@cfgs);
+    return $value;
+}
+
+sub access_cfg_bool ($$) {
+    my ($def, @keys) = @_;
+    parse_cfg_bool($keys[0], $def, access_cfg(@keys, 'RETURN-UNDEF'));
+}
+
+sub string_to_ssh ($) {
+    my ($spec) = @_;
+    if ($spec =~ m/\s/) {
+       return qw(sh -ec), 'exec '.$spec.' "$@"', 'x';
+    } else {
+       return ($spec);
+    }
+}
+
+sub access_cfg_ssh () {
+    my $gitssh = access_cfg('ssh', 'RETURN-UNDEF');
+    if (!defined $gitssh) {
+       return @ssh;
+    } else {
+       return string_to_ssh $gitssh;
+    }
+}
+
+sub access_runeinfo ($) {
+    my ($info) = @_;
+    return ": dgit ".access_basedistro()." $info ;";
+}
+
+sub access_someuserhost ($) {
+    my ($some) = @_;
+    my $user = access_cfg("$some-user-force", 'RETURN-UNDEF');
+    defined($user) && length($user) or
+       $user = access_cfg("$some-user",'username');
+    my $host = access_cfg("$some-host");
+    return length($user) ? "$user\@$host" : $host;
+}
+
+sub access_gituserhost () {
+    return access_someuserhost('git');
+}
+
+sub access_giturl (;$) {
+    my ($optional) = @_;
+    my $url = access_cfg('git-url','RETURN-UNDEF');
+    my $suffix;
+    if (!length $url) {
+       my $proto = access_cfg('git-proto', 'RETURN-UNDEF');
+       return undef unless defined $proto;
+       $url =
+           $proto.
+           access_gituserhost().
+           access_cfg('git-path');
+    } else {
+       $suffix = access_cfg('git-url-suffix','RETURN-UNDEF');
+    }
+    $suffix //= '.git';
+    return "$url/$package$suffix";
+}             
+
+sub parsecontrolfh ($$;$) {
+    my ($fh, $desc, $allowsigned) = @_;
+    our $dpkgcontrolhash_noissigned;
+    my $c;
+    for (;;) {
+       my %opts = ('name' => $desc);
+       $opts{allow_pgp}= $allowsigned || !$dpkgcontrolhash_noissigned;
+       $c = Dpkg::Control::Hash->new(%opts);
+       $c->parse($fh,$desc) or die "parsing of $desc failed";
+       last if $allowsigned;
+       last if $dpkgcontrolhash_noissigned;
+       my $issigned= $c->get_option('is_pgp_signed');
+       if (!defined $issigned) {
+           $dpkgcontrolhash_noissigned= 1;
+           seek $fh, 0,0 or die "seek $desc: $!";
+       } elsif ($issigned) {
+           fail "control file $desc is (already) PGP-signed. ".
+               " Note that dgit push needs to modify the .dsc and then".
+               " do the signature itself";
+       } else {
+           last;
+       }
+    }
+    return $c;
+}
+
+sub parsecontrol {
+    my ($file, $desc, $allowsigned) = @_;
+    my $fh = new IO::Handle;
+    open $fh, '<', $file or die "$file: $!";
+    my $c = parsecontrolfh($fh,$desc,$allowsigned);
+    $fh->error and die $!;
+    close $fh;
+    return $c;
+}
+
+sub getfield ($$) {
+    my ($dctrl,$field) = @_;
+    my $v = $dctrl->{$field};
+    return $v if defined $v;
+    fail "missing field $field in ".$dctrl->get_option('name');
+}
+
+sub parsechangelog {
+    my $c = Dpkg::Control::Hash->new(name => 'parsed changelog');
+    my $p = new IO::Handle;
+    my @cmd = (qw(dpkg-parsechangelog), @_);
+    open $p, '-|', @cmd or die $!;
+    $c->parse($p);
+    $?=0; $!=0; close $p or failedcmd @cmd;
+    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: $!";
+    return $d;
+}
+
+sub parse_dscdata () {
+    my $dscfh = new IO::File \$dscdata, '<' or die $!;
+    printdebug Dumper($dscdata) if $debuglevel>1;
+    $dsc = parsecontrolfh($dscfh,$dscurl,1);
+    printdebug Dumper($dsc) if $debuglevel>1;
+}
+
+our %rmad;
+
+sub archive_query ($;@) {
+    my ($method) = shift @_;
+    fail "this operation does not support multiple comma-separated suites"
+       if $isuite =~ m/,/;
+    my $query = access_cfg('archive-query','RETURN-UNDEF');
+    $query =~ s/^(\w+):// or badcfg "invalid archive-query method \`$query'";
+    my $proto = $1;
+    my $data = $'; #';
+    { no strict qw(refs); &{"${method}_${proto}"}($proto,$data,@_); }
+}
+
+sub archive_query_prepend_mirror {
+    my $m = access_cfg('mirror');
+    return map { [ $_->[0], $m.$_->[1], @$_[2..$#$_] ] } @_;
+}
+
+sub pool_dsc_subpath ($$) {
+    my ($vsn,$component) = @_; # $package is implict arg
+    my $prefix = substr($package, 0, $package =~ m/^l/ ? 4 : 1);
+    return "/pool/$component/$prefix/$package/".dscfn($vsn);
+}
+
+sub cfg_apply_map ($$$) {
+    my ($varref, $what, $mapspec) = @_;
+    return unless $mapspec;
+
+    printdebug "config $what EVAL{ $mapspec; }\n";
+    $_ = $$varref;
+    eval "package Dgit::Config; $mapspec;";
+    die $@ if $@;
+    $$varref = $_;
+}
+
+#---------- `ftpmasterapi' archive query method (nascent) ----------
+
+sub archive_api_query_cmd ($) {
+    my ($subpath) = @_;
+    my @cmd = (@curl, qw(-sS));
+    my $url = access_cfg('archive-query-url');
+    if ($url =~ m#^https://([-.0-9a-z]+)/#) {
+       my $host = $1;
+       my $keys = access_cfg('archive-query-tls-key','RETURN-UNDEF') //'';
+       foreach my $key (split /\:/, $keys) {
+           $key =~ s/\%HOST\%/$host/g;
+           if (!stat $key) {
+               fail "for $url: stat $key: $!" unless $!==ENOENT;
+               next;
+           }
+           fail "config requested specific TLS key but do not know".
+               " how to get curl to use exactly that EE key ($key)";
+#          push @cmd, "--cacert", $key, "--capath", "/dev/enoent";
+#           # Sadly the above line does not work because of changes
+#           # to gnutls.   The real fix for #790093 may involve
+#           # new curl options.
+           last;
+       }
+       # Fixing #790093 properly will involve providing a value
+       # for this on clients.
+       my $kargs = access_cfg('archive-query-tls-curl-ca-args','RETURN-UNDEF');
+       push @cmd, split / /, $kargs if defined $kargs;
+    }
+    push @cmd, $url.$subpath;
+    return @cmd;
+}
+
+sub api_query ($$;$) {
+    use JSON;
+    my ($data, $subpath, $ok404) = @_;
+    badcfg "ftpmasterapi archive query method takes no data part"
+       if length $data;
+    my @cmd = archive_api_query_cmd($subpath);
+    my $url = $cmd[$#cmd];
+    push @cmd, qw(-w %{http_code});
+    my $json = cmdoutput @cmd;
+    unless ($json =~ s/\d+\d+\d$//) {
+       failedcmd_report_cmd undef, @cmd;
+       fail "curl failed to print 3-digit HTTP code";
+    }
+    my $code = $&;
+    return undef if $code eq '404' && $ok404;
+    fail "fetch of $url gave HTTP code $code"
+       unless $url =~ m#^file://# or $code =~ m/^2/;
+    return decode_json($json);
+}
+
+sub canonicalise_suite_ftpmasterapi {
+    my ($proto,$data) = @_;
+    my $suites = api_query($data, 'suites');
+    my @matched;
+    foreach my $entry (@$suites) {
+       next unless grep { 
+           my $v = $entry->{$_};
+           defined $v && $v eq $isuite;
+       } qw(codename name);
+       push @matched, $entry;
+    }
+    fail "unknown suite $isuite" unless @matched;
+    my $cn;
+    eval {
+       @matched==1 or die "multiple matches for suite $isuite\n";
+       $cn = "$matched[0]{codename}";
+       defined $cn or die "suite $isuite info has no codename\n";
+       $cn =~ m/^$suite_re$/ or die "suite $isuite maps to bad codename\n";
+    };
+    die "bad ftpmaster api response: $@\n".Dumper(\@matched)
+       if length $@;
+    return $cn;
+}
+
+sub archive_query_ftpmasterapi {
+    my ($proto,$data) = @_;
+    my $info = api_query($data, "dsc_in_suite/$isuite/$package");
+    my @rows;
+    my $digester = Digest::SHA->new(256);
+    foreach my $entry (@$info) {
+       eval {
+           my $vsn = "$entry->{version}";
+           my ($ok,$msg) = version_check $vsn;
+           die "bad version: $msg\n" unless $ok;
+           my $component = "$entry->{component}";
+           $component =~ m/^$component_re$/ or die "bad component";
+           my $filename = "$entry->{filename}";
+           $filename && $filename !~ m#[^-+:._~0-9a-zA-Z/]|^[/.]|/[/.]#
+               or die "bad filename";
+           my $sha256sum = "$entry->{sha256sum}";
+           $sha256sum =~ m/^[0-9a-f]+$/ or die "bad sha256sum";
+           push @rows, [ $vsn, "/pool/$component/$filename",
+                         $digester, $sha256sum ];
+       };
+       die "bad ftpmaster api response: $@\n".Dumper($entry)
+           if length $@;
+    }
+    @rows = sort { -version_compare($a->[0],$b->[0]) } @rows;
+    return archive_query_prepend_mirror @rows;
+}
+
+sub file_in_archive_ftpmasterapi {
+    my ($proto,$data,$filename) = @_;
+    my $pat = $filename;
+    $pat =~ s/_/\\_/g;
+    $pat = "%/$pat";
+    $pat =~ s#[^-+_.0-9a-z/]# sprintf '%%%02x', ord $& #ge;
+    my $info = api_query($data, "file_in_archive/$pat", 1);
+}
+
+#---------- `aptget' archive query method ----------
+
+our $aptget_base;
+our $aptget_releasefile;
+our $aptget_configpath;
+
+sub aptget_aptget   () { return @aptget,   qw(-c), $aptget_configpath; }
+sub aptget_aptcache () { return @aptcache, qw(-c), $aptget_configpath; }
+
+sub aptget_cache_clean {
+    runcmd_ordryrun_local qw(sh -ec),
+       'cd "$1"; find -atime +30 -type f -print0 | xargs -0r rm --',
+       'x', $aptget_base;
+}
+
+sub aptget_lock_acquire () {
+    my $lockfile = "$aptget_base/lock";
+    open APTGET_LOCK, '>', $lockfile or die "open $lockfile: $!";
+    flock APTGET_LOCK, LOCK_EX or die "lock $lockfile: $!";
+}
+
+sub aptget_prep ($) {
+    my ($data) = @_;
+    return if defined $aptget_base;
+
+    badcfg "aptget archive query method takes no data part"
+       if length $data;
+
+    my $cache = $ENV{XDG_CACHE_DIR} // "$ENV{HOME}/.cache";
+
+    ensuredir $cache;
+    ensuredir "$cache/dgit";
+    my $cachekey =
+       access_cfg('aptget-cachekey','RETURN-UNDEF')
+       // access_nomdistro();
+
+    $aptget_base = "$cache/dgit/aptget";
+    ensuredir $aptget_base;
+
+    my $quoted_base = $aptget_base;
+    die "$quoted_base contains bad chars, cannot continue"
+       if $quoted_base =~ m/["\\]/; # apt.conf(5) says no escaping :-/
+
+    ensuredir $aptget_base;
+
+    aptget_lock_acquire();
+
+    aptget_cache_clean();
+
+    $aptget_configpath = "$aptget_base/apt.conf#$cachekey";
+    my $sourceslist = "source.list#$cachekey";
+
+    my $aptsuites = $isuite;
+    cfg_apply_map(\$aptsuites, 'suite map',
+                 access_cfg('aptget-suite-map', 'RETURN-UNDEF'));
+
+    open SRCS, ">", "$aptget_base/$sourceslist" or die $!;
+    printf SRCS "deb-src %s %s %s\n",
+       access_cfg('mirror'),
+       $aptsuites,
+       access_cfg('aptget-components')
+       or die $!;
+
+    ensuredir "$aptget_base/cache";
+    ensuredir "$aptget_base/lists";
+
+    open CONF, ">", $aptget_configpath or die $!;
+    print CONF <<END;
+Debug::NoLocking "true";
+APT::Get::List-Cleanup "false";
+#clear APT::Update::Post-Invoke-Success;
+Dir::Etc::SourceList "$quoted_base/$sourceslist";
+Dir::State::Lists "$quoted_base/lists";
+Dir::Etc::preferences "$quoted_base/preferences";
+Dir::Cache::srcpkgcache "$quoted_base/cache/srcs#$cachekey";
+Dir::Cache::pkgcache "$quoted_base/cache/pkgs#$cachekey";
+END
+
+    foreach my $key (qw(
+                       Dir::Cache
+                       Dir::State
+                       Dir::Cache::Archives
+                       Dir::Etc::SourceParts
+                       Dir::Etc::preferencesparts
+                     )) {
+       ensuredir "$aptget_base/$key";
+       print CONF "$key \"$quoted_base/$key\";\n" or die $!;
+    };
+
+    my $oldatime = (time // die $!) - 1;
+    foreach my $oldlist (<$aptget_base/lists/*Release>) {
+       next unless stat_exists $oldlist;
+       my ($mtime) = (stat _)[9];
+       utime $oldatime, $mtime, $oldlist or die "$oldlist $!";
+    }
+
+    runcmd_ordryrun_local aptget_aptget(), qw(update);
+
+    my @releasefiles;
+    foreach my $oldlist (<$aptget_base/lists/*Release>) {
+       next unless stat_exists $oldlist;
+       my ($atime) = (stat _)[8];
+       next if $atime == $oldatime;
+       push @releasefiles, $oldlist;
+    }
+    my @inreleasefiles = grep { m#/InRelease$# } @releasefiles;
+    @releasefiles = @inreleasefiles if @inreleasefiles;
+    die "apt updated wrong number of Release files (@releasefiles), erk"
+       unless @releasefiles == 1;
+
+    ($aptget_releasefile) = @releasefiles;
+}
+
+sub canonicalise_suite_aptget {
+    my ($proto,$data) = @_;
+    aptget_prep($data);
+
+    my $release = parsecontrol $aptget_releasefile, "Release file", 1;
+
+    foreach my $name (qw(Codename Suite)) {
+       my $val = $release->{$name};
+       if (defined $val) {
+           printdebug "release file $name: $val\n";
+           $val =~ m/^$suite_re$/o or fail
+ "Release file ($aptget_releasefile) specifies intolerable $name";
+           cfg_apply_map(\$val, 'suite rmap',
+                         access_cfg('aptget-suite-rmap', 'RETURN-UNDEF'));
+           return $val
+       }
+    }
+    return $isuite;
+}
+
+sub archive_query_aptget {
+    my ($proto,$data) = @_;
+    aptget_prep($data);
+
+    ensuredir "$aptget_base/source";
+    foreach my $old (<$aptget_base/source/*.dsc>) {
+       unlink $old or die "$old: $!";
+    }
+
+    my $showsrc = cmdoutput aptget_aptcache(), qw(showsrc), $package;
+    return () unless $showsrc =~ m/^package:\s*\Q$package\E\s*$/mi;
+    # avoids apt-get source failing with ambiguous error code
+
+    runcmd_ordryrun_local
+       shell_cmd 'cd "$1"/source; shift', $aptget_base,
+       aptget_aptget(), qw(--download-only --only-source source), $package;
+
+    my @dscs = <$aptget_base/source/*.dsc>;
+    fail "apt-get source did not produce a .dsc" unless @dscs;
+    fail "apt-get source produced several .dscs (@dscs)" unless @dscs==1;
+
+    my $pre_dsc = parsecontrol $dscs[0], $dscs[0], 1;
+
+    use URI::Escape;
+    my $uri = "file://". uri_escape $dscs[0];
+    $uri =~ s{\%2f}{/}gi;
+    return [ (getfield $pre_dsc, 'Version'), $uri ];
+}
+
+#---------- `dummyapicat' archive query method ----------
+
+sub archive_query_dummycatapi { archive_query_ftpmasterapi @_; }
+sub canonicalise_suite_dummycatapi { canonicalise_suite_ftpmasterapi @_; }
+
+sub file_in_archive_dummycatapi ($$$) {
+    my ($proto,$data,$filename) = @_;
+    my $mirror = access_cfg('mirror');
+    $mirror =~ s#^file://#/# or die "$mirror ?";
+    my @out;
+    my @cmd = (qw(sh -ec), '
+            cd "$1"
+            find -name "$2" -print0 |
+            xargs -0r sha256sum
+        ', qw(x), $mirror, $filename);
+    debugcmd "-|", @cmd;
+    open FIA, "-|", @cmd or die $!;
+    while (<FIA>) {
+       chomp or die;
+       printdebug "| $_\n";
+       m/^(\w+)  (\S+)$/ or die "$_ ?";
+       push @out, { sha256sum => $1, filename => $2 };
+    }
+    close FIA or die failedcmd @cmd;
+    return \@out;
+}
+
+#---------- `madison' archive query method ----------
+
+sub archive_query_madison {
+    return archive_query_prepend_mirror
+       map { [ @$_[0..1] ] } madison_get_parse(@_);
+}
+
+sub madison_get_parse {
+    my ($proto,$data) = @_;
+    die unless $proto eq 'madison';
+    if (!length $data) {
+       $data= access_cfg('madison-distro','RETURN-UNDEF');
+       $data //= access_basedistro();
+    }
+    $rmad{$proto,$data,$package} ||= cmdoutput
+       qw(rmadison -asource),"-s$isuite","-u$data",$package;
+    my $rmad = $rmad{$proto,$data,$package};
+
+    my @out;
+    foreach my $l (split /\n/, $rmad) {
+       $l =~ m{^ \s*( [^ \t|]+ )\s* \|
+                  \s*( [^ \t|]+ )\s* \|
+                  \s*( [^ \t|/]+ )(?:/([^ \t|/]+))? \s* \|
+                  \s*( [^ \t|]+ )\s* }x or die "$rmad ?";
+       $1 eq $package or die "$rmad $package ?";
+       my $vsn = $2;
+       my $newsuite = $3;
+       my $component;
+       if (defined $4) {
+           $component = $4;
+       } else {
+           $component = access_cfg('archive-query-default-component');
+       }
+       $5 eq 'source' or die "$rmad ?";
+       push @out, [$vsn,pool_dsc_subpath($vsn,$component),$newsuite];
+    }
+    return sort { -version_compare($a->[0],$b->[0]); } @out;
+}
+
+sub canonicalise_suite_madison {
+    # madison canonicalises for us
+    my @r = madison_get_parse(@_);
+    @r or fail
+       "unable to canonicalise suite using package $package".
+       " which does not appear to exist in suite $isuite;".
+       " --existing-package may help";
+    return $r[0][2];
+}
+
+sub file_in_archive_madison { return undef; }
+
+#---------- `sshpsql' archive query method ----------
+
+sub sshpsql ($$$) {
+    my ($data,$runeinfo,$sql) = @_;
+    if (!length $data) {
+       $data= access_someuserhost('sshpsql').':'.
+           access_cfg('sshpsql-dbname');
+    }
+    $data =~ m/:/ or badcfg "invalid sshpsql method string \`$data'";
+    my ($userhost,$dbname) = ($`,$'); #';
+    my @rows;
+    my @cmd = (access_cfg_ssh, $userhost,
+              access_runeinfo("ssh-psql $runeinfo").
+              " export LC_MESSAGES=C; export LC_CTYPE=C;".
+              " ".shellquote qw(psql -A), $dbname, qw(-c), $sql);
+    debugcmd "|",@cmd;
+    open P, "-|", @cmd or die $!;
+    while (<P>) {
+       chomp or die;
+       printdebug(">|$_|\n");
+       push @rows, $_;
+    }
+    $!=0; $?=0; close P or failedcmd @cmd;
+    @rows or die;
+    my $nrows = pop @rows;
+    $nrows =~ s/^\((\d+) rows?\)$/$1/ or die "$nrows ?";
+    @rows == $nrows+1 or die "$nrows ".(scalar @rows)." ?";
+    @rows = map { [ split /\|/, $_ ] } @rows;
+    my $ncols = scalar @{ shift @rows };
+    die if grep { scalar @$_ != $ncols } @rows;
+    return @rows;
+}
+
+sub sql_injection_check {
+    foreach (@_) { die "$_ $& ?" if m{[^-+=:_.,/0-9a-zA-Z]}; }
+}
+
+sub archive_query_sshpsql ($$) {
+    my ($proto,$data) = @_;
+    sql_injection_check $isuite, $package;
+    my @rows = sshpsql($data, "archive-query $isuite $package", <<END);
+        SELECT source.version, component.name, files.filename, files.sha256sum
+          FROM source
+          JOIN src_associations ON source.id = src_associations.source
+          JOIN suite ON suite.id = src_associations.suite
+          JOIN dsc_files ON dsc_files.source = source.id
+          JOIN files_archive_map ON files_archive_map.file_id = dsc_files.file
+          JOIN component ON component.id = files_archive_map.component_id
+          JOIN files ON files.id = dsc_files.file
+         WHERE ( suite.suite_name='$isuite' OR suite.codename='$isuite' )
+           AND source.source='$package'
+           AND files.filename LIKE '%.dsc';
+END
+    @rows = sort { -version_compare($a->[0],$b->[0]) } @rows;
+    my $digester = Digest::SHA->new(256);
+    @rows = map {
+       my ($vsn,$component,$filename,$sha256sum) = @$_;
+       [ $vsn, "/pool/$component/$filename",$digester,$sha256sum ];
+    } @rows;
+    return archive_query_prepend_mirror @rows;
+}
+
+sub canonicalise_suite_sshpsql ($$) {
+    my ($proto,$data) = @_;
+    sql_injection_check $isuite;
+    my @rows = sshpsql($data, "canonicalise-suite $isuite", <<END);
+        SELECT suite.codename
+          FROM suite where suite_name='$isuite' or codename='$isuite';
+END
+    @rows = map { $_->[0] } @rows;
+    fail "unknown suite $isuite" unless @rows;
+    die "ambiguous $isuite: @rows ?" if @rows>1;
+    return $rows[0];
+}
+
+sub file_in_archive_sshpsql ($$$) { return undef; }
+
+#---------- `dummycat' archive query method ----------
+
+sub canonicalise_suite_dummycat ($$) {
+    my ($proto,$data) = @_;
+    my $dpath = "$data/suite.$isuite";
+    if (!open C, "<", $dpath) {
+       $!==ENOENT or die "$dpath: $!";
+       printdebug "dummycat canonicalise_suite $isuite $dpath ENOENT\n";
+       return $isuite;
+    }
+    $!=0; $_ = <C>;
+    chomp or die "$dpath: $!";
+    close C;
+    printdebug "dummycat canonicalise_suite $isuite $dpath = $_\n";
+    return $_;
+}
+
+sub archive_query_dummycat ($$) {
+    my ($proto,$data) = @_;
+    canonicalise_suite();
+    my $dpath = "$data/package.$csuite.$package";
+    if (!open C, "<", $dpath) {
+       $!==ENOENT or die "$dpath: $!";
+       printdebug "dummycat query $csuite $package $dpath ENOENT\n";
+       return ();
+    }
+    my @rows;
+    while (<C>) {
+       next if m/^\#/;
+       next unless m/\S/;
+       die unless chomp;
+       printdebug "dummycat query $csuite $package $dpath | $_\n";
+       my @row = split /\s+/, $_;
+       @row==2 or die "$dpath: $_ ?";
+       push @rows, \@row;
+    }
+    C->error and die "$dpath: $!";
+    close C;
+    return archive_query_prepend_mirror
+       sort { -version_compare($a->[0],$b->[0]); } @rows;
+}
+
+sub file_in_archive_dummycat () { return undef; }
+
+#---------- tag format handling ----------
+
+sub access_cfg_tagformats () {
+    split /\,/, access_cfg('dgit-tag-format');
+}
+
+sub access_cfg_tagformats_can_splitbrain () {
+    my %y = map { $_ => 1 } access_cfg_tagformats;
+    foreach my $needtf (qw(new maint)) {
+       next if $y{$needtf};
+       return 0;
+    }
+    return 1;
+}
+
+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 { $_ =~ m/^(?:old|new)$/ } 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 () {
+    return if defined $csuite;
+    fail "cannot operate on $isuite suite" if $isuite eq 'UNRELEASED';
+    $csuite = archive_query('canonicalise_suite');
+    if ($isuite ne $csuite) {
+       progress "canonical suite name for $isuite is $csuite";
+    } else {
+       progress "canonical suite name is $csuite";
+    }
+}
+
+sub get_archive_dsc () {
+    canonicalise_suite();
+    my @vsns = archive_query('archive_query');
+    foreach my $vinfo (@vsns) {
+       my ($vsn,$vsn_dscurl,$digester,$digest) = @$vinfo;
+       $dscurl = $vsn_dscurl;
+       $dscdata = url_get($dscurl);
+       if (!$dscdata) {
+           $skew_warning_vsn = $vsn if !defined $skew_warning_vsn;
+           next;
+       }
+       if ($digester) {
+           $digester->reset();
+           $digester->add($dscdata);
+           my $got = $digester->hexdigest();
+           $got eq $digest or
+               fail "$dscurl has hash $got but".
+                   " archive told us to expect $digest";
+       }
+       parse_dscdata();
+       my $fmt = getfield $dsc, 'Format';
+       $format_ok{$fmt} or forceable_fail [qw(unsupported-source-format)],
+           "unsupported source format $fmt, sorry";
+           
+       $dsc_checked = !!$digester;
+       printdebug "get_archive_dsc: Version ".(getfield $dsc, 'Version')."\n";
+       return;
+    }
+    $dsc = undef;
+    printdebug "get_archive_dsc: nothing in archive, returning undef\n";
+}
+
+sub check_for_git ();
+sub check_for_git () {
+    # returns 0 or 1
+    my $how = access_cfg('git-check');
+    if ($how eq 'ssh-cmd') {
+       my @cmd =
+           (access_cfg_ssh, access_gituserhost(),
+            access_runeinfo("git-check $package").
+            " set -e; cd ".access_cfg('git-path').";".
+            " if test -d $package.git; then echo 1; else echo 0; fi");
+       my $r= cmdoutput @cmd;
+       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
+           $instead_distro= cfg("dgit-distro.$usedistro.diverts.$divert");
+           $instead_distro =~ s{^/}{ access_basedistro()."/" }e;
+           progress "diverting to $divert (using config for $instead_distro)";
+           return check_for_git();
+       }
+       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');
+       my $suffix = access_cfg('git-check-suffix','git-suffix',
+                               'RETURN-UNDEF') // '.git';
+       my $url = "$prefix/$package$suffix";
+       my @cmd = (@curl, qw(-sS -I), $url);
+       my $result = cmdoutput @cmd;
+       $result =~ s/^\S+ 200 .*\n\r?\n//;
+       # curl -sS -I with https_proxy prints
+       # HTTP/1.0 200 Connection established
+       $result =~ m/^\S+ (404|200) /s or
+           fail "unexpected results from git check query - ".
+               Dumper($prefix, $result);
+       my $code = $1;
+       if ($code eq '404') {
+           return 0;
+       } elsif ($code eq '200') {
+           return 1;
+       } else {
+           die;
+       }
+    } elsif ($how eq 'true') {
+       return 1;
+    } elsif ($how eq 'false') {
+       return 0;
+    } else {
+       badcfg "unknown git-check \`$how'";
+    }
+}
+
+sub create_remote_git_repo () {
+    my $how = access_cfg('git-create');
+    if ($how eq 'ssh-cmd') {
+       runcmd_ordryrun
+           (access_cfg_ssh, access_gituserhost(),
+            access_runeinfo("git-create $package").
+            "set -e; cd ".access_cfg('git-path').";".
+            " cp -a _template $package.git");
+    } elsif ($how eq 'true') {
+       # nothing to do
+    } else {
+       badcfg "unknown git-create \`$how'";
+    }
+}
+
+our ($dsc_hash,$lastpush_mergeinput);
+
+our $ud = '.git/dgit/unpack';
+
+sub prep_ud (;$) {
+    my ($d) = @_;
+    $d //= $ud;
+    rmtree($d);
+    mkpath '.git/dgit';
+    mkdir $d or die $!;
+}
+
+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 $!;
+}
+
+sub git_write_tree () {
+    my $tree = cmdoutput @git, qw(write-tree);
+    $tree =~ m/^\w+$/ or die "$tree ?";
+    return $tree;
+}
+
+sub git_add_write_tree () {
+    runcmd @git, qw(add -Af .);
+    return git_write_tree();
+}
+
+sub remove_stray_gits ($) {
+    my ($what) = @_;
+    my @gitscmd = qw(find -name .git -prune -print0);
+    debugcmd "|",@gitscmd;
+    open GITS, "-|", @gitscmd or die $!;
+    {
+       local $/="\0";
+       while (<GITS>) {
+           chomp or die;
+           print STDERR "$us: warning: removing from $what: ",
+               (messagequote $_), "\n";
+           rmtree $_;
+       }
+    }
+    $!=0; $?=0; close GITS or failedcmd @gitscmd;
+}
+
+sub mktree_in_ud_from_only_subdir ($;$) {
+    my ($what,$raw) = @_;
+
+    # changes into the subdir
+    my (@dirs) = <*/.>;
+    die "expected one subdir but found @dirs ?" unless @dirs==1;
+    $dirs[0] =~ m#^([^/]+)/\.$# or die;
+    my $dir = $1;
+    changedir $dir;
+
+    remove_stray_gits($what);
+    mktree_in_ud_here();
+    if (!$raw) {
+       my ($format, $fopts) = get_source_format();
+       if (madformat($format)) {
+           rmtree '.pc';
+       }
+    }
+
+    my $tree=git_add_write_tree();
+    return ($tree,$dir);
+}
+
+our @files_csum_info_fields = 
+    (['Checksums-Sha256','Digest::SHA', 'new(256)', 'sha256sum'],
+     ['Checksums-Sha1',  'Digest::SHA', 'new(1)',   'sha1sum'],
+     ['Files',           'Digest::MD5', 'new()',    'md5sum']);
+
+sub dsc_files_info () {
+    foreach my $csumi (@files_csum_info_fields) {
+       my ($fname, $module, $method) = @$csumi;
+       my $field = $dsc->{$fname};
+       next unless defined $field;
+       eval "use $module; 1;" or die $@;
+       my @out;
+       foreach (split /\n/, $field) {
+           next unless m/\S/;
+           m/^(\w+) (\d+) (\S+)$/ or
+               fail "could not parse .dsc $fname line \`$_'";
+           my $digester = eval "$module"."->$method;" or die $@;
+           push @out, {
+               Hash => $1,
+               Bytes => $2,
+               Filename => $3,
+               Digester => $digester,
+           };
+       }
+       return @out;
+    }
+    fail "missing any supported Checksums-* or Files field in ".
+       $dsc->get_option('name');
+}
+
+sub dsc_files () {
+    map { $_->{Filename} } dsc_files_info();
+}
+
+sub files_compare_inputs (@) {
+    my $inputs = \@_;
+    my %record;
+    my %fchecked;
+
+    my $showinputs = sub {
+       return join "; ", map { $_->get_option('name') } @$inputs;
+    };
+
+    foreach my $in (@$inputs) {
+       my $expected_files;
+       my $in_name = $in->get_option('name');
+
+       printdebug "files_compare_inputs $in_name\n";
+
+       foreach my $csumi (@files_csum_info_fields) {
+           my ($fname) = @$csumi;
+           printdebug "files_compare_inputs $in_name $fname\n";
+
+           my $field = $in->{$fname};
+           next unless defined $field;
+
+           my @files;
+           foreach (split /\n/, $field) {
+               next unless m/\S/;
+
+               my ($info, $f) = m/^(\w+ \d+) (?:\S+ \S+ )?(\S+)$/ or
+                   fail "could not parse $in_name $fname line \`$_'";
+
+               printdebug "files_compare_inputs $in_name $fname $f\n";
+
+               push @files, $f;
+
+               my $re = \ $record{$f}{$fname};
+               if (defined $$re) {
+                   $fchecked{$f}{$in_name} = 1;
+                   $$re eq $info or
+                       fail "hash or size of $f varies in $fname fields".
+                       " (between: ".$showinputs->().")";
+               } else {
+                   $$re = $info;
+               }
+           }
+           @files = sort @files;
+           $expected_files //= \@files;
+           "@$expected_files" eq "@files" or
+               fail "file list in $in_name varies between hash fields!";
+       }
+       $expected_files or
+           fail "$in_name has no files list field(s)";
+    }
+    printdebug "files_compare_inputs ".Dumper(\%fchecked, \%record)
+       if $debuglevel>=2;
+
+    grep { keys %$_ == @$inputs-1 } values %fchecked
+       or fail "no file appears in all file lists".
+       " (looked in: ".$showinputs->().")";
+}
+
+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 changes_update_origs_from_dsc ($$$$) {
+    my ($dsc, $changes, $upstreamvsn, $changesfile) = @_;
+    my %changes_f;
+    printdebug "checking origs needed ($upstreamvsn)...\n";
+    $_ = getfield $changes, 'Files';
+    m/^\w+ \d+ (\S+ \S+) \S+$/m or
+       fail "cannot find section/priority from .changes Files field";
+    my $placementinfo = $1;
+    my %changed;
+    printdebug "checking origs needed placement '$placementinfo'...\n";
+    foreach my $l (split /\n/, getfield $dsc, 'Files') {
+       $l =~ m/\S+$/ or next;
+       my $file = $&;
+       printdebug "origs $file | $l\n";
+       next unless is_orig_file_of_vsn $file, $upstreamvsn;
+       printdebug "origs $file is_orig\n";
+       my $have = archive_query('file_in_archive', $file);
+       if (!defined $have) {
+           print STDERR <<END;
+archive does not support .orig check; hope you used --ch:--sa/-sd if needed
+END
+           return;
+       }
+       my $found_same = 0;
+       my @found_differ;
+       printdebug "origs $file \$#\$have=$#$have\n";
+       foreach my $h (@$have) {
+           my $same = 0;
+           my @differ;
+           foreach my $csumi (@files_csum_info_fields) {
+               my ($fname, $module, $method, $archivefield) = @$csumi;
+               next unless defined $h->{$archivefield};
+               $_ = $dsc->{$fname};
+               next unless defined;
+               m/^(\w+) .* \Q$file\E$/m or
+                   fail ".dsc $fname missing entry for $file";
+               if ($h->{$archivefield} eq $1) {
+                   $same++;
+               } else {
+                   push @differ,
+ "$archivefield: $h->{$archivefield} (archive) != $1 (local .dsc)";
+               }
+           }
+           die "$file ".Dumper($h)." ?!" if $same && @differ;
+           $found_same++
+               if $same;
+           push @found_differ, "archive $h->{filename}: ".join "; ", @differ
+               if @differ;
+       }
+       printdebug "origs $file f.same=$found_same".
+           " #f._differ=$#found_differ\n";
+       if (@found_differ && !$found_same) {
+           fail join "\n",
+               "archive contains $file with different checksum",
+               @found_differ;
+       }
+       # Now we edit the changes file to add or remove it
+       foreach my $csumi (@files_csum_info_fields) {
+           my ($fname, $module, $method, $archivefield) = @$csumi;
+           next unless defined $changes->{$fname};
+           if ($found_same) {
+               # in archive, delete from .changes if it's there
+               $changed{$file} = "removed" if
+                   $changes->{$fname} =~ s/^.* \Q$file\E$(?:)\n//m;
+           } elsif ($changes->{$fname} =~ m/^.* \Q$file\E$(?:)\n/m) {
+               # not in archive, but it's here in the .changes
+           } else {
+               my $dsc_data = getfield $dsc, $fname;
+               $dsc_data =~ m/^(.* \Q$file\E$)\n/m or die "$dsc_data $file ?";
+               my $extra = $1;
+               $extra =~ s/ \d+ /$&$placementinfo /
+                   or die "$fname $extra >$dsc_data< ?"
+                   if $fname eq 'Files';
+               $changes->{$fname} .= "\n". $extra;
+               $changed{$file} = "added";
+           }
+       }
+    }
+    if (%changed) {
+       foreach my $file (keys %changed) {
+           progress sprintf
+               "edited .changes for archive .orig contents: %s %s",
+               $changed{$file}, $file;
+       }
+       my $chtmp = "$changesfile.tmp";
+       $changes->save($chtmp);
+       if (act_local()) {
+           rename $chtmp,$changesfile or die "$changesfile $!";
+       } else {
+           progress "[new .changes left in $changesfile]";
+       }
+    } else {
+       progress "$changesfile already has appropriate .orig(s) (if any)";
+    }
+}
+
+sub make_commit ($) {
+    my ($file) = @_;
+    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';
+    $author =~ s#,.*##ms;
+    my $date = cmdoutput qw(date), '+%s %z', qw(-d), getfield($clogp,'Date');
+    my $authline = "$author $date";
+    $authline =~ m/$git_authline_re/o or
+       fail "unexpected commit author line format \`$authline'".
+       " (was generated from changelog Maintainer field)";
+    return ($1,$2,$3) if wantarray;
+    return $authline;
+}
+
+sub vendor_patches_distro ($$) {
+    my ($checkdistro, $what) = @_;
+    return unless defined $checkdistro;
+
+    my $series = "debian/patches/\L$checkdistro\E.series";
+    printdebug "checking for vendor-specific $series ($what)\n";
+
+    if (!open SERIES, "<", $series) {
+       die "$series $!" unless $!==ENOENT;
+       return;
+    }
+    while (<SERIES>) {
+       next unless m/\S/;
+       next if m/^\s+\#/;
+
+       print STDERR <<END;
+
+Unfortunately, this source package uses a feature of dpkg-source where
+the same source package unpacks to different source code on different
+distros.  dgit cannot safely operate on such packages on affected
+distros, because the meaning of source packages is not stable.
+
+Please ask the distro/maintainer to remove the distro-specific series
+files and use a different technique (if necessary, uploading actually
+different packages, if different distros are supposed to have
+different code).
+
+END
+       fail "Found active distro-specific series file for".
+           " $checkdistro ($what): $series, cannot continue";
+    }
+    die "$series $!" if SERIES->error;
+    close SERIES;
+}
+
+sub check_for_vendor_patches () {
+    # This dpkg-source feature doesn't seem to be documented anywhere!
+    # But it can be found in the changelog (reformatted):
+
+    #   commit  4fa01b70df1dc4458daee306cfa1f987b69da58c
+    #   Author: Raphael Hertzog <hertzog@debian.org>
+    #   Date: Sun  Oct  3  09:36:48  2010 +0200
+
+    #   dpkg-source: correctly create .pc/.quilt_series with alternate
+    #   series files
+    #   
+    #   If you have debian/patches/ubuntu.series and you were
+    #   unpacking the source package on ubuntu, quilt was still
+    #   directed to debian/patches/series instead of
+    #   debian/patches/ubuntu.series.
+    #   
+    #   debian/changelog                        |    3 +++
+    #   scripts/Dpkg/Source/Package/V3/quilt.pm |    4 +++-
+    #   2 files changed, 6 insertions(+), 1 deletion(-)
+
+    use Dpkg::Vendor;
+    vendor_patches_distro($ENV{DEB_VENDOR}, "DEB_VENDOR");
+    vendor_patches_distro(Dpkg::Vendor::get_current_vendor(),
+                        "Dpkg::Vendor \`current vendor'");
+    vendor_patches_distro(access_basedistro(),
+                         "(base) distro being accessed");
+    vendor_patches_distro(access_nomdistro(),
+                         "(nominal) distro being accessed");
+}
+
+sub generate_commits_from_dsc () {
+    # See big comment in fetch_from_archive, below.
+    # See also README.dsc-import.
+    prep_ud();
+    changedir $ud;
+
+    my @dfi = dsc_files_info();
+    foreach my $fi (@dfi) {
+       my $f = $fi->{Filename};
+       die "$f ?" if $f =~ m#/|^\.|\.dsc$|\.tmp$#;
+
+       printdebug "considering linking $f: ";
+
+       link_ltarget "../../../../$f", $f
+           or ((printdebug "($!) "), 0)
+           or $!==&ENOENT
+           or die "$f $!";
+
+       printdebug "linked.\n";
+
+       complete_file_from_dsc('.', $fi)
+           or next;
+
+       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 = upstreamversion $dsc->{version};
+    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";
+       remove_stray_gits($f);
+       mktree_in_ud_here();
+       
+       my ($tree) = git_add_write_tree();
+       my $tentries = cmdoutput @git, qw(ls-tree -z), $tree;
+       if ($tentries =~ m/^\d+ tree (\w+)\t[^\000]+\000$/s) {
+           $tree = $1;
+           printdebug "one subtree $1\n";
+       } else {
+           printdebug "multiple subtrees\n";
+       }
+       changedir "..";
+       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("source package");
+    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';
+       $dappliedtree = git_add_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 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
+
+[dgit import $treeimporthow $package $cversion]
+END
+
+    close C or die $!;
+    my $rawimport_hash = make_commit qw(../commit.tmp);
+
+    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];
+
+       my $path = $ENV{PATH} or die;
+
+       foreach my $use_absurd (qw(0 1)) {
+           runcmd @git, qw(checkout -q unpa);
+           runcmd @git, qw(update-ref -d refs/heads/patch-queue/unpa);
+           local $ENV{PATH} = $path;
+           if ($use_absurd) {
+               chomp $@;
+               progress "warning: $@";
+               $path = "$absurdity:$path";
+               progress "$us: trying slow absurd-git-apply...";
+               rename "../../gbp-pq-output","../../gbp-pq-output.0"
+                   or $!==ENOENT
+                   or die $!;
+           }
+           eval {
+               die "forbid absurd git-apply\n" if $use_absurd
+                   && forceing [qw(import-gitapply-no-absurd)];
+               die "only absurd git-apply!\n" if !$use_absurd
+                   && forceing [qw(import-gitapply-absurd)];
+
+               local $ENV{DGIT_ABSURD_DEBUG} = $debuglevel if $use_absurd;
+               local $ENV{PATH} = $path                    if $use_absurd;
+
+               my @showcmd = (gbp_pq, qw(import));
+               my @realcmd = shell_cmd
+                   'exec >/dev/null 2>>../../gbp-pq-output', @showcmd;
+               debugcmd "+",@realcmd;
+               if (system @realcmd) {
+                   die +(shellquote @showcmd).
+                       " failed: ".
+                       failedcmd_waitstatus()."\n";
+               }
+
+               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;
+           };
+           last unless $@;
+       }
+       if ($@) {
+           { local $@; eval { runcmd qw(cat ../../gbp-pq-output); }; }
+           die $@;
+       }
+    }
+
+    progress "synthesised git commit from .dsc $cversion";
+
+    my $rawimport_mergeinput = {
+        Commit => $rawimport_hash,
+        Info => "Import of source package",
+    };
+    my @output = ($rawimport_mergeinput);
+
+    if ($lastpush_mergeinput) {
+       my $oldclogp = mergeinfo_getclogp($lastpush_mergeinput);
+       my $oversion = getfield $oldclogp, 'Version';
+       my $vcmp =
+           version_compare($oversion, $cversion);
+       if ($vcmp < 0) {
+           @output = ($rawimport_mergeinput, $lastpush_mergeinput,
+               { Message => <<END, ReverseParents => 1 });
+Record $package ($cversion) in archive suite $csuite
+END
+       } elsif ($vcmp > 0) {
+           print STDERR <<END or die $!;
+
+Version actually in archive:   $cversion (older)
+Last version pushed with dgit: $oversion (newer or same)
+$later_warning_msg
+END
+            @output = $lastpush_mergeinput;
+        } else {
+           # 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 '../../../..';
+    rmtree($ud);
+    return @output;
+}
+
+sub complete_file_from_dsc ($$) {
+    our ($dstdir, $fi) = @_;
+    # Ensures that we have, in $dir, the file $fi, with the correct
+    # contents.  (Downloading it from alongside $dscurl if necessary.)
+
+    my $f = $fi->{Filename};
+    my $tf = "$dstdir/$f";
+    my $downloaded = 0;
+
+    if (stat_exists $tf) {
+       progress "using existing $f";
+    } else {
+       printdebug "$tf does not exist, need to fetch\n";
+       my $furl = $dscurl;
+       $furl =~ s{/[^/]+$}{};
+       $furl .= "/$f";
+       die "$f ?" unless $f =~ m/^\Q${package}\E_/;
+       die "$f ?" if $f =~ m#/#;
+       runcmd_ordryrun_local @curl,qw(-f -o),$tf,'--',"$furl";
+       return 0 if !act_local();
+       $downloaded = 1;
+    }
+
+    open F, "<", "$tf" or die "$tf: $!";
+    $fi->{Digester}->reset();
+    $fi->{Digester}->addfile(*F);
+    F->error and die $!;
+    my $got = $fi->{Digester}->hexdigest();
+    $got eq $fi->{Hash} or
+       fail "file $f has hash $got but .dsc".
+           " demands hash $fi->{Hash} ".
+           ($downloaded ? "(got wrong file from archive!)"
+            : "(perhaps you should delete this file?)");
+
+    return 1;
+}
+
+sub ensure_we_have_orig () {
+    my @dfi = dsc_files_info();
+    foreach my $fi (@dfi) {
+       my $f = $fi->{Filename};
+       next unless is_orig_file_in_dsc($f, \@dfi);
+       complete_file_from_dsc('..', $fi)
+           or next;
+    }
+}
+
+sub git_fetch_us () {
+    # 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/$_" }
+       (quiltmode_splitbrain
+        ? (map { $_->('*',access_nomdistro) }
+           \&debiantag_new, \&debiantag_maintview)
+        : debiantags('*',access_nomdistro));
+    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.
+
+    printdebug "git_fetch_us specs @specs\n";
+
+    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 (;;) {
+       printdebug "git_fetch_us iteration $fetch_iteration\n";
+        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 {
+           !m/\*$/ && !exists $wantr{"refs/$_"} ? () :
+           "+refs/$_:".lrfetchrefs."/$_";
+       } @specs;
+
+       printdebug "git_fetch_us fspecs @fspecs\n";
+
+       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 @tagpats = debiantags('*',access_nomdistro);
+
+    git_for_each_ref([map { "refs/tags/$_" } @tagpats], sub {
+       my ($objid,$objtype,$fullrefname,$reftail) = @_;
+       printdebug "currently $fullrefname=$objid\n";
+       $here{$fullrefname} = $objid;
+    });
+    git_for_each_ref([map { lrfetchrefs."/tags/".$_ } @tagpats], sub {
+       my ($objid,$objtype,$fullrefname,$reftail) = @_;
+       my $lref = "refs".substr($fullrefname, length(lrfetchrefs));
+       printdebug "offered $lref=$objid\n";
+       if (!defined $here{$lref}) {
+           my @upd = (@git, qw(update-ref), $lref, $objid, '');
+           runcmd_ordryrun_local @upd;
+           lrfetchref_used $fullrefname;
+       } elsif ($here{$lref} eq $objid) {
+           lrfetchref_used $fullrefname;
+       } else {
+           print STDERR \
+               "Not updateting $lref from $here{$lref} to $objid.\n";
+       }
+    });
+}
+
+sub mergeinfo_getclogp ($) {
+    # Ensures thit $mi->{Clogp} exists and returns it
+    my ($mi) = @_;
+    $mi->{Clogp} = commit_getclogp($mi->{Commit});
+}
+
+sub mergeinfo_version ($) {
+    return getfield( (mergeinfo_getclogp $_[0]), 'Version' );
+}
+
+sub fetch_from_archive_record_1 ($) {
+    my ($hash) = @_;
+    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
+}
+
+sub fetch_from_archive_record_2 ($) {
+    my ($hash) = @_;
+    my @upd_cmd = (@git, qw(update-ref -m), 'dgit fetch', lrref(), $hash);
+    if (act_local()) {
+       cmdoutput @upd_cmd;
+    } else {
+       dryrun_report @upd_cmd;
+    }
+}
+
+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.
+    # If there is nothing in the archive, leaves lrref alone and
+    # returns undef.  git_fetch_us must have already been called.
+    get_archive_dsc();
+
+    if ($dsc) {
+       foreach my $field (@ourdscfield) {
+           $dsc_hash = $dsc->{$field};
+           last if defined $dsc_hash;
+       }
+       if (defined $dsc_hash) {
+           $dsc_hash =~ m/\w+/ or fail "invalid hash in .dsc \`$dsc_hash'";
+           $dsc_hash = $&;
+           progress "last upload to archive specified git hash";
+       } else {
+           progress "last upload to archive has NO git hash";
+       }
+    } else {
+       progress "no version available from the archive";
+    }
+
+    # 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 pseudo-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";
+    $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: $objid $fullrefname\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) {
+       ensure_we_have_orig();
+       if (!$lastpush_hash || $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.
+Commit referred to by archive: $dsc_hash
+Last version pushed with dgit: $lastpush_hash
+$later_warning_msg
+END
+           @mergeinputs = ($lastpush_mergeinput);
+       } else {
+           # 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) {
+       @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
+       @mergeinputs = ($lastpush_mergeinput);
+       print STDERR <<END or die $!;
+
+Package not found in the archive, but has allegedly been pushed using dgit.
+$later_warning_msg
+END
+    } else {
+       printdebug "nothing found!\n";
+       if (defined $skew_warning_vsn) {
+           print STDERR <<END or die $!;
+
+Warning: relevant archive skew detected.
+Archive allegedly contains $skew_warning_vsn
+But we were not able to obtain any version from the archive or git.
+
+END
+       }
+       unshift @end, $del_lrfetchrefs;
+       return undef;
+    }
+
+    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};
+    }
+    printdebug "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)')
+       if $lastpush_hash;
+    $chkff->($lastfetch_hash, 'local tracking tip (last fetch)');
+
+    fetch_from_archive_record_1($hash);
+
+    if (defined $skew_warning_vsn) {
+       mkpath '.git/dgit';
+       printdebug "SKEW CHECK WANT $skew_warning_vsn\n";
+       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) {
+           print STDERR <<END or die $!;
+
+Warning: archive skew detected.  Using the available version:
+Archive allegedly contains    $skew_warning_vsn
+We were able to obtain only   $got_vsn
+
+END
+       }
+    }
+
+    if ($lastfetch_hash ne $hash) {
+       fetch_from_archive_record_2($hash);
+    }
+
+    lrfetchref_used lrfetchref();
+
+    unshift @end, $del_lrfetchrefs;
+    return $hash;
+}
+
+sub set_local_git_config ($$) {
+    my ($k, $v) = @_;
+    runcmd @git, qw(config), $k, $v;
+}
+
+sub setup_mergechangelogs (;$) {
+    my ($always) = @_;
+    return unless $always || access_cfg_bool(1, 'setup-mergechangelogs');
+
+    my $driver = 'dpkg-mergechangelogs';
+    my $cb = "merge.$driver";
+    my $attrs = '.git/info/attributes';
+    ensuredir '.git/info';
+
+    open NATTRS, ">", "$attrs.new" or die "$attrs.new $!";
+    if (!open ATTRS, "<", $attrs) {
+       $!==ENOENT or die "$attrs: $!";
+    } else {
+       while (<ATTRS>) {
+           chomp;
+           next if m{^debian/changelog\s};
+           print NATTRS $_, "\n" or die $!;
+       }
+       ATTRS->error and die $!;
+       close ATTRS;
+    }
+    print NATTRS "debian/changelog merge=$driver\n" or die $!;
+    close NATTRS;
+
+    set_local_git_config "$cb.name", 'debian/changelog merge driver';
+    set_local_git_config "$cb.driver", 'dpkg-mergechangelogs -m %O %A %B %A';
+
+    rename "$attrs.new", "$attrs" or die "$attrs: $!";
+}
+
+sub setup_useremail (;$) {
+    my ($always) = @_;
+    return unless $always || access_cfg_bool(1, 'setup-useremail');
+
+    my $setup = sub {
+       my ($k, $envvar) = @_;
+       my $v = access_cfg("user-$k", 'RETURN-UNDEF') // $ENV{$envvar};
+       return unless defined $v;
+       set_local_git_config "user.$k", $v;
+    };
+
+    $setup->('email', 'DEBEMAIL');
+    $setup->('name', 'DEBFULLNAME');
+}
+
+sub 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();
+}
+
+sub multisuite_suite_child ($$$) {
+    my ($tsuite, $merginputs, $fn) = @_;
+    # in child, sets things up, calls $fn->(), and returns undef
+    # in parent, returns canonical suite name for $tsuite
+    my $canonsuitefh = IO::File::new_tmpfile;
+    my $pid = fork // die $!;
+    if (!$pid) {
+       $isuite = $tsuite;
+       $us .= " [$isuite]";
+       $debugprefix .= " ";
+       progress "fetching $tsuite...";
+       canonicalise_suite();
+       print $canonsuitefh $csuite, "\n" or die $!;
+       close $canonsuitefh or die $!;
+       $fn->();
+       return undef;
+    }
+    waitpid $pid,0 == $pid or die $!;
+    fail "failed to obtain $tsuite: ".waitstatusmsg() if $? && $?!=256*4;
+    seek $canonsuitefh,0,0 or die $!;
+    local $csuite = <$canonsuitefh>;
+    die $! unless defined $csuite && chomp $csuite;
+    if ($? == 256*4) {
+       printdebug "multisuite $tsuite missing\n";
+       return $csuite;
+    }
+    printdebug "multisuite $tsuite ok (canon=$csuite)\n";
+    push @$merginputs, {
+        Ref => lrref,
+        Info => $csuite,
+    };
+    return $csuite;
+}
+
+sub fork_for_multisuite ($) {
+    my ($before_fetch_merge) = @_;
+    # if nothing unusual, just returns ''
+    #
+    # if multisuite:
+    # returns 0 to caller in child, to do first of the specified suites
+    # in child, $csuite is not yet set
+    #
+    # returns 1 to caller in parent, to finish up anything needed after
+    # in parent, $csuite is set to canonicalised portmanteau
+
+    my $org_isuite = $isuite;
+    my @suites = split /\,/, $isuite;
+    return '' unless @suites > 1;
+    printdebug "fork_for_multisuite: @suites\n";
+
+    my @mergeinputs;
+
+    my $cbasesuite = multisuite_suite_child($suites[0], \@mergeinputs,
+                                           sub { });
+    return 0 unless defined $cbasesuite;
+
+    fail "package $package missing in (base suite) $cbasesuite"
+       unless @mergeinputs;
+
+    my @csuites = ($cbasesuite);
+
+    $before_fetch_merge->();
+
+    foreach my $tsuite (@suites[1..$#suites]) {
+       my $csubsuite = multisuite_suite_child($tsuite, \@mergeinputs,
+                                              sub {
+            @end = ();
+            fetch();
+           exit 0;
+       });
+       # xxx collecte the ref here
+
+       $csubsuite =~ s/^\Q$cbasesuite\E-/-/;
+       push @csuites, $csubsuite;
+    }
+
+    foreach my $mi (@mergeinputs) {
+       my $ref = git_get_ref $mi->{Ref};
+       die "$mi->{Ref} ?" unless length $ref;
+       $mi->{Commit} = $ref;
+    }
+
+    $csuite = join ",", @csuites;
+
+    my $previous = git_get_ref lrref;
+    if ($previous) {
+       unshift @mergeinputs, {
+            Commit => $previous,
+            Info => "local combined tracking branch",
+            Warning =>
+ "archive seems to have rewound: local tracking branch is ahead!",
+        };
+    }
+
+    foreach my $ix (0..$#mergeinputs) {
+       $mergeinputs[$ix]{Index} = $ix;
+    }
+
+    @mergeinputs = sort {
+       -version_compare(mergeinfo_version $a,
+                        mergeinfo_version $b) # highest version first
+           or
+       $a->{Index} <=> $b->{Index}; # earliest in spec first
+    } @mergeinputs;
+
+    my @needed;
+
+  NEEDED:
+    foreach my $mi (@mergeinputs) {
+       printdebug "multisuite merge check $mi->{Info}\n";
+       foreach my $previous (@needed) {
+           next unless is_fast_fwd $mi->{Commit}, $previous->{Commit};
+           printdebug "multisuite merge un-needed $previous->{Info}\n";
+           next NEEDED;
+       }
+       push @needed, $mi;
+       printdebug "multisuite merge this-needed\n";
+       $mi->{Character} = '+';
+    }
+
+    $needed[0]{Character} = '*';
+
+    my $output = $needed[0]{Commit};
+
+    if (@needed > 1) {
+       printdebug "multisuite merge nontrivial\n";
+       my $tree = cmdoutput qw(git rev-parse), $needed[0]{Commit}.':';
+
+       my $commit = "tree $tree\n";
+       my $msg = "Combine archive branches $csuite [dgit]\n\n".
+           "Input branches:\n";
+
+       foreach my $mi (sort { $a->{Index} <=> $b->{Index} } @mergeinputs) {
+           printdebug "multisuite merge include $mi->{Info}\n";
+           $mi->{Character} //= ' ';
+           $commit .= "parent $mi->{Commit}\n";
+           $msg .= sprintf " %s  %-25s %s\n",
+               $mi->{Character},
+               (mergeinfo_version $mi),
+               $mi->{Info};
+       }
+       my $authline = clogp_authline mergeinfo_getclogp $needed[0];
+       $msg .= "\nKey\n".
+           " * marks the highest version branch, which choose to use\n".
+           " + marks each branch which was not already an ancestor\n\n".
+           "[dgit multi-suite $csuite]\n";
+       $commit .=
+           "author $authline\n".
+           "committer $authline\n\n";
+       $output = make_commit_text $commit.$msg;
+       printdebug "multisuite merge generated $output\n";
+    }
+
+    fetch_from_archive_record_1($output);
+    fetch_from_archive_record_2($output);
+
+    progress "calculated combined tracking suite $csuite";
+
+    return 1;
+}
+
+sub clone_set_head () {
+    open H, "> .git/HEAD" or die $!;
+    print H "ref: ".lref()."\n" or die $!;
+    close H or die $!;
+}
+sub clone_finish ($) {
+    my ($dstdir) = @_;
+    runcmd @git, qw(reset --hard), lrref();
+    runcmd qw(bash -ec), <<'END';
+        set -o pipefail
+        git ls-tree -r --name-only -z HEAD | \
+        xargs -0r touch -h -r . --
+END
+    printdone "ready for work in $dstdir";
+}
+
+sub clone ($) {
+    my ($dstdir) = @_;
+    badusage "dry run makes no sense with clone" unless act_local();
+
+    my $multi_fetched = fork_for_multisuite(sub {
+        printdebug "multi clone before fetch merge\n";
+        changedir $dstdir;
+    });
+    if ($multi_fetched) {
+        printdebug "multi clone after fetch merge\n";
+       clone_set_head();
+       clone_finish($dstdir);
+       exit 0;
+    }
+    printdebug "clone main body\n";
+
+    canonicalise_suite();
+    my $hasgit = check_for_git();
+    mkdir $dstdir or fail "create \`$dstdir': $!";
+    changedir $dstdir;
+    runcmd @git, qw(init -q);
+    clone_set_head();
+    my $giturl = access_giturl(1);
+    if (defined $giturl) {
+       runcmd @git, qw(remote add), 'origin', $giturl;
+    }
+    if ($hasgit) {
+       progress "fetching existing git history";
+       git_fetch_us();
+       runcmd_ordryrun_local @git, qw(fetch origin);
+    } else {
+       progress "starting new git history";
+    }
+    fetch_from_archive() or no_such_package;
+    my $vcsgiturl = $dsc->{'Vcs-Git'};
+    if (length $vcsgiturl) {
+       $vcsgiturl =~ s/\s+-b\s+\S+//g;
+       runcmd @git, qw(remote add vcs-git), $vcsgiturl;
+    }
+    setup_new_tree();
+    clone_finish($dstdir);
+}
+
+sub fetch () {
+    canonicalise_suite();
+    if (check_for_git()) {
+       git_fetch_us();
+    }
+    fetch_from_archive() or no_such_package();
+    printdone "fetched into ".lrref();
+}
+
+sub pull () {
+    my $multi_fetched = fork_for_multisuite(sub { });
+    fetch() unless $multi_fetched; # parent
+    return if $multi_fetched eq '0'; # child
+    runcmd_ordryrun_local @git, qw(merge -m),"Merge from $csuite [dgit]",
+        lrref();
+    printdone "fetched to ".lrref()." and merged into HEAD";
+}
+
+sub check_not_dirty () {
+    foreach my $f (qw(local-options local-patch-header)) {
+       if (stat_exists "debian/source/$f") {
+           fail "git tree contains debian/source/$f";
+       }
+    }
+
+    return if $ignoredirty;
+
+    my @cmd = (@git, qw(diff --quiet HEAD));
+    debugcmd "+",@cmd;
+    $!=0; $?=-1; system @cmd;
+    return if !$?;
+    if ($?==256) {
+       fail "working tree is dirty (does not match HEAD)";
+    } else {
+       failedcmd @cmd;
+    }
+}
+
+sub commit_admin ($) {
+    my ($m) = @_;
+    progress "$m";
+    runcmd_ordryrun_local @git, qw(commit -m), $m;
+}
+
+sub commit_quilty_patch () {
+    my $output = cmdoutput @git, qw(status --porcelain);
+    my %adds;
+    foreach my $l (split /\n/, $output) {
+       next unless $l =~ m/\S/;
+       if ($l =~ m{^(?:\?\?| M) (.pc|debian/patches)}) {
+           $adds{$1}++;
+       }
+    }
+    delete $adds{'.pc'}; # if there wasn't one before, don't add it
+    if (!%adds) {
+       progress "nothing quilty to commit, ok.";
+       return;
+    }
+    my @adds = map { s/[][*?\\]/\\$&/g; $_; } sort keys %adds;
+    runcmd_ordryrun_local @git, qw(add -f), @adds;
+    commit_admin <<END
+Commit Debian 3.0 (quilt) metadata
+
+[dgit ($our_version) quilt-fixup]
+END
+}
+
+sub get_source_format () {
+    my %options;
+    if (open F, "debian/source/options") {
+       while (<F>) {
+           next if m/^\s*\#/;
+           next unless m/\S/;
+           s/\s+$//; # ignore missing final newline
+           if (m/\s*\#\s*/) {
+               my ($k, $v) = ($`, $'); #');
+               $v =~ s/^"(.*)"$/$1/;
+               $options{$k} = $v;
+           } else {
+               $options{$_} = 1;
+           }
+       }
+       F->error and die $!;
+       close F;
+    } else {
+       die $! unless $!==&ENOENT;
+    }
+
+    if (!open F, "debian/source/format") {
+       die $! unless $!==&ENOENT;
+       return '';
+    }
+    $_ = <F>;
+    F->error and die $!;
+    chomp;
+    return ($_, \%options);
+}
+
+sub madformat_wantfixup ($) {
+    my ($format) = @_;
+    return 0 unless $format eq '3.0 (quilt)';
+    our $quilt_mode_warned;
+    if ($quilt_mode eq 'nocheck') {
+       progress "Not doing any fixup of \`$format' due to".
+           " ----no-quilt-fixup or --quilt=nocheck"
+           unless $quilt_mode_warned++;
+       return 0;
+    }
+    progress "Format \`$format', need to check/update patch stack"
+       unless $quilt_mode_warned++;
+    return 1;
+}
+
+sub maybe_split_brain_save ($$$) {
+    my ($headref, $dgitview, $msg) = @_;
+    # => message fragment "$saved" describing disposition of $dgitview
+    return "commit id $dgitview" unless defined $split_brain_save;
+    my @cmd = (shell_cmd "cd ../../../..",
+              @git, qw(update-ref -m),
+              "dgit --dgit-view-save $msg HEAD=$headref",
+              $split_brain_save, $dgitview);
+    runcmd @cmd;
+    return "and left in $split_brain_save";
+}
+
+# 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
+committer $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'
+    #
+
+    return $dgitview unless defined $archive_hash;
+
+    printdebug "splitbrain_pseudomerge...\n";
+
+    my $i_arch_v = pseudomerge_version_check($clogp, $archive_hash);
+
+    if (!defined $overwrite_version) {
+       progress "Checking that HEAD inciudes all changes in archive...";
+    }
+
+    return $dgitview if is_fast_fwd $archive_hash, $dgitview;
+
+    if (defined $overwrite_version) {
+    } elsif (!eval {
+       my $t_dep14 = debiantag_maintview $i_arch_v->[0], access_nomdistro;
+       my $i_dep14 = infopair_lrf_tag_lookup($t_dep14, "maintainer view tag");
+       my $t_dgit = debiantag_new $i_arch_v->[0], access_nomdistro;
+       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);
+       infopair_cond_ff($i_dep14, [ $maintview, 'HEAD' ]);
+       1;
+    }) {
+       print STDERR <<END;
+$us: check failed (maybe --overwrite is needed, consult documentation)
+END
+       die "$@";
+    }
+
+    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 $i_arch_v->[0]
+END_OVERWR
+Make fast forward from $i_arch_v->[0]
+END_MAKEFF
+
+    maybe_split_brain_save $maintview, $r, "pseudomerge";
+
+    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);
+
+    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) = @_;
+
+    my $clogp = Dpkg::Control::Hash->new();
+    $clogp->load($clogpfn) or die;
+
+    my $clogpackage = getfield $clogp, 'Source';
+    $package //= $clogpackage;
+    fail "-p specified $package but changelog specified $clogpackage"
+       unless $package eq $clogpackage;
+    my $cversion = getfield $clogp, 'Version';
+    my $tag = debiantag($cversion, access_nomdistro);
+    runcmd @git, qw(check-ref-format), $tag;
+
+    my $dscfn = dscfn($cversion);
+
+    return ($clogp, $cversion, $dscfn);
+}
+
+sub push_parse_dsc ($$$) {
+    my ($dscfn,$dscfnwhat, $cversion) = @_;
+    $dsc = parsecontrol($dscfn,$dscfnwhat);
+    my $dversion = getfield $dsc, 'Version';
+    my $dscpackage = getfield $dsc, 'Source';
+    ($dscpackage eq $package && $dversion eq $cversion) or
+       fail "$dscfn is for $dscpackage $dversion".
+           " but debian/changelog is for $package $cversion";
+}
+
+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',
+        };
+    } elsif ($dodep14tag eq 'no' ? 0
+            : $dodep14tag eq 'want' ? access_cfg_tagformats_can_splitbrain
+            : $dodep14tag eq 'always'
+            ? (access_cfg_tagformats_can_splitbrain or fail <<END)
+--dep14tag-always (or equivalent in config) means server must support
+ both "new" and "maint" tag formats, but config says it doesn't.
+END
+           : die "$dodep14tag ?") {
+       push @tagwants, {
+           TagFn => \&debiantag_maintview,
+           Objid => $dgithead,
+           TfSuffix => '-dgit',
+           View => 'dgit',
+        };
+    };
+    foreach my $tw (@tagwants) {
+       $tw->{Tag} = $tw->{TagFn}($cversion, access_nomdistro);
+       $tw->{Tfn} = sub { $tfbase.$tw->{TfSuffix}.$_[0]; };
+    }
+    printdebug 'push_tagwants: ', Dumper(\@_, \@tagwants);
+    return @tagwants;
+}
+
+sub push_mktags ($$ $$ $) {
+    my ($clogp,$dscfn,
+       $changesfile,$changesfilewhat,
+        $tagwants) = @_;
+
+    die unless $tagwants->[0]{View} eq 'dgit';
+
+    $dsc->{$ourdscfield[0]} = $tagwants->[0]{Objid};
+    $dsc->save("$dscfn.tmp") or die $!;
+
+    my $changes = parsecontrol($changesfile,$changesfilewhat);
+    foreach my $field (qw(Source Distribution Version)) {
+       $changes->{$field} eq $clogp->{$field} or
+           fail "changes field $field \`$changes->{$field}'".
+               " does not match changelog \`$clogp->{$field}'";
+    }
+
+    my $cversion = getfield $clogp, 'Version';
+    my $clogsuite = getfield $clogp, 'Distribution';
+
+    # We make the git tag by hand because (a) that makes it easier
+    # to control the "tagger" (b) we can do remote signing
+    my $authline = clogp_authline $clogp;
+    my $delibs = join(" ", "",@deliberatelies);
+    my $declaredistro = access_nomdistro();
+
+    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
+
+END
+       if ($tw->{View} eq 'dgit') {
+           print TO <<END or die $!;
+$package release $cversion for $clogsuite ($csuite) [dgit]
+[dgit distro=$declaredistro$delibs]
+END
+           foreach my $ref (sort keys %previously) {
+               print TO <<END or die $!;
+[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 $!;
+
+       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;
+    };
+
+    my @r = map { $mktag->($_); } @$tagwants;
+    return @r;
+}
+
+sub sign_changes ($) {
+    my ($changesfile) = @_;
+    if ($sign) {
+       my @debsign_cmd = @debsign;
+       push @debsign_cmd, "-k$keyid" if defined $keyid;
+       push @debsign_cmd, "-p$gpg[0]" if $gpg[0] ne 'gpg';
+       push @debsign_cmd, $changesfile;
+       runcmd_ordryrun @debsign_cmd;
+    }
+}
+
+sub dopush () {
+    printdebug "actually entering push\n";
+
+    supplementary_message(<<'END');
+Push failed, while checking state of the archive.
+You can retry the push, after fixing the problem, if you like.
+END
+    if (check_for_git()) {
+       git_fetch_us();
+    }
+    my $archive_hash = fetch_from_archive();
+    if (!$archive_hash) {
+       $new_package or
+           fail "package appears to be new in this suite;".
+               " if this is intentional, use --new";
+    }
+
+    supplementary_message(<<'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
+    select_tagformat();
+
+    my $clogpfn = ".git/dgit/changelog.822.tmp";
+    runcmd shell_cmd "exec >$clogpfn", qw(dpkg-parsechangelog);
+
+    responder_send_file('parsed-changelog', $clogpfn);
+
+    my ($clogp, $cversion, $dscfn) =
+       push_parse_changelog("$clogpfn");
+
+    my $dscpath = "$buildproductsdir/$dscfn";
+    stat_exists $dscpath or
+       fail "looked for .dsc $dscpath, but $!;".
+           " maybe you forgot to build";
+
+    responder_send_file('dsc', $dscpath);
+
+    push_parse_dsc($dscpath, $dscfn, $cversion);
+
+    my $format = getfield $dsc, 'Format';
+    printdebug "format $format\n";
+
+    my $actualhead = git_rev_parse('HEAD');
+    my $dgithead = $actualhead;
+    my $maintviewhead = undef;
+
+    my $upstreamversion = upstreamversion $clogp->{Version};
+
+    if (madformat_wantfixup($format)) {
+       # user might have not used dgit build, so maybe do this now:
+       if (quiltmode_splitbrain()) {
+           changedir $ud;
+           quilt_make_fake_dsc($upstreamversion);
+           my $cachekey;
+           ($dgithead, $cachekey) =
+               quilt_check_splitbrain_cache($actualhead, $upstreamversion);
+           $dgithead or fail
+ "--quilt=$quilt_mode but no cached dgit view:
+ perhaps tree changed since dgit build[-source] ?";
+           $split_brain = 1;
+           $dgithead = splitbrain_pseudomerge($clogp,
+                                              $actualhead, $dgithead,
+                                              $archive_hash);
+           $maintviewhead = $actualhead;
+           changedir '../../../..';
+           prep_ud(); # so _only_subdir() works, below
+       } else {
+           commit_quilty_patch();
+       }
+    }
+
+    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, $dgithead)) {
+           # ok
+       } elsif (deliberately_not_fast_forward) {
+           $forceflag = '+';
+       } else {
+           fail "dgit push: HEAD is not a descendant".
+               " of the archive's version.\n".
+               "To overwrite the archive's contents,".
+               " pass --overwrite[=VERSION].\n".
+               "To rewind history, if permitted by the archive,".
+               " use --deliberately-not-fast-forward.";
+       }
+    }
+
+    changedir $ud;
+    progress "checking that $dscfn corresponds to HEAD";
+    runcmd qw(dpkg-source -x --),
+        $dscpath =~ m#^/# ? $dscpath : "../../../$dscpath";
+    my ($tree,$dir) = mktree_in_ud_from_only_subdir("source package");
+    check_for_vendor_patches() if madformat($dsc->{format});
+    changedir '../../../..';
+    my @diffcmd = (@git, qw(diff --quiet), $tree, $dgithead);
+    debugcmd "+",@diffcmd;
+    $!=0; $?=-1;
+    my $r = system @diffcmd;
+    if ($r) {
+       if ($r==256) {
+           my $diffs = cmdoutput @git, qw(diff --stat), $tree, $dgithead;
+           fail <<END
+HEAD specifies a different tree to $dscfn:
+$diffs
+Perhaps you forgot to build.  Or perhaps there is a problem with your
+ source tree (see dgit(7) for some hints).  To see a full diff, run
+   git diff $tree HEAD
+END
+       } else {
+           failedcmd @diffcmd;
+       }
+    }
+    if (!$changesfile) {
+       my $pat = changespat $cversion;
+       my @cs = glob "$buildproductsdir/$pat";
+       fail "failed to find unique changes file".
+           " (looked for $pat in $buildproductsdir);".
+           " perhaps you need to use dgit -C"
+           unless @cs==1;
+       ($changesfile) = @cs;
+    } else {
+       $changesfile = "$buildproductsdir/$changesfile";
+    }
+
+    # Check that changes and .dsc agree enough
+    $changesfile =~ m{[^/]*$};
+    my $changes = parsecontrol($changesfile,$&);
+    files_compare_inputs($dsc, $changes)
+       unless forceing [qw(dsc-changes-mismatch)];
+
+    # Perhaps adjust .dsc to contain right set of origs
+    changes_update_origs_from_dsc($dsc, $changes, $upstreamversion,
+                                 $changesfile)
+       unless forceing [qw(changes-origs-exactly)];
+
+    # Checks complete, we're going to try and go ahead:
+
+    responder_send_file('changes',$changesfile);
+    responder_send_command("param head $dgithead");
+    responder_send_command("param csuite $csuite");
+    responder_send_command("param tagformat $tagformat");
+    if (defined $maintviewhead) {
+       die unless ($protovsn//4) >= 4;
+       responder_send_command("param maint-view $maintviewhead");
+    }
+
+    if (deliberately_not_fast_forward) {
+       git_for_each_ref(lrfetchrefs, sub {
+           my ($objid,$objtype,$lrfetchrefname,$reftail) = @_;
+           my $rrefname= substr($lrfetchrefname, length(lrfetchrefs) + 1);
+           responder_send_command("previously $rrefname=$objid");
+           $previously{$rrefname} = $objid;
+       });
+    }
+
+    my @tagwants = push_tagwants($cversion, $dgithead, $maintviewhead,
+                                ".git/dgit/tag");
+    my @tagobjfns;
+
+    supplementary_message(<<'END');
+Push failed, while signing the tag.
+You can retry the push, after fixing the problem, if you like.
+END
+    # If we manage to sign but fail to record it anywhere, it's fine.
+    if ($we_are_responder) {
+       @tagobjfns = map { $_->{Tfn}('.signed-tmp') } @tagwants;
+       responder_receive_files('signed-tag', @tagobjfns);
+    } else {
+       @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
+
+    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.
+If you want to try again, you should use a new version number.
+END
+    if (!check_for_git()) {
+       create_remote_git_repo();
+    }
+
+    my @pushrefs = $forceflag.$dgithead.":".rrref();
+    foreach my $tw (@tagwants) {
+       push @pushrefs, $forceflag."refs/tags/$tw->{Tag}";
+    }
+
+    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');
+Push failed, while obtaining signatures on the .changes and .dsc.
+If it was just that the signature failed, you may try again by using
+debsign by hand to sign the changes
+   $changesfile
+and then dput to complete the upload.
+If you need to change the package, you must use a new version number.
+END
+    if ($we_are_responder) {
+       my $dryrunsuffix = act_local() ? "" : ".tmp";
+       responder_receive_files('signed-dsc-changes',
+                               "$dscpath$dryrunsuffix",
+                               "$changesfile$dryrunsuffix");
+    } else {
+       if (act_local()) {
+           rename "$dscpath.tmp",$dscpath or die "$dscfn $!";
+       } else {
+           progress "[new .dsc left in $dscpath.tmp]";
+       }
+       sign_changes $changesfile;
+    }
+
+    supplementary_message(<<END);
+Push failed, while uploading package(s) to the archive server.
+You can retry the upload of exactly these same files with dput of:
+  $changesfile
+If that .changes file is broken, you will need to use a new version
+number for your next attempt at the upload.
+END
+    my $host = access_cfg('upload-host','RETURN-UNDEF');
+    my @hostarg = defined($host) ? ($host,) : ();
+    runcmd_ordryrun @dput, @hostarg, $changesfile;
+    printdone "pushed and uploaded $cversion";
+
+    supplementary_message('');
+    responder_send_command("complete");
+}
+
+sub cmd_clone {
+    parseopts();
+    notpushing();
+    my $dstdir;
+    badusage "-p is not allowed with clone; specify as argument instead"
+       if defined $package;
+    if (@ARGV==1) {
+       ($package) = @ARGV;
+    } elsif (@ARGV==2 && $ARGV[1] =~ m#^\w#) {
+       ($package,$isuite) = @ARGV;
+    } elsif (@ARGV==2 && $ARGV[1] =~ m#^[./]#) {
+       ($package,$dstdir) = @ARGV;
+    } elsif (@ARGV==3) {
+       ($package,$isuite,$dstdir) = @ARGV;
+    } else {
+       badusage "incorrect arguments to dgit clone";
+    }
+    $dstdir ||= "$package";
+
+    if (stat_exists $dstdir) {
+       fail "$dstdir already exists";
+    }
+
+    my $cwd_remove;
+    if ($rmonerror && !$dryrun_level) {
+       $cwd_remove= getcwd();
+       unshift @end, sub { 
+           return unless defined $cwd_remove;
+           if (!chdir "$cwd_remove") {
+               return if $!==&ENOENT;
+               die "chdir $cwd_remove: $!";
+           }
+           printdebug "clone rmonerror removing $dstdir\n";
+           if (stat $dstdir) {
+               rmtree($dstdir) or die "remove $dstdir: $!\n";
+           } elsif (grep { $! == $_ }
+                    (ENOENT, ENOTDIR, EACCES, EPERM, ELOOP)) {
+           } else {
+               print STDERR "check whether to remove $dstdir: $!\n";
+           }
+       };
+    }
+
+    clone($dstdir);
+    $cwd_remove = undef;
+}
+
+sub branchsuite () {
+    my $branch = cmdoutput_errok @git, qw(symbolic-ref HEAD);
+    if ($branch =~ m#$lbranch_re#o) {
+       return $1;
+    } else {
+       return undef;
+    }
+}
+
+sub fetchpullargs () {
+    notpushing();
+    if (!defined $package) {
+       my $sourcep = parsecontrol('debian/control','debian/control');
+       $package = getfield $sourcep, 'Source';
+    }
+    if (@ARGV==0) {
+       $isuite = branchsuite();
+       if (!$isuite) {
+           my $clogp = parsechangelog();
+           $isuite = getfield $clogp, 'Distribution';
+       }
+    } elsif (@ARGV==1) {
+       ($isuite) = @ARGV;
+    } else {
+       badusage "incorrect arguments to dgit fetch or dgit pull";
+    }
+}
+
+sub cmd_fetch {
+    parseopts();
+    fetchpullargs();
+    my $multi_fetched = fork_for_multisuite(sub { });
+    exit 0 if $multi_fetched;
+    fetch();
+}
+
+sub cmd_pull {
+    parseopts();
+    fetchpullargs();
+    if (quiltmode_splitbrain()) {
+       my ($format, $fopts) = get_source_format();
+       madformat($format) and fail <<END
+dgit pull not yet supported in split view mode (--quilt=$quilt_mode)
+END
+    }
+    pull();
+}
+
+sub cmd_push {
+    parseopts();
+    pushing();
+    badusage "-p is not allowed with dgit push" if defined $package;
+    check_not_dirty();
+    my $clogp = parsechangelog();
+    $package = getfield $clogp, 'Source';
+    my $specsuite;
+    if (@ARGV==0) {
+    } elsif (@ARGV==1) {
+       ($specsuite) = (@ARGV);
+    } else {
+       badusage "incorrect arguments to dgit push";
+    }
+    $isuite = getfield $clogp, 'Distribution';
+    if ($new_package) {
+       local ($package) = $existing_package; # this is a hack
+       canonicalise_suite();
+    } else {
+       canonicalise_suite();
+    }
+    if (defined $specsuite &&
+       $specsuite ne $isuite &&
+       $specsuite ne $csuite) {
+           fail "dgit push: changelog specifies $isuite ($csuite)".
+               " but command line specifies $specsuite";
+    }
+    dopush();
+}
+
+#---------- remote commands' implementation ----------
+
+sub cmd_remote_push_build_host {
+    my ($nrargs) = shift @ARGV;
+    my (@rargs) = @ARGV[0..$nrargs-1];
+    @ARGV = @ARGV[$nrargs..$#ARGV];
+    die unless @rargs;
+    my ($dir,$vsnwant) = @rargs;
+    # vsnwant is a comma-separated list; we report which we have
+    # chosen in our ready response (so other end can tell if they
+    # offered several)
+    $debugprefix = ' ';
+    $we_are_responder = 1;
+    $us .= " (build host)";
+
+    pushing();
+
+    open PI, "<&STDIN" or die $!;
+    open STDIN, "/dev/null" or die $!;
+    open PO, ">&STDOUT" or die $!;
+    autoflush PO 1;
+    open STDOUT, ">&STDERR" or die $!;
+    autoflush STDOUT 1;
+
+    $vsnwant //= 1;
+    ($protovsn) = grep {
+       $vsnwant =~ m{^(?:.*,)?$_(?:,.*)?$}
+    } @rpushprotovsn_support;
+
+    fail "build host has dgit rpush protocol versions ".
+       (join ",", @rpushprotovsn_support).
+        " but invocation host has $vsnwant"
+       unless defined $protovsn;
+
+    responder_send_command("dgit-remote-push-ready $protovsn");
+    rpush_handle_protovsn_bothends();
+    changedir $dir;
+    &cmd_push;
+}
+
+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)
+
+sub rpush_handle_protovsn_bothends () {
+    if ($protovsn < 4) {
+       need_tagformat 'old', "rpush negotiated protocol $protovsn";
+    }
+    select_tagformat();
+}
+
+our $i_tmp;
+
+sub i_cleanup {
+    local ($@, $?);
+    my $report = i_child_report();
+    if (defined $report) {
+       printdebug "($report)\n";
+    } elsif ($i_child_pid) {
+       printdebug "(killing build host child $i_child_pid)\n";
+       kill 15, $i_child_pid;
+    }
+    if (defined $i_tmp && !defined $initiator_tempdir) {
+       changedir "/";
+       eval { rmtree $i_tmp; };
+    }
+}
+
+END { i_cleanup(); }
+
+sub i_method {
+    my ($base,$selector,@args) = @_;
+    $selector =~ s/\-/_/g;
+    { no strict qw(refs); &{"${base}_${selector}"}(@args); }
+}
+
+sub cmd_rpush {
+    pushing();
+    my $host = nextarg;
+    my $dir;
+    if ($host =~ m/^((?:[^][]|\[[^][]*\])*)\:/) {
+       $host = $1;
+       $dir = $'; #';
+    } else {
+       $dir = nextarg;
+    }
+    $dir =~ s{^-}{./-};
+    my @rargs = ($dir);
+    push @rargs, join ",", @rpushprotovsn_support;
+    my @rdgit;
+    push @rdgit, @dgit;
+    push @rdgit, @ropts;
+    push @rdgit, qw(remote-push-build-host), (scalar @rargs), @rargs;
+    push @rdgit, @ARGV;
+    my @cmd = (@ssh, $host, shellquote @rdgit);
+    debugcmd "+",@cmd;
+
+    if (defined $initiator_tempdir) {
+       rmtree $initiator_tempdir;
+       mkdir $initiator_tempdir, 0700 or die "$initiator_tempdir: $!";
+       $i_tmp = $initiator_tempdir;
+    } else {
+       $i_tmp = tempdir();
+    }
+    $i_child_pid = open2(\*RO, \*RI, @cmd);
+    changedir $i_tmp;
+    ($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+)(?: (.*))?$/;
+           ($1,$2);
+       };
+       i_method "i_resp", $icmd, $iargs;
+    }
+}
+
+sub i_resp_progress ($) {
+    my ($rhs) = @_;
+    my $msg = protocol_read_bytes \*RO, $rhs;
+    progress $msg;
+}
+
+sub i_resp_supplementary_message ($) {
+    my ($rhs) = @_;
+    $supplementary_message = protocol_read_bytes \*RO, $rhs;
+}
+
+sub i_resp_complete {
+    my $pid = $i_child_pid;
+    $i_child_pid = undef; # prevents killing some other process with same pid
+    printdebug "waiting for build host child $pid...\n";
+    my $got = waitpid $pid, 0;
+    die $! unless $got == $pid;
+    die "build host child failed $?" if $?;
+
+    i_cleanup();
+    printdebug "all done\n";
+    exit 0;
+}
+
+sub i_resp_file ($) {
+    my ($keyword) = @_;
+    my $localname = i_method "i_localname", $keyword;
+    my $localpath = "$i_tmp/$localname";
+    stat_exists $localpath and
+       badproto \*RO, "file $keyword ($localpath) twice";
+    protocol_receive_file \*RO, $localpath;
+    i_method "i_file", $keyword;
+}
+
+our %i_param;
+
+sub i_resp_param ($) {
+    $_[0] =~ m/^(\S+) (.*)$/ or badproto \*RO, "bad param spec";
+    $i_param{$1} = $2;
+}
+
+sub i_resp_previously ($) {
+    $_[0] =~ m#^(refs/tags/\S+)=(\w+)$#
+       or badproto \*RO, "bad previously spec";
+    my $r = system qw(git check-ref-format), $1;
+    die "bad previously ref spec ($r)" if $r;
+    $previously{$1} = $2;
+}
+
+our %i_wanted;
+
+sub i_resp_want ($) {
+    my ($keyword) = @_;
+    die "$keyword ?" if $i_wanted{$keyword}++;
+    my @localpaths = i_method "i_want", $keyword;
+    printdebug "[[  $keyword @localpaths\n";
+    foreach my $localpath (@localpaths) {
+       protocol_send_file \*RI, $localpath;
+    }
+    print RI "files-end\n" or die $!;
+}
+
+our ($i_clogp, $i_version, $i_dscfn, $i_changesfn);
+
+sub i_localname_parsed_changelog {
+    return "remote-changelog.822";
+}
+sub i_file_parsed_changelog {
+    ($i_clogp, $i_version, $i_dscfn) =
+       push_parse_changelog "$i_tmp/remote-changelog.822";
+    die if $i_dscfn =~ m#/|^\W#;
+}
+
+sub i_localname_dsc {
+    defined $i_dscfn or badproto \*RO, "dsc (before parsed-changelog)";
+    return $i_dscfn;
+}
+sub i_file_dsc { }
+
+sub i_localname_changes {
+    defined $i_dscfn or badproto \*RO, "dsc (before parsed-changelog)";
+    $i_changesfn = $i_dscfn;
+    $i_changesfn =~ s/\.dsc$/_dgit.changes/ or die;
+    return $i_changesfn;
+}
+sub i_file_changes { }
+
+sub i_want_signed_tag {
+    printdebug Dumper(\%i_param, $i_dscfn);
+    defined $i_param{'head'} && defined $i_dscfn && defined $i_clogp
+       && defined $i_param{'csuite'}
+       or badproto \*RO, "premature desire for signed-tag";
+    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;
+
+    my @tagwants = push_tagwants $i_version, $head, $maintview, "tag";
+
+    return
+       push_mktags $i_clogp, $i_dscfn,
+           $i_changesfn, 'remote changes',
+           \@tagwants;
+}
+
+sub i_want_signed_dsc_changes {
+    rename "$i_dscfn.tmp","$i_dscfn" or die "$i_dscfn $!";
+    sign_changes $i_changesfn;
+    return ($i_dscfn, $i_changesfn);
+}
+
+#---------- building etc. ----------
+
+our $version;
+our $sourcechanges;
+our $dscfn;
+
+#----- `3.0 (quilt)' handling -----
+
+our $fakeeditorenv = 'DGIT_FAKE_EDITOR_QUILT';
+
+sub quiltify_dpkg_commit ($$$;$) {
+    my ($patchname,$author,$msg, $xinfo) = @_;
+    $xinfo //= '';
+
+    mkpath '.git/dgit';
+    my $descfn = ".git/dgit/quilt-description.tmp";
+    open O, '>', $descfn or die "$descfn: $!";
+    $msg =~ s/\n+/\n\n/;
+    print O <<END or die $!;
+From: $author
+${xinfo}Subject: $msg
+---
+
+END
+    close O or die $!;
+
+    {
+       local $ENV{'EDITOR'} = cmdoutput qw(realpath --), $0;
+       local $ENV{'VISUAL'} = $ENV{'EDITOR'};
+       local $ENV{$fakeeditorenv} = cmdoutput qw(realpath --), $descfn;
+       runcmd @dpkgsource, qw(--commit --include-removal .), $patchname;
+    }
+}
+
+sub quiltify_trees_differ ($$;$$$) {
+    my ($x,$y,$finegrained,$ignorenamesr,$unrepres) = @_;
+    # 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
+    # if $unrepres is defined, array ref to which is appeneded
+    #  a list of unrepresentable changes (removals of upstream files
+    #  (as messages)
+    local $/=undef;
+    my @cmd = (@git, qw(diff-tree -z));
+    push @cmd, qw(--name-only) unless $unrepres;
+    push @cmd, qw(-r) if $finegrained || $unrepres;
+    push @cmd, $x, $y;
+    my $diffs= cmdoutput @cmd;
+    my $r = 0;
+    my @lmodes;
+    foreach my $f (split /\0/, $diffs) {
+       if ($unrepres && !@lmodes) {
+           @lmodes = $f =~ m/^\:(\w+) (\w+) \w+ \w+ / or die "$_ ?";
+           next;
+       }
+       my ($oldmode,$newmode) = @lmodes;
+       @lmodes = ();
+
+       next if $f =~ m#^debian(?:/.*)?$#s;
+
+       if ($unrepres) {
+           eval {
+               die "not a plain file\n"
+                   unless $newmode =~ m/^10\d{4}$/ ||
+                          $oldmode =~ m/^10\d{4}$/;
+               if ($oldmode =~ m/[^0]/ &&
+                   $newmode =~ m/[^0]/) {
+                   die "mode changed\n" if $oldmode ne $newmode;
+               } else {
+                   die "non-default mode\n"
+                       unless $newmode =~ m/^100644$/ ||
+                              $oldmode =~ m/^100644$/;
+               }
+           };
+           if ($@) {
+               local $/="\n"; chomp $@;
+               push @$unrepres, [ $f, "$@ ($oldmode->$newmode)" ];
+           }
+       }
+
+       my $isignore = $f =~ m#^(?:.*/)?.gitignore$#s;
+       $r |= $isignore ? 02 : 01;
+       $ignorenamesr->{$f}=1 if $ignorenamesr && $isignore;
+    }
+    printdebug "quiltify_trees_differ $x $y => $r\n";
+    return $r;
+}
+
+sub quiltify_tree_sentinelfiles ($) {
+    # lists the `sentinel' files present in the tree
+    my ($x) = @_;
+    my $r = cmdoutput @git, qw(ls-tree --name-only), $x,
+        qw(-- debian/rules debian/control);
+    $r =~ s/\n/,/g;
+    return $r;
+}
+
+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];
+    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->{O2H} & 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/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_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->{O2H} & 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 ($our_version) update-gitignore]
+---
+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 <<END
+Commit patch to update .gitignore
+
+[dgit ($our_version) update-gitignore-quilt-fixup]
+END
+    }
+
+    my $dgitview = git_rev_parse 'HEAD';
+
+    changedir '../../../..';
+    # When we no longer need to support squeeze, use --create-reflog
+    # instead of this:
+    ensuredir ".git/logs/refs/dgit-intern";
+    my $makelogfh = new IO::File ".git/logs/refs/$splitbraincache", '>>'
+      or die $!;
+
+    my $oldcache = git_get_ref "refs/$splitbraincache";
+    if ($oldcache eq $dgitview) {
+       my $tree = cmdoutput qw(git rev-parse), "$dgitview:";
+       # git update-ref doesn't always update, in this case.  *sigh*
+       my $dummy = make_commit_text <<END;
+tree $tree
+parent $dgitview
+author Dgit <dgit\@example.com> 1000000000 +0000
+committer Dgit <dgit\@example.com> 1000000000 +0000
+
+Dummy commit - do not use
+END
+       runcmd @git, qw(update-ref -m), "dgit $our_version - dummy",
+           "refs/$splitbraincache", $dummy;
+    }
+    runcmd @git, qw(update-ref -m), $cachekey, "refs/$splitbraincache",
+       $dgitview;
+
+    changedir '.git/dgit/unpack/work';
+
+    my $saved = maybe_split_brain_save $headref, $dgitview, "converted";
+    progress "dgit view: created ($saved)";
+}
+
+sub quiltify ($$$$) {
+    my ($clogp,$target,$oldtiptree,$failsuggestion) = @_;
+
+    # Quilt patchification algorithm
+    #
+    # We search backwards through the history of the main tree's HEAD
+    # (T) looking for a start commit S whose tree object is identical
+    # to to the patch tip tree (ie the tree corresponding to the
+    # current dpkg-committed patch series).  For these purposes
+    # `identical' disregards anything in debian/ - this wrinkle is
+    # necessary because dpkg-source treates debian/ specially.
+    #
+    # We can only traverse edges where at most one of the ancestors'
+    # trees differs (in changes outside in debian/).  And we cannot
+    # handle edges which change .pc/ or debian/patches.  To avoid
+    # going down a rathole we avoid traversing edges which introduce
+    # debian/rules or debian/control.  And we set a limit on the
+    # number of edges we are willing to look at.
+    #
+    # If we succeed, we walk forwards again.  For each traversed edge
+    # PC (with P parent, C child) (starting with P=S and ending with
+    # C=T) to we do this:
+    #  - git checkout C
+    #  - dpkg-source --commit with a patch name and message derived from C
+    # After traversing PT, we git commit the changes which
+    # should be contained within debian/patches.
+
+    # 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:
+    #  $p = {
+    #      Commit => $git_commit_id,
+    #      Child => $c,                          # or undef if P=T
+    #      Whynot => $reason_edge_PC_unsuitable, # in @nots only
+    #      Nontrivial => true iff $p..$c has relevant changes
+    #  };
+
+    my @todo;
+    my @nots;
+    my $sref_S;
+    my $max_work=100;
+    my %considered; # saves being exponential on some weird graphs
+
+    my $t_sentinels = quiltify_tree_sentinelfiles $target;
+
+    my $not = sub {
+       my ($search,$whynot) = @_;
+       printdebug " search NOT $search->{Commit} $whynot\n";
+       $search->{Whynot} = $whynot;
+       push @nots, $search;
+       no warnings qw(exiting);
+       next;
+    };
+
+    push @todo, {
+       Commit => $target,
+    };
+
+    while (@todo) {
+       my $c = shift @todo;
+       next if $considered{$c->{Commit}}++;
+
+       $not->($c, "maximum search space exceeded") if --$max_work <= 0;
+
+       printdebug "quiltify investigate $c->{Commit}\n";
+
+       # are we done?
+       if (!quiltify_trees_differ $c->{Commit}, $oldtiptree) {
+           printdebug " search finished hooray!\n";
+           $sref_S = $c;
+           last;
+       }
+
+       if ($quilt_mode eq 'nofix') {
+           fail "quilt fixup required but quilt mode is \`nofix'\n".
+               "HEAD commit $c->{Commit} differs from tree implied by ".
+               " debian/patches (tree object $oldtiptree)";
+       }
+       if ($quilt_mode eq 'smash') {
+           printdebug " search quitting smash\n";
+           last;
+       }
+
+       my $c_sentinels = quiltify_tree_sentinelfiles $c->{Commit};
+       $not->($c, "has $c_sentinels not $t_sentinels")
+           if $c_sentinels ne $t_sentinels;
+
+       my $commitdata = cmdoutput @git, qw(cat-file commit), $c->{Commit};
+       $commitdata =~ m/\n\n/;
+       $commitdata =~ $`;
+       my @parents = ($commitdata =~ m/^parent (\w+)$/gm);
+       @parents = map { { Commit => $_, Child => $c } } @parents;
+
+       $not->($c, "root commit") if !@parents;
+
+       foreach my $p (@parents) {
+           $p->{Nontrivial}= quiltify_trees_differ $p->{Commit},$c->{Commit};
+       }
+       my $ndiffers = grep { $_->{Nontrivial} } @parents;
+       $not->($c, "merge ($ndiffers nontrivial parents)") if $ndiffers > 1;
+
+       foreach my $p (@parents) {
+           printdebug "considering C=$c->{Commit} P=$p->{Commit}\n";
+
+           my @cmd= (@git, qw(diff-tree -r --name-only),
+                     $p->{Commit},$c->{Commit}, qw(-- debian/patches .pc));
+           my $patchstackchange = cmdoutput @cmd;
+           if (length $patchstackchange) {
+               $patchstackchange =~ s/\n/,/g;
+               $not->($p, "changed $patchstackchange");
+           }
+
+           printdebug " search queue P=$p->{Commit} ",
+               ($p->{Nontrivial} ? "NT" : "triv"),"\n";
+           push @todo, $p;
+       }
+    }
+
+    if (!$sref_S) {
+       printdebug "quiltify want to smash\n";
+
+       my $abbrev = sub {
+           my $x = $_[0]{Commit};
+           $x =~ s/(.*?[0-9a-z]{8})[0-9a-z]*$/$1/;
+           return $x;
+       };
+       my $reportnot = sub {
+           my ($notp) = @_;
+           my $s = $abbrev->($notp);
+           my $c = $notp->{Child};
+           $s .= "..".$abbrev->($c) if $c;
+           $s .= ": ".$notp->{Whynot};
+           return $s;
+       };
+       if ($quilt_mode eq 'linear') {
+           print STDERR "$us: quilt fixup cannot be linear.  Stopped at:\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') {
+       } elsif ($quilt_mode eq 'auto') {
+           progress "quilt fixup cannot be linear, smashing...";
+       } else {
+           die "$quilt_mode ?";
+       }
+
+       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";
+
+       quiltify_dpkg_commit "auto-$version-$target-$time",
+           (getfield $clogp, 'Maintainer'),
+           "Automatically generated patch ($clogp->{Version})\n".
+           "Last (up to) $ncommits git changes, FYI:\n\n". $msg;
+       return;
+    }
+
+    progress "quiltify linearisation planning successful, executing...";
+
+    for (my $p = $sref_S;
+        my $c = $p->{Child};
+        $p = $p->{Child}) {
+       printdebug "quiltify traverse $p->{Commit}..$c->{Commit}\n";
+       next unless $p->{Nontrivial};
+
+       my $cc = $c->{Commit};
+
+       my $commitdata = cmdoutput @git, qw(cat-file commit), $cc;
+       $commitdata =~ m/\n\n/ or die "$c ?";
+       $commitdata = $`;
+       my $msg = $'; #';
+       $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;
+       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/[.:]$//;
+            use Text::Iconv;
+           eval {
+               my $converter = new Text::Iconv qw(UTF-8 ASCII//TRANSLIT);
+               my $translitname = $converter->convert($patchname);
+               die unless defined $translitname;
+               $patchname = $translitname;
+           };
+           print STDERR
+               "dgit: patch title transliteration error: $@"
+               if $@;
+           $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";
+            $index++) { }
+       $!==ENOENT or die "$patchname$index $!";
+
+       runcmd @git, qw(checkout -q), $cc;
+
+       # We use the tip's changelog so that dpkg-source doesn't
+       # produce complaining messages from dpkg-parsechangelog.  None
+       # of the information dpkg-source gets from the changelog is
+       # actually relevant - it gets put into the original message
+       # which dpkg-source provides our stunt editor, and then
+       # overwritten.
+       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);
+    }
+
+    runcmd @git, qw(checkout -q master);
+}
+
+sub build_maybe_quilt_fixup () {
+    my ($format,$fopts) = get_source_format;
+    return unless madformat_wantfixup $format;
+    # sigh
+
+    check_for_vendor_patches();
+
+    if (quiltmode_splitbrain) {
+       fail <<END unless access_cfg_tagformats_can_splitbrain;
+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');
+
+    prep_ud();
+    changedir $ud;
+
+    my $upstreamversion = upstreamversion $version;
+
+    if ($fopts->{'single-debian-patch'}) {
+       quilt_fixup_singlepatch($clogp, $headref, $upstreamversion);
+    } else {
+       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);
+}
+
+sub quilt_fixup_mkwork ($) {
+    my ($headref) = @_;
+
+    mkdir "work" or die $!;
+    changedir "work";
+    mktree_in_ud_here();
+    runcmd @git, qw(reset -q --hard), $headref;
+}
+
+sub quilt_fixup_linkorigs ($$) {
+    my ($upstreamversion, $fn) = @_;
+    # calls $fn->($leafname);
+
+    foreach my $f (<../../../../*>) { #/){
+       my $b=$f; $b =~ s{.*/}{};
+       {
+           local ($debuglevel) = $debuglevel-1;
+           printdebug "QF linkorigs $b, $f ?\n";
+       }
+       next unless is_orig_file_of_vsn $b, $upstreamversion;
+       printdebug "QF linkorigs $b, $f Y\n";
+       link_ltarget $f, $b or die "$b $!";
+        $fn->($b);
+    }
+}
+
+sub quilt_fixup_delete_pc () {
+    runcmd @git, qw(rm -rqf .pc);
+    commit_admin <<END
+Commit removal of .pc (quilt series tracking data)
+
+[dgit ($our_version) upgrade quilt-remove-pc]
+END
+}
+
+sub quilt_fixup_singlepatch ($$$) {
+    my ($clogp, $headref, $upstreamversion) = @_;
+
+    progress "starting quiltify (single-debian-patch)";
+
+    # dpkg-source --commit generates new patches even if
+    # single-debian-patch is in debian/source/options.  In order to
+    # get it to generate debian/patches/debian-changes, it is
+    # necessary to build the source package.
+
+    quilt_fixup_linkorigs($upstreamversion, sub { });
+    quilt_fixup_mkwork($headref);
+
+    rmtree("debian/patches");
+
+    runcmd @dpkgsource, qw(-b .);
+    changedir "..";
+    runcmd @dpkgsource, qw(-x), (srcfn $version, ".dsc");
+    rename srcfn("$upstreamversion", "/debian/patches"), 
+           "work/debian/patches";
+
+    changedir "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 $sfk =~ m/^\$0\b/ || $sfk =~ 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(log -g), '--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);
+       my $saved = maybe_split_brain_save $headref, $cachehit, "cache-hit";
+       if ($cachehit ne $headref) {
+           progress "dgit view: found cached ($saved)";
+           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) = @_;
+
+    progress "examining quilt state (multiple patches, $quilt_mode mode)";
+
+    # Our objective is:
+    #  - honour any existing .pc in case it has any strangeness
+    #  - determine the git commit corresponding to the tip of
+    #    the patch stack (if there is one)
+    #  - if there is such a git commit, convert each subsequent
+    #    git commit into a quilt patch with dpkg-source --commit
+    #  - otherwise convert all the differences in the tree into
+    #    a single git commit
+    #
+    # To do this we:
+
+    # Our git tree doesn't necessarily contain .pc.  (Some versions of
+    # dgit would include the .pc in the git tree.)  If there isn't
+    # one, we need to generate one by unpacking the patches that we
+    # have.
+    #
+    # We first look for a .pc in the git tree.  If there is one, we
+    # will use it.  (This is not the normal case.)
+    #
+    # Otherwise need to regenerate .pc so that dpkg-source --commit
+    # can work.  We do this as follows:
+    #     1. Collect all relevant .orig from parent directory
+    #     2. Generate a debian.tar.gz out of
+    #         debian/{patches,rules,source/format,source/options}
+    #     3. Generate a fake .dsc containing just these fields:
+    #          Format Source Version Files
+    #     4. Extract the fake .dsc
+    #        Now the fake .dsc has a .pc directory.
+    # (In fact we do this in every case, because in future we will
+    # want to search for a good base commit for generating patches.)
+    #
+    # Then we can actually do the dpkg-source --commit
+    #     1. Make a new working tree with the same object
+    #        store as our main tree and check out the main
+    #        tree's HEAD.
+    #     2. Copy .pc from the fake's extraction, if necessary
+    #     3. Run dpkg-source --commit
+    #     4. If the result has changes to debian/, then
+    #          - git add them them
+    #          - git add .pc if we had a .pc in-tree
+    #          - git commit
+    #     5. If we had a .pc in-tree, delete it, and git commit
+    #     6. Back in the main tree, fast forward to the new HEAD
+
+    # Another situation we may have to cope with is gbp-style
+    # 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).
+    #
+    # 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.
+
+    my $splitbrain_cachekey;
+
+    quilt_make_fake_dsc($upstreamversion);
+
+    if (quiltmode_splitbrain()) {
+       my $cachehit;
+       ($cachehit, $splitbrain_cachekey) =
+           quilt_check_splitbrain_cache($headref, $upstreamversion);
+       return if $cachehit;
+    }
+
+    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 $!";
+
+    changedir 'fake';
+
+    remove_stray_gits("source package");
+    mktree_in_ud_here();
+
+    rmtree '.pc';
+
+    my $unapplied=git_add_write_tree();
+    printdebug "fake orig tree object $unapplied\n";
+
+    ensuredir '.pc';
+
+    my @bbcmd = (qw(sh -ec), 'exec dpkg-source --before-build . >/dev/null');
+    $!=0; $?=-1;
+    if (system @bbcmd) {
+       failedcmd @bbcmd if $? < 0;
+       fail <<END;
+failed to apply your git tree's patch stack (from debian/patches/) to
+ the corresponding upstream tarball(s).  Your source tree and .orig
+ are probably too inconsistent.  dgit can only fix up certain kinds of
+ anomaly (depending on the quilt mode).  See --quilt= in dgit(1).
+END
+    }
+
+    changedir '..';
+
+    quilt_fixup_mkwork($headref);
+
+    my $mustdeletepc=0;
+    if (stat_exists ".pc") {
+        -d _ or die;
+       progress "Tree already contains .pc - will use it then delete it.";
+        $mustdeletepc=1;
+    } else {
+        rename '../fake/.pc','.pc' or die $!;
+    }
+
+    changedir '../fake';
+    rmtree '.pc';
+    my $oldtiptree=git_add_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 @unrepres;
+    my $diffbits = {
+        # H = user's HEAD
+        # O = orig, without patches applied
+        # A = "applied", ie orig with H's debian/patches applied
+        O2H => quiltify_trees_differ($unapplied,$headref,   1,
+                                    \%editedignores, \@unrepres),
+        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(O2H O2A H2A)) {
+            push @dl, ($diffbits->{$v} & $b) ? '##' : '==';
+        }
+    }
+    printdebug "differences \@dl @dl.\n";
+
+    progress sprintf
+"$us: base trees orig=%.20s o+d/p=%.20s",
+              $unapplied, $oldtiptree;
+    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];
+
+    if (@unrepres) {
+       print STDERR "dgit:  cannot represent change: $_->[1]: $_->[0]\n"
+           foreach @unrepres;
+       forceable_fail [qw(unrepresentable)], <<END;
+HEAD has changes to .orig[s] which are not representable by `3.0 (quilt)'
+END
+    }
+
+    my @failsuggestion;
+    if (!($diffbits->{O2H} & $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 $!;
+    } else {
+       close P;
+    }
+
+    commit_quilty_patch();
+
+    if ($mustdeletepc) {
+        quilt_fixup_delete_pc();
+    }
+}
+
+sub quilt_fixup_editor () {
+    my $descfn = $ENV{$fakeeditorenv};
+    my $editing = $ARGV[$#ARGV];
+    open I1, '<', $descfn or die "$descfn: $!";
+    open I2, '<', $editing or die "$editing: $!";
+    unlink $editing or die "$editing: $!";
+    open O, '>', $editing or die "$editing: $!";
+    while (<I1>) { print O or die $!; } I1->error and die $!;
+    my $copying = 0;
+    while (<I2>) {
+       $copying ||= m/^\-\-\- /;
+       next unless $copying;
+       print O or die $!;
+    }
+    I2->error and die $!;
+    close O or die $1;
+    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 -----
+
+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 () {
+    return if $clean_using_builder;
+    if ($cleanmode eq 'dpkg-source') {
+       maybe_apply_patches_dirtily();
+       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);
+    } elsif ($cleanmode eq 'git-ff') {
+       runcmd_ordryrun_local @git, qw(clean -xdff);
+    } elsif ($cleanmode eq 'check') {
+       my $leftovers = cmdoutput @git, qw(clean -xdn);
+       if (length $leftovers) {
+           print STDERR $leftovers, "\n" or die $!;
+           fail "tree contains uncommitted files and --clean=check specified";
+       }
+    } elsif ($cleanmode eq 'none') {
+    } else {
+       die "$cleanmode ?";
+    }
+}
+
+sub cmd_clean () {
+    badusage "clean takes no additional arguments" if @ARGV;
+    notpushing();
+    clean_tree();
+    maybe_unapply_patches_again();
+}
+
+sub build_prep_early () {
+    our $build_prep_early_done //= 0;
+    return if $build_prep_early_done++;
+    notpushing();
+    badusage "-p is not allowed when building" if defined $package;
+    my $clogp = parsechangelog();
+    $isuite = getfield $clogp, 'Distribution';
+    $package = getfield $clogp, 'Source';
+    $version = getfield $clogp, 'Version';
+    check_not_dirty();
+}
+
+sub build_prep () {
+    build_prep_early();
+    clean_tree();
+    build_maybe_quilt_fixup();
+    if ($rmchanges) {
+       my $pat = changespat $version;
+       foreach my $f (glob "$buildproductsdir/$pat") {
+           if (act_local()) {
+               unlink $f or fail "remove old changes file $f: $!";
+           } else {
+               progress "would remove $f";
+           }
+       }
+    }
+}
+
+sub changesopts_initial () {
+    my @opts =@changesopts[1..$#changesopts];
+}
+
+sub changesopts_version () {
+    if (!defined $changes_since_version) {
+       my @vsns = archive_query('archive_query');
+       my @quirk = access_quirk();
+       if ($quirk[0] eq 'backports') {
+           local $isuite = $quirk[2];
+           local $csuite;
+           canonicalise_suite();
+           push @vsns, archive_query('archive_query');
+       }
+       if (@vsns) {
+           @vsns = map { $_->[0] } @vsns;
+           @vsns = sort { -version_compare($a, $b) } @vsns;
+           $changes_since_version = $vsns[0];
+           progress "changelog will contain changes since $vsns[0]";
+       } else {
+           $changes_since_version = '_';
+           progress "package seems new, not specifying -v<version>";
+       }
+    }
+    if ($changes_since_version ne '_') {
+       return ("-v$changes_since_version");
+    } else {
+       return ();
+    }
+}
+
+sub changesopts () {
+    return (changesopts_initial(), changesopts_version());
+}
+
+sub massage_dbp_args ($;$) {
+    my ($cmd,$xargs) = @_;
+    # We need to:
+    #
+    #  - if we're going to split the source build out so we can
+    #    do strange things to it, massage the arguments to dpkg-buildpackage
+    #    so that the main build doessn't build source (or add an argument
+    #    to stop it building source by default).
+    #
+    #  - add -nc to stop dpkg-source cleaning the source tree,
+    #    unless we're not doing a split build and want dpkg-source
+    #    as cleanmode, in which case we can do nothing
+    #
+    # return values:
+    #    0 - source will NOT need to be built separately by caller
+    #   +1 - source will need to be built separately by caller
+    #   +2 - source will need to be built separately by caller AND
+    #        dpkg-buildpackage should not in fact be run at all!
+    debugcmd '#massaging#', @$cmd if $debuglevel>1;
+#print STDERR "MASS0 ",Dumper($cmd, $xargs, $need_split_build_invocation);
+    if ($cleanmode eq 'dpkg-source' && !$need_split_build_invocation) {
+       $clean_using_builder = 1;
+       return 0;
+    }
+    # -nc has the side effect of specifying -b if nothing else specified
+    # and some combinations of -S, -b, et al, are errors, rather than
+    # later simply overriding earlie.  So we need to:
+    #  - search the command line for these options
+    #  - pick the last one
+    #  - perhaps add our own as a default
+    #  - perhaps adjust it to the corresponding non-source-building version
+    my $dmode = '-F';
+    foreach my $l ($cmd, $xargs) {
+       next unless $l;
+       @$l = grep { !(m/^-[SgGFABb]$/s and $dmode=$_) } @$l;
+    }
+    push @$cmd, '-nc';
+#print STDERR "MASS1 ",Dumper($cmd, $xargs, $dmode);
+    my $r = 0;
+    if ($need_split_build_invocation) {
+       printdebug "massage split $dmode.\n";
+       $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;
+}
+
+sub in_parent (&) {
+    my ($fn) = @_;
+    my $wasdir = must_getcwd();
+    changedir "..";
+    $fn->();
+    changedir $wasdir;
+}    
+
+sub postbuild_mergechanges ($) { # must run with CWD=.. (eg in in_parent)
+    my ($msg_if_onlyone) = @_;
+    # If there is only one .changes file, fail with $msg_if_onlyone,
+    # or if that is undef, be a no-op.
+    # Returns the changes file to report to the user.
+    my $pat = changespat $version;
+    my @changesfiles = glob $pat;
+    @changesfiles = sort {
+       ($b =~ m/_source\.changes$/ <=> $a =~ m/_source\.changes$/)
+           or $a cmp $b
+    } @changesfiles;
+    my $result;
+    if (@changesfiles==1) {
+       fail <<END.$msg_if_onlyone if defined $msg_if_onlyone;
+only one changes file from build (@changesfiles)
+END
+       $result = $changesfiles[0];
+    } elsif (@changesfiles==2) {
+       my $binchanges = parsecontrol($changesfiles[1], "binary changes file");
+       foreach my $l (split /\n/, getfield $binchanges, 'Files') {
+           fail "$l found in binaries changes file $binchanges"
+               if $l =~ m/\.dsc$/;
+       }
+       runcmd_ordryrun_local @mergechanges, @changesfiles;
+       my $multichanges = changespat $version,'multi';
+       if (act_local()) {
+           stat_exists $multichanges or fail "$multichanges: $!";
+           foreach my $cf (glob $pat) {
+               next if $cf eq $multichanges;
+               rename "$cf", "$cf.inmulti" or fail "$cf\{,.inmulti}: $!";
+           }
+       }
+       $result = $multichanges;
+    } else {
+       fail "wrong number of different changes files (@changesfiles)";
+    }
+    printdone "build successful, results in $result\n" or die $!;
+}
+
+sub midbuild_checkchanges () {
+    my $pat = changespat $version;
+    return if $rmchanges;
+    my @unwanted = map { s#^\.\./##; $_; } glob "../$pat";
+    @unwanted = grep { $_ ne changespat $version,'source' } @unwanted;
+    fail <<END
+changes files other than source matching $pat already present; building would result in ambiguity about the intended results.
+Suggest you delete @unwanted.
+END
+       if @unwanted;
+}
+
+sub midbuild_checkchanges_vanilla ($) {
+    my ($wantsrc) = @_;
+    midbuild_checkchanges() if $wantsrc == 1;
+}
+
+sub postbuild_mergechanges_vanilla ($) {
+    my ($wantsrc) = @_;
+    if ($wantsrc == 1) {
+       in_parent {
+           postbuild_mergechanges(undef);
+       };
+    } else {
+       printdone "build successful\n";
+    }
+}
+
+sub cmd_build {
+    my @dbp = (@dpkgbuildpackage, qw(-us -uc), changesopts_initial(), @ARGV);
+    my $wantsrc = massage_dbp_args \@dbp;
+    if ($wantsrc > 0) {
+       build_source();
+       midbuild_checkchanges_vanilla $wantsrc;
+    } else {
+       build_prep();
+    }
+    if ($wantsrc < 2) {
+       push @dbp, changesopts_version();
+       maybe_apply_patches_dirtily();
+       runcmd_ordryrun_local @dbp;
+    }
+    maybe_unapply_patches_again();
+    postbuild_mergechanges_vanilla $wantsrc;
+}
+
+sub pre_gbp_build {
+    $quilt_mode //= 'gbp';
+}
+
+sub cmd_gbp_build {
+    build_prep_early();
+
+    # gbp can make .origs out of thin air.  In my tests it does this
+    # even for a 1.0 format package, with no origs present.  So I
+    # guess it keys off just the version number.  We don't know
+    # exactly what .origs ought to exist, but let's assume that we
+    # should run gbp if: the version has an upstream part and the main
+    # orig is absent.
+    my $upstreamversion = upstreamversion $version;
+    my $origfnpat = srcfn $upstreamversion, '.orig.tar.*';
+    my $gbp_make_orig = $version =~ m/-/ && !(() = glob "../$origfnpat");
+
+    if ($gbp_make_orig) {
+       clean_tree();
+       $cleanmode = 'none'; # don't do it again
+       $need_split_build_invocation = 1;
+    }
+
+    my @dbp = @dpkgbuildpackage;
+
+    my $wantsrc = massage_dbp_args \@dbp, \@ARGV;
+
+    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 ($gbp_make_orig) {
+       ensuredir '.git/dgit';
+       my $ok = '.git/dgit/origs-gen-ok';
+       unlink $ok or $!==&ENOENT or die $!;
+       my @origs_cmd = @cmd;
+       push @origs_cmd, qw(--git-cleaner=true);
+       push @origs_cmd, "--git-prebuild=touch $ok .git/dgit/no-such-dir/ok";
+       push @origs_cmd, @ARGV;
+       if (act_local()) {
+           debugcmd @origs_cmd;
+           system @origs_cmd;
+           do { local $!; stat_exists $ok; }
+               or failedcmd @origs_cmd;
+       } else {
+           dryrun_report @origs_cmd;
+       }
+    }
+
+    if ($wantsrc > 0) {
+       build_source();
+       midbuild_checkchanges_vanilla $wantsrc;
+    } else {
+       if (!$clean_using_builder) {
+           push @cmd, '--git-cleaner=true';
+       }
+       build_prep();
+    }
+    maybe_unapply_patches_again();
+    if ($wantsrc < 2) {
+       push @cmd, changesopts();
+       runcmd_ordryrun_local @cmd, @ARGV;
+    }
+    postbuild_mergechanges_vanilla $wantsrc;
+}
+sub cmd_git_build { cmd_gbp_build(); } # compatibility with <= 1.0
+
+sub build_source {
+    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';
+    if (act_local()) {
+       unlink "../$sourcechanges" or $!==ENOENT
+           or fail "remove $sourcechanges: $!";
+    }
+    $dscfn = dscfn($version);
+    if ($our_cleanmode eq 'dpkg-source') {
+       maybe_apply_patches_dirtily();
+       runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc -S),
+           changesopts();
+    } elsif ($our_cleanmode eq 'dpkg-source-d') {
+       maybe_apply_patches_dirtily();
+       runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc -S -d),
+           changesopts();
+    } else {
+       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",
+           @dpkggenchanges, qw(-S), changesopts();
+    }
+}
+
+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";
+}
+
+sub cmd_sbuild {
+    build_source();
+    midbuild_checkchanges();
+    in_parent {
+       if (act_local()) {
+           stat_exists $dscfn or fail "$dscfn (in parent directory): $!";
+           stat_exists $sourcechanges
+               or fail "$sourcechanges (in parent directory): $!";
+       }
+       runcmd_ordryrun_local @sbuild, qw(-d), $isuite, @ARGV, $dscfn;
+    };
+    maybe_unapply_patches_again();
+    in_parent {
+       postbuild_mergechanges(<<END);
+perhaps you need to pass -A ?  (sbuild's default is to build only
+arch-specific binaries; dgit 1.4 used to override that.)
+END
+    };
+}    
+
+sub cmd_quilt_fixup {
+    badusage "incorrect arguments to dgit quilt-fixup" if @ARGV;
+    my $clogp = parsechangelog();
+    $version = getfield $clogp, 'Version';
+    $package = getfield $clogp, 'Source';
+    check_not_dirty();
+    clean_tree();
+    build_maybe_quilt_fixup();
+}
+
+sub cmd_import_dsc {
+    my $needsig = 0;
+
+    while (@ARGV) {
+       last unless $ARGV[0] =~ m/^-/;
+       $_ = shift @ARGV;
+       last if m/^--?$/;
+       if (m/^--require-valid-signature$/) {
+           $needsig = 1;
+       } else {
+           badusage "unknown dgit import-dsc sub-option \`$_'";
+       }
+    }
+
+    badusage "usage: dgit import-dsc .../PATH/TO/.DSC BRANCH" unless @ARGV==2;
+    my ($dscfn, $dstbranch) = @ARGV;
+
+    badusage "dry run makes no sense with import-dsc" unless act_local();
+
+    my $force = $dstbranch =~ s/^\+//   ? +1 :
+               $dstbranch =~ s/^\.\.// ? -1 :
+                                           0;
+    my $info = $force ? " $&" : '';
+    $info = "$dscfn$info";
+
+    my $specbranch = $dstbranch;
+    $dstbranch = "refs/heads/$dstbranch" unless $dstbranch =~ m#^refs/#;
+    $dstbranch = cmdoutput @git, qw(check-ref-format --normalize), $dstbranch;
+
+    my @symcmd = (@git, qw(symbolic-ref -q HEAD));
+    my $chead = cmdoutput_errok @symcmd;
+    defined $chead or $?==256 or failedcmd @symcmd;
+
+    fail "$dstbranch is checked out - will not update it"
+       if defined $chead and $chead eq $dstbranch;
+
+    my $oldhash = git_get_ref $dstbranch;
+
+    open D, "<", $dscfn or fail "open import .dsc ($dscfn): $!";
+    $dscdata = do { local $/ = undef; <D>; };
+    D->error and fail "read $dscfn: $!";
+    close C;
+
+    # we don't normally need this so import it here
+    use Dpkg::Source::Package;
+    my $dp = new Dpkg::Source::Package filename => $dscfn,
+       require_valid_signature => $needsig;
+    {
+       local $SIG{__WARN__} = sub {
+           print STDERR $_[0];
+           return unless $needsig;
+           fail "import-dsc signature check failed";
+       };
+       if (!$dp->is_signed()) {
+           warn "$us: warning: importing unsigned .dsc\n";
+       } else {
+           my $r = $dp->check_signature();
+           die "->check_signature => $r" if $needsig && $r;
+       }
+    }
+
+    parse_dscdata();
+
+    my $dgit_commit = $dsc->{$ourdscfield[0]};
+    if (defined $dgit_commit && 
+       !forceing [qw(import-dsc-with-dgit-field)]) {
+       $dgit_commit =~ m/\w+/ or fail "invalid hash in .dsc";
+       progress "dgit: import-dsc of .dsc with Dgit field, using git hash";
+       my @cmd = (qw(sh -ec),
+                  "echo $dgit_commit | git cat-file --batch-check");
+       my $objgot = cmdoutput @cmd;
+       if ($objgot =~ m#^\w+ missing\b#) {
+           fail <<END
+.dsc contains Dgit field referring to object $dgit_commit
+Your git tree does not have that object.  Try `git fetch' from a
+plausible server (browse.dgit.d.o? alioth?), and try the import-dsc again.
+END
+       }
+       if ($oldhash && !is_fast_fwd $oldhash, $dgit_commit) {
+           if ($force > 0) {
+               progress "Not fast forward, forced update.";
+           } else {
+               fail "Not fast forward to $dgit_commit";
+           }
+       }
+       @cmd = (@git, qw(update-ref -m), "dgit import-dsc (Dgit): $info",
+               $dstbranch, $dgit_commit);
+       runcmd @cmd;
+       progress "dgit: import-dsc updated git ref $dstbranch";
+       return 0;
+    }
+
+    fail <<END
+Branch $dstbranch already exists
+Specify ..$specbranch for a pseudo-merge, binding in existing history
+Specify  +$specbranch to overwrite, discarding existing history
+END
+       if $oldhash && !$force;
+
+    $package = getfield $dsc, 'Source';
+    my @dfi = dsc_files_info();
+    foreach my $fi (@dfi) {
+       my $f = $fi->{Filename};
+       my $here = "../$f";
+       next if lstat $here;
+       fail "stat $here: $!" unless $! == ENOENT;
+       my $there = $dscfn;
+       if ($dscfn =~ m#^(?:\./+)?\.\./+#) {
+           $there = $';
+       } elsif ($dscfn =~ m#^/#) {
+           $there = $dscfn;
+       } else {
+           fail "cannot import $dscfn which seems to be inside working tree!";
+       }
+       $there =~ s#/+[^/]+$## or
+           fail "cannot import $dscfn which seems to not have a basename";
+       $there .= "/$f";
+       symlink $there, $here or fail "symlink $there to $here: $!";
+       progress "made symlink $here -> $there";
+#      print STDERR Dumper($fi);
+    }
+    my @mergeinputs = generate_commits_from_dsc();
+    die unless @mergeinputs == 1;
+
+    my $newhash = $mergeinputs[0]{Commit};
+
+    if ($oldhash) {
+       if ($force > 0) {
+           progress "Import, forced update - synthetic orphan git history.";
+       } elsif ($force < 0) {
+           progress "Import, merging.";
+           my $tree = cmdoutput @git, qw(rev-parse), "$newhash:";
+           my $version = getfield $dsc, 'Version';
+           my $clogp = commit_getclogp $newhash;
+           my $authline = clogp_authline $clogp;
+           $newhash = make_commit_text <<END;
+tree $tree
+parent $newhash
+parent $oldhash
+author $authline
+committer $authline
+
+Merge $package ($version) import into $dstbranch
+END
+       } else {
+           die; # caught earlier
+       }
+    }
+
+    my @cmd = (@git, qw(update-ref -m), "dgit import-dsc: $info",
+              $dstbranch, $newhash);
+    runcmd @cmd;
+    progress "dgit: import-dsc results are in in git ref $dstbranch";
+}
+
+sub cmd_archive_api_query {
+    badusage "need only 1 subpath argument" unless @ARGV==1;
+    my ($subpath) = @ARGV;
+    my @cmd = archive_api_query_cmd($subpath);
+    push @cmd, qw(-f);
+    debugcmd ">",@cmd;
+    exec @cmd or fail "exec curl: $!\n";
+}
+
+sub cmd_clone_dgit_repos_server {
+    badusage "need destination argument" unless @ARGV==1;
+    my ($destdir) = @ARGV;
+    $package = '_dgit-repos-server';
+    my @cmd = (@git, qw(clone), access_giturl(), $destdir);
+    debugcmd ">",@cmd;
+    exec @cmd or fail "exec git clone: $!\n";
+}
+
+sub cmd_setup_mergechangelogs {
+    badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
+    setup_mergechangelogs(1);
+}
+
+sub cmd_setup_useremail {
+    badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
+    setup_useremail(1);
+}
+
+sub cmd_setup_new_tree {
+    badusage "no arguments allowed to dgit setup-tree" if @ARGV;
+    setup_new_tree();
+}
+
+#---------- argument parsing and main program ----------
+
+sub cmd_version {
+    print "dgit version $our_version\n" or die $!;
+    exit 0;
+}
+
+our (%valopts_long, %valopts_short);
+our @rvalopts;
+
+sub defvalopt ($$$$) {
+    my ($long,$short,$val_re,$how) = @_;
+    my $oi = { Long => $long, Short => $short, Re => $val_re, How => $how };
+    $valopts_long{$long} = $oi;
+    $valopts_short{$short} = $oi;
+    # $how subref should:
+    #   do whatever assignemnt or thing it likes with $_[0]
+    #   if the option should not be passed on to remote, @rvalopts=()
+    # or $how can be a scalar ref, meaning simply assign the value
+}
+
+defvalopt '--since-version', '-v', '[^_]+|_', \$changes_since_version;
+defvalopt '--distro',        '-d', '.+',      \$idistro;
+defvalopt '',                '-k', '.+',      \$keyid;
+defvalopt '--existing-package','', '.*',      \$existing_package;
+defvalopt '--build-products-dir','','.*',     \$buildproductsdir;
+defvalopt '--clean',       '', $cleanmode_re, \$cleanmode;
+defvalopt '--package',   '-p',   $package_re, \$package;
+defvalopt '--quilt',     '', $quilt_modes_re, \$quilt_mode;
+
+defvalopt '', '-C', '.+', sub {
+    ($changesfile) = (@_);
+    if ($changesfile =~ s#^(.*)/##) {
+       $buildproductsdir = $1;
+    }
+};
+
+defvalopt '--initiator-tempdir','','.*', sub {
+    ($initiator_tempdir) = (@_);
+    $initiator_tempdir =~ m#^/# or
+       badusage "--initiator-tempdir must be used specify an".
+       " absolute, not relative, directory."
+};
+
+sub parseopts () {
+    my $om;
+
+    if (defined $ENV{'DGIT_SSH'}) {
+       @ssh = string_to_ssh $ENV{'DGIT_SSH'};
+    } elsif (defined $ENV{'GIT_SSH'}) {
+       @ssh = ($ENV{'GIT_SSH'});
+    }
+
+    my $oi;
+    my $val;
+    my $valopt = sub {
+       my ($what) = @_;
+       @rvalopts = ($_);
+       if (!defined $val) {
+           badusage "$what needs a value" unless @ARGV;
+           $val = shift @ARGV;
+           push @rvalopts, $val;
+       }
+       badusage "bad value \`$val' for $what" unless
+           $val =~ m/^$oi->{Re}$(?!\n)/s;
+       my $how = $oi->{How};
+       if (ref($how) eq 'SCALAR') {
+           $$how = $val;
+       } else {
+           $how->($val);
+       }
+       push @ropts, @rvalopts;
+    };
+
+    while (@ARGV) {
+       last unless $ARGV[0] =~ m/^-/;
+       $_ = shift @ARGV;
+       last if m/^--?$/;
+       if (m/^--/) {
+           if (m/^--dry-run$/) {
+               push @ropts, $_;
+               $dryrun_level=2;
+           } elsif (m/^--damp-run$/) {
+               push @ropts, $_;
+               $dryrun_level=1;
+           } elsif (m/^--no-sign$/) {
+               push @ropts, $_;
+               $sign=0;
+           } elsif (m/^--help$/) {
+               cmd_help();
+           } elsif (m/^--version$/) {
+               cmd_version();
+           } elsif (m/^--new$/) {
+               push @ropts, $_;
+               $new_package=1;
+           } elsif (m/^--([-0-9a-z]+)=(.+)/s &&
+                    ($om = $opts_opt_map{$1}) &&
+                    length $om->[0]) {
+               push @ropts, $_;
+               $om->[0] = $2;
+           } elsif (m/^--([-0-9a-z]+):(.*)/s &&
+                    !$opts_opt_cmdonly{$1} &&
+                    ($om = $opts_opt_map{$1})) {
+               push @ropts, $_;
+               push @$om, $2;
+           } elsif (m/^--(gbp|dpm)$/s) {
+               push @ropts, "--quilt=$1";
+               $quilt_mode = $1;
+           } elsif (m/^--ignore-dirty$/s) {
+               push @ropts, $_;
+               $ignoredirty = 1;
+           } elsif (m/^--no-quilt-fixup$/s) {
+               push @ropts, $_;
+               $quilt_mode = 'nocheck';
+           } 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/^--dep14tag$/s) {
+               push @ropts, $_;
+               $dodep14tag= 'want';
+           } elsif (m/^--no-dep14tag$/s) {
+               push @ropts, $_;
+               $dodep14tag= 'no';
+           } elsif (m/^--always-dep14tag$/s) {
+               push @ropts, $_;
+               $dodep14tag= 'always';
+           } elsif (m/^--delayed=(\d+)$/s) {
+               push @ropts, $_;
+               push @dput, $_;
+           } elsif (m/^--dgit-view-save=(.+)$/s) {
+               push @ropts, $_;
+               $split_brain_save = $1;
+               $split_brain_save =~ s#^(?!refs/)#refs/heads/#;
+           } elsif (m/^--(no-)?rm-old-changes$/s) {
+               push @ropts, $_;
+               $rmchanges = !$1;
+           } elsif (m/^--deliberately-($deliberately_re)$/s) {
+               push @ropts, $_;
+               push @deliberatelies, $&;
+           } elsif (m/^--force-(.*)/ && defined $forceopts{$1}) {
+               push @ropts, $&;
+               $forceopts{$1} = 1;
+               $_='';
+           } elsif (m/^--force-/) {
+               print STDERR
+                   "$us: warning: ignoring unknown force option $_\n";
+               $_='';
+           } 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, $_;
+               $need_split_build_invocation = 1;
+           } elsif (m/^(--[-0-9a-z]+)(=|$)/ && ($oi = $valopts_long{$1})) {
+               $val = $2 ? $' : undef; #';
+               $valopt->($oi->{Long});
+           } else {
+               badusage "unknown long option \`$_'";
+           }
+       } else {
+           while (m/^-./s) {
+               if (s/^-n/-/) {
+                   push @ropts, $&;
+                   $dryrun_level=2;
+               } elsif (s/^-L/-/) {
+                   push @ropts, $&;
+                   $dryrun_level=1;
+               } elsif (s/^-h/-/) {
+                   cmd_help();
+               } elsif (s/^-D/-/) {
+                   push @ropts, $&;
+                   $debuglevel++;
+                   enabledebug();
+               } elsif (s/^-N/-/) {
+                   push @ropts, $&;
+                   $new_package=1;
+               } elsif (m/^-m/) {
+                   push @ropts, $&;
+                   push @changesopts, $_;
+                   $_ = '';
+               } elsif (s/^-wn$//s) {
+                   push @ropts, $&;
+                   $cleanmode = 'none';
+               } elsif (s/^-wg$//s) {
+                   push @ropts, $&;
+                   $cleanmode = 'git';
+               } elsif (s/^-wgf$//s) {
+                   push @ropts, $&;
+                   $cleanmode = 'git-ff';
+               } elsif (s/^-wd$//s) {
+                   push @ropts, $&;
+                   $cleanmode = 'dpkg-source';
+               } elsif (s/^-wdd$//s) {
+                   push @ropts, $&;
+                   $cleanmode = 'dpkg-source-d';
+               } elsif (s/^-wc$//s) {
+                   push @ropts, $&;
+                   $cleanmode = 'check';
+               } elsif (s/^-c([^=]*)\=(.*)$//s) {
+                   push @git, '-c', $&;
+                   $gitcfgs{cmdline}{$1} = [ $2 ];
+               } elsif (s/^-c([^=]+)$//s) {
+                   push @git, '-c', $&;
+                   $gitcfgs{cmdline}{$1} = [ 'true' ];
+               } elsif (m/^-[a-zA-Z]/ && ($oi = $valopts_short{$&})) {
+                   $val = $'; #';
+                   $val = undef unless length $val;
+                   $valopt->($oi->{Short});
+                   $_ = '';
+               } else {
+                   badusage "unknown short option \`$_'";
+               }
+           }
+       }
+    }
+}
+
+sub check_env_sanity () {
+    my $blocked = new POSIX::SigSet;
+    sigprocmask SIG_UNBLOCK, $blocked, $blocked or die $!;
+
+    eval {
+       foreach my $name (qw(PIPE CHLD)) {
+           my $signame = "SIG$name";
+           my $signum = eval "POSIX::$signame" // die;
+           ($SIG{$name} // 'DEFAULT') eq 'DEFAULT' or
+               die "$signame is set to something other than SIG_DFL\n";
+           $blocked->ismember($signum) and
+               die "$signame is blocked\n";
+       }
+    };
+    return unless $@;
+    chomp $@;
+    fail <<END;
+On entry to dgit, $@
+This is a bug produced by something in in your execution environment.
+Giving up.
+END
+}
+
+
+sub finalise_opts_opts () {
+    foreach my $k (keys %opts_opt_map) {
+       my $om = $opts_opt_map{$k};
+
+       my $v = access_cfg("cmd-$k", 'RETURN-UNDEF');
+       if (defined $v) {
+           badcfg "cannot set command for $k"
+               unless length $om->[0];
+           $om->[0] = $v;
+       }
+
+       foreach my $c (access_cfg_cfgs("opts-$k")) {
+           my @vl =
+               map { $_ ? @$_ : () }
+               map { $gitcfgs{$_}{$c} }
+               reverse @gitcfgsources;
+           printdebug "CL $c ", (join " ", map { shellquote } @vl),
+               "\n" if $debuglevel >= 4;
+           next unless @vl;
+           badcfg "cannot configure options for $k"
+               if $opts_opt_cmdonly{$k};
+           my $insertpos = $opts_cfg_insertpos{$k};
+           @$om = ( @$om[0..$insertpos-1],
+                    @vl,
+                    @$om[$insertpos..$#$om] );
+       }
+    }
+}
+
+if ($ENV{$fakeeditorenv}) {
+    git_slurp_config();
+    quilt_fixup_editor();
+}
+
+parseopts();
+check_env_sanity();
+git_slurp_config();
+
+print STDERR "DRY RUN ONLY\n" if $dryrun_level > 1;
+print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n"
+    if $dryrun_level == 1;
+if (!@ARGV) {
+    print STDERR $helpmsg or die $!;
+    exit 8;
+}
+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');
+}
+
+if (!defined $quilt_mode) {
+    local $access_forpush;
+    $quilt_mode = cfg('dgit.force.quilt-mode', 'RETURN-UNDEF')
+       // access_cfg('quilt-mode', 'RETURN-UNDEF')
+       // 'linear';
+    $quilt_mode =~ m/^($quilt_modes_re)$/ 
+       or badcfg "unknown quilt-mode \`$quilt_mode'";
+    $quilt_mode = $1;
+}
+
+if (!defined $dodep14tag) {
+    local $access_forpush;
+    $dodep14tag = access_cfg('dep14tag', 'RETURN-UNDEF') // 'want';
+    $dodep14tag =~ m/^($dodep14tag_re)$/ 
+       or badcfg "unknown dep14tag setting \`$dodep14tag'";
+    $dodep14tag = $1;
+}
+
+$need_split_build_invocation ||= quiltmode_splitbrain();
+
+if (!defined $cleanmode) {
+    local $access_forpush;
+    $cleanmode = access_cfg('clean-mode', 'RETURN-UNDEF');
+    $cleanmode //= 'dpkg-source';
+
+    badcfg "unknown clean-mode \`$cleanmode'" unless
+       $cleanmode =~ m/^($cleanmode_re)$(?!\n)/s;
+}
+
+my $fn = ${*::}{"cmd_$cmd"};
+$fn or badusage "unknown operation $cmd";
+$fn->();
diff --git a/dgit-maint-gbp.7.pod b/dgit-maint-gbp.7.pod
new file mode 100644 (file)
index 0000000..c31dfa5
--- /dev/null
@@ -0,0 +1,126 @@
+=head1 NAME
+
+dgit - tutorial for package maintainers already using git-buildpackage(1)
+
+=head1 INTRODUCTION
+
+This document explains how B<dgit> can be incorporated into a
+git-buildpackage(1) package-maintenance workflow.  This should be read
+jointly with git-buildpackage(1)'s documentation.  Some reasons why
+you might want to incorporate B<dgit> into your existing workflow:
+
+=over 4
+
+=item
+
+Benefit from dgit's safety catches.  In particular, ensure that your
+upload always matches exactly your git HEAD.
+
+=item
+
+Provide a better, more detailed git history to downstream dgit users,
+such as people using dgit to do an NMU (see dgit-nmu-simple(7) and
+dgit-user(7)).
+
+=back
+
+Note that we assume a patches-unapplied repository: the upstream
+source committed to the git repository is unpatched.
+git-buildpackage(1) can work with patched-applied repositories, but is
+normally used with patches-unapplied.
+
+=head1 GIT CONFIGURATION
+
+If you run
+
+=over 4
+
+    % git config dgit.default.quilt-mode gbp
+
+=back
+
+in your repository, you can omit I<--gbp> wherever it occurs below.
+
+Note that this does require that you always work from your gbp master
+branch, never the dgit patches-applied branch.
+
+=head1 BUILDING
+
+You can perform builds like this:
+
+=over 4
+
+    % dgit [--allow-dirty] gbp-build [OPTIONS]
+
+=back
+
+where I<--allow-dirty> is needed for testing uncommitted changes, and
+I<OPTIONS> are any further options to be passed on to
+gbp-buildpackage(1).
+
+When you are ready to build for upload, you will probably want to use
+sbuild(1) or pbuilder(1), or do a source-only upload.  Either
+
+=over 4
+
+    % dgit --rm-old-changes --gbp sbuild
+
+=back
+
+or
+
+=over 4
+
+    % dgit --rm-old-changes gbp-build --git-pbuilder
+
+=back
+
+or
+
+=over 4
+
+    % dgit --rm-old-changes --gbp build-source
+
+=back
+
+We use I<--rm-old-changes> to ensure that there is exactly one changes
+file corresponding to this package, so we can be confident we're
+uploading what we intend (though B<dgit push> will do some safety
+checks).
+
+Note that all of the commands in this section are not required to
+upload with dgit.  You can invoke gbp-buildpackage(1), pbuilder(1) and
+sbuild(1) directly.  However, the defaults for these tools may leave
+you with something that dgit will refuse to upload because it doesn't
+match your git HEAD.  As a general rule, leave all signing and tagging
+to dgit.
+
+=head1 UPLOADING
+
+Don't use I<--git-tag>: B<dgit push> will do this for you.  To upload:
+
+=over 4
+
+    % dgit --gbp push
+
+=back
+
+This will push your git history to the dgit-repos, but you probably
+want to follow it up with a push to alioth.
+
+You will need to pass I<--overwrite> if the previous upload was not
+performed with dgit.
+
+=head1 INCORPORATING NMUS
+
+B<dgit pull> can't yet incorporate NMUs into patches-unapplied gbp
+branches.  You can just apply the NMU diff the traditional way.  The
+next upload will require I<--overwrite>.
+
+=head1 SEE ALSO
+
+dgit(1), dgit(7)
+
+=head1 AUTHOR
+
+This tutorial was written and is maintained by Sean Whitton <spwhitton@spwhitton.name>.
diff --git a/dgit-maint-merge.7.pod b/dgit-maint-merge.7.pod
new file mode 100644 (file)
index 0000000..dc5bfa3
--- /dev/null
@@ -0,0 +1,436 @@
+=head1 NAME
+
+dgit - tutorial for package maintainers, using a workflow centered around git-merge(1)
+
+=head1 INTRODUCTION
+
+This document describes elements of a workflow for maintaining a
+non-native Debian package using B<dgit>.  The workflow makes the
+following opinionated assumptions:
+
+=over 4
+
+=item
+
+Git histories should be the non-linear histories produced by
+git-merge(1), preserving all information about divergent development
+that was later brought together.
+
+=item
+
+Maintaining convenient and powerful git workflows takes priority over
+the usefulness of the raw Debian source package.  The Debian archive
+is thought of as an output format.
+
+For example, we don't spend time curating a series of quilt patches.
+However, the information such a series would contain is readily
+available from B<dgit-repos>.
+
+=item
+
+It is more important to have the Debian package's git history be a
+descendent of upstream's git history than to use exactly the orig.tar
+that upstream makes available for download.
+
+=back
+
+=head1 GIT CONFIGURATION
+
+Add the following to your ~/.gitconfig to teach git-archive(1) how to
+compress orig tarballs:
+
+=over 4
+
+    [tar "tar.xz"]
+       command = xz -c
+    [tar "tar.gz"]
+       command = gzip -c
+
+=back
+
+=head1 INITIAL DEBIANISATION
+
+This section explains how to start using this workflow with a new
+package.  It should be skipped when converting an existing package to
+this workflow.
+
+=head2 When upstream tags releases in git
+
+Suppose that the latest stable upstream release is 1.2.2, and this has
+been tagged '1.2.2' by upstream.
+
+=over 4
+
+    % git clone -oupstream https://some.upstream/foo.git
+    % cd foo
+    % git verify-tag 1.2.2
+    % git reset --hard 1.2.2
+    % git branch --unset-upstream
+
+=back
+
+The final command detachs your master branch from the upstream remote,
+so that git doesn't try to push anything there, or merge unreleased
+upstream commits.  If you want to maintain a copy of your packaging
+branch on B<alioth.debian.org> in addition to B<dgit-repos>, you can
+do something like this:
+
+=over 4
+
+    % git remote add -f origin git.debian.org:/git/collab-maint/foo.git
+    % git push --follow-tags -u origin master
+
+=back
+
+Now go ahead and Debianise your package.  Just make commits on the
+master branch, adding things in the I<debian/> directory.  If you need
+to patch the upstream source, just make commits that change files
+outside of the I<debian/> directory.  It is best to separate commits
+that touch I<debian/> from commits that touch upstream source, so that
+the latter can be cherry-picked by upstream.
+
+Note that there is no need to maintain a separate 'upstream' branch,
+unless you also happen to be involved in upstream development.  We
+work with upstream tags rather than any branches, except when
+forwarding patches (see FORWARDING PATCHES UPSTREAM, below).
+
+Finally, you need an orig tarball.  Generate one with git-archive(1):
+
+=over 4
+
+    % git archive -o ../foo_1.2.2.orig.tar.xz 1.2.2
+
+=back
+
+If you are using the version 1.0 source package format, replace 'xz'
+with 'gz'.
+
+This tarball is ephemeral and easily regenerated, so we don't commit
+it anywhere (e.g. with tools like pristine-tar(1)).
+
+=head3 Verifying upstream's tarball releases
+
+=over 4
+
+It can be a good idea to compare upstream's released tarballs with the
+release tags, at least for the first upload of the package.  If they
+are different, you might need to add some additional steps to your
+I<debian/rules>, such as running autotools.
+
+A convenient way to perform this check is to import the tarball as
+described in the following section, using a different value for
+'upstream-tag', and then use git-diff(1) to compare the imported
+tarball to the release tag.  If they are the same, you can use
+upstream's tarball instead of running git-archive(1).
+
+=back
+
+=head2 When upstream releases only tarballs
+
+We need a virtual upstream branch with virtual release tags.
+gbp-import-orig(1) can manage this for us.  To begin
+
+=over 4
+
+    % mkdir foo
+    % cd foo
+    % git init
+
+=back
+
+Now create I<debian/gbp.conf>:
+
+=over 4
+
+    [DEFAULT]
+    upstream-branch = upstream
+    debian-branch = master
+    upstream-tag = %(version)s
+
+    sign-tags = True
+    pristine-tar = False
+    pristine-tar-commit = False
+
+=back
+
+Then we can import the upstream version:
+
+=over 4
+
+    % git add debian/gbp.conf && git commit -m "create gbp.conf"
+    % gbp import-orig ../foo_1.2.2.orig.tar.xz
+
+=back
+
+You are now ready to proceed as above, making commits to both the
+upstream source and the I<debian/> directory.
+
+If you want to maintain a copy of your repository on
+B<alioth.debian.org>, you should push both the origin and the upstream
+branches:
+
+=over 4
+
+    % git remote add -f origin git.debian.org:/git/collab-maint/foo.git
+    % git push --follow-tags -u origin master upstream
+
+=back
+
+=head1 CONVERTING AN EXISTING PACKAGE
+
+This section explains how to convert an existing Debian package to
+this workflow.  It should be skipped when debianising a new package.
+
+=head2 No existing git history
+
+=over 4
+
+    % dgit clone foo
+    % cd foo
+    % git remote add -f upstream https://some.upstream/foo.git
+
+=back
+
+=head2 Existing git history using another workflow
+
+First, dump any existing patch queue:
+
+=over 4
+
+    % git rm -rf debian/patches
+    % git commit -m "drop existing quilt patch queue"
+
+=back
+
+Then make new upstream tags available:
+
+=over 4
+
+    % git remote add -f upstream https://some.upstream/foo.git
+
+=back
+
+Now you simply need to ensure that your git HEAD is dgit-compatible,
+i.e., it is exactly what you would get if you ran B<dpkg-buildpackage
+-i\.git/ -I.git -S> and then unpacked the resultant source package.
+
+To achieve this, you might need to delete
+I<debian/source/local-options>.  One way to have dgit check your
+progress is to run B<dgit build-source>.
+
+The first dgit push will require I<--overwrite>.
+
+=head1 SOURCE PACKAGE CONFIGURATION
+
+=head2 debian/source/options
+
+We set some source package options such that dgit can transparently
+handle the "dropping" and "refreshing" of changes to the upstream
+source:
+
+=over 4
+
+    single-debian-patch
+    auto-commit
+
+=back
+
+You don't need to create this file if you are using the version 1.0
+source package format.
+
+=head2 Sample text for README.source
+
+It is a good idea to explain how a user can obtain a break down of the
+changes to the upstream source:
+
+=over 4
+
+The Debian packaging of foo is maintained using dgit.  For the sake of
+an efficient workflow, Debian modifications to the upstream source are
+squashed into a single diff, rather than a series of quilt patches.
+To obtain a patch queue for package version 1.2.3-1:
+
+=over 4
+
+    # apt-get install dgit
+    % dgit clone foo
+    % cd foo
+    % git log --oneline 1.2.3..debian/1.2.3-1 -- . ':!debian'
+
+=back
+
+See dgit(1), dgit(7) and dgit-maint-merge(7) for more information.
+
+=back
+
+=head1 BUILDING AND UPLOADING
+
+Use B<dgit build>, B<dgit sbuild>, B<dgit build-source>, and B<dgit
+push> as detailed in dgit(1).  If any command fails, dgit will provide
+a carefully-worded error message explaining what you should do.  If
+it's not clear, file a bug against dgit.  Remember to pass I<--new>
+for the first upload.
+
+As an alternative to B<dgit build> and friends, you can use a tool
+like gitpkg(1).  This works because like dgit, gitpkg(1) enforces that
+HEAD has exactly the contents of the source package.  gitpkg(1) is
+highly configurable, and one dgit user reports using it to produce and
+test multiple source packages, from different branches corresponding
+to each of the current Debian suites.
+
+If you want to skip dgit's checks while iterating on a problem with
+the package build (for example, you don't want to commit your changes
+to git), you can just run dpkg-buildpackage(1) or debuild(1) instead.
+
+=head1 NEW UPSTREAM RELEASES
+
+=head2 When upstream tags releases in git
+
+It's a good idea to preview the merge of the new upstream release.
+First, just check for any new or deleted files that may need
+accounting for in your copyright file:
+
+=over 4
+
+    % git remote update
+    % git diff --stat master..1.2.3 -- . ':!debian'
+
+=back
+
+You can then review the full merge diff:
+
+=over 4
+
+    % git merge-tree `git merge-base master 1.2.3` master 1.2.3 | $PAGER
+
+=back
+
+Once you're satisfied with what will be merged, update your package:
+
+=over 4
+
+    % git archive -o ../foo_1.2.3.orig.tar.xz 1.2.3
+    % git merge 1.2.3
+    % dch -v1.2.3-1 New upstream release.
+    % git add debian/changelog && git commit -m changelog
+
+=back
+
+and you are ready to try a build.
+
+Again, if you are using the version 1.0 source package format, replace
+'xz' with 'gz'.
+
+=head2 When upstream releases only tarballs
+
+You will need the I<debian/gbp.conf> from "When upstream releases only
+tarballs", above.
+
+Then, either
+
+=over 4
+
+    % gbp import-orig ../foo_1.2.2.orig.tar.xz
+
+=back
+
+or if you have a working watch file
+
+=over 4
+
+    % gbp import-orig --uscan
+
+=back
+
+=head1 HANDLING DFSG-NON-FREE MATERIAL
+
+=head2 When upstream tags releases in git
+
+We create a DFSG-clean tag to merge to master:
+
+=over 4
+
+    % git checkout -b pre-dfsg 1.2.3
+    % git rm evil.bin
+    % git commit -m "upstream version 1.2.3 DFSG-cleaned"
+    % git tag -s 1.2.3+dfsg
+    % git checkout master
+    % git branch -D pre-dfsg
+
+=back
+
+Before merging the new 1.2.3+dfsg tag to master, you should first
+determine whether it would be legally dangerous for the non-free
+material to be publicly accessible in the git history on
+B<dgit-repos>.
+
+If it would be dangerous, there is a big problem;
+in this case please consult your archive administrators
+(for Debian this is the dgit administrator dgit-owner@debian.org
+and the ftpmasters ftpmaster@ftp-master.debian.org).
+
+=head2 When upstream releases only tarballs
+
+The easiest way to handle this is to add a B<Files-Excluded> field to
+I<debian/copyright>, and a B<uversionmangle> setting in
+I<debian/watch>.  See uscan(1).  Alternatively, see the I<--filter>
+option detailed in gbp-import-orig(1).
+
+=head1 FORWARDING PATCHES UPSTREAM
+
+The basic steps are:
+
+=over 4
+
+=item 1.
+
+Create a new branch based off upstream's master branch.
+
+=item 2.
+
+git-cherry-pick(1) commits from your master branch onto your new
+branch.
+
+=item 3.
+
+Push the branch somewhere and ask upstream to merge it, or use
+git-format-patch(1) or git-request-pull(1).
+
+=back
+
+For example (and it is only an example):
+
+=over 4
+
+    % # fork foo.git on GitHub
+    % git remote add -f fork git@github.com:spwhitton/foo.git
+    % git checkout -b fix-error upstream/master
+    % git config branch.fix-error.pushRemote fork
+    % git cherry-pick master^2
+    % git push
+    % # submit pull request on GitHub
+
+=back
+
+Note that when you merge an upstream release containing your forwarded
+patches, git and dgit will transparently handle "dropping" the patches
+that have been forwarded, "retaining" the ones that haven't.
+
+=head1 INCORPORATING NMUS
+
+=over 4
+
+    % dgit pull
+
+=back
+
+Alternatively, you can apply the NMU diff to your repository.  The
+next push will then require I<--overwrite>.
+
+=head1 SEE ALSO
+
+dgit(1), dgit(7)
+
+=head1 AUTHOR
+
+This tutorial was written and is maintained by Sean Whitton <spwhitton@spwhitton.name>.  It contains contributions from other dgit contributors too - see the dgit copyright file.
diff --git a/dgit-maint-native.7.pod b/dgit-maint-native.7.pod
new file mode 100644 (file)
index 0000000..03aee59
--- /dev/null
@@ -0,0 +1,116 @@
+=head1 NAME
+
+dgit - tutorial for package maintainers of Debian-native packages
+
+=head1 INTRODUCTION
+
+This document describes elements of a workflow for using B<dgit> to
+maintain a Debian package that uses one of the native source formats
+("1.0" & "3.0 (native)").
+
+=over 4
+
+=item
+
+We expect that your git history is fast-forwarding.
+
+=item
+
+You should be prepared to tolerate a small amount of
+ugliness in your git history
+in the form of merges which stitch
+the dgit-generated archive view
+into your maintainer history.
+
+This is to handle uploads that were not made with dgit,
+such as the uploads you made before switching to this workflow,
+or NMUs.
+
+=back
+
+=head2 Benefits
+
+=over 4
+
+=item
+
+Benefit from dgit's safety catches.  In particular, ensure that your
+upload always matches exactly your git HEAD.
+
+=item
+
+Provide a better,
+more detailed history
+to downstream dgit users.
+
+=item
+
+Incorporate an NMU with one command.
+
+=back
+
+=head1 FIRST PUSH WITH DGIT
+
+You do not need to do anything special to your tree
+to push with dgit.
+
+Simply prepare your git tree in the usual way, and then:
+
+=over 4
+
+    % dgit -wgf sbuild -A -c sid
+    % dgit -wgf --overwrite push
+
+=back
+
+(Do not make any tags yourself: dgit push will do that.)
+
+The --overwrite option tells dgit that you are expecting
+that your git history is not a descendant of the
+history which dgit synthesised from the previous
+non-dgit uploads.
+
+dgit will make a merge commit
+on your branch
+but without making any code changes
+(ie, a pseudo-merge)
+so that your history,
+which will be pushed to the dgit git server,
+is fast forward from the dgit archive view.
+
+=head1 SUBSEQUENT PUSHES
+
+=over 4
+
+    % dgit -wgf push
+
+=back
+
+That's it.
+
+=head1 INCORPORATING AN NMU
+
+=over 4
+
+    % dgit pull
+
+=back
+
+That's it.
+
+Or, if you would prefer to review t