chiark / gitweb /
Merge branch 'master' into wip.remote
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Tue, 22 Oct 2013 13:53:11 +0000 (14:53 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Tue, 22 Oct 2013 13:53:11 +0000 (14:53 +0100)
Conflicts:
debian/changelog
dgit

1  2 
debian/changelog
dgit
dgit.1

diff --combined debian/changelog
@@@ -1,26 -1,13 +1,36 @@@
+ dgit (0.16) unstable; urgency=high
+   * Format `(3.0) quilt' fixup does not mind extraneous other files
+     in the build tree (e.g., build products and logs).  Closes: #727053.
+   * Set autoflush on stdout, to get better ordering of debugging
+     etc. output when stdout is redirected.
+   * New --damp-run mode, for more convenient and fuller testing etc.
+  -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Tue, 22 Oct 2013 13:06:54 +0100
 +dgit (0.16~experimental3) experimental; urgency=low
 +
 +  * Minor fixes arising from changes since 0.15.
 +
 + -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Mon, 21 Oct 2013 15:31:59 +0100
 +
 +dgit (0.16~experimental2) experimental; urgency=low
 +
 +  * WIP remote functionality.  Untested, do not use.
 +  * Some code motion and cleanups.
 +  * push actually takes an optional suite, like it says in the synopsis.
 +  * Command execution reports from --dry-run go to stderr.
 +  * Support --gpg=... to provide a replacement command for gpg.
 +  * Support --ssh=... and --ssh:... to affect how we run ssh.
 +
 + -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Mon, 21 Oct 2013 14:29:56 +0100
 +
 +dgit (0.16~experimental1) experimental; urgency=low
 +
 +  * WIP remote functionality.  Untested, do not use.
 +
 + -- Ian Jackson <ijackson@chiark.greenend.org.uk>  Tue, 24 Sep 2013 23:08:27 +0100
 +
  dgit (0.15) unstable; urgency=low
  
    * Better handling of packages pushed using dgit and stuck in NEW.
diff --combined dgit
--- 1/dgit
--- 2/dgit
+++ b/dgit
@@@ -24,18 -24,15 +24,18 @@@ use Data::Dumper
  use LWP::UserAgent;
  use Dpkg::Control::Hash;
  use File::Path;
 +use File::Temp qw(tempdir);
  use File::Basename;
  use Dpkg::Version;
  use POSIX;
 +use IPC::Open2;
  
  our $our_version = 'UNRELEASED'; ###substituted###
  
  our $isuite = 'unstable';
  our $idistro;
  our $package;
 +our @ropts;
  
  our $sign = 1;
  our $dryrun = 0;
@@@ -45,7 -42,6 +45,7 @@@ our $ignoredirty = 0
  our $noquilt = 0;
  our $existing_package = 'dpkg';
  our $cleanmode = 'dpkg-source';
 +our $we_are_responder;
  
  our %format_ok = map { $_=>1 } ("1.0","3.0 (native)","3.0 (quilt)");
  
@@@ -53,10 -49,7 +53,10 @@@ our (@git) = qw(git)
  our (@dget) = qw(dget);
  our (@dput) = qw(dput);
  our (@debsign) = qw(debsign);
 +our (@gpg) = qw(gpg);
  our (@sbuild) = qw(sbuild -A);
 +our (@ssh) = 'ssh';
 +our (@dgit) = qw(dgit);
  our (@dpkgbuildpackage) = qw(dpkg-buildpackage -i\.git/ -I.git);
  our (@dpkgsource) = qw(dpkg-source -i\.git/ -I.git);
  our (@dpkggenchanges) = qw(dpkg-genchanges);
@@@ -66,23 -59,20 +66,25 @@@ our (@changesopts) = ('')
  our %opts_opt_map = ('dget' => \@dget,
                     'dput' => \@dput,
                     'debsign' => \@debsign,
 +                     'gpg' => \@gpg,
                       'sbuild' => \@sbuild,
 +                     'ssh' => \@ssh,
 +                     'dgit' => \@dgit,
                       'dpkg-source' => \@dpkgsource,
                       'dpkg-buildpackage' => \@dpkgbuildpackage,
                       'dpkg-genchanges' => \@dpkggenchanges,
                       'ch' => \@changesopts,
                       'mergechanges' => \@mergechanges);
  
 +our %opts_opt_cmdonly = ('gpg' => 1);
 +
  our $keyid;
  
  our $debug = 0;
  open DEBUG, ">/dev/null" or die $!;
  
+ autoflush STDOUT 1;
  our $remotename = 'dgit';
  our @ourdscfield = qw(Dgit Vcs-Dgit-Master);
  our $branchprefix = 'dgit';
@@@ -113,9 -103,6 +115,9 @@@ sub dscfn ($) 
  sub changesopts () { return @changesopts[1..$#changesopts]; }
  
  our $us = 'dgit';
 +our $debugprefix = ' ';
 +
 +sub printdebug { print DEBUG $debugprefix, @_ or die $!; }
  
  sub fail { die "$us: @_\n"; }
  
@@@ -131,152 -118,6 +133,152 @@@ sub fetchspec () 
      return  "+".rrref().":".lrref();
  }
  
 +#---------- remote protocol support, common ----------
 +
 +# remote push initiator/responder protocol:
 +#  < dgit-remote-push-ready [optional extra info ignored by old initiators]
 +#
 +#  > file parsed-changelog
 +#  [indicates that output of dpkg-parsechangelog follows]
 +#  > data-block NBYTES
 +#  > [NBYTES bytes of data (no newline)]
 +#  [maybe some more blocks]
 +#  > data-end
 +#
 +#  > file dsc
 +#  [etc]
 +#
 +#  > file changes
 +#  [etc]
 +#
 +#  > param head HEAD
 +#
 +#  > want signed-tag
 +#  [indicates that signed tag is wanted]
 +#  < data-block NBYTES
 +#  < [NBYTES bytes of data (no newline)]
 +#  [maybe some more blocks]
 +#  < data-end
 +#  < files-end
 +#
 +#  > want signed-dsc-changes
 +#  < data-block NBYTES    [transfer of signed dsc]
 +#  [etc]
 +#  < data-block NBYTES    [transfer of signed changes]
 +#  [etc]
 +#  < files-end
 +#
 +#  > complete
 +
 +sub badproto ($$) {
 +    my ($fh, $m) = @_;
 +    fail "connection lost: $!" if $fh->error;
 +    fail "connection terminated" if $fh->eof;
 +    fail "protocol violation; $m not expected";
 +}
 +
 +sub protocol_expect (&$) {
 +    my ($match, $fh) = @_;
 +    local $_;
 +    $_ = <$fh>;
 +    defined && chomp or badproto $fh, "eof";
 +    if (wantarray) {
 +      my @r = &$match;
 +      return @r if @r;
 +    } else {
 +      my $r = &$match;
 +      return $r if $r;
 +    }
 +    badproto $fh, "\`$_'";
 +}
 +
 +sub protocol_send_file ($$) {
 +    my ($fh, $ourfn) = @_;
 +    open PF, "<", $ourfn or die "$ourfn: $!";
 +    for (;;) {
 +      my $d;
 +      my $got = read PF, $d, 65536;
 +      die "$ourfn: $!" unless defined $got;
 +      last if $got;
 +      print $fh "data-block ".length($d)."\n" or die $!;
 +      print $d or die $!;
 +    }
 +    print $fh "data-end\n" or die $!;
 +    close PF;
 +}
 +
 +sub protocol_read_bytes ($$) {
 +    my ($fh, $nbytes) = @_;
 +    $nbytes =~ m/^\d{1,6}$/ or badproto \*RO, "bad byte count";
 +    my $d;
 +    my $got = read $fh, $d, $nbytes;
 +    $got==$nbytes or badproto $fh, "eof during data block";
 +    return $d;
 +}
 +
 +sub protocol_receive_file ($$) {
 +    my ($fh, $ourfn) = @_;
 +    open PF, ">", $ourfn or die "$ourfn: $!";
 +    for (;;) {
 +      my ($y,$l) = protocol_expect {
 +          m/^data-block (.*})$|data-end$/;
 +          length $1 ? (1,$1) : (0);
 +      } \*STDIN;
 +      last unless $y;
 +      my $d = protocol_read_bytes \*STDIN, $1;
 +      print PF $d or die $!;
 +    }
 +    printdebug "received into $ourfn\n";
 +}
 +
 +#---------- remote protocol support, responder ----------
 +
 +sub responder_send_command ($) {
 +    my ($command) = @_;
 +    return unless $we_are_responder;
 +    # called even without $we_are_responder
 +    printdebug "<< $command\n";
 +    print $command, "\n" or die $!;
 +}    
 +
 +sub responder_send_file ($$) {
 +    my ($keyword, $ourfn) = @_;
 +    return unless $we_are_responder;
 +    printdebug "[[ $keyword $ourfn\n";
 +    responder_send_command "file $keyword";
 +    protocol_send_file \*STDOUT, $ourfn;
 +}
 +
 +sub responder_receive_files ($@) {
 +    my ($keyword, @ourfns) = @_;
 +    die unless $we_are_responder;
 +    printdebug "]] $keyword @ourfns\n";
 +    responder_send_command "want $keyword";
 +    foreach my $fn (@ourfns) {
 +      protocol_receive_file \*STDIN, $fn;
 +    }
 +    protocol_expect { m/^files-end$/ } \*STDIN;
 +}
 +
 +#---------- remote protocol support, initiator ----------
 +
 +sub initiator_expect (&) {
 +    my ($match) = @_;
 +    protocol_expect { &$match } \*RO;
 +}
 +
 +#---------- end remote code ----------
 +
 +sub progress {
 +    if ($we_are_responder) {
 +      my $m = join '', @_;
 +      responder_send_command "progress ".length($m) or die $!;
 +      print $m or die $!;
 +    } else {
 +      print @_, "\n";
 +    }
 +}
 +
  our $ua;
  
  sub url_get {
        $ua->env_proxy;
      }
      my $what = $_[$#_];
 -    print "downloading $what...\n";
 +    progress "downloading $what...";
      my $r = $ua->get(@_) or die $!;
      return undef if $r->code == 404;
      $r->is_success or fail "failed to fetch $what: ".$r->status_line;
  
  our ($dscdata,$dscurl,$dsc,$skew_warning_vsn);
  
 -sub printcmd {
 -    my $fh = shift @_;
 -    my $intro = shift @_;
 -    print $fh $intro or die $!;
 +sub shellquote {
 +    my @out;
      local $_;
      foreach my $a (@_) {
        $_ = $a;
        if (s{['\\]}{\\$&}g || m{\s} || m{[^-_./0-9a-z]}i) {
 -          print $fh " '$_'" or die $!;
 +          push @out, "'$_'";
        } else {
 -          print $fh " $_" or die $!;
 +          push @out, $_;
        }
      }
 +    return join ' ', @out;
 +}
 +
 +sub printcmd {
 +    my $fh = shift @_;
 +    my $intro = shift @_;
 +    print $fh $intro," " or die $!;
 +    print $fh shellquote @_ or die $!;
      print $fh "\n" or die $!;
  }
  
@@@ -330,31 -165,31 +332,31 @@@ sub failedcmd 
  }
  
  sub runcmd {
 -    printcmd(\*DEBUG,"+",@_) if $debug>0;
 +    printcmd(\*DEBUG,$debugprefix."+",@_) if $debug>0;
      $!=0; $?=0;
      failedcmd @_ if system @_;
  }
  
  sub printdone {
      if (!$dryrun) {
 -      print "dgit ok: @_\n";
 +      progress "dgit ok: @_";
      } else {
 -      print "would be ok: @_ (but dry run only)\n";
 +      progress "would be ok: @_ (but dry run only)";
      }
  }
  
  sub cmdoutput_errok {
      die Dumper(\@_)." ?" if grep { !defined } @_;
 -    printcmd(\*DEBUG,"|",@_) if $debug>0;
 +    printcmd(\*DEBUG,$debugprefix."|",@_) if $debug>0;
      open P, "-|", @_ or die $!;
      my $d;
      $!=0; $?=0;
      { local $/ = undef; $d = <P>; }
      die $! if P->error;
 -    if (!close P) { print DEBUG "=>!$?\n" if $debug>0; return undef; }
 +    if (!close P) { printdebug "=>!$?\n" if $debug>0; return undef; }
      chomp $d;
      $d =~ m/^.*/;
 -    print DEBUG "=> \`$&'",(length $' ? '...' : ''),"\n" if $debug>0; #';
 +    printdebug "=> \`$&'",(length $' ? '...' : ''),"\n" if $debug>0; #';
      return $d;
  }
  
@@@ -365,7 -200,7 +367,7 @@@ sub cmdoutput 
  }
  
  sub dryrun_report {
 -    printcmd(\*STDOUT,"#",@_);
 +    printcmd(\*STDERR,$debugprefix."#",@_);
  }
  
  sub runcmd_ordryrun {
      }
  }
  
+ sub runcmd_ordryrun_local {
+     if ($dryrun <= 1) {
+       runcmd @_;
+     } else {
+       dryrun_report @_;
+     }
+ }
  sub shell_cmd {
      my ($first_shell, @cmd) = @_;
      return qw(sh -ec), $first_shell.'; exec "$@"', 'x', @cmd;
@@@ -387,10 -230,10 +397,11 @@@ main usages
    dgit [dgit-opts] fetch|pull [dgit-opts] [suite]
    dgit [dgit-opts] build [git-buildpackage-opts|dpkg-buildpackage-opts]
    dgit [dgit-opts] push [dgit-opts] [suite]
 +  dgit [dgit-opts] rpush build-host:build-dir ...
  important dgit options:
    -k<keyid>           sign tag and package with <keyid> instead of default
    --dry-run -n        do not change anything, but go through the motions
+   --damp-run -L       like --dry-run but make local changes, without signing
    --new -N            allow introducing a new package
    --debug -D          increase debug level
    -c<name>=<value>    set git config option (used directly by dgit too)
@@@ -405,11 -248,6 +416,11 @@@ sub badusage 
      exit 8;
  }
  
 +sub nextarg {
 +    @ARGV or badusage "too few arguments";
 +    return scalar shift @ARGV;
 +}
 +
  sub cmd_help () {
      print $helpmsg or die $!;
      exit 0;
@@@ -463,24 -301,6 +474,24 @@@ sub access_cfg (@) 
      return $value;
  }
  
 +sub string_to_ssh ($) {
 +    my ($spec) = @_;
 +    if ($spec =~ m/\s/) {
 +      return qw(sh -ec), 'exec '.$spec.' "$@"', 'x';
 +    } else {
 +      return ($spec);
 +    }
 +}
 +
 +sub access_cfg_ssh () {
 +    my $gitssh = access_cfg('ssh', 'RETURN-UNDEF');
 +    if (!defined $gitssh) {
 +      return @ssh;
 +    } else {
 +      return string_to_ssh $gitssh;
 +    }
 +}
 +
  sub access_someuserhost ($) {
      my ($some) = @_;
      my $user = access_cfg("$some-user",'username');
@@@ -572,7 -392,7 +583,7 @@@ sub archive_query_sshdakls ($$) 
      my ($proto,$data) = @_;
      $data =~ s/:.*// or badcfg "invalid sshdakls method string \`$data'";
      my $dakls = cmdoutput
 -      access_cfg('ssh'), $data, qw(dak ls -asource),"-s$isuite",$package;
 +      access_cfg_ssh, $data, qw(dak ls -asource),"-s$isuite",$package;
      return madison_parse($dakls);
  }
  
@@@ -580,7 -400,7 +591,7 @@@ sub canonicalise_suite_sshdakls ($$) 
      my ($proto,$data) = @_;
      $data =~ m/:/ or badcfg "invalid sshdakls method string \`$data'";
      my @cmd =
 -      (access_cfg('ssh'), $`,
 +      (access_cfg_ssh, $`,
         "set -e; cd $';".
         " if test -h $isuite; then readlink $isuite; exit 0; fi;".
         " if test -d $isuite; then echo $isuite; exit 0; fi;".
@@@ -630,7 -450,7 +641,7 @@@ sub canonicalise_suite () 
      $csuite = archive_query('canonicalise_suite');
      if ($isuite ne $csuite) {
        # madison canonicalises for us
 -      print "canonical suite name for $isuite is $csuite\n";
 +      progress "canonical suite name for $isuite is $csuite";
      }
  }
  
@@@ -646,9 -466,9 +657,9 @@@ sub get_archive_dsc () 
            next;
        }
        my $dscfh = new IO::File \$dscdata, '<' or die $!;
 -      print DEBUG Dumper($dscdata) if $debug>1;
 +      printdebug Dumper($dscdata) if $debug>1;
        $dsc = parsecontrolfh($dscfh,$dscurl, allow_pgp=>1);
 -      print DEBUG Dumper($dsc) if $debug>1;
 +      printdebug Dumper($dsc) if $debug>1;
        my $fmt = getfield $dsc, 'Format';
        fail "unsupported source format $fmt, sorry" unless $format_ok{$fmt};
        return;
@@@ -661,7 -481,7 +672,7 @@@ sub check_for_git () 
      my $how = access_cfg('git-check');
      if ($how eq 'ssh-cmd') {
        my @cmd =
 -          (access_cfg('ssh'),access_gituserhost(),
 +          (access_cfg_ssh, access_gituserhost(),
             " set -e; cd ".access_cfg('git-path').";".
             " if test -d $package.git; then echo 1; else echo 0; fi");
        my $r= cmdoutput @cmd;
@@@ -676,7 -496,7 +687,7 @@@ sub create_remote_git_repo () 
      my $how = access_cfg('git-create');
      if ($how eq 'ssh-cmd') {
        runcmd_ordryrun
 -          (access_cfg('ssh'),access_gituserhost(),
 +          (access_cfg_ssh, access_gituserhost(),
             "set -e; cd ".access_cfg('git-path').";".
             " cp -a _template $package.git");
      } else {
@@@ -753,18 -573,6 +764,18 @@@ sub make_commit ($) 
      return cmdoutput @git, qw(hash-object -w -t commit), $file;
  }
  
 +sub clogp_authline ($) {
 +    my ($clogp) = @_;
 +    my $author = getfield $clogp, 'Maintainer';
 +    $author =~ s#,.*##ms;
 +    my $date = cmdoutput qw(date), '+%s %z', qw(-d), getfield($clogp,'Date');
 +    my $authline = "$author $date";
 +    $authline =~ m/^[^<>]+ \<\S+\> \d+ [-+]\d+$/ or
 +      fail "unexpected commit author line format \`$authline'".
 +      " (was generated from changelog Maintainer field)";
 +    return $authline;
 +}
 +
  sub generate_commit_from_dsc () {
      prep_ud();
      chdir $ud or die $!;
      my ($tree,$dir) = mktree_in_ud_from_only_subdir();
      runcmd qw(sh -ec), 'dpkg-parsechangelog >../changelog.tmp';
      my $clogp = parsecontrol('../changelog.tmp',"commit's changelog");
 -    my $date = cmdoutput qw(date), '+%s %z', qw(-d), getfield($clogp,'Date');
 -    my $author = getfield $clogp, 'Maintainer';
 -    $author =~ s#,.*##ms;
 -    my $authline = "$author $date";
 -    $authline =~ m/^[^<>]+ \<\S+\> \d+ [-+]\d+$/ or
 -      fail "unexpected commit author line format \`$authline'".
 -          " (was generated from changelog Maintainer field)";
 +    my $authline = clogp_authline $clogp;
      my $changes = getfield $clogp, 'Changes';
      open C, ">../commit.tmp" or die $!;
      print C <<END or die $!;
@@@ -800,7 -614,7 +811,7 @@@ EN
      close C or die $!;
      my $outputhash = make_commit qw(../commit.tmp);
      my $cversion = getfield $clogp, 'Version';
 -    print "synthesised git commit from .dsc $cversion\n";
 +    progress "synthesised git commit from .dsc $cversion";
      if ($lastpush_hash) {
        runcmd @git, qw(reset --hard), $lastpush_hash;
        runcmd qw(sh -ec), 'dpkg-parsechangelog >>../changelogold.tmp';
@@@ -855,7 -669,7 +866,7 @@@ sub ensure_we_have_orig () 
                fail "existing file $f has hash $got but .dsc".
                    " demands hash $fi->{Hash}".
                    " (perhaps you should delete this file?)";
 -          print "using existing $f\n";
 +          progress "using existing $f";
            next;
        } else {
            die "$f $!" unless $!==&ENOENT;
        $origurl .= "/$f";
        die "$f ?" unless $f =~ m/^${package}_/;
        die "$f ?" if $f =~ m#/#;
-       runcmd_ordryrun shell_cmd 'cd ..', @dget,'--',$origurl;
+       runcmd_ordryrun_local shell_cmd 'cd ..', @dget,'--',$origurl;
      }
  }
  
@@@ -886,7 -700,7 +897,7 @@@ sub is_fast_fwd ($$) 
  }
  
  sub git_fetch_us () {
-     runcmd_ordryrun @git, qw(fetch),access_giturl(),fetchspec();
+     runcmd_ordryrun_local @git, qw(fetch),access_giturl(),fetchspec();
  }
  
  sub fetch_from_archive () {
        if (defined $dsc_hash) {
            $dsc_hash =~ m/\w+/ or fail "invalid hash in .dsc \`$dsc_hash'";
            $dsc_hash = $&;
 -          print "last upload to archive specified git hash\n";
 +          progress "last upload to archive specified git hash";
        } else {
 -          print "last upload to archive has NO git hash\n";
 +          progress "last upload to archive has NO git hash";
        }
      } else {
 -      print "no version available from the archive\n";
 +      progress "no version available from the archive";
      }
  
      my $lrref_fn = ".git/".lrref();
      } else {
        die "$lrref_fn $!";
      }
 -    print DEBUG "previous reference hash=$lastpush_hash\n";
 +    printdebug "previous reference hash=$lastpush_hash\n";
      my $hash;
      if (defined $dsc_hash) {
        fail "missing git history even though dsc has hash -".
@@@ -954,7 -768,7 +965,7 @@@ Package not found in the archive, but h
  $later_warning_msg
  END
      } else {
 -      print DEBUG "nothing found!\n";
 +      printdebug "nothing found!\n";
        if (defined $skew_warning_vsn) {
            print STDERR <<END or die $!;
  
@@@ -966,7 -780,7 +977,7 @@@ EN
        }
        return 0;
      }
 -    print DEBUG "current hash=$hash\n";
 +    printdebug "current hash=$hash\n";
      if ($lastpush_hash) {
        fail "not fast forward on last upload branch!".
            " (archive's version left in DGIT_ARCHIVE)"
      }
      if (defined $skew_warning_vsn) {
        mkpath '.git/dgit';
 -      print DEBUG "SKEW CHECK WANT $skew_warning_vsn\n";
 +      printdebug "SKEW CHECK WANT $skew_warning_vsn\n";
        my $clogf = ".git/dgit/changelog.tmp";
        runcmd shell_cmd "exec >$clogf",
            @git, qw(cat-file blob), "$hash:debian/changelog";
        my $gotclogp = parsechangelog("-l$clogf");
        my $got_vsn = getfield $gotclogp, 'Version';
 -      print DEBUG "SKEW CHECK GOT $got_vsn\n";
 +      printdebug "SKEW CHECK GOT $got_vsn\n";
        if (version_compare_string($got_vsn, $skew_warning_vsn) < 0) {
            print STDERR <<END or die $!;
  
@@@ -993,7 -807,7 +1004,7 @@@ EN
      }
      if ($lastpush_hash ne $hash) {
        my @upd_cmd = (@git, qw(update-ref -m), 'dgit fetch', lrref(), $hash);
-       if (!$dryrun) {
+       if ($dryrun <= 1) {
            cmdoutput @upd_cmd;
        } else {
            dryrun_report @upd_cmd;
  sub clone ($) {
      my ($dstdir) = @_;
      canonicalise_suite();
-     badusage "dry run makes no sense with clone" if $dryrun;
+     badusage "dry run makes no sense with clone" if $dryrun > 1;
      mkdir $dstdir or die "$dstdir $!";
      chdir "$dstdir" or die "$dstdir $!";
      runcmd @git, qw(init -q);
      close H or die $!;
      runcmd @git, qw(remote add), 'origin', access_giturl();
      if (check_for_git()) {
 -      print "fetching existing git history\n";
 +      progress "fetching existing git history";
        git_fetch_us();
-       runcmd_ordryrun @git, qw(fetch origin);
+       runcmd_ordryrun_local @git, qw(fetch origin);
      } else {
 -      print "starting new git history\n";
 +      progress "starting new git history";
      }
      fetch_from_archive() or no_such_package;
      runcmd @git, qw(reset --hard), lrref();
@@@ -1036,7 -850,7 +1047,7 @@@ sub fetch () 
  
  sub pull () {
      fetch();
-     runcmd_ordryrun @git, qw(merge -m),"Merge from $csuite [dgit]",
+     runcmd_ordryrun_local @git, qw(merge -m),"Merge from $csuite [dgit]",
          lrref();
      printdone "fetched to ".lrref()." and merged into HEAD";
  }
  sub check_not_dirty () {
      return if $ignoredirty;
      my @cmd = (@git, qw(diff --quiet HEAD));
 -    printcmd(\*DEBUG,"+",@cmd) if $debug>0;
 +    printcmd(\*DEBUG,$debugprefix."+",@cmd) if $debug>0;
      $!=0; $?=0; system @cmd;
      return if !$! && !$?;
      if (!$! && $?==256) {
  sub commit_quilty_patch () {
      my $output = cmdoutput @git, qw(status --porcelain);
      my %adds;
-     my $bad=0;
      foreach my $l (split /\n/, $output) {
        next unless $l =~ m/\S/;
        if ($l =~ m{^(?:\?\?| M) (.pc|debian/patches)}) {
            $adds{$1}++;
-       } else {
-           print STDERR "git status: $l\n";
-           $bad++;
        }
      }
-     fail "unexpected output from git status (is tree clean?)" if $bad;
      if (!%adds) {
 -      print "nothing quilty to commit, ok.\n";
 +      progress "nothing quilty to commit, ok.";
        return;
      }
-     runcmd_ordryrun @git, qw(add), sort keys %adds;
+     runcmd_ordryrun_local @git, qw(add), sort keys %adds;
      my $m = "Commit Debian 3.0 (quilt) metadata";
 -    print "$m\n";
 +    progress "$m";
-     runcmd_ordryrun @git, qw(commit -m), $m;
+     runcmd_ordryrun_local @git, qw(commit -m), $m;
  }
  
  sub madformat ($) {
      my ($format) = @_;
      return 0 unless $format eq '3.0 (quilt)';
 -    print "Format \`$format', urgh\n";
 +    progress "Format \`$format', urgh";
      if ($noquilt) {
 -      print "Not doing any fixup of \`$format' due to --no-quilt-fixup";
 +      progress "Not doing any fixup of \`$format' due to --no-quilt-fixup";
        return 0;
      }
      return 1;
  }
  
 -sub dopush () {
 -    print DEBUG "actually entering push\n";
 -    my $clogp = parsechangelog();
 +sub push_parse_changelog ($) {
 +    my ($clogpfn) = @_;
 +
 +    my $clogp = Dpkg::Control::Hash->new();
 +    $clogp->load($clogpfn);
 +
      $package = getfield $clogp, 'Source';
      my $cversion = getfield $clogp, 'Version';
 +    my $tag = debiantag($cversion);
 +    runcmd @git, qw(check-ref-format), $tag;
 +
      my $dscfn = dscfn($cversion);
 -    stat "../$dscfn" or
 -      fail "looked for .dsc $dscfn, but $!;".
 -          " maybe you forgot to build";
 -    $dsc = parsecontrol("../$dscfn","$dscfn");
 -    my $dscpackage = getfield $dsc, 'Source';
 -    my $format = getfield $dsc, 'Format';
 +
 +    return ($clogp, $cversion, $tag, $dscfn);
 +}
 +
 +sub push_parse_dsc ($$$) {
 +    my ($dscfn,$dscfnwhat, $cversion) = @_;
 +    $dsc = parsecontrol($dscfn,$dscfnwhat);
      my $dversion = getfield $dsc, 'Version';
 +    my $dscpackage = getfield $dsc, 'Source';
      ($dscpackage eq $package && $dversion eq $cversion) or
        fail "$dsc is for $dscpackage $dversion".
            " but debian/changelog is for $package $cversion";
 -    print DEBUG "format $format\n";
 +}
 +
 +sub push_mktag ($$$$$$$$) {
 +    my ($head,$clogp,$tag,
 +      $dsc,$dscfn,
 +      $changesfile,$changesfilewhat,
 +      $tfn) = @_;
 +
 +    $dsc->{$ourdscfield[0]} = $head;
 +    $dsc->save("$dscfn.tmp") or die $!;
 +
 +    my $changes = parsecontrol($changesfile,$changesfilewhat);
 +    foreach my $field (qw(Source Distribution Version)) {
 +      $changes->{$field} eq $clogp->{$field} or
 +          fail "changes field $field \`$changes->{$field}'".
 +              " does not match changelog \`$clogp->{$field}'";
 +    }
 +
 +    my $cversion = getfield $clogp, 'Version';
 +
 +    # We make the git tag by hand because (a) that makes it easier
 +    # to control the "tagger" (b) we can do remote signing
 +    my $authline = clogp_authline $clogp;
 +    open TO, '>', $tfn->('.tmp') or die $!;
 +    print TO <<END or die $!;
 +object $head
 +type commit
 +tag $tag
 +tagger $authline
 +
 +$package release $cversion for $csuite [dgit]
 +END
 +    close TO or die $!;
 +
 +    my $tagobjfn = $tfn->('.tmp');
 +    if ($sign) {
 +      if (!defined $keyid) {
 +          $keyid = access_cfg('keyid','RETURN-UNDEF');
 +      }
 +      unlink $tfn->('.tmp.asc') or $!==&ENOENT or die $!;
 +      my @sign_cmd = (@gpg, qw(--detach-sign --armor));
 +      push @sign_cmd, qw(-u),$keyid if defined $keyid;
 +      push @sign_cmd, $tfn->('.tmp');
 +      runcmd_ordryrun @sign_cmd;
 +      if (!$dryrun) {
 +          $tagobjfn = $tfn->('.signed.tmp');
 +          runcmd shell_cmd "exec >$tagobjfn", qw(cat --),
 +              $tfn->('.tmp'), $tfn->('.tmp.asc');
 +      }
 +    }
 +
 +    return ($tagobjfn);
 +}
 +
 +sub sign_changes ($) {
 +    my ($changesfile) = @_;
 +    if ($sign) {
 +      my @debsign_cmd = @debsign;
 +      push @debsign_cmd, "-k$keyid" if defined $keyid;
 +      push @debsign_cmd, "-p$gpg[0]" if $gpg[0] ne 'gpg';
 +      push @debsign_cmd, $changesfile;
 +      runcmd_ordryrun @debsign_cmd;
 +    }
 +}
 +
 +sub dopush () {
 +    printdebug "actually entering push\n";
 +    prep_ud();
 +
 +    my $clogpfn = ".git/dgit/changelog.822.tmp";
 +    runcmd shell_cmd "exec >$clogpfn", qw(dpkg-parsechangelog);
 +
 +    responder_send_file('parsed-changelog', $clogpfn);
 +
 +    my ($clogp, $cversion, $tag, $dscfn) =
 +      push_parse_changelog("$clogpfn");
 +
 +    stat "../$dscfn" or
 +      fail "looked for .dsc $dscfn, but $!;".
 +          " maybe you forgot to build";
 +
 +    responder_send_file('dsc', "../$dscfn");
 +
 +    push_parse_dsc("../$dscfn", $dscfn, $cversion);
 +
 +    my $format = getfield $dsc, 'Format';
 +    printdebug "format $format\n";
      if (madformat($format)) {
        commit_quilty_patch();
      }
      check_not_dirty();
 -    prep_ud();
      chdir $ud or die $!;
 -    print "checking that $dscfn corresponds to HEAD\n";
 +    progress "checking that $dscfn corresponds to HEAD";
      runcmd qw(dpkg-source -x --), "../../../../$dscfn";
      my ($tree,$dir) = mktree_in_ud_from_only_subdir();
      chdir '../../../..' or die $!;
 -    printcmd \*DEBUG,"+",@_;
      my @diffcmd = (@git, qw(diff --exit-code), $tree);
 +    printcmd \*DEBUG,$debugprefix."+",@diffcmd;
      $!=0; $?=0;
      if (system @diffcmd) {
        if ($! && $?==256) {
  #    runcmd @git, qw(fetch -p ), "$alioth_git/$package.git",
  #        map { lref($_).":".rref($_) }
  #        (uploadbranch());
 -    $dsc->{$ourdscfield[0]} = rev_parse('HEAD');
 -    $dsc->save("../$dscfn.tmp") or die $!;
 +    my $head = rev_parse('HEAD');
      if (!$changesfile) {
        my $multi = "../${package}_".(stripepoch $cversion)."_multi.changes";
        if (stat "$multi") {
            ($changesfile) = @cs;
        }
      }
 -    my $changes = parsecontrol($changesfile,$changesfile);
 -    foreach my $field (qw(Source Distribution Version)) {
 -      $changes->{$field} eq $clogp->{$field} or
 -          fail "changes field $field \`$changes->{$field}'".
 -              " does not match changelog \`$clogp->{$field}'";
 -    }
 -    my $tag = debiantag($dversion);
 +
 +    responder_send_file('changes',$changesfile);
 +
 +    my $tfn = sub { ".git/dgit/tag$_[0]"; };
 +    my ($tagobjfn) =
 +      $we_are_responder
 +      ? responder_receive_files('signed-tag', $tfn->('.signed.tmp'))
 +      : push_mktag($head,$clogp,$tag,
 +                   $dsc,"../$dscfn",
 +                   $changesfile,$changesfile,
 +                               $tfn);
 +
 +    my $tag_obj_hash = cmdoutput @git, qw(hash-object -w -t tag), $tagobjfn;
 +    runcmd_ordryrun @git, qw(verify-tag), $tag_obj_hash;
-     runcmd_ordryrun @git, qw(update-ref), "refs/tags/$tag", $tag_obj_hash;
++    runcmd_ordryrun_local @git, qw(update-ref), "refs/tags/$tag", $tag_obj_hash;
 +    runcmd_ordryrun @git, qw(tag -v --), $tag;
 +
      if (!check_for_git()) {
        create_remote_git_repo();
      }
      runcmd_ordryrun @git, qw(push),access_giturl(),"HEAD:".rrref();
      runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), 'HEAD';
 -    if (!$dryrun) {
 -      rename "../$dscfn.tmp","../$dscfn" or die "$dscfn $!";
 -    } else {
 -      print "[new .dsc left in $dscfn.tmp]\n";
 -    }
 -    if ($sign) {
 -      if (!defined $keyid) {
 -          $keyid = access_cfg('keyid','RETURN-UNDEF');
 -      }
 -      my @tag_cmd = (@git, qw(tag -m),
 -                     "$package release $dversion for $csuite [dgit]");
 -      if ($dryrun != 1) {
 -          push @tag_cmd, qw(-s);
 -          push @tag_cmd, qw(-u),$keyid if defined $keyid;
 +
 +    if (!$we_are_responder) {
-       if (!$dryrun) {
++      if ($dryrun <= 1) {
 +          rename "../$dscfn.tmp","../$dscfn" or die "$dscfn $!";
 +      } else {
 +          progress "[new .dsc left in $dscfn.tmp]";
        }
 -      push @tag_cmd, $tag;
 -      runcmd_ordryrun_local @tag_cmd;
 -      my @debsign_cmd = @debsign;
 -      push @debsign_cmd, "-k$keyid" if defined $keyid;
 -      push @debsign_cmd, $changesfile;
 -      runcmd_ordryrun @debsign_cmd;
      }
-       my $dryrunsuffix = $dryrun ? ".tmp" : "";
 +
 +    if ($we_are_responder) {
++      my $dryrunsuffix = $dryrun > 1 ? ".tmp" : "";
 +      responder_receive_files('signed-dsc-changes',
 +                              "../$dscfn$dryrunsuffix",
 +                              "$changesfile$dryrunsuffix");
 +    } else {
 +      sign_changes $changesfile;
 +    }
 +
      runcmd_ordryrun @git, qw(push),access_giturl(),"refs/tags/$tag";
      my $host = access_cfg('upload-host','RETURN-UNDEF');
      my @hostarg = defined($host) ? ($host,) : ();
      runcmd_ordryrun @dput, @hostarg, $changesfile;
 -    printdone "pushed and uploaded $dversion";
 +    printdone "pushed and uploaded $cversion";
 +
 +    responder_send_command("complete");
  }
  
  sub cmd_clone {
@@@ -1332,7 -1041,7 +1338,7 @@@ sub fetchpullargs () 
            $isuite = getfield $clogp, 'Distribution';
        }
        canonicalise_suite();
 -      print "fetching from suite $csuite\n";
 +      progress "fetching from suite $csuite";
      } elsif (@ARGV==1) {
        ($isuite) = @ARGV;
        canonicalise_suite();
@@@ -1359,24 -1068,15 +1365,24 @@@ sub cmd_push 
      check_not_dirty();
      my $clogp = parsechangelog();
      $package = getfield $clogp, 'Source';
 +    my $specsuite;
      if (@ARGV==0) {
 -      $isuite = getfield $clogp, 'Distribution';
 -      if ($new_package) {
 -          local ($package) = $existing_package; # this is a hack
 -          canonicalise_suite();
 -      }
 +    } elsif (@ARGV==1) {
 +      ($specsuite) = (@ARGV);
      } else {
        badusage "incorrect arguments to dgit push";
      }
 +    $isuite = getfield $clogp, 'Distribution';
 +    if ($new_package) {
 +      local ($package) = $existing_package; # this is a hack
 +      canonicalise_suite();
 +    }
 +    if (defined $specsuite && $specsuite ne $isuite) {
 +      canonicalise_suite();
 +      $csuite eq $specsuite or
 +          fail "dgit push: changelog specifies $isuite ($csuite)".
 +              " but command line specifies $specsuite";
 +    }
      if (check_for_git()) {
        git_fetch_us();
      }
      dopush();
  }
  
 +#---------- remote commands' implementation ----------
 +
 +sub cmd_remote_push_responder {
 +    my ($nrargs) = shift @ARGV;
 +    my (@rargs) = @ARGV[0..$nrargs-1];
 +    @ARGV = @ARGV[$nrargs..$#ARGV];
 +    die unless @rargs;
 +    my ($dir) = @rargs;
 +    chdir $dir or die "$dir: $!";
 +    $we_are_responder = 1;
 +    $debugprefix = ' ';
 +    autoflush STDOUT 1;
 +    responder_send_command("dgit-remote-push-ready");
 +    &cmd_push;
 +}
 +
 +our $i_tmp;
 +
 +sub i_cleanup {
 +    local ($@);
 +    return unless defined $i_tmp;
 +    chdir "/" or die $!;
 +    eval { rmtree $i_tmp; };
 +}
 +
 +sub i_method {
 +    my ($base,$selector,@args) = @_;
 +    $selector =~ s/\-/_/g;
 +    { no strict qw(refs); &{"${base}_${selector}"}(@args); }
 +}
 +
 +sub cmd_rpush {
 +    my $host = nextarg;
 +    my $dir;
 +    if ($host =~ m/^((?:[^][]|\[[^][]*\])*)\:/) {
 +      $host = $1;
 +      $dir = $'; #';
 +    } else {
 +      $dir = nextarg;
 +    }
 +    $dir =~ s{^-}{./-};
 +    my @rargs = ($dir);
 +    my @rdgit;
 +    push @rdgit, @dgit;
 +    push @rdgit, @ropts;
 +    push @rdgit, qw(remote-push-responder), (scalar @rargs), @rargs;
 +    push @rdgit, @ARGV;
 +    my @cmd = (@ssh, $host, shellquote @rdgit);
 +    printcmd \*DEBUG,$debugprefix."+",@cmd;
 +    eval {
 +      $i_tmp = tempdir();
 +      my $pid = open2(\*RO, \*RI, @cmd);
 +      chdir $i_tmp or die "$i_tmp $!";
 +      initiator_expect { m/^dgit-remote-push-ready/ };
 +      for (;;) {
 +          my ($icmd,$iargs) = initiator_expect {
 +              m/^(\S+)(?: (.*))?$/;
 +              ($1,$2);
 +          };
 +          i_method "i_resp", $icmd, $iargs;
 +      }
 +    };
 +    i_cleanup();
 +    die $@;
 +}
 +
 +sub i_resp_progress ($) {
 +    my ($rhs) = @_;
 +    my $msg = protocol_read_bytes \*RO, $rhs;
 +    progress $msg;
 +}
 +
 +sub i_resp_complete {
 +    i_cleanup();
 +    exit 0;
 +}
 +
 +sub i_resp_file ($) {
 +    my ($keyword) = @_;
 +    my $localname = i_method "i_localname", $keyword;
 +    my $localpath = "$i_tmp/$localname";
 +    stat $localpath and badproto \*RO, "file $keyword ($localpath) twice";
 +    protocol_receive_file \*RO, $localpath;
 +}
 +
 +our %i_param;
 +
 +sub i_param ($) {
 +    $_[0] =~ m/^(\S+) (.*)$/;
 +    $i_param{$1} = $2;
 +}
 +
 +our %i_wanted;
 +
 +sub i_resp_want ($) {
 +    my ($keyword) = @_;
 +    die "$keyword ?" if $i_wanted{$keyword}++;
 +    my @localpaths = i_method "i_want", $keyword;
 +    printdebug "]]  $keyword @localpaths\n";
 +    foreach my $localpath (@localpaths) {
 +      protocol_send_file \*RI, $localpath;
 +    }
 +    print RI "end-files\n" or die $!;
 +}
 +
 +our ($i_clogp, $i_version, $i_tag, $i_dscfn);
 +
 +sub i_localname_parsed_changelog { return "remote-changelog.822"; }
 +sub i_localname_changes { return "remote.changes"; }
 +sub i_localname_dsc {
 +    ($i_clogp, $i_version, $i_tag, $i_dscfn) =
 +      push_parse_changelog 'remote-changelog.822';
 +    die if $i_dscfn =~ m#/|^\W#;
 +    return $i_dscfn;
 +}
 +
 +sub i_want_signed_tag {
 +    defined $i_param{'head'} && defined $i_dscfn
 +      or badproto \*RO, "sequencing error";
 +    my $head = $i_param{'head'};
 +    die if $head =~ m/[^0-9a-f]/ || $head !~ m/^../;
 +
 +    push_parse_dsc $i_dscfn, 'remote dsc', 
 +
 +    push_mktag $head, $i_clogp, $i_tag,
 +        $dsc, $i_dscfn,
 +        'remote.changes', 'remote changes',
 +        'tag.tag';
 +
 +    return 'tag.tag';
 +}
 +
 +sub i_want_signed_dsc_changes {
 +    rename "$i_dscfn.tmp","$i_dscfn" or die "$i_dscfn $!";
 +    sign_changes 'remote.changes';
 +    return ($i_dscfn, 'remote.changes');
 +}
 +
 +#---------- building etc. ----------
 +
  our $version;
  our $sourcechanges;
  our $dscfn;
@@@ -1577,7 -1137,7 +1583,7 @@@ EN
        local $ENV{'EDITOR'} = cmdoutput qw(realpath --), $0;
        local $ENV{'VISUAL'} = $ENV{'EDITOR'};
        local $ENV{$fakeeditorenv} = cmdoutput qw(realpath --), $descfn;
-       runcmd_ordryrun @dpkgsource, qw(--commit .), $patchname;
+       runcmd_ordryrun_local @dpkgsource, qw(--commit .), $patchname;
      }
  
      if (!open P, '>>', ".pc/applied-patches") {
@@@ -1622,7 -1182,7 +1628,7 @@@ sub cmd_build 
      badusage "dgit build implies --clean=dpkg-source"
        if $cleanmode ne 'dpkg-source';
      build_prep();
-     runcmd_ordryrun @dpkgbuildpackage, qw(-us -uc), changesopts(), @ARGV;
+     runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc), changesopts(), @ARGV;
      printdone "build successful\n";
  }
  
@@@ -1638,7 -1198,7 +1644,7 @@@ sub cmd_git_build 
        push @cmd, "--git-debian-branch=".lbranch();
      }
      push @cmd, changesopts();
-     runcmd_ordryrun @cmd, @ARGV;
+     runcmd_ordryrun_local @cmd, @ARGV;
      printdone "build successful\n";
  }
  
@@@ -1647,10 -1207,11 +1653,11 @@@ sub build_source 
      $sourcechanges = "${package}_".(stripepoch $version)."_source.changes";
      $dscfn = dscfn($version);
      if ($cleanmode eq 'dpkg-source') {
-       runcmd_ordryrun (@dpkgbuildpackage, qw(-us -uc -S)), changesopts();
+       runcmd_ordryrun_local (@dpkgbuildpackage, qw(-us -uc -S)),
+           changesopts();
      } else {
        if ($cleanmode eq 'git') {
-           runcmd_ordryrun @git, qw(clean -xdf);
+           runcmd_ordryrun_local @git, qw(clean -xdf);
        } elsif ($cleanmode eq 'none') {
        } else {
            die "$cleanmode ?";
        my $pwd = cmdoutput qw(env - pwd);
        my $leafdir = basename $pwd;
        chdir ".." or die $!;
-       runcmd_ordryrun @dpkgsource, qw(-b --), $leafdir;
+       runcmd_ordryrun_local @dpkgsource, qw(-b --), $leafdir;
        chdir $pwd or die $!;
-       runcmd_ordryrun qw(sh -ec),
+       runcmd_ordryrun_local qw(sh -ec),
            'exec >$1; shift; exec "$@"','x',
            "../$sourcechanges",
            @dpkggenchanges, qw(-S), changesopts();
@@@ -1677,7 -1238,7 +1684,7 @@@ sub cmd_sbuild 
      build_source();
      chdir ".." or die $!;
      my $pat = "${package}_".(stripepoch $version)."_*.changes";
-     if (!$dryrun) {
+     if ($dryrun <= 1) {
        stat $dscfn or fail "$dscfn (in parent directory): $!";
        stat $sourcechanges or fail "$sourcechanges (in parent directory): $!";
        foreach my $cf (glob $pat) {
      runcmd_ordryrun @sbuild, @ARGV, qw(-d), $isuite, $dscfn;
      runcmd_ordryrun @mergechanges, glob $pat;
      my $multichanges = "${package}_".(stripepoch $version)."_multi.changes";
-     if (!$dryrun) {
+     if ($dryrun <= 1) {
        stat $multichanges or fail "$multichanges: $!";
      }
      printdone "build successful, results in $multichanges\n" or die $!;
@@@ -1701,8 -1262,6 +1708,8 @@@ sub cmd_quilt_fixup 
      build_maybe_quilt_fixup();
  }
  
 +#---------- argument parsing and main program ----------
 +
  sub cmd_version {
      print "dgit version $our_version\n" or die $!;
      exit 0;
  
  sub parseopts () {
      my $om;
 +
 +    if (defined $ENV{'DGIT_SSH'}) {
 +      @ssh = string_to_ssh $ENV{'DGIT_SSH'};
 +    } elsif (defined $ENV{'GIT_SSH'}) {
 +      @ssh = ($ENV{'GIT_SSH'});
 +    }
 +
      while (@ARGV) {
        last unless $ARGV[0] =~ m/^-/;
        $_ = shift @ARGV;
        last if m/^--?$/;
        if (m/^--/) {
            if (m/^--dry-run$/) {
 +              push @ropts, $_;
+               $dryrun=2;
+           } elsif (m/^--damp-run$/) {
++              push @ropts, $_;
                $dryrun=1;
            } elsif (m/^--no-sign$/) {
 +              push @ropts, $_;
                $sign=0;
            } elsif (m/^--help$/) {
                cmd_help();
            } elsif (m/^--version$/) {
                cmd_version();
            } elsif (m/^--new$/) {
 +              push @ropts, $_;
                $new_package=1;
            } elsif (m/^--(\w+)=(.*)/s &&
                     ($om = $opts_opt_map{$1}) &&
                     length $om->[0]) {
 +              push @ropts, $_;
                $om->[0] = $2;
            } elsif (m/^--(\w+):(.*)/s &&
 +                   !$opts_opt_cmdonly{$1} &&
                     ($om = $opts_opt_map{$1})) {
 +              push @ropts, $_;
                push @$om, $2;
            } elsif (m/^--existing-package=(.*)/s) {
 +              push @ropts, $_;
                $existing_package = $1;
            } elsif (m/^--distro=(.*)/s) {
 +              push @ropts, $_;
                $idistro = $1;
            } elsif (m/^--clean=(dpkg-source|git|none)$/s) {
 +              push @ropts, $_;
                $cleanmode = $1;
            } elsif (m/^--clean=(.*)$/s) {
                badusage "unknown cleaning mode \`$1'";
            } elsif (m/^--ignore-dirty$/s) {
 +              push @ropts, $_;
                $ignoredirty = 1;
            } elsif (m/^--no-quilt-fixup$/s) {
 +              push @ropts, $_;
                $noquilt = 1;
            } else {
                badusage "unknown long option \`$_'";
        } else {
            while (m/^-./s) {
                if (s/^-n/-/) {
-                   push @ropts, $_;
++                  push @ropts, $&;
+                   $dryrun=2;
+               } elsif (s/^-L/-/) {
++                  push @ropts, $&;
                    $dryrun=1;
                } elsif (s/^-h/-/) {
                    cmd_help();
                } elsif (s/^-D/-/) {
 +                  push @ropts, $&;
                    open DEBUG, ">&STDERR" or die $!;
 +                  autoflush DEBUG 1;
                    $debug++;
                } elsif (s/^-N/-/) {
 +                  push @ropts, $&;
                    $new_package=1;
                } elsif (m/^-[vm]/) {
 +                  push @ropts, $&;
                    push @changesopts, $_;
                    $_ = '';
                } elsif (s/^-c(.*=.*)//s) {
 +                  push @ropts, $&;
                    push @git, '-c', $1;
                } elsif (s/^-d(.*)//s) {
 +                  push @ropts, $&;
                    $idistro = $1;
                } elsif (s/^-C(.*)//s) {
 +                  push @ropts, $&;
                    $changesfile = $1;
                } elsif (s/^-k(.*)//s) {
                    $keyid=$1;
                } elsif (s/^-wn//s) {
 +                  push @ropts, $&;
                    $cleanmode = 'none';
                } elsif (s/^-wg//s) {
 +                  push @ropts, $&;
                    $cleanmode = 'git';
                } elsif (s/^-wd//s) {
 +                  push @ropts, $&;
                    $cleanmode = 'dpkg-source';
                } else {
                    badusage "unknown short option \`$_'";
@@@ -1819,7 -1353,8 +1832,8 @@@ if ($ENV{$fakeeditorenv}) 
  delete $ENV{'DGET_UNPACK'};
  
  parseopts();
- print STDERR "DRY RUN ONLY\n" if $dryrun;
+ print STDERR "DRY RUN ONLY\n" if $dryrun > 1;
+ print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n" if $dryrun == 1;
  if (!@ARGV) {
      print STDERR $helpmsg or die $!;
      exit 8;
diff --combined dgit.1
--- 1/dgit.1
--- 2/dgit.1
+++ b/dgit.1
@@@ -20,10 -20,6 +20,10 @@@ dgit \- git integration with the Debia
  [\fIsuite\fP]
  .br
  .B dgit
 +[\fIdgit\-opts\fP] \fBrpush\fR \fIbuild-host\fR\fB:\fR\fIbuild-dir\fR
 +[\fIpush args...\fR]
 +.br
 +.B dgit
  [\fIdgit\-opts\fP] \fIaction\fR ...
  .SH DESCRIPTION
  .B dgit
@@@ -129,7 -125,7 +129,7 @@@ will be passed on to git-buildpackage
  
  Tagging, signing and actually uploading should be left to dgit push.
  .TP
 -.B dgit push
 +\fBdgit push\fR [\fIsuite\fP]
  Does an `upload', pushing the current HEAD to the archive (as a source
  package) and to dgit-repos (as git commits).  The package must already
  have been built ready for upload, with the .dsc and .changes
@@@ -143,31 -139,11 +143,31 @@@ signed tag, and finally uses dput to up
  archive.
  
  dgit push always uses the package, suite and version specified in the
 -debian/changelog and the .dsc, which must agree.
 +debian/changelog and the .dsc, which must agree.  If the command line
 +specifies a suite then that must match too.
  
  If dgit push fails while uploading, it is fine to simply retry the
  dput on the .changes file at your leisure.
  .TP
 +\fBdgit rpush\fR \fIbuild-host\fR\fB:\fR\fIbuild-dir\fR [\fIpush args...\fR]
 +Pushes the contents of the specified directory on a remote machine.
 +This is like running dgit push on build-host with build-dir as the
 +current directory; however, signing operations are done on the
 +invoking host.  This allows you to do a push when the system which has
 +the source code and the build outputs has no access to the key.
 +
 +However, the build-host must be able to ssh to the dgit repos.  If
 +this is not already the case, you must organise it separately, for
 +example by the use of ssh agent forwarding.
 +
 +The remaining arguments are treated just as dgit push would handle
 +them.
 +
 +build-host and build\-dir can be passed as separate
 +arguments; this is assumed to be the case if the first argument
 +contains no : (except perhaps on in [ ], to support IPv6 address
 +literals).
 +.TP
  .B dgit quilt-fixup
  Looks to see if the tree is one which dpkg-source cannot properly
  represent.  If it isn't, dgit will fix it up for you (in quilt terms,
@@@ -186,6 -162,11 +186,11 @@@ actually update the output(s).  For pus
  the required checks and leaves the new .dsc in a temporary file,
  but does not sign, tag, push or upload.
  .TP
+ .BR --damp-run | -L
+ Go through many more of the motions: do everything that doesn't
+ involve either signing things, or making changes on the public
+ servers.
+ .TP
  .BI -k keyid
  Use
  .I keyid
@@@ -259,34 -240,9 +264,34 @@@ Specifies alternative programs to use i
  .BR dpkg-buildpackage ,
  .BR dpkg-genchanges ,
  .BR sbuild ,
 +.BR gpg ,
 +.BR ssh ,
 +.BR dgit ,
  or
  .BR mergechanges .
 -This applies only when the program is invoked directly by dgit.
 +
 +For dpkg-buildpackage, dpkg-genchanges, mergechanges and sbuild,
 +this applies only when the program is invoked directly by dgit.
 +
 +For dgit, specifies the command to run on the remote host when dgit
 +rpush needs to invoke a remote copy of itself.  (dgit also reinvokes
 +itself as the EDITOR for dpkg-source --commit; this is done using
 +argv[0], and is not affected by --dget=).
 +
 +For ssh, the default value is taken from the
 +.B DGIT_SSH
 +or
 +.B GIT_SSH
 +environment variables, if set (see below).  And, for ssh, when accessing the
 +archive and dgit-repos, this command line setting is overridden by the
 +git config variables
 +.BI dgit-distro. distro .ssh
 +and
 +.B .dgit.default.ssh
 +(which can in turn be overridden with -c).  Also, when dgit is using
 +git to access dgit-repos, only git's idea of what ssh to use (eg,
 +.BR GIT_SSH )
 +is relevant.
  .TP
  .RI \fB--dget:\fR option |\fB--dput:\fR option |...
  Specifies a single additional option to pass to
  .BR dpkg-buildpackage ,
  .BR dpkg-genchanges ,
  .BR sbuild ,
 +.BR ssh ,
 +.BR dgit ,
  or
  .BR mergechanges .
  Can be repeated as necessary.
 -This applies only when the program is invoked directly by dgit.
 -Usually, for passing options to dpkg-genchanges, use
 +
 +For dpkg-buildpackage, dpkg-genchanges, mergechanges and sbuild,
 +this applies only when the program is invoked directly by dgit.
 +Usually, for passing options to dpkg-genchanges, you should use
  .BR --ch: \fIoption\fR.
 +
 +See notes above regarding ssh and dgit.
 +
 +NB that --gpg:option is not supported (because debsign does not
 +have that facility).  But see -k.
  .TP
  .BR -d "\fIdistro\fR | " --distro= \fIdistro\fR
  Specifies that the suite to be operated on is part of distro
@@@ -596,21 -543,6 +601,21 @@@ on the dgit command line
  .BR dgit.default. *
  for each
  .BR dgit-distro. \fIdistro\fR . *
 +.SH ENVIRONMENT VARIABLES
 +.TP
 +.BR DGIT_SSH ", " GIT_SSH
 +specify an alternative default program (and perhaps arguments) to use
 +instead of ssh.  DGIT_SSH is consulted first and may contain arguments;
 +if it contains any whitespace will be passed to the shell.  GIT_SSH
 +specifies just the program; no arguments can be specified, so dgit
 +interprets it the same way as git does.
 +See
 +also the --ssh= and --ssh: options.
 +.TP
 +.BR gpg ", " dpkg- "..., " debsign ", " git ", " dget ", " dput ", " LWP::UserAgent
 +and other subprograms and modules used by dgit are affected by various
 +environment variables.  Consult the documentaton for those programs
 +for details.
  .SH BUGS
  We should be using some kind of vhost/vpath setup for the git repos on
  alioth, so that they can be moved later if and when this turns out to