chiark / gitweb /
git-debrebase: wip new-upstream, ready for testing
[dgit.git] / dgit
diff --git a/dgit b/dgit
index afe209e..49350ee 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;
@@ -49,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;
@@ -92,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)?";
 
@@ -100,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));
@@ -111,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) = ('');
@@ -148,6 +153,7 @@ sub parseopts_late_defaults();
 sub setup_gitattrs(;$);
 sub check_gitattrs($$);
 
+our $playground;
 our $keyid;
 
 autoflush STDOUT 1;
@@ -251,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;
@@ -283,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:
@@ -495,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; }
 
@@ -544,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
@@ -569,7 +568,7 @@ sub nextarg {
 }
 
 sub pre_help () {
-    no_local_git_cfg();
+    not_necessarily_a_tree();
 }
 sub cmd_help () {
     print $helpmsg or die $!;
@@ -647,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;
     }
 }
 
@@ -709,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 () {
@@ -1006,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;
@@ -1702,30 +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
-                         core.sharedRepository
-                         core.compression core.looseCompression
-                         core.bigFileThreshold core.fsyncObjectFiles)) {
-       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 () {
@@ -1758,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;
@@ -1888,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;
@@ -2100,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: ";
 
@@ -2426,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);
@@ -2510,8 +2511,8 @@ END
            @output = $lastpush_mergeinput;
        }
     }
-    changedir '../../../..';
-    rmtree($ud);
+    changedir $maindir;
+    rmtree $playground;
     return @output;
 }
 
@@ -3208,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
@@ -3268,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';
@@ -3307,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) {
@@ -3353,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};
@@ -3381,15 +3383,15 @@ sub setup_gitattrs (;$) {
 END
        return;
     }
-    my $af = ".git/info/attributes";
-    ensuredir '.git/info';
+    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;
@@ -3625,6 +3627,7 @@ sub clone ($) {
     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";
@@ -3639,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);
@@ -3688,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 ($) {
@@ -3778,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);
@@ -3879,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
@@ -3915,6 +3910,7 @@ sub splitbrain_pseudomerge ($$$$) {
     #
 
     return $dgitview unless defined $archive_hash;
+    return $dgitview if deliberately_not_fast_forward();
 
     printdebug "splitbrain_pseudomerge...\n";
 
@@ -4187,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);
@@ -4216,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();
@@ -4260,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;
@@ -4338,7 +4363,7 @@ END
     }
 
     my @tagwants = push_tagwants($cversion, $dgithead, $maintviewhead,
-                                ".git/dgit/tag");
+                                dgit_privdir()."/tag");
     my @tagobjfns;
 
     supplementary_message(<<'END');
@@ -4428,7 +4453,7 @@ END
 }
 
 sub pre_clone () {
-    no_local_git_cfg();
+    not_necessarily_a_tree();
 }
 sub cmd_clone {
     parseopts();
@@ -4478,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;
@@ -4531,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();
@@ -4555,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();
 }
 
@@ -4641,7 +4677,7 @@ sub i_method {
 }
 
 sub pre_rpush () {
-    no_local_git_cfg();
+    not_necessarily_a_tree();
 }
 sub cmd_rpush {
     my $host = nextarg;
@@ -4861,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/;
@@ -5054,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";
@@ -5079,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)";
@@ -5378,7 +5414,7 @@ END
     my $headref = git_rev_parse('HEAD');
 
     prep_ud();
-    changedir $ud;
+    changedir $playground;
 
     my $upstreamversion = upstreamversion $version;
 
@@ -5390,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 ($) {
@@ -5408,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;
@@ -5487,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 $!;
@@ -5501,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)
 
@@ -5535,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;
@@ -5665,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";
@@ -5857,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();
 }
@@ -6102,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;
@@ -6140,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()) {
@@ -6162,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 {
@@ -6414,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;
@@ -6433,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;
@@ -6445,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"
@@ -6454,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';
@@ -6836,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"};