chiark / gitweb /
changelog: start 2.10~
[dgit.git] / dgit
diff --git a/dgit b/dgit
index 1a5b88a5ef16284a0b09dc3affbb5e2fe0d119dc..8264f3e576a625e37cecb9c30bfe70f3596fd9a4 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -2,7 +2,7 @@
 # dgit
 # Integration between git and Debian-style archives
 #
-# Copyright (C)2013-2015 Ian Jackson
+# 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
@@ -41,6 +41,7 @@ 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;
@@ -66,6 +67,7 @@ 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 $split_brain_save;
 our $we_are_responder;
 our $initiator_tempdir;
 our $patches_applied_dirtily = 00;
@@ -73,6 +75,13 @@ 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]+';
@@ -86,7 +95,7 @@ our $splitbraincache = 'dgit-intern/quilt-cache';
 
 our (@git) = qw(git);
 our (@dget) = qw(dget);
-our (@curl) = qw(curl -f);
+our (@curl) = qw(curl);
 our (@dput) = qw(dput);
 our (@debsign) = qw(debsign);
 our (@gpg) = qw(gpg);
@@ -144,6 +153,11 @@ 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);
@@ -208,6 +222,12 @@ sub changespat ($;$) {
     return "${package}_".(stripepoch $vsn)."_".($arch//'*').".changes";
 }
 
+sub upstreamversion ($) {
+    my ($vsn) = @_;
+    $vsn =~ s/-[^-]+$//;
+    return $vsn;
+}
+
 our $us = 'dgit';
 initdebug('');
 
@@ -222,6 +242,20 @@ END {
 
 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;
@@ -551,7 +585,7 @@ our %defcfg = ('dgit.default.distro' => 'debian',
               'dgit.default.ssh' => 'ssh',
               'dgit.default.archive-query' => 'madison:',
               'dgit.default.sshpsql-dbname' => 'service=projectb',
-              'dgit.default.dgit-tag-format' => 'old,new,maint',
+              '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"
@@ -561,7 +595,6 @@ our %defcfg = ('dgit.default.distro' => 'debian',
               'dgit-distro.debian.git-check' => 'url',
               'dgit-distro.debian.git-check-suffix' => '/info/refs',
               'dgit-distro.debian.new-private-pushers' => 't',
-              'dgit-distro.debian.dgit-tag-format' => 'new',
               'dgit-distro.debian/push.git-url' => '',
               'dgit-distro.debian/push.git-host' => 'push.dgit.debian.org',
               'dgit-distro.debian/push.git-user-force' => 'dgit',
@@ -594,42 +627,55 @@ our %defcfg = ('dgit.default.distro' => 'debian',
               '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' => "ftpmasterapi:",
+              '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 %gitcfg;
+our %gitcfgs;
+our @gitcfgsources = qw(cmdline local global system);
 
 sub git_slurp_config () {
     local ($debuglevel) = $debuglevel-2;
     local $/="\0";
 
-    my @cmd = (@git, qw(config -z --get-regexp .*));
-    debugcmd "|",@cmd;
-
-    open GITS, "-|", @cmd or die $!;
-    while (<GITS>) {
-       chomp or die;
-       printdebug "=> ", (messagequote $_), "\n";
-       m/\n/ or die "$_ ?";
-       push @{ $gitcfg{$`} }, $'; #';
+    # 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;
     }
-    $!=0; $?=0;
-    close GITS
-       or ($!==0 && $?==256)
-       or failedcmd @cmd;
 }
 
 sub git_get_config ($) {
     my ($c) = @_;
-    my $l = $gitcfg{$c};
-    printdebug"C $c ".(defined $l ? messagequote "'$l'" : "undef")."\n"
-       if $debuglevel >= 4;
-    $l or return undef;
-    @$l==1 or badcfg "multiple values for $c" if @$l > 1;
-    return $l->[0];
+    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 {
@@ -922,15 +968,22 @@ sub must_getcwd () {
     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) = @_;
+sub archive_query ($;@) {
+    my ($method) = shift @_;
     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); }
+    { no strict qw(refs); &{"${method}_${proto}"}($proto,$data,@_); }
 }
 
 sub pool_dsc_subpath ($$) {
@@ -943,7 +996,7 @@ sub pool_dsc_subpath ($$) {
 
 sub archive_api_query_cmd ($) {
     my ($subpath) = @_;
-    my @cmd = qw(curl -sS);
+    my @cmd = (@curl, qw(-sS));
     my $url = access_cfg('archive-query-url');
     if ($url =~ m#^https://([-.0-9a-z]+)/#) {
        my $host = $1;
@@ -971,17 +1024,27 @@ sub archive_api_query_cmd ($) {
     return @cmd;
 }
 
-sub api_query ($$) {
+sub api_query ($$;$) {
     use JSON;
-    my ($data, $subpath) = @_;
+    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 () {
+sub canonicalise_suite_ftpmasterapi {
     my ($proto,$data) = @_;
     my $suites = api_query($data, 'suites');
     my @matched;
@@ -1005,7 +1068,7 @@ sub canonicalise_suite_ftpmasterapi () {
     return $cn;
 }
 
-sub archive_query_ftpmasterapi () {
+sub archive_query_ftpmasterapi {
     my ($proto,$data) = @_;
     my $info = api_query($data, "dsc_in_suite/$isuite/$package");
     my @rows;
@@ -1032,6 +1095,42 @@ sub archive_query_ftpmasterapi () {
     return @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);
+}
+
+#---------- `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 {
@@ -1080,6 +1179,8 @@ sub canonicalise_suite_madison {
     return $r[0][2];
 }
 
+sub file_in_archive_madison { return undef; }
+
 #---------- `sshpsql' archive query method ----------
 
 sub sshpsql ($$$) {
@@ -1155,6 +1256,8 @@ END
     return $rows[0];
 }
 
+sub file_in_archive_sshpsql ($$$) { return undef; }
+
 #---------- `dummycat' archive query method ----------
 
 sub canonicalise_suite_dummycat ($$) {
@@ -1196,6 +1299,8 @@ sub archive_query_dummycat ($$) {
     return sort { -version_compare($a->[0],$b->[0]); } @rows;
 }
 
+sub file_in_archive_dummycat () { return undef; }
+
 #---------- tag format handling ----------
 
 sub access_cfg_tagformats () {
@@ -1268,12 +1373,11 @@ sub get_archive_dsc () {
                fail "$dscurl has hash $got but".
                    " archive told us to expect $digest";
        }
-       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;
+       parse_dscdata();
        my $fmt = getfield $dsc, 'Format';
-       fail "unsupported source format $fmt, sorry" unless $format_ok{$fmt};
+       $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;
@@ -1309,7 +1413,7 @@ sub check_for_git () {
        my $suffix = access_cfg('git-check-suffix','git-suffix',
                                'RETURN-UNDEF') // '.git';
        my $url = "$prefix/$package$suffix";
-       my @cmd = (qw(curl -sS -I), $url);
+       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
@@ -1414,10 +1518,13 @@ sub mktree_in_ud_from_only_subdir (;$) {
     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 (['Checksums-Sha256','Digest::SHA', 'new(256)'],
-                      ['Checksums-Sha1',  'Digest::SHA', 'new(1)'],
-                      ['Files',           'Digest::MD5', 'new()']) {
+    foreach my $csumi (@files_csum_info_fields) {
        my ($fname, $module, $method) = @$csumi;
        my $field = $dsc->{$fname};
        next unless defined $field;
@@ -1445,6 +1552,65 @@ 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;
@@ -1461,6 +1627,101 @@ sub is_orig_file_of_vsn ($$) {
     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;
+       }
+       print "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;
@@ -1575,10 +1836,15 @@ sub generate_commits_from_dsc () {
        my $f = $fi->{Filename};
        die "$f ?" if $f =~ m#/|^\.|\.dsc$|\.tmp$#;
 
-       link_ltarget "../../../$f", $f
+       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;
 
@@ -1595,8 +1861,7 @@ sub generate_commits_from_dsc () {
     # from the debian/changelog, so we record the tree objects now and
     # make them into commits later.
     my @tartrees;
-    my $upstreamv = $dsc->{version};
-    $upstreamv =~ s/-[^-]+$//;
+    my $upstreamv = upstreamversion $dsc->{version};
     my $orig_f_base = srcfn $upstreamv, '';
 
     foreach my $fi (@dfi) {
@@ -1771,7 +2036,7 @@ sub generate_commits_from_dsc () {
        printdebug "import clog $r1clogp->{version} becomes r1\n";
     }
     die $! if CLOGS->error;
-    close CLOGS or $?==(SIGPIPE<<8) or failedcmd @clogcmd;
+    close CLOGS or $?==SIGPIPE or failedcmd @clogcmd;
 
     $clogp or fail "package changelog has no entries!";
 
@@ -1857,25 +2122,54 @@ END
        local $ENV{GIT_AUTHOR_EMAIL} = $authline[1];
        local $ENV{GIT_AUTHOR_DATE} =  $authline[2];
 
-       eval {
-           runcmd shell_cmd 'exec >/dev/null 2>../../gbp-pq-output',
-               gbp_pq, qw(import);
-       };
-       if ($@) {
-           { local $@; eval { runcmd qw(cat ../../gbp-pq-output); }; }
-           die $@;
-       }
+       my $path = $ENV{PATH} or die;
+
+       foreach my $use_absurd (qw(0 1)) {
+           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{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;
+               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;
+               $rawimport_hash = $gapplied;
+           };
+           last unless $@;
+       }
+       if ($@) {
+           { local $@; eval { runcmd qw(cat ../../gbp-pq-output); }; }
+           die $@;
+       }
     }
 
     progress "synthesised git commit from .dsc $cversion";
@@ -1928,12 +2222,13 @@ sub complete_file_from_dsc ($$) {
     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(-o),$tf,'--',"$furl";
+       runcmd_ordryrun_local @curl,qw(-f -o),$tf,'--',"$furl";
        return 0 if !act_local();
        $downloaded = 1;
     }
@@ -1976,7 +2271,7 @@ sub git_fetch_us () {
     push @specs, qw(heads/*) if deliberately_not_fast_forward;
 
     # This is rather miserable:
-    # When git-fetch --prune is passed a fetchspec ending with a *,
+    # 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
@@ -1986,17 +2281,19 @@ sub git_fetch_us () {
     # 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
+    # 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
+    # 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
+    # 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;
@@ -2012,6 +2309,7 @@ sub git_fetch_us () {
     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!";
        }
@@ -2028,7 +2326,7 @@ sub git_fetch_us () {
            my ($objid,$rrefname) = ($1,$2);
            if (!$wanted_rref->($rrefname)) {
                print STDERR <<END;
-warning: git-ls-remote @look reported $rrefname; this is silly, ignoring it.
+warning: git ls-remote @look reported $rrefname; this is silly, ignoring it.
 END
                next;
            }
@@ -2039,10 +2337,12 @@ END
 
        # OK, now %want is exactly what we want for refs in @specs
        my @fspecs = map {
-           return () if !m/\*$/ && !exists $wantr{"refs/$_"};
+           !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;
@@ -2061,11 +2361,11 @@ END
            if (!exists $wantr{$rrefname}) {
                if ($wanted_rref->($rrefname)) {
                    printdebug <<END;
-git-fetch @fspecs created $lrefname which git-ls-remote @look didn't list.
+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.
+warning: git fetch @fspecs created $lrefname; this is silly, deleting it.
 END
                }
                runcmd_ordryrun_local @git, qw(update-ref -d), $lrefname;
@@ -2080,24 +2380,24 @@ END
            next if $got eq $want;
            if (!defined $objgot{$want}) {
                print STDERR <<END;
-warning: git-ls-remote suggests we want $lrefname
+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:  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
+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;
+               "dgit fetch git fetch fixup", $lrefname, $want;
            $lrfetchrefs_f{$lrefname} = $want;
        }
        last;
     }
-    printdebug "git_fetch_us: git-fetch --no-insane emulation complete\n",
+    printdebug "git_fetch_us: git fetch --no-insane emulation complete\n",
        Dumper(\%lrfetchrefs_f);
 
     my %here;
@@ -2136,6 +2436,8 @@ sub mergeinfo_version ($) {
 }
 
 sub fetch_from_archive () {
+    ensure_setup_existing_tree();
+
     # Ensures that lrref() is what is actually in the archive, one way
     # or another, according to us - ie this client's
     # appropritaely-updated archive view.  Also returns the commit id.
@@ -2274,11 +2576,8 @@ sub fetch_from_archive () {
     };
 
     if (defined $dsc_hash) {
-       fail "missing remote git history even though dsc has hash -".
-           " could not find ref ".rref()." at ".access_giturl()
-           unless $lastpush_hash;
        ensure_we_have_orig();
-       if ($dsc_hash eq $lastpush_hash) {
+       if (!$lastpush_hash || $dsc_hash eq $lastpush_hash) {
            @mergeinputs = $dsc_mergeinput
        } elsif (is_fast_fwd($dsc_hash,$lastpush_hash)) {
            print STDERR <<END or die $!;
@@ -2445,7 +2744,8 @@ END
        die "$lasth $hash $what ?" unless is_fast_fwd($lasth, $hash);
     };
 
-    $chkff->($lastpush_hash, 'dgit repo server tip (last push)');
+    $chkff->($lastpush_hash, 'dgit repo server tip (last push)')
+       if $lastpush_hash;
     $chkff->($lastfetch_hash, 'local tracking tip (last fetch)');
 
     runcmd @git, qw(update-ref -m), "dgit fetch $csuite",
@@ -2535,6 +2835,13 @@ sub setup_useremail (;$) {
     $setup->('name', 'DEBFULLNAME');
 }
 
+sub ensure_setup_existing_tree () {
+    my $k = "remote.$remotename.skipdefaultupdate";
+    my $c = git_get_config $k;
+    return if defined $c;
+    set_local_git_config $k, 'true';
+}
+
 sub setup_new_tree () {
     setup_mergechangelogs();
     setup_useremail();
@@ -2570,6 +2877,11 @@ sub clone ($) {
     }
     setup_new_tree();
     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 -r . --
+END
     printdone "ready for work in $dstdir";
 }
 
@@ -2630,7 +2942,11 @@ sub commit_quilty_patch () {
     }
     my @adds = map { s/[][*?\\]/\\$&/g; $_; } sort keys %adds;
     runcmd_ordryrun_local @git, qw(add -f), @adds;
-    commit_admin "Commit Debian 3.0 (quilt) metadata";
+    commit_admin <<END
+Commit Debian 3.0 (quilt) metadata
+
+[dgit ($our_version) quilt-fixup]
+END
 }
 
 sub get_source_format () {
@@ -2679,6 +2995,18 @@ sub madformat_wantfixup ($) {
     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)
 
@@ -2797,39 +3125,50 @@ sub splitbrain_pseudomerge ($$$$) {
     #   this:                                   $dgitview'
     #
 
+    return $dgitview unless defined $archive_hash;
+
     printdebug "splitbrain_pseudomerge...\n";
 
     my $i_arch_v = pseudomerge_version_check($clogp, $archive_hash);
 
-    return $dgitview unless defined $archive_hash;
-
     if (!defined $overwrite_version) {
        progress "Checking that HEAD inciudes all changes in archive...";
     }
 
     return $dgitview if is_fast_fwd $archive_hash, $dgitview;
 
-    my $t_dep14 = debiantag_maintview $i_arch_v->[0], access_basedistro;
-    my $i_dep14 = infopair_lrf_tag_lookup($t_dep14, "maintainer view tag");
-    my $t_dgit = debiantag_new $i_arch_v->[0], access_basedistro;
-    my $i_dgit = infopair_lrf_tag_lookup($t_dgit, "dgit view tag");
-    my $i_archive = [ $archive_hash, "current archive contents" ];
-
-    printdebug "splitbrain_pseudomerge i_archive @$i_archive\n";
-
-    infopair_cond_equal($i_dgit, $i_archive);
-    infopair_cond_ff($i_dep14, $i_dgit);
-    $overwrite_version // infopair_cond_ff($i_dep14, [ $maintview, 'HEAD' ]);
+    if (defined $overwrite_version) {
+    } elsif (!eval {
+       my $t_dep14 = debiantag_maintview $i_arch_v->[0], access_basedistro;
+       my $i_dep14 = infopair_lrf_tag_lookup($t_dep14, "maintainer view tag");
+       my $t_dgit = debiantag_new $i_arch_v->[0], access_basedistro;
+       my $i_dgit = infopair_lrf_tag_lookup($t_dgit, "dgit view tag");
+       my $i_archive = [ $archive_hash, "current archive contents" ];
+
+       printdebug "splitbrain_pseudomerge i_archive @$i_archive\n";
+
+       infopair_cond_equal($i_dgit, $i_archive);
+       infopair_cond_ff($i_dep14, $i_dgit);
+       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 $overwrite_version
+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;
 }      
@@ -2841,16 +3180,6 @@ sub plain_overwrite_pseudomerge ($$$) {
 
     my $i_arch_v = pseudomerge_version_check($clogp, $archive_hash);
 
-    my @tagformats = access_cfg_tagformats();
-    my @t_overwr =
-       map { $_->($i_arch_v->[0], access_basedistro) }
-       (grep { m/^(?:old|hist)$/ } @tagformats)
-       ? \&debiantags : \&debiantag_new;
-    my $i_overwr = infopair_lrf_tag_lookup \@t_overwr, "previous version tag";
-    my $i_archive = [ $archive_hash, "current archive contents" ];
-
-    infopair_cond_equal($i_overwr, $i_archive);
-
     return $head if is_fast_fwd $archive_hash, $head;
 
     my $m = "Declare fast forward from $i_arch_v->[0]";
@@ -2871,7 +3200,10 @@ sub push_parse_changelog ($) {
     my $clogp = Dpkg::Control::Hash->new();
     $clogp->load($clogpfn) or die;
 
-    $package = getfield $clogp, 'Source';
+    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_basedistro);
     runcmd @git, qw(check-ref-format), $tag;
@@ -3068,21 +3400,22 @@ END
     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()) {
-           my $upstreamversion = $clogp->{Version};
-           $upstreamversion =~ s/-[^-]*$//;
            changedir $ud;
            quilt_make_fake_dsc($upstreamversion);
-           my ($dgitview, $cachekey) =
+           my $cachekey;
+           ($dgithead, $cachekey) =
                quilt_check_splitbrain_cache($actualhead, $upstreamversion);
-           $dgitview or fail
+           $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, $dgitview,
+                                              $actualhead, $dgithead,
                                               $archive_hash);
            $maintviewhead = $actualhead;
            changedir '../../../..';
@@ -3123,17 +3456,20 @@ END
     my ($tree,$dir) = mktree_in_ud_from_only_subdir();
     check_for_vendor_patches() if madformat($dsc->{format});
     changedir '../../../..';
-    my $diffopt = $debuglevel>0 ? '--exit-code' : '--quiet';
-    my @diffcmd = (@git, qw(diff), $diffopt, $tree, $dgithead);
+    my @diffcmd = (@git, qw(diff --quiet), $tree, $dgithead);
     debugcmd "+",@diffcmd;
     $!=0; $?=-1;
     my $r = system @diffcmd;
     if ($r) {
        if ($r==256) {
-           fail "$dscfn specifies a different tree to your HEAD commit;".
-               " perhaps you forgot to build".
-               ($diffopt eq '--exit-code' ? "" :
-                " (run with -D to see full diff output)");
+           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;
        }
@@ -3150,6 +3486,17 @@ END
        $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);
@@ -3217,7 +3564,8 @@ END
        push @pushrefs, $forceflag."refs/tags/$tw->{Tag}";
     }
 
-    runcmd_ordryrun @git, qw(push),access_giturl(), @pushrefs;
+    runcmd_ordryrun @git,
+       qw(-c push.followTags=false push), access_giturl(), @pushrefs;
     runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), $dgithead;
 
     supplementary_message(<<'END');
@@ -3288,7 +3636,7 @@ sub cmd_clone {
            }
            if (stat $dstdir) {
                rmtree($dstdir) or die "remove $dstdir: $!\n";
-           } elsif (!grep { $! == $_ }
+           } elsif (grep { $! == $_ }
                     (ENOENT, ENOTDIR, EACCES, EPERM, ELOOP)) {
            } else {
                print STDERR "check whether to remove $dstdir: $!\n";
@@ -3340,6 +3688,12 @@ sub cmd_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();
 }
 
@@ -3637,13 +3991,10 @@ sub quiltify_dpkg_commit ($$$;$) {
     mkpath '.git/dgit';
     my $descfn = ".git/dgit/quilt-description.tmp";
     open O, '>', $descfn or die "$descfn: $!";
-    $msg =~ s/\s+$//g;
-    $msg =~ s/\n/\n /g;
-    $msg =~ s/^\s+$/ ./mg;
+    $msg =~ s/\n+/\n\n/;
     print O <<END or die $!;
-Description: $msg
-Author: $author
-$xinfo
+From: $author
+${xinfo}Subject: $msg
 ---
 
 END
@@ -3657,22 +4008,51 @@ END
     }
 }
 
-sub quiltify_trees_differ ($$;$$) {
-    my ($x,$y,$finegrained,$ignorenamesr) = @_;
+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 --name-only -z));
-    push @cmd, qw(-r) if $finegrained;
+    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 "deleted\n" unless $newmode =~ m/[^0]/;
+               die "not a plain file\n" unless $newmode =~ m/^10\d{4}$/;
+               if ($oldmode =~ m/[^0]/) {
+                   die "mode changed\n" if $oldmode ne $newmode;
+               } else {
+                   die "non-default mode\n" unless $newmode =~ m/^100644$/;
+               }
+           };
+           if ($@) {
+               local $/="\n"; chomp $@;
+               push @$unrepres, [ $f, $@ ];
+           }
+       }
+
        my $isignore = $f =~ m#^(?:.*/)?.gitignore$#s;
        $r |= $isignore ? 02 : 01;
        $ignorenamesr->{$f}=1 if $ignorenamesr && $isignore;
@@ -3716,7 +4096,7 @@ sub quiltify_splitbrain ($$$$$$) {
     local $ENV{GIT_AUTHOR_DATE} =  $authline[2];
 
     if ($quilt_mode =~ m/gbp|unapplied/ &&
-       ($diffbits->{H2O} & 01)) {
+       ($diffbits->{O2H} & 01)) {
        my $msg =
  "--quilt=$quilt_mode specified, implying patches-unapplied git tree\n".
  " but git tree differs from orig in upstream files.";
@@ -3750,7 +4130,7 @@ END
  .gitignores: but, such patches exist in debian/patches.
 END
     }
-    if (($diffbits->{H2O} & 02) && # user has modified .gitignore
+    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";
@@ -3781,21 +4161,44 @@ END
        print SERIES "auto-gitignore\n" or die $!;
        close SERIES or die  $!;
         runcmd @git, qw(add -- debian/patches/series), $gipatch;
-        commit_admin "Commit patch to update .gitignore";
+        commit_admin <<END
+Commit patch to update .gitignore
+
+[dgit ($our_version) update-gitignore-quilt-fixup]
+END
     }
 
-    my $dgitview = git_rev_parse 'refs/heads/dgit-view';
+    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;
 
-    progress "dgit view: created (commit id $dgitview)";
-
     changedir '.git/dgit/unpack/work';
+
+    my $saved = maybe_split_brain_save $headref, $dgitview, "converted";
+    progress "dgit view: created ($saved)";
 }
 
 sub quiltify ($$$$) {
@@ -3976,15 +4379,75 @@ sub quiltify ($$$$) {
        $commitdata =~ m/^author (.*) \d+ [-+0-9]+$/m or die "$cc ?";
        my $author = $1;
 
+       my $commitdate = cmdoutput
+           @git, qw(log -n1 --pretty=format:%aD), $cc;
+
        $msg =~ s/^(.*)\n*/$1\n/ or die "$cc $msg ?";
 
+       my $strip_nls = sub { $msg =~ s/\n+$//; $msg .= "\n"; };
+       $strip_nls->();
+
        my $title = $1;
-       my $patchname = $title;
-       $patchname =~ s/[.:]$//;
-       $patchname =~ y/ A-Z/-a-z/;
-       $patchname =~ y/-a-z0-9_.+=~//cd;
-       $patchname =~ s/^\W/x-$&/;
-       $patchname = substr($patchname,0,40);
+       my $patchname;
+       my $patchdir;
+
+       my $gbp_check_suitable = sub {
+           $_ = shift;
+           my ($what) = @_;
+
+           eval {
+               die "contains unexpected slashes\n" if m{//} || m{/$};
+               die "contains leading punctuation\n" if m{^\W} || m{/\W};
+               die "contains bad character(s)\n" if m{[^-a-z0-9_.+=~/]}i;
+               die "too long" if length > 200;
+           };
+           return $_ unless $@;
+           print STDERR "quiltifying commit $cc:".
+               " ignoring/dropping Gbp-Pq $what: $@";
+           return undef;
+       };
+
+       if ($msg =~ s/^ (?: gbp(?:-pq)? : \s* name \s+ |
+                          gbp-pq-name: \s* )
+                      (\S+) \s* \n //ixm) {
+           $patchname = $gbp_check_suitable->($1, 'Name');
+       }
+       if ($msg =~ s/^ (?: gbp(?:-pq)? : \s* topic \s+ |
+                          gbp-pq-topic: \s* )
+                      (\S+) \s* \n //ixm) {
+           $patchdir = $gbp_check_suitable->($1, 'Topic');
+       }
+
+       $strip_nls->();
+
+       if (!defined $patchname) {
+           $patchname = $title;
+           $patchname =~ s/[.:]$//;
+            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";
@@ -4002,6 +4465,7 @@ sub quiltify ($$$$) {
        runcmd @git, qw(checkout -q), $target, qw(debian/changelog);
 
        quiltify_dpkg_commit "$patchname$index", $author, $msg,
+           "Date: $commitdate\n".
            "X-Dgit-Generated: $clogp->{Version} $cc\n";
 
        runcmd @git, qw(checkout -q), $cc, qw(debian/changelog);
@@ -4033,8 +4497,7 @@ END
     prep_ud();
     changedir $ud;
 
-    my $upstreamversion=$version;
-    $upstreamversion =~ s/-[^-]*$//;
+    my $upstreamversion = upstreamversion $version;
 
     if ($fopts->{'single-debian-patch'}) {
        quilt_fixup_singlepatch($clogp, $headref, $upstreamversion);
@@ -4077,7 +4540,11 @@ sub quilt_fixup_linkorigs ($$) {
 
 sub quilt_fixup_delete_pc () {
     runcmd @git, qw(rm -rqf .pc);
-    commit_admin "Commit removal of .pc (quilt series tracking data)";
+    commit_admin <<END
+Commit removal of .pc (quilt series tracking data)
+
+[dgit ($our_version) upgrade quilt-remove-pc]
+END
 }
 
 sub quilt_fixup_singlepatch ($$$) {
@@ -4171,7 +4638,7 @@ sub quilt_check_splitbrain_cache ($$) {
     my $srcshash = Digest::SHA->new(256);
     my %sfs = ( %INC, '$0(dgit)' => $0 );
     foreach my $sfk (sort keys %sfs) {
-       next unless m/^\$0\b/ || m{^Debian/Dgit\b};
+       next unless $sfk =~ m/^\$0\b/ || $sfk =~ m{^Debian/Dgit\b};
        $srcshash->add($sfk,"  ");
        $srcshash->add(hashfile($sfs{$sfk}));
        $srcshash->add("\n");
@@ -4179,7 +4646,7 @@ sub quilt_check_splitbrain_cache ($$) {
     push @cachekey, $srcshash->hexdigest();
     $splitbrain_cachekey = "@cachekey";
 
-    my @cmd = (@git, qw(reflog), '--pretty=format:%H %gs',
+    my @cmd = (@git, qw(log -g), '--pretty=format:%H %gs',
               $splitbraincache);
     printdebug "splitbrain cachekey $splitbrain_cachekey\n";
     debugcmd "|(probably)",@cmd;
@@ -4200,8 +4667,9 @@ sub quilt_check_splitbrain_cache ($$) {
            
        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 (commit id $cachehit)";
+           progress "dgit view: found cached ($saved)";
            runcmd @git, qw(checkout -q -b dgit-view), $cachehit;
            $split_brain = 1;
            return ($cachehit, $splitbrain_cachekey);
@@ -4259,10 +4727,10 @@ sub quilt_fixup_multipatch ($$$) {
     #     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
+    #          - 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
@@ -4271,7 +4739,7 @@ sub quilt_fixup_multipatch ($$$) {
     # 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).
+    # (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
@@ -4320,8 +4788,17 @@ sub quilt_fixup_multipatch ($$$) {
 
     ensuredir '.pc';
 
-    runcmd qw(sh -ec),
-        'exec dpkg-source --before-build . >/dev/null';
+    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 '..';
 
@@ -4348,37 +4825,50 @@ sub quilt_fixup_multipatch ($$$) {
     # 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
-        H2O => quiltify_trees_differ($headref,  $unapplied, 1,\%editedignores),
+        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(H2O O2A H2A)) {
+        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->{H2O} & $diffbits->{O2A})) {
+    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 ?";
+        " --[quilt=]gbp --[quilt=]dpm --quilt=unapplied ?";
 
     if (quiltmode_splitbrain()) {
        quiltify_splitbrain($clogp, $unapplied, $headref,
@@ -4485,15 +4975,21 @@ sub cmd_clean () {
     maybe_unapply_patches_again();
 }
 
-sub build_prep () {
+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;
-    check_not_dirty();
-    clean_tree();
     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;
@@ -4594,11 +5090,87 @@ sub massage_dbp_args ($;$) {
     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();
     }
@@ -4608,10 +5180,32 @@ sub cmd_build {
        runcmd_ordryrun_local @dbp;
     }
     maybe_unapply_patches_again();
-    printdone "build successful\n";
+    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;
@@ -4627,8 +5221,27 @@ sub 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';
@@ -4637,14 +5250,10 @@ sub cmd_gbp_build {
     }
     maybe_unapply_patches_again();
     if ($wantsrc < 2) {
-       unless (grep { m/^--git-debian-branch|^--git-ignore-branch/ } @ARGV) {
-           canonicalise_suite();
-           push @cmd, "--git-debian-branch=".lbranch();
-       }
        push @cmd, changesopts();
        runcmd_ordryrun_local @cmd, @ARGV;
     }
-    printdone "build successful\n";
+    postbuild_mergechanges_vanilla $wantsrc;
 }
 sub cmd_git_build { cmd_gbp_build(); } # compatibility with <= 1.0
 
@@ -4717,47 +5326,22 @@ sub cmd_build_source {
 
 sub cmd_sbuild {
     build_source();
-    my $pat = changespat $version;
-    if (!$rmchanges) {
-       my @unwanted = map { s#^\.\./##; $_; } glob "../$pat";
-       @unwanted = grep { $_ ne changespat $version,'source' } @unwanted;
-       fail "changes files other than source matching $pat".
-           " already present (@unwanted);".
-           " building would result in ambiguity about the intended results"
-           if @unwanted;
-    }
-    my $wasdir = must_getcwd();
-    changedir "..";
-    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;
-    my @changesfiles = glob $pat;
-    @changesfiles = sort {
-       ($b =~ m/_source\.changes$/ <=> $a =~ m/_source\.changes$/)
-           or $a cmp $b
-    } @changesfiles;
-    fail "wrong number of different changes files (@changesfiles)"
-       unless @changesfiles==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}: $!";
+    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): $!";
        }
-    }
-    changedir $wasdir;
+       runcmd_ordryrun_local @sbuild, qw(-d), $isuite, @ARGV, $dscfn;
+    };
     maybe_unapply_patches_again();
-    printdone "build successful, results in $multichanges\n" or die $!;
+    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 {
@@ -4770,10 +5354,162 @@ sub cmd_quilt_fixup {
     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';
+           $newhash = make_commit_text <<END;
+tree $tree
+parent $newhash
+parent $oldhash
+
+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";
 }
@@ -4829,10 +5565,9 @@ 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 { push @git, '-c', @_; };
-
 defvalopt '', '-C', '.+', sub {
     ($changesfile) = (@_);
     if ($changesfile =~ s#^(.*)/##) {
@@ -4908,6 +5643,9 @@ sub parseopts () {
                     ($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;
@@ -4923,12 +5661,27 @@ sub parseopts () {
            } elsif (m/^--overwrite=(.+)$/s) {
                push @ropts, $_;
                $overwrite_version = $1;
+           } 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, $_;
@@ -4983,6 +5736,12 @@ sub parseopts () {
                } 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;
@@ -4996,6 +5755,30 @@ sub parseopts () {
     }
 }
 
+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};
@@ -5008,16 +5791,18 @@ sub finalise_opts_opts () {
        }
 
        foreach my $c (access_cfg_cfgs("opts-$k")) {
-           my $vl = $gitcfg{$c};
-           printdebug "CL $c ",
-               ($vl ? join " ", map { shellquote } @$vl : ""),
+           my @vl =
+               map { $_ ? @$_ : () }
+               map { $gitcfgs{$_}{$c} }
+               reverse @gitcfgsources;
+           printdebug "CL $c ", (join " ", map { shellquote } @vl),
                "\n" if $debuglevel >= 4;
-           next unless $vl;
+           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,
+                    @vl,
                     @$om[$insertpos..$#$om] );
        }
     }
@@ -5029,6 +5814,7 @@ if ($ENV{$fakeeditorenv}) {
 }
 
 parseopts();
+check_env_sanity();
 git_slurp_config();
 
 print STDERR "DRY RUN ONLY\n" if $dryrun_level > 1;
@@ -5041,6 +5827,9 @@ if (!@ARGV) {
 my $cmd = shift @ARGV;
 $cmd =~ y/-/_/;
 
+my $pre_fn = ${*::}{"pre_$cmd"};
+$pre_fn->() if $pre_fn;
+
 if (!defined $rmchanges) {
     local $access_forpush;
     $rmchanges = access_cfg_bool(0, 'rm-old-changes');