chiark / gitweb /
git-debrebase: test suite: lib-gdr: move t-nmu-upload-[12]
[dgit.git] / dgit
diff --git a/dgit b/dgit
index edb242816306059e7b401880b0a9f9585ad7a527..49350ee70782bd08eb10d4a005d315c3f4e58252 100755 (executable)
--- a/dgit
+++ b/dgit
@@ -2,7 +2,8 @@
 # dgit
 # Integration between git and Debian-style archives
 #
-# Copyright (C)2013-2016 Ian Jackson
+# Copyright (C)2013-2017 Ian Jackson
+# Copyright (C)2017 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
@@ -19,7 +20,7 @@
 
 use strict;
 
-use Debian::Dgit;
+use Debian::Dgit qw(:DEFAULT :playground);
 setup_sigwarn();
 
 use IO::Handle;
@@ -30,6 +31,8 @@ use File::Path;
 use File::Temp qw(tempdir);
 use File::Basename;
 use Dpkg::Version;
+use Dpkg::Compression;
+use Dpkg::Compression::Process;
 use POSIX;
 use IPC::Open2;
 use Digest::SHA;
@@ -47,6 +50,8 @@ our $absurdity = undef; ###substituted###
 our @rpushprotovsn_support = qw(4 3 2); # 4 is new tag format
 our $protovsn;
 
+our $cmd;
+our $subcommand;
 our $isuite;
 our $idistro;
 our $package;
@@ -90,7 +95,7 @@ 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_comp_re = qr{orig(?:-$extra_orig_namepart_re)?};
 our $orig_f_sig_re = '\\.(?:asc|gpg|pgp)';
 our $orig_f_tail_re = "$orig_f_comp_re\\.tar(?:\\.\\w+)?(?:$orig_f_sig_re)?";
 
@@ -98,6 +103,8 @@ our $git_authline_re = '^([^<>]+) \<(\S+)\> (\d+ [-+]\d+)$';
 our $splitbraincache = 'dgit-intern/quilt-cache';
 our $rewritemap = 'dgit-rewrite/map';
 
+our @dpkg_source_ignores = qw(-i(?:^|/)\.git(?:/|$) -I.git);
+
 our (@git) = qw(git);
 our (@dget) = qw(dget);
 our (@curl) = (qw(curl --proto-redir), '-all,http,https', qw(-L));
@@ -109,8 +116,8 @@ 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 (@dpkgbuildpackage) = (qw(dpkg-buildpackage), @dpkg_source_ignores);
+our (@dpkgsource) = (qw(dpkg-source), @dpkg_source_ignores);
 our (@dpkggenchanges) = qw(dpkg-genchanges);
 our (@mergechanges) = qw(mergechanges -f);
 our (@gbp_build) = ('');
@@ -146,6 +153,7 @@ sub parseopts_late_defaults();
 sub setup_gitattrs(;$);
 sub check_gitattrs($$);
 
+our $playground;
 our $keyid;
 
 autoflush STDOUT 1;
@@ -249,12 +257,6 @@ sub no_such_package () {
     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;
@@ -281,6 +283,10 @@ sub gbp_pq {
     return opts_opt_multi_cmd @gbp_pq;
 }
 
+sub dgit_privdir () {
+    our $dgit_privdir_made //= ensure_a_playground 'dgit';
+}
+
 #---------- remote protocol support, common ----------
 
 # remote push initiator/responder protocol:
@@ -493,12 +499,6 @@ sub url_get {
 
 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; }
 
@@ -542,6 +542,7 @@ main usages:
   dgit [dgit-opts] build [dpkg-buildpackage-opts]
   dgit [dgit-opts] sbuild [sbuild-opts]
   dgit [dgit-opts] push [dgit-opts] [suite]
+  dgit [dgit-opts] push-source [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
@@ -567,7 +568,7 @@ sub nextarg {
 }
 
 sub pre_help () {
-    no_local_git_cfg();
+    not_necessarily_a_tree();
 }
 sub cmd_help () {
     print $helpmsg or die $!;
@@ -645,32 +646,17 @@ our %defcfg = ('dgit.default.distro' => 'debian',
 
 our %gitcfgs;
 our @gitcfgsources = qw(cmdline local global system);
+our $invoked_in_git_tree = 1;
 
 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;
+       $gitcfgs{$src} = git_slurp_config_src $src;
     }
 }
 
@@ -707,9 +693,10 @@ sub cfg {
        "$us: distro or suite appears not to be (properly) supported";
 }
 
-sub no_local_git_cfg () {
+sub not_necessarily_a_tree () {
     # needs to be called from pre_*
     @gitcfgsources = grep { $_ ne 'local' } @gitcfgsources;
+    $invoked_in_git_tree = 0;
 }
 
 sub access_basedistro__noalias () {
@@ -1004,19 +991,13 @@ sub commit_getclogp ($) {
     our %commit_getclogp_memo;
     my $memo = $commit_getclogp_memo{$objid};
     return $memo if $memo;
-    mkpath '.git/dgit';
-    my $mclog = ".git/dgit/clog-$objid";
+
+    my $mclog = dgit_privdir()."clog";
     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;
@@ -1700,27 +1681,14 @@ sub create_remote_git_repo () {
 our ($dsc_hash,$lastpush_mergeinput);
 our ($dsc_distro, $dsc_hint_tag, $dsc_hint_url);
 
-our $ud = '.git/dgit/unpack';
 
-sub prep_ud (;$) {
-    my ($d) = @_;
-    $d //= $ud;
-    rmtree($d);
-    mkpath '.git/dgit';
-    mkdir $d or die $!;
+sub prep_ud () {
+    dgit_privdir(); # ensures that $dgit_privdir_made is based on $maindir
+    $playground = fresh_playground 'dgit/unpack';
 }
 
 sub mktree_in_ud_here () {
-    runcmd qw(git init -q);
-    runcmd qw(git config gc.auto 0);
-    foreach my $copy (qw(user.email user.name user.useConfigOnly)) {
-       my $v = $gitcfgs{local}{$copy};
-       next unless $v;
-       runcmd qw(git config), $copy, $_ foreach @$v;
-    }
-    rmtree('.git/objects');
-    symlink '../../../../objects','.git/objects' or die $!;
-    setup_gitattrs(1);
+    playtree_setup $gitcfgs{local};
 }
 
 sub git_write_tree () {
@@ -1753,8 +1721,8 @@ sub remove_stray_gits ($) {
 
 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;
@@ -1883,6 +1851,40 @@ sub is_orig_file_of_vsn ($$) {
     return 1;
 }
 
+# This function determines whether a .changes file is source-only from
+# the point of view of dak.  Thus, it permits *_source.buildinfo
+# files.
+#
+# It does not, however, permit any other buildinfo files.  After a
+# source-only upload, the buildds will try to upload files like
+# foo_1.2.3_amd64.buildinfo.  If the package maintainer included files
+# named like this in their (otherwise) source-only upload, the uploads
+# of the buildd can be rejected by dak.  Fixing the resultant
+# situation can require manual intervention.  So we block such
+# .buildinfo files when the user tells us to perform a source-only
+# upload (such as when using the push-source subcommand with the -C
+# option, which calls this function).
+#
+# Note, though, that when dgit is told to prepare a source-only
+# upload, such as when subcommands like build-source and push-source
+# without -C are used, dgit has a more restrictive notion of
+# source-only .changes than dak: such uploads will never include
+# *_source.buildinfo files.  This is because there is no use for such
+# files when using a tool like dgit to produce the source package, as
+# dgit ensures the source is identical to git HEAD.
+sub test_source_only_changes ($) {
+    my ($changes) = @_;
+    foreach my $l (split /\n/, getfield $changes, 'Files') {
+        $l =~ m/\S+$/ or next;
+        # \.tar\.[a-z0-9]+ covers orig.tar and the tarballs in native packages
+        unless ($& =~ m/(?:\.dsc|\.diff\.gz|\.tar\.[a-z0-9]+|_source\.buildinfo)$/) {
+            print "purportedly source-only changes polluted by $&\n";
+            return 0;
+        }
+    }
+    return 1;
+}
+
 sub changes_update_origs_from_dsc ($$$$) {
     my ($dsc, $changes, $upstreamvsn, $changesfile) = @_;
     my %changes_f;
@@ -2095,13 +2097,13 @@ sub generate_commits_from_dsc () {
     # See big comment in fetch_from_archive, below.
     # See also README.dsc-import.
     prep_ud();
-    changedir $ud;
+    changedir $playground;
 
     my @dfi = dsc_files_info();
     foreach my $fi (@dfi) {
        my $f = $fi->{Filename};
        die "$f ?" if $f =~ m#/|^\.|\.dsc$|\.tmp$#;
-       my $upper_f = "../../../../$f";
+       my $upper_f = "$maindir/../$f";
 
        printdebug "considering reusing $f: ";
 
@@ -2421,6 +2423,10 @@ END
 
        my $path = $ENV{PATH} or die;
 
+       # we use ../../gbp-pq-output, which (given that we are in
+       # $playground/PLAYTREE, and $playground is .git/dgit/unpack,
+       # is .git/dgit.
+
        foreach my $use_absurd (qw(0 1)) {
            runcmd @git, qw(checkout -q unpa);
            runcmd @git, qw(update-ref -d refs/heads/patch-queue/unpa);
@@ -2505,8 +2511,8 @@ END
            @output = $lastpush_mergeinput;
        }
     }
-    changedir '../../../..';
-    rmtree($ud);
+    changedir $maindir;
+    rmtree $playground;
     return @output;
 }
 
@@ -3203,7 +3209,7 @@ END
        my $author = clogp_authline $useclogp;
        my $cversion = getfield $useclogp, 'Version';
 
-       my $mcf = ".git/dgit/mergecommit";
+       my $mcf = dgit_privdir()."/mergecommit";
        open MC, ">", $mcf or die "$mcf $!";
        print MC <<END or die $!;
 tree $tree
@@ -3263,7 +3269,6 @@ END
     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';
@@ -3302,8 +3307,9 @@ sub setup_mergechangelogs (;$) {
 
     my $driver = 'dpkg-mergechangelogs';
     my $cb = "merge.$driver";
-    my $attrs = '.git/info/attributes';
-    ensuredir '.git/info';
+    confess unless defined $maindir;
+    my $attrs = "$maindir_gitcommon/info/attributes";
+    ensuredir "$maindir_gitcommon/info";
 
     open NATTRS, ">", "$attrs.new" or die "$attrs.new $!";
     if (!open ATTRS, "<", $attrs) {
@@ -3348,15 +3354,16 @@ sub ensure_setup_existing_tree () {
     set_local_git_config $k, 'true';
 }
 
-sub open_gitattrs () {
-    my $gai = new IO::File ".git/info/attributes"
+sub open_main_gitattrs () {
+    confess 'internal error no maindir' unless defined $maindir;
+    my $gai = new IO::File "$maindir_gitcommon/info/attributes"
        or $!==ENOENT
-       or die "open .git/info/attributes: $!";
+       or die "open $maindir_gitcommon/info/attributes: $!";
     return $gai;
 }
 
 sub is_gitattrs_setup () {
-    my $gai = open_gitattrs();
+    my $gai = open_main_gitattrs();
     return 0 unless $gai;
     while (<$gai>) {
        return 1 if m{^\[attr\]dgit-defuse-attrs\s};
@@ -3376,14 +3383,15 @@ sub setup_gitattrs (;$) {
 END
        return;
     }
-    my $af = ".git/info/attributes";
+    my $af = "$maindir_gitcommon/info/attributes";
+    ensuredir "$maindir_gitcommon/info";
     open GAO, "> $af.new" or die $!;
     print GAO <<END or die $!;
 *      dgit-defuse-attrs
-[attr]dgit-defuse-attrs        -text -eol -crlf -ident -filter
+[attr]dgit-defuse-attrs        $negate_harmful_gitattrs
 # ^ see GITATTRIBUTES in dgit(7) and dgit setup-new-tree in dgit(1)
 END
-    my $gai = open_gitattrs();
+    my $gai = open_main_gitattrs();
     if ($gai) {
        while (<$gai>) {
            chomp;
@@ -3495,6 +3503,7 @@ sub fork_for_multisuite ($) {
     $before_fetch_merge->();
 
     foreach my $tsuite (@suites[1..$#suites]) {
+       $tsuite =~ s/^-/$cbasesuite-/;
        my $csubsuite = multisuite_suite_child($tsuite, \@mergeinputs,
                                               sub {
             @end = ();
@@ -3609,18 +3618,22 @@ END
 }
 
 sub clone ($) {
+    # in multisuite, returns twice!
+    # once in parent after first suite fetched,
+    # and then again in child after everything is finished
     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;
+       record_maindir();
     });
     if ($multi_fetched) {
         printdebug "multi clone after fetch merge\n";
        clone_set_head();
        clone_finish($dstdir);
-       exit 0;
+       return;
     }
     printdebug "clone main body\n";
 
@@ -3629,6 +3642,7 @@ sub clone ($) {
     mkdir $dstdir or fail "create \`$dstdir': $!";
     changedir $dstdir;
     runcmd @git, qw(init -q);
+    record_maindir();
     setup_new_tree();
     clone_set_head();
     my $giturl = access_giturl(1);
@@ -3678,15 +3692,7 @@ sub check_not_dirty () {
 
     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;
-    }
+    git_check_unmodified();
 }
 
 sub commit_admin ($) {
@@ -3768,7 +3774,7 @@ 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 ../../../..",
+    my @cmd = (shell_cmd 'cd "$1"; shift', $maindir,
               @git, qw(update-ref -m),
               "dgit --dgit-view-save $msg HEAD=$headref",
               $split_brain_save, $dgitview);
@@ -3869,8 +3875,7 @@ sub pseudomerge_make_commit ($$$$ $$) {
        : !length  $overwrite_version ? " --overwrite"
        : " --overwrite=".$overwrite_version;
 
-    mkpath '.git/dgit';
-    my $pmf = ".git/dgit/pseudomerge";
+    my $pmf = dgit_privdir()."/pseudomerge";
     open MC, ">", $pmf or die "$pmf $!";
     print MC <<END or die $!;
 tree $tree
@@ -3905,6 +3910,7 @@ sub splitbrain_pseudomerge ($$$$) {
     #
 
     return $dgitview unless defined $archive_hash;
+    return $dgitview if deliberately_not_fast_forward();
 
     printdebug "splitbrain_pseudomerge...\n";
 
@@ -4177,7 +4183,7 @@ END
     rpush_handle_protovsn_bothends() if $we_are_initiator;
     select_tagformat();
 
-    my $clogpfn = ".git/dgit/changelog.822.tmp";
+    my $clogpfn = dgit_privdir()."/changelog.822.tmp";
     runcmd shell_cmd "exec >$clogpfn", qw(dpkg-parsechangelog);
 
     responder_send_file('parsed-changelog', $clogpfn);
@@ -4206,20 +4212,20 @@ END
     if (madformat_wantfixup($format)) {
        # user might have not used dgit build, so maybe do this now:
        if (quiltmode_splitbrain()) {
-           changedir $ud;
+           changedir $playground;
            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] ?";
+ perhaps HEAD changed since dgit build[-source] ?";
            $split_brain = 1;
            $dgithead = splitbrain_pseudomerge($clogp,
                                               $actualhead, $dgithead,
                                               $archive_hash);
            $maintviewhead = $actualhead;
-           changedir '../../../..';
+           changedir $maindir;
            prep_ud(); # so _only_subdir() works, below
        } else {
            commit_quilty_patch();
@@ -4250,26 +4256,55 @@ END
        }
     }
 
-    changedir $ud;
+    changedir $playground;
     progress "checking that $dscfn corresponds to HEAD";
     runcmd qw(dpkg-source -x --),
-        $dscpath =~ m#^/# ? $dscpath : "../../../$dscpath";
+        $dscpath =~ m#^/# ? $dscpath : "$maindir/$dscpath";
     my ($tree,$dir) = mktree_in_ud_from_only_subdir("source package");
     check_for_vendor_patches() if madformat($dsc->{format});
-    changedir '../../../..';
+    changedir $maindir;
     my @diffcmd = (@git, qw(diff --quiet), $tree, $dgithead);
     debugcmd "+",@diffcmd;
     $!=0; $?=-1;
     my $r = system @diffcmd;
     if ($r) {
        if ($r==256) {
+           my $referent = $split_brain ? $dgithead : 'HEAD';
            my $diffs = cmdoutput @git, qw(diff --stat), $tree, $dgithead;
-           fail <<END
+
+           my @mode_changes;
+           my $raw = cmdoutput @git,
+               qw(diff --no-renames -z -r --raw), $tree, $dgithead;
+           my $changed;
+           foreach (split /\0/, $raw) {
+               if (defined $changed) {
+                   push @mode_changes, "$changed: $_\n" if $changed;
+                   $changed = undef;
+                   next;
+               } elsif (m/^:0+ 0+ /) {
+                   $changed = '';
+               } elsif (m/^:(?:10*)?(\d+) (?:10*)?(\d+) /) {
+                   $changed = "Mode change from $1 to $2"
+               } else {
+                   die "$_ ?";
+               }
+           }
+           if (@mode_changes) {
+               fail <<END.(join '', @mode_changes).<<END;
+HEAD specifies a different tree to $dscfn:
+$diffs
+END
+There is a problem with your source tree (see dgit(7) for some hints).
+To see a full diff, run git diff $tree $referent
+END
+           }
+
+           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
+   git diff $tree $referent
 END
        } else {
            failedcmd @diffcmd;
@@ -4328,7 +4363,7 @@ END
     }
 
     my @tagwants = push_tagwants($cversion, $dgithead, $maintviewhead,
-                                ".git/dgit/tag");
+                                dgit_privdir()."/tag");
     my @tagobjfns;
 
     supplementary_message(<<'END');
@@ -4418,7 +4453,7 @@ END
 }
 
 sub pre_clone () {
-    no_local_git_cfg();
+    not_necessarily_a_tree();
 }
 sub cmd_clone {
     parseopts();
@@ -4468,13 +4503,8 @@ sub cmd_clone {
 }
 
 sub branchsuite () {
-    my @cmd = (@git, qw(symbolic-ref -q HEAD));
-    my $branch = cmdoutput_errok @cmd;
-    if (!defined $branch) {
-       $?==256 or failedcmd @cmd;
-       return undef;
-    }
-    if ($branch =~ m#$lbranch_re#o) {
+    my $branch = git_get_symref();
+    if (defined $branch && $branch =~ m#$lbranch_re#o) {
        return $1;
     } else {
        return undef;
@@ -4521,21 +4551,18 @@ END
     pull();
 }
 
-sub cmd_push {
+sub prep_push () {
     parseopts();
-    badusage "-p is not allowed with dgit push" if defined $package;
+    build_or_push_prep_early();
+    pushing();
     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";
+       badusage "incorrect arguments to dgit $subcommand";
     }
-    $isuite = getfield $clogp, 'Distribution';
-    pushing();
     if ($new_package) {
        local ($package) = $existing_package; # this is a hack
        canonicalise_suite();
@@ -4545,9 +4572,28 @@ sub cmd_push {
     if (defined $specsuite &&
        $specsuite ne $isuite &&
        $specsuite ne $csuite) {
-           fail "dgit push: changelog specifies $isuite ($csuite)".
+           fail "dgit $subcommand: changelog specifies $isuite ($csuite)".
                " but command line specifies $specsuite";
     }
+}
+
+sub cmd_push {
+    prep_push();
+    dopush();
+}
+
+sub cmd_push_source {
+    prep_push();
+    if ($changesfile) {
+        my $changes = parsecontrol("$buildproductsdir/$changesfile",
+                                   "source changes file");
+        unless (test_source_only_changes($changes)) {
+            fail "user-specified changes file is not source-only";
+        }
+    } else {
+        # Building a source package is very fast, so just do it
+        build_source_for_push();
+    }
     dopush();
 }
 
@@ -4631,7 +4677,7 @@ sub i_method {
 }
 
 sub pre_rpush () {
-    no_local_git_cfg();
+    not_necessarily_a_tree();
 }
 sub cmd_rpush {
     my $host = nextarg;
@@ -4851,7 +4897,7 @@ sub quiltify_dpkg_commit ($$$;$) {
     my ($patchname,$author,$msg, $xinfo) = @_;
     $xinfo //= '';
 
-    mkpath '.git/dgit';
+    mkpath '.git/dgit'; # we are in playtree
     my $descfn = ".git/dgit/quilt-description.tmp";
     open O, '>', $descfn or die "$descfn: $!";
     $msg =~ s/\n+/\n\n/;
@@ -4883,7 +4929,7 @@ sub quiltify_trees_differ ($$;$$$) {
     #  a list of unrepresentable changes (removals of upstream files
     #  (as messages)
     local $/=undef;
-    my @cmd = (@git, qw(diff-tree -z));
+    my @cmd = (@git, qw(diff-tree -z --no-renames));
     push @cmd, qw(--name-only) unless $unrepres;
     push @cmd, qw(-r) if $finegrained || $unrepres;
     push @cmd, $x, $y;
@@ -4902,16 +4948,23 @@ sub quiltify_trees_differ ($$;$$$) {
 
        if ($unrepres) {
            eval {
-               die "not a plain file\n"
-                   unless $newmode =~ m/^10\d{4}$/ ||
-                          $oldmode =~ m/^10\d{4}$/;
+               die "not a plain file or symlink\n"
+                   unless $newmode =~ m/^(?:10|12)\d{4}$/ ||
+                          $oldmode =~ m/^(?:10|12)\d{4}$/;
                if ($oldmode =~ m/[^0]/ &&
                    $newmode =~ m/[^0]/) {
-                   die "mode changed\n" if $oldmode ne $newmode;
+                   # both old and new files exist
+                   die "mode or type changed\n" if $oldmode ne $newmode;
+                   die "modified symlink\n" unless $newmode =~ m/^10/;
+               } elsif ($oldmode =~ m/[^0]/) {
+                   # deletion
+                   die "deletion of symlink\n"
+                       unless $oldmode =~ m/^10/;
                } else {
-                   die "non-default mode\n"
-                       unless $newmode =~ m/^100644$/ ||
-                              $oldmode =~ m/^100644$/;
+                   # creation
+                   die "creation with non-default mode\n"
+                       unless $newmode =~ m/^100644$/ or
+                              $newmode =~ m/^120000$/;
                }
            };
            if ($@) {
@@ -5037,11 +5090,11 @@ END
 
     my $dgitview = git_rev_parse 'HEAD';
 
-    changedir '../../../..';
+    changedir $maindir;
     # 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", '>>'
+    ensuredir "$maindir_gitcommon/logs/refs/dgit-intern";
+    my $makelogfh = new IO::File "$maindir_gitcommon/logs/refs/$splitbraincache", '>>'
       or die $!;
 
     my $oldcache = git_get_ref "refs/$splitbraincache";
@@ -5062,7 +5115,7 @@ END
     runcmd @git, qw(update-ref -m), $cachekey, "refs/$splitbraincache",
        $dgitview;
 
-    changedir '.git/dgit/unpack/work';
+    changedir "$playground/work";
 
     my $saved = maybe_split_brain_save $headref, $dgitview, "converted";
     progress "dgit view: created ($saved)";
@@ -5266,6 +5319,7 @@ sub quiltify ($$$$) {
                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 "is series file\n" if m{$series_filename_re}o;
                die "too long" if length > 200;
            };
            return $_ unless $@;
@@ -5304,6 +5358,7 @@ sub quiltify ($$$$) {
            $patchname =~ y/-a-z0-9_.+=~//cd;
            $patchname =~ s/^\W/x-$&/;
            $patchname = substr($patchname,0,40);
+           $patchname .= ".patch";
        }
        if (!defined $patchdir) {
            $patchdir = '';
@@ -5359,7 +5414,7 @@ END
     my $headref = git_rev_parse('HEAD');
 
     prep_ud();
-    changedir $ud;
+    changedir $playground;
 
     my $upstreamversion = upstreamversion $version;
 
@@ -5371,9 +5426,9 @@ END
 
     die 'bug' if $split_brain && !$need_split_build_invocation;
 
-    changedir '../../../..';
+    changedir $maindir;
     runcmd_ordryrun_local
-        @git, qw(pull --ff-only -q .git/dgit/unpack/work master);
+        @git, qw(pull --ff-only -q), "$playground/work", qw(master);
 }
 
 sub quilt_fixup_mkwork ($) {
@@ -5389,7 +5444,7 @@ sub quilt_fixup_linkorigs ($$) {
     my ($upstreamversion, $fn) = @_;
     # calls $fn->($leafname);
 
-    foreach my $f (<../../../../*>) { #/){
+    foreach my $f (<$maindir/../*>) { #/){
        my $b=$f; $b =~ s{.*/}{};
        {
            local ($debuglevel) = $debuglevel-1;
@@ -5468,12 +5523,12 @@ END
                  debian/control debian/changelog);
     foreach my $maybe (qw(debian/patches debian/source/options
                           debian/tests/control)) {
-        next unless stat_exists "../../../$maybe";
+        next unless stat_exists "$maindir/$maybe";
         push @files, $maybe;
     }
 
     my $debtar= srcfn $fakeversion,'.debian.tar.gz';
-    runcmd qw(env GZIP=-1n tar -zcf), "./$debtar", qw(-C ../../..), @files;
+    runcmd qw(env GZIP=-1n tar -zcf), "./$debtar", qw(-C), $maindir, @files;
 
     $dscaddfile->($debtar);
     close $fakedsc or die $!;
@@ -5482,7 +5537,7 @@ END
 sub quilt_check_splitbrain_cache ($$) {
     my ($headref, $upstreamversion) = @_;
     # Called only if we are in (potentially) split brain mode.
-    # Called in $ud.
+    # Called in playground.
     # Computes the cache key and looks in the cache.
     # Returns ($dgit_view_commitid, $cachekey) or (undef, $cachekey)
 
@@ -5516,8 +5571,8 @@ sub quilt_check_splitbrain_cache ($$) {
     debugcmd "|(probably)",@cmd;
     my $child = open GC, "-|";  defined $child or die $!;
     if (!$child) {
-       chdir '../../..' or die $!;
-       if (!stat ".git/logs/refs/$splitbraincache") {
+       chdir $maindir or die $!;
+       if (!stat "$maindir_gitcommon/logs/refs/$splitbraincache") {
            $! == ENOENT or die $!;
            printdebug ">(no reflog)\n";
            exit 0;
@@ -5646,6 +5701,7 @@ sub quilt_fixup_multipatch ($$$) {
 
     rmtree '.pc';
 
+    rmtree 'debian'; # git checkout commitish paths does not delete!
     runcmd @git, qw(checkout -f), $headref, qw(-- debian);
     my $unapplied=git_add_write_tree();
     printdebug "fake orig tree object $unapplied\n";
@@ -5838,14 +5894,18 @@ sub cmd_clean () {
     maybe_unapply_patches_again();
 }
 
-sub build_prep_early () {
-    our $build_prep_early_done //= 0;
-    return if $build_prep_early_done++;
-    badusage "-p is not allowed when building" if defined $package;
+sub build_or_push_prep_early () {
+    our $build_or_push_prep_early_done //= 0;
+    return if $build_or_push_prep_early_done++;
+    badusage "-p is not allowed with dgit $subcommand" if defined $package;
     my $clogp = parsechangelog();
     $isuite = getfield $clogp, 'Distribution';
     $package = getfield $clogp, 'Source';
     $version = getfield $clogp, 'Version';
+}
+
+sub build_prep_early () {
+    build_or_push_prep_early();
     notpushing();
     check_not_dirty();
 }
@@ -6083,15 +6143,17 @@ sub cmd_gbp_build {
     }
     my @cmd = opts_opt_multi_cmd @gbp_build;
 
-    push @cmd, (qw(-us -uc --git-no-sign-tags), "--git-builder=@dbp");
+    push @cmd, (qw(-us -uc --git-no-sign-tags),
+               "--git-builder=".(shellquote @dbp));
 
     if ($gbp_make_orig) {
-       ensuredir '.git/dgit';
-       my $ok = '.git/dgit/origs-gen-ok';
+       my $priv = dgit_privdir();
+       my $ok = "$priv/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, "--git-prebuild=".
+            "touch ".(shellquote $ok)." ".(shellquote "$priv/no-such-dir/ok");
        push @origs_cmd, @ARGV;
        if (act_local()) {
            debugcmd @origs_cmd;
@@ -6121,21 +6183,14 @@ sub cmd_gbp_build {
 }
 sub cmd_git_build { cmd_gbp_build(); } # compatibility with <= 1.0
 
+sub build_source_for_push {
+    build_source();
+    maybe_unapply_patches_again();
+    $changesfile = $sourcechanges;
+}
+
 sub build_source {
     build_prep_early();
-    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()) {
@@ -6143,43 +6198,33 @@ sub build_source {
            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();
+    my @cmd = (@dpkgsource, qw(-b --));
+    if ($split_brain) {
+        changedir $playground;
+        runcmd_ordryrun_local @cmd, "work";
+        my @udfiles = <${package}_*>;
+        changedir $maindir;
+        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 "$playground/$f", "../$f" or $!==ENOENT
+              or fail "put in place new source file ($f): $!";
+        }
     } 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();
+        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 {
@@ -6339,7 +6384,10 @@ END
     foreach my $fi (@dfi) {
        my $f = $fi->{Filename};
        my $here = "../$f";
-       next if lstat $here;
+       if (lstat $here) {
+           next if stat $here;
+           fail "lstat $here works but stat gives $! !";
+       }
        fail "stat $here: $!" unless $! == ENOENT;
        my $there = $dscfn;
        if ($dscfn =~ m#^(?:\./+)?\.\./+#) {
@@ -6350,8 +6398,10 @@ END
            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";
+           fail "import $dscfn requires ../$f, but it does not exist";
        $there .= "/$f";
+       my $test = $there =~ m{^/} ? $there : "../$there";
+       stat $test or fail "import $dscfn requires $test, but: $!";
        symlink $there, $here or fail "symlink $there to $here: $!";
        progress "made symlink $here -> $there";
 #      print STDERR Dumper($fi);
@@ -6390,11 +6440,12 @@ END
 }
 
 sub pre_archive_api_query () {
-    no_local_git_cfg();
+    not_necessarily_a_tree();
 }
 sub cmd_archive_api_query {
     badusage "need only 1 subpath argument" unless @ARGV==1;
     my ($subpath) = @ARGV;
+    local $isuite = 'DGIT-API-QUERY-CMD';
     my @cmd = archive_api_query_cmd($subpath);
     push @cmd, qw(-f);
     debugcmd ">",@cmd;
@@ -6409,7 +6460,7 @@ sub repos_server_url () {
 }    
 
 sub pre_clone_dgit_repos_server () {
-    no_local_git_cfg();
+    not_necessarily_a_tree();
 }
 sub cmd_clone_dgit_repos_server {
     badusage "need destination argument" unless @ARGV==1;
@@ -6421,7 +6472,7 @@ sub cmd_clone_dgit_repos_server {
 }
 
 sub pre_print_dgit_repos_server_source_url () {
-    no_local_git_cfg();
+    not_necessarily_a_tree();
 }
 sub cmd_print_dgit_repos_server_source_url {
     badusage "no arguments allowed to dgit print-dgit-repos-server-source-url"
@@ -6430,6 +6481,15 @@ sub cmd_print_dgit_repos_server_source_url {
     print $url, "\n" or die $!;
 }
 
+sub pre_print_dpkg_source_ignores {
+    not_necessarily_a_tree();
+}
+sub cmd_print_dpkg_source_ignores {
+    badusage "no arguments allowed to dgit print-dpkg-source-ignores"
+       if @ARGV;
+    print "@dpkg_source_ignores\n" or die $!;
+}
+
 sub cmd_setup_mergechangelogs {
     badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
     local $isuite = 'DGIT-SETUP-TREE';
@@ -6812,12 +6872,13 @@ if (!@ARGV) {
     print STDERR $helpmsg or die $!;
     exit 8;
 }
-my $cmd = shift @ARGV;
+$cmd = $subcommand = shift @ARGV;
 $cmd =~ y/-/_/;
 
 my $pre_fn = ${*::}{"pre_$cmd"};
 $pre_fn->() if $pre_fn;
 
+record_maindir if $invoked_in_git_tree;
 git_slurp_config();
 
 my $fn = ${*::}{"cmd_$cmd"};