chiark / gitweb /
4c3d224431e748ab04c183c1523800a2c201fc05
[dgit.git] / dgit
1 #!/usr/bin/perl -w
2 # dgit
3 # Integration between git and Debian-style archives
4 #
5 # Copyright (C)2013 Ian Jackson
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 use strict;
21
22 use IO::Handle;
23 use Data::Dumper;
24 use LWP::UserAgent;
25 use Dpkg::Control::Hash;
26 use File::Path;
27 use File::Temp qw(tempdir);
28 use File::Basename;
29 use Dpkg::Version;
30 use POSIX;
31 use IPC::Open2;
32
33 our $our_version = 'UNRELEASED'; ###substituted###
34
35 our $isuite = 'unstable';
36 our $idistro;
37 our $package;
38 our @ropts;
39
40 our $sign = 1;
41 our $dryrun_level = 0;
42 our $changesfile;
43 our $new_package = 0;
44 our $ignoredirty = 0;
45 our $noquilt = 0;
46 our $existing_package = 'dpkg';
47 our $cleanmode = 'dpkg-source';
48 our $we_are_responder;
49 our $initiator_tempdir;
50
51 our %format_ok = map { $_=>1 } ("1.0","3.0 (native)","3.0 (quilt)");
52
53 our (@git) = qw(git);
54 our (@dget) = qw(dget);
55 our (@dput) = qw(dput);
56 our (@debsign) = qw(debsign);
57 our (@gpg) = qw(gpg);
58 our (@sbuild) = qw(sbuild -A);
59 our (@ssh) = 'ssh';
60 our (@dgit) = qw(dgit);
61 our (@dpkgbuildpackage) = qw(dpkg-buildpackage -i\.git/ -I.git);
62 our (@dpkgsource) = qw(dpkg-source -i\.git/ -I.git);
63 our (@dpkggenchanges) = qw(dpkg-genchanges);
64 our (@mergechanges) = qw(mergechanges -f);
65 our (@changesopts) = ('');
66
67 our %opts_opt_map = ('dget' => \@dget,
68                      'dput' => \@dput,
69                      'debsign' => \@debsign,
70                      'gpg' => \@gpg,
71                      'sbuild' => \@sbuild,
72                      'ssh' => \@ssh,
73                      'dgit' => \@dgit,
74                      'dpkg-source' => \@dpkgsource,
75                      'dpkg-buildpackage' => \@dpkgbuildpackage,
76                      'dpkg-genchanges' => \@dpkggenchanges,
77                      'ch' => \@changesopts,
78                      'mergechanges' => \@mergechanges);
79
80 our %opts_opt_cmdonly = ('gpg' => 1);
81
82 our $keyid;
83
84 our $debug = 0;
85 open DEBUG, ">/dev/null" or die $!;
86
87 autoflush STDOUT 1;
88
89 our $remotename = 'dgit';
90 our @ourdscfield = qw(Dgit Vcs-Dgit-Master);
91 our $branchprefix = 'dgit';
92 our $csuite;
93
94 sub lbranch () { return "$branchprefix/$csuite"; }
95 my $lbranch_re = '^refs/heads/'.$branchprefix.'/([^/.]+)$';
96 sub lref () { return "refs/heads/".lbranch(); }
97 sub lrref () { return "refs/remotes/$remotename/$branchprefix/$csuite"; }
98 sub rrref () { return "refs/$branchprefix/$csuite"; }
99 sub debiantag ($) { 
100     my ($v) = @_;
101     $v =~ y/~:/_%/;
102     return "debian/$v";
103 }
104
105 sub stripepoch ($) {
106     my ($vsn) = @_;
107     $vsn =~ s/^\d+\://;
108     return $vsn;
109 }
110
111 sub dscfn ($) {
112     my ($vsn) = @_;
113     return "${package}_".(stripepoch $vsn).".dsc";
114 }
115
116 sub changesopts () { return @changesopts[1..$#changesopts]; }
117
118 our $us = 'dgit';
119 our $debugprefix = '';
120
121 sub printdebug { print DEBUG $debugprefix, @_ or die $!; }
122
123 sub fail { die "$us: @_\n"; }
124
125 sub badcfg { print STDERR "$us: invalid configuration: @_\n"; exit 12; }
126
127 sub no_such_package () {
128     print STDERR "$us: package $package does not exist in suite $isuite\n";
129     exit 4;
130 }
131
132 sub fetchspec () {
133     local $csuite = '*';
134     return  "+".rrref().":".lrref();
135 }
136
137 sub changedir ($) {
138     my ($newdir) = @_;
139     printdebug "CD $newdir\n";
140     chdir $newdir or die "chdir: $newdir: $!";
141 }
142
143 #---------- remote protocol support, common ----------
144
145 # remote push initiator/responder protocol:
146 #  < dgit-remote-push-ready [optional extra info ignored by old initiators]
147 #
148 #  > file parsed-changelog
149 #  [indicates that output of dpkg-parsechangelog follows]
150 #  > data-block NBYTES
151 #  > [NBYTES bytes of data (no newline)]
152 #  [maybe some more blocks]
153 #  > data-end
154 #
155 #  > file dsc
156 #  [etc]
157 #
158 #  > file changes
159 #  [etc]
160 #
161 #  > param head HEAD
162 #
163 #  > want signed-tag
164 #  [indicates that signed tag is wanted]
165 #  < data-block NBYTES
166 #  < [NBYTES bytes of data (no newline)]
167 #  [maybe some more blocks]
168 #  < data-end
169 #  < files-end
170 #
171 #  > want signed-dsc-changes
172 #  < data-block NBYTES    [transfer of signed dsc]
173 #  [etc]
174 #  < data-block NBYTES    [transfer of signed changes]
175 #  [etc]
176 #  < files-end
177 #
178 #  > complete
179
180 sub badproto ($$) {
181     my ($fh, $m) = @_;
182     fail "connection lost: $!" if $fh->error;
183     fail "protocol violation; $m not expected";
184 }
185
186 sub protocol_expect (&$) {
187     my ($match, $fh) = @_;
188     local $_;
189     $_ = <$fh>;
190     defined && chomp or badproto $fh, "eof";
191     if (wantarray) {
192         my @r = &$match;
193         return @r if @r;
194     } else {
195         my $r = &$match;
196         return $r if $r;
197     }
198     badproto $fh, "\`$_'";
199 }
200
201 sub protocol_send_file ($$) {
202     my ($fh, $ourfn) = @_;
203     open PF, "<", $ourfn or die "$ourfn: $!";
204     for (;;) {
205         my $d;
206         my $got = read PF, $d, 65536;
207         die "$ourfn: $!" unless defined $got;
208         last if !$got;
209         print $fh "data-block ".length($d)."\n" or die $!;
210         print $fh $d or die $!;
211     }
212     PF->error and die "$ourfn $!";
213     print $fh "data-end\n" or die $!;
214     close PF;
215 }
216
217 sub protocol_read_bytes ($$) {
218     my ($fh, $nbytes) = @_;
219     $nbytes =~ m/^[1-9]\d{0,5}$/ or badproto \*RO, "bad byte count";
220     my $d;
221     my $got = read $fh, $d, $nbytes;
222     $got==$nbytes or badproto $fh, "eof during data block";
223     return $d;
224 }
225
226 sub protocol_receive_file ($$) {
227     my ($fh, $ourfn) = @_;
228     open PF, ">", $ourfn or die "$ourfn: $!";
229     for (;;) {
230         my ($y,$l) = protocol_expect {
231             m/^data-block (.*)$/ ? (1,$1) :
232             m/^data-end$/ ? (0,) :
233             ();
234         } $fh;
235         last unless $y;
236         my $d = protocol_read_bytes $fh, $l;
237         print PF $d or die $!;
238     }
239     close PF or die $!;
240     printdebug "() $ourfn\n";
241 }
242
243 #---------- remote protocol support, responder ----------
244
245 sub responder_send_command ($) {
246     my ($command) = @_;
247     return unless $we_are_responder;
248     # called even without $we_are_responder
249     printdebug "<< $command\n";
250     print PO $command, "\n" or die $!;
251 }    
252
253 sub responder_send_file ($$) {
254     my ($keyword, $ourfn) = @_;
255     return unless $we_are_responder;
256     printdebug "[[ $keyword $ourfn\n";
257     responder_send_command "file $keyword";
258     protocol_send_file \*PO, $ourfn;
259 }
260
261 sub responder_receive_files ($@) {
262     my ($keyword, @ourfns) = @_;
263     die unless $we_are_responder;
264     printdebug "]] $keyword @ourfns\n";
265     responder_send_command "want $keyword";
266     foreach my $fn (@ourfns) {
267         protocol_receive_file \*PI, $fn;
268     }
269     protocol_expect { m/^files-end$/ } \*PI;
270 }
271
272 #---------- remote protocol support, initiator ----------
273
274 sub initiator_expect (&) {
275     my ($match) = @_;
276     protocol_expect { &$match } \*RO;
277 }
278
279 #---------- end remote code ----------
280
281 sub progress {
282     if ($we_are_responder) {
283         my $m = join '', @_;
284         responder_send_command "progress ".length($m) or die $!;
285         print PO $m or die $!;
286     } else {
287         print @_, "\n";
288     }
289 }
290
291 our $ua;
292
293 sub url_get {
294     if (!$ua) {
295         $ua = LWP::UserAgent->new();
296         $ua->env_proxy;
297     }
298     my $what = $_[$#_];
299     progress "downloading $what...";
300     my $r = $ua->get(@_) or die $!;
301     return undef if $r->code == 404;
302     $r->is_success or fail "failed to fetch $what: ".$r->status_line;
303     return $r->decoded_content();
304 }
305
306 our ($dscdata,$dscurl,$dsc,$skew_warning_vsn);
307
308 sub shellquote {
309     my @out;
310     local $_;
311     foreach my $a (@_) {
312         $_ = $a;
313         if (s{['\\]}{\\$&}g || m{\s} || m{[^-_./0-9a-z]}i) {
314             push @out, "'$_'";
315         } else {
316             push @out, $_;
317         }
318     }
319     return join ' ', @out;
320 }
321
322 sub printcmd {
323     my $fh = shift @_;
324     my $intro = shift @_;
325     print $fh $intro," " or die $!;
326     print $fh shellquote @_ or die $!;
327     print $fh "\n" or die $!;
328 }
329
330 sub failedcmd {
331     { local ($!); printcmd \*STDERR, "$_[0]: failed command:", @_ or die $!; };
332     if ($!) {
333         fail "failed to fork/exec: $!";
334     } elsif (!($? & 0xff)) {
335         fail "subprocess failed with error exit status ".($?>>8);
336     } elsif ($?) {
337         fail "subprocess crashed (wait status $?)";
338     } else {
339         fail "subprocess produced invalid output";
340     }
341 }
342
343 sub runcmd {
344     printcmd(\*DEBUG,$debugprefix."+",@_) if $debug>0;
345     $!=0; $?=0;
346     failedcmd @_ if system @_;
347 }
348
349 sub act_local () { return $dryrun_level <= 1; }
350 sub act_scary () { return !$dryrun_level; }
351
352 sub printdone {
353     if (!$dryrun_level) {
354         progress "dgit ok: @_";
355     } else {
356         progress "would be ok: @_ (but dry run only)";
357     }
358 }
359
360 sub cmdoutput_errok {
361     die Dumper(\@_)." ?" if grep { !defined } @_;
362     printcmd(\*DEBUG,$debugprefix."|",@_) if $debug>0;
363     open P, "-|", @_ or die $!;
364     my $d;
365     $!=0; $?=0;
366     { local $/ = undef; $d = <P>; }
367     die $! if P->error;
368     if (!close P) { printdebug "=>!$?\n" if $debug>0; return undef; }
369     chomp $d;
370     $d =~ m/^.*/;
371     printdebug "=> \`$&'",(length $' ? '...' : ''),"\n" if $debug>0; #';
372     return $d;
373 }
374
375 sub cmdoutput {
376     my $d = cmdoutput_errok @_;
377     defined $d or failedcmd @_;
378     return $d;
379 }
380
381 sub dryrun_report {
382     printcmd(\*STDERR,$debugprefix."#",@_);
383 }
384
385 sub runcmd_ordryrun {
386     if (act_scary()) {
387         runcmd @_;
388     } else {
389         dryrun_report @_;
390     }
391 }
392
393 sub runcmd_ordryrun_local {
394     if (act_local()) {
395         runcmd @_;
396     } else {
397         dryrun_report @_;
398     }
399 }
400
401 sub shell_cmd {
402     my ($first_shell, @cmd) = @_;
403     return qw(sh -ec), $first_shell.'; exec "$@"', 'x', @cmd;
404 }
405
406 our $helpmsg = <<END;
407 main usages:
408   dgit [dgit-opts] clone [dgit-opts] package [suite] [./dir|/dir]
409   dgit [dgit-opts] fetch|pull [dgit-opts] [suite]
410   dgit [dgit-opts] build [git-buildpackage-opts|dpkg-buildpackage-opts]
411   dgit [dgit-opts] push [dgit-opts] [suite]
412   dgit [dgit-opts] rpush build-host:build-dir ...
413 important dgit options:
414   -k<keyid>           sign tag and package with <keyid> instead of default
415   --dry-run -n        do not change anything, but go through the motions
416   --damp-run -L       like --dry-run but make local changes, without signing
417   --new -N            allow introducing a new package
418   --debug -D          increase debug level
419   -c<name>=<value>    set git config option (used directly by dgit too)
420 END
421
422 our $later_warning_msg = <<END;
423 Perhaps the upload is stuck in incoming.  Using the version from git.
424 END
425
426 sub badusage {
427     print STDERR "$us: @_\n", $helpmsg or die $!;
428     exit 8;
429 }
430
431 sub nextarg {
432     @ARGV or badusage "too few arguments";
433     return scalar shift @ARGV;
434 }
435
436 sub cmd_help () {
437     print $helpmsg or die $!;
438     exit 0;
439 }
440
441 our %defcfg = ('dgit.default.distro' => 'debian',
442                'dgit.default.username' => '',
443                'dgit.default.archive-query-default-component' => 'main',
444                'dgit.default.ssh' => 'ssh',
445                'dgit-distro.debian.git-host' => 'git.debian.org',
446                'dgit-distro.debian.git-proto' => 'git+ssh://',
447                'dgit-distro.debian.git-path' => '/git/dgit-repos/repos',
448                'dgit-distro.debian.git-check' => 'ssh-cmd',
449                'dgit-distro.debian.git-create' => 'ssh-cmd',
450                'dgit-distro.debian.sshdakls-host' => 'coccia.debian.org',
451                'dgit-distro.debian.sshdakls-dir' =>
452                    '/srv/ftp-master.debian.org/ftp/dists',
453                'dgit-distro.debian.upload-host' => 'ftp-master', # for dput
454                'dgit-distro.debian.mirror' => 'http://ftp.debian.org/debian/');
455
456 sub cfg {
457     foreach my $c (@_) {
458         return undef if $c =~ /RETURN-UNDEF/;
459         my @cmd = (@git, qw(config --), $c);
460         my $v;
461         {
462             local ($debug) = $debug-1;
463             $v = cmdoutput_errok @cmd;
464         };
465         if ($?==0) {
466             return $v;
467         } elsif ($?!=256) {
468             failedcmd @cmd;
469         }
470         my $dv = $defcfg{$c};
471         return $dv if defined $dv;
472     }
473     badcfg "need value for one of: @_";
474 }
475
476 sub access_distro () {
477     return cfg("dgit-suite.$isuite.distro",
478                "dgit.default.distro");
479 }
480
481 sub access_cfg (@) {
482     my (@keys) = @_;
483     my $distro = $idistro || access_distro();
484     my $value = cfg(map { ("dgit-distro.$distro.$_",
485                            "dgit.default.$_") } @keys);
486     return $value;
487 }
488
489 sub string_to_ssh ($) {
490     my ($spec) = @_;
491     if ($spec =~ m/\s/) {
492         return qw(sh -ec), 'exec '.$spec.' "$@"', 'x';
493     } else {
494         return ($spec);
495     }
496 }
497
498 sub access_cfg_ssh () {
499     my $gitssh = access_cfg('ssh', 'RETURN-UNDEF');
500     if (!defined $gitssh) {
501         return @ssh;
502     } else {
503         return string_to_ssh $gitssh;
504     }
505 }
506
507 sub access_someuserhost ($) {
508     my ($some) = @_;
509     my $user = access_cfg("$some-user",'username');
510     my $host = access_cfg("$some-host");
511     return length($user) ? "$user\@$host" : $host;
512 }
513
514 sub access_gituserhost () {
515     return access_someuserhost('git');
516 }
517
518 sub access_giturl () {
519     my $url = access_cfg('git-url','RETURN-UNDEF');
520     if (!defined $url) {
521         $url =
522             access_cfg('git-proto').
523             access_gituserhost().
524             access_cfg('git-path');
525     }
526     return "$url/$package.git";
527 }              
528
529 sub parsecontrolfh ($$@) {
530     my ($fh, $desc, @opts) = @_;
531     my %opts = ('name' => $desc, @opts);
532     my $c = Dpkg::Control::Hash->new(%opts);
533     $c->parse($fh) or die "parsing of $desc failed";
534     return $c;
535 }
536
537 sub parsecontrol {
538     my ($file, $desc) = @_;
539     my $fh = new IO::Handle;
540     open $fh, '<', $file or die "$file: $!";
541     my $c = parsecontrolfh($fh,$desc);
542     $fh->error and die $!;
543     close $fh;
544     return $c;
545 }
546
547 sub getfield ($$) {
548     my ($dctrl,$field) = @_;
549     my $v = $dctrl->{$field};
550     return $v if defined $v;
551     fail "missing field $field in ".$v->get_option('name');
552 }
553
554 sub parsechangelog {
555     my $c = Dpkg::Control::Hash->new();
556     my $p = new IO::Handle;
557     my @cmd = (qw(dpkg-parsechangelog), @_);
558     open $p, '-|', @cmd or die $!;
559     $c->parse($p);
560     $?=0; $!=0; close $p or failedcmd @cmd;
561     return $c;
562 }
563
564 our %rmad;
565
566 sub archive_query ($) {
567     my ($method) = @_;
568     my $query = access_cfg('archive-query','RETURN-UNDEF');
569     if (!defined $query) {
570         my $distro = access_distro();
571         if ($distro eq 'debian') {
572             $query = "sshdakls:".
573                 access_someuserhost('sshdakls').':'.
574                 access_cfg('sshdakls-dir');
575         } else {
576             $query = "madison:$distro";
577         }
578     }
579     $query =~ s/^(\w+):// or badcfg "invalid archive-query method \`$query'";
580     my $proto = $1;
581     my $data = $'; #';
582     { no strict qw(refs); &{"${method}_${proto}"}($proto,$data); }
583 }
584
585 sub archive_query_madison ($$) {
586     my ($proto,$data) = @_;
587     die unless $proto eq 'madison';
588     $rmad{$package} ||= cmdoutput
589         qw(rmadison -asource),"-s$isuite","-u$data",$package;
590     my $rmad = $rmad{$package};
591     return madison_parse($rmad);
592 }
593
594 sub archive_query_sshdakls ($$) {
595     my ($proto,$data) = @_;
596     $data =~ s/:.*// or badcfg "invalid sshdakls method string \`$data'";
597     my $dakls = cmdoutput
598         access_cfg_ssh, $data, qw(dak ls -asource),"-s$isuite",$package;
599     return madison_parse($dakls);
600 }
601
602 sub canonicalise_suite_sshdakls ($$) {
603     my ($proto,$data) = @_;
604     $data =~ m/:/ or badcfg "invalid sshdakls method string \`$data'";
605     my @cmd =
606         (access_cfg_ssh, $`,
607          "set -e; cd $';".
608          " if test -h $isuite; then readlink $isuite; exit 0; fi;".
609          " if test -d $isuite; then echo $isuite; exit 0; fi;".
610          " exit 1");
611     my $dakls = cmdoutput @cmd;
612     failedcmd @cmd unless $dakls =~ m/^\w/;
613     return $dakls;
614 }
615
616 sub madison_parse ($) {
617     my ($rmad) = @_;
618     my @out;
619     foreach my $l (split /\n/, $rmad) {
620         $l =~ m{^ \s*( [^ \t|]+ )\s* \|
621                   \s*( [^ \t|]+ )\s* \|
622                   \s*( [^ \t|/]+ )(?:/([^ \t|/]+))? \s* \|
623                   \s*( [^ \t|]+ )\s* }x or die "$rmad $?";
624         $1 eq $package or die "$rmad $package ?";
625         my $vsn = $2;
626         my $newsuite = $3;
627         my $component;
628         if (defined $4) {
629             $component = $4;
630         } else {
631             $component = access_cfg('archive-query-default-component');
632         }
633         $5 eq 'source' or die "$rmad ?";
634         my $prefix = substr($package, 0, $package =~ m/^l/ ? 4 : 1);
635         my $subpath = "/pool/$component/$prefix/$package/".dscfn($vsn);
636         push @out, [$vsn,$subpath,$newsuite];
637     }
638     return sort { -version_compare_string($a->[0],$b->[0]); } @out;
639 }
640
641 sub canonicalise_suite_madison ($$) {
642     my @r = archive_query_madison($_[0],$_[1]);
643     @r or fail
644         "unable to canonicalise suite using package $package".
645         " which does not appear to exist in suite $isuite;".
646         " --existing-package may help";
647     return $r[0][2];
648 }
649
650 sub canonicalise_suite () {
651     return if defined $csuite;
652     fail "cannot operate on $isuite suite" if $isuite eq 'UNRELEASED';
653     $csuite = archive_query('canonicalise_suite');
654     if ($isuite ne $csuite) {
655         # madison canonicalises for us
656         progress "canonical suite name for $isuite is $csuite";
657     }
658 }
659
660 sub get_archive_dsc () {
661     canonicalise_suite();
662     my @vsns = archive_query('archive_query');
663     foreach my $vinfo (@vsns) {
664         my ($vsn,$subpath) = @$vinfo;
665         $dscurl = access_cfg('mirror').$subpath;
666         $dscdata = url_get($dscurl);
667         if (!$dscdata) {
668             $skew_warning_vsn = $vsn if !defined $skew_warning_vsn;
669             next;
670         }
671         my $dscfh = new IO::File \$dscdata, '<' or die $!;
672         printdebug Dumper($dscdata) if $debug>1;
673         $dsc = parsecontrolfh($dscfh,$dscurl, allow_pgp=>1);
674         printdebug Dumper($dsc) if $debug>1;
675         my $fmt = getfield $dsc, 'Format';
676         fail "unsupported source format $fmt, sorry" unless $format_ok{$fmt};
677         return;
678     }
679     $dsc = undef;
680 }
681
682 sub check_for_git () {
683     # returns 0 or 1
684     my $how = access_cfg('git-check');
685     if ($how eq 'ssh-cmd') {
686         my @cmd =
687             (access_cfg_ssh, access_gituserhost(),
688              " set -e; cd ".access_cfg('git-path').";".
689              " if test -d $package.git; then echo 1; else echo 0; fi");
690         my $r= cmdoutput @cmd;
691         failedcmd @cmd unless $r =~ m/^[01]$/;
692         return $r+0;
693     } else {
694         badcfg "unknown git-check \`$how'";
695     }
696 }
697
698 sub create_remote_git_repo () {
699     my $how = access_cfg('git-create');
700     if ($how eq 'ssh-cmd') {
701         runcmd_ordryrun
702             (access_cfg_ssh, access_gituserhost(),
703              "set -e; cd ".access_cfg('git-path').";".
704              " cp -a _template $package.git");
705     } else {
706         badcfg "unknown git-create \`$how'";
707     }
708 }
709
710 our ($dsc_hash,$lastpush_hash);
711
712 our $ud = '.git/dgit/unpack';
713
714 sub prep_ud () {
715     rmtree($ud);
716     mkpath '.git/dgit';
717     mkdir $ud or die $!;
718 }
719
720 sub mktree_in_ud_from_only_subdir () {
721     # changes into the subdir
722     my (@dirs) = <*/.>;
723     die unless @dirs==1;
724     $dirs[0] =~ m#^([^/]+)/\.$# or die;
725     my $dir = $1;
726     changedir $dir;
727     fail "source package contains .git directory" if stat '.git';
728     die $! unless $!==&ENOENT;
729     runcmd qw(git init -q);
730     rmtree('.git/objects');
731     symlink '../../../../objects','.git/objects' or die $!;
732     runcmd @git, qw(add -Af);
733     my $tree = cmdoutput @git, qw(write-tree);
734     $tree =~ m/^\w+$/ or die "$tree ?";
735     return ($tree,$dir);
736 }
737
738 sub dsc_files_info () {
739     foreach my $csumi (['Checksums-Sha256','Digest::SHA', 'new(256)'],
740                        ['Checksums-Sha1',  'Digest::SHA', 'new(1)'],
741                        ['Files',           'Digest::MD5', 'new()']) {
742         my ($fname, $module, $method) = @$csumi;
743         my $field = $dsc->{$fname};
744         next unless defined $field;
745         eval "use $module; 1;" or die $@;
746         my @out;
747         foreach (split /\n/, $field) {
748             next unless m/\S/;
749             m/^(\w+) (\d+) (\S+)$/ or
750                 fail "could not parse .dsc $fname line \`$_'";
751             my $digester = eval "$module"."->$method;" or die $@;
752             push @out, {
753                 Hash => $1,
754                 Bytes => $2,
755                 Filename => $3,
756                 Digester => $digester,
757             };
758         }
759         return @out;
760     }
761     fail "missing any supported Checksums-* or Files field in ".
762         $dsc->get_option('name');
763 }
764
765 sub dsc_files () {
766     map { $_->{Filename} } dsc_files_info();
767 }
768
769 sub is_orig_file ($) {
770     local ($_) = @_;
771     m/\.orig(?:-\w+)?\.tar\.\w+$/;
772 }
773
774 sub make_commit ($) {
775     my ($file) = @_;
776     return cmdoutput @git, qw(hash-object -w -t commit), $file;
777 }
778
779 sub clogp_authline ($) {
780     my ($clogp) = @_;
781     my $author = getfield $clogp, 'Maintainer';
782     $author =~ s#,.*##ms;
783     my $date = cmdoutput qw(date), '+%s %z', qw(-d), getfield($clogp,'Date');
784     my $authline = "$author $date";
785     $authline =~ m/^[^<>]+ \<\S+\> \d+ [-+]\d+$/ or
786         fail "unexpected commit author line format \`$authline'".
787         " (was generated from changelog Maintainer field)";
788     return $authline;
789 }
790
791 sub generate_commit_from_dsc () {
792     prep_ud();
793     changedir $ud;
794     my @files;
795     foreach my $f (dsc_files()) {
796         die "$f ?" if $f =~ m#/|^\.|\.dsc$|\.tmp$#;
797         push @files, $f;
798         link "../../../$f", $f
799             or $!==&ENOENT
800             or die "$f $!";
801     }
802     runcmd @dget, qw(--), $dscurl;
803     foreach my $f (grep { is_orig_file($_) } @files) {
804         link $f, "../../../../$f"
805             or $!==&EEXIST
806             or die "$f $!";
807     }
808     my ($tree,$dir) = mktree_in_ud_from_only_subdir();
809     runcmd qw(sh -ec), 'dpkg-parsechangelog >../changelog.tmp';
810     my $clogp = parsecontrol('../changelog.tmp',"commit's changelog");
811     my $authline = clogp_authline $clogp;
812     my $changes = getfield $clogp, 'Changes';
813     open C, ">../commit.tmp" or die $!;
814     print C <<END or die $!;
815 tree $tree
816 author $authline
817 committer $authline
818
819 $changes
820
821 # imported from the archive
822 END
823     close C or die $!;
824     my $outputhash = make_commit qw(../commit.tmp);
825     my $cversion = getfield $clogp, 'Version';
826     progress "synthesised git commit from .dsc $cversion";
827     if ($lastpush_hash) {
828         runcmd @git, qw(reset --hard), $lastpush_hash;
829         runcmd qw(sh -ec), 'dpkg-parsechangelog >>../changelogold.tmp';
830         my $oldclogp = parsecontrol('../changelogold.tmp','previous changelog');
831         my $oversion = getfield $oldclogp, 'Version';
832         my $vcmp =
833             version_compare_string($oversion, $cversion);
834         if ($vcmp < 0) {
835             # git upload/ is earlier vsn than archive, use archive
836             open C, ">../commit2.tmp" or die $!;
837             print C <<END or die $!;
838 tree $tree
839 parent $lastpush_hash
840 parent $outputhash
841 author $authline
842 committer $authline
843
844 Record $package ($cversion) in archive suite $csuite
845 END
846             $outputhash = make_commit qw(../commit2.tmp);
847         } elsif ($vcmp > 0) {
848             print STDERR <<END or die $!;
849
850 Version actually in archive:    $cversion (older)
851 Last allegedly pushed/uploaded: $oversion (newer or same)
852 $later_warning_msg
853 END
854             $outputhash = $lastpush_hash;
855         } else {
856             $outputhash = $lastpush_hash;
857         }
858     }
859     changedir '../../../..';
860     runcmd @git, qw(update-ref -m),"dgit fetch import $cversion",
861             'DGIT_ARCHIVE', $outputhash;
862     cmdoutput @git, qw(log -n2), $outputhash;
863     # ... gives git a chance to complain if our commit is malformed
864     rmtree($ud);
865     return $outputhash;
866 }
867
868 sub ensure_we_have_orig () {
869     foreach my $fi (dsc_files_info()) {
870         my $f = $fi->{Filename};
871         next unless is_orig_file($f);
872         if (open F, "<", "../$f") {
873             $fi->{Digester}->reset();
874             $fi->{Digester}->addfile(*F);
875             F->error and die $!;
876             my $got = $fi->{Digester}->hexdigest();
877             $got eq $fi->{Hash} or
878                 fail "existing file $f has hash $got but .dsc".
879                     " demands hash $fi->{Hash}".
880                     " (perhaps you should delete this file?)";
881             progress "using existing $f";
882             next;
883         } else {
884             die "$f $!" unless $!==&ENOENT;
885         }
886         my $origurl = $dscurl;
887         $origurl =~ s{/[^/]+$}{};
888         $origurl .= "/$f";
889         die "$f ?" unless $f =~ m/^${package}_/;
890         die "$f ?" if $f =~ m#/#;
891         runcmd_ordryrun_local shell_cmd 'cd ..', @dget,'--',$origurl;
892     }
893 }
894
895 sub rev_parse ($) {
896     return cmdoutput @git, qw(rev-parse), "$_[0]~0";
897 }
898
899 sub is_fast_fwd ($$) {
900     my ($ancestor,$child) = @_;
901     my @cmd = (@git, qw(merge-base), $ancestor, $child);
902     my $mb = cmdoutput_errok @cmd;
903     if (defined $mb) {
904         return rev_parse($mb) eq rev_parse($ancestor);
905     } else {
906         $?==256 or failedcmd @cmd;
907         return 0;
908     }
909 }
910
911 sub git_fetch_us () {
912     runcmd_ordryrun_local @git, qw(fetch),access_giturl(),fetchspec();
913 }
914
915 sub fetch_from_archive () {
916     # ensures that lrref() is what is actually in the archive,
917     #  one way or another
918     get_archive_dsc();
919
920     if ($dsc) {
921         foreach my $field (@ourdscfield) {
922             $dsc_hash = $dsc->{$field};
923             last if defined $dsc_hash;
924         }
925         if (defined $dsc_hash) {
926             $dsc_hash =~ m/\w+/ or fail "invalid hash in .dsc \`$dsc_hash'";
927             $dsc_hash = $&;
928             progress "last upload to archive specified git hash";
929         } else {
930             progress "last upload to archive has NO git hash";
931         }
932     } else {
933         progress "no version available from the archive";
934     }
935
936     my $lrref_fn = ".git/".lrref();
937     if (open H, $lrref_fn) {
938         $lastpush_hash = <H>;
939         chomp $lastpush_hash;
940         die "$lrref_fn $lastpush_hash ?" unless $lastpush_hash =~ m/^\w+$/;
941     } elsif ($! == &ENOENT) {
942         $lastpush_hash = '';
943     } else {
944         die "$lrref_fn $!";
945     }
946     printdebug "previous reference hash=$lastpush_hash\n";
947     my $hash;
948     if (defined $dsc_hash) {
949         fail "missing git history even though dsc has hash -".
950             " could not find commit $dsc_hash".
951             " (should be in ".access_giturl()."#".rrref().")"
952             unless $lastpush_hash;
953         $hash = $dsc_hash;
954         ensure_we_have_orig();
955         if ($dsc_hash eq $lastpush_hash) {
956         } elsif (is_fast_fwd($dsc_hash,$lastpush_hash)) {
957             print STDERR <<END or die $!;
958
959 Git commit in archive is behind the last version allegedly pushed/uploaded.
960 Commit referred to by archive:  $dsc_hash
961 Last allegedly pushed/uploaded: $lastpush_hash
962 $later_warning_msg
963 END
964             $hash = $lastpush_hash;
965         } else {
966             fail "archive's .dsc refers to ".$dsc_hash.
967                 " but this is an ancestor of ".$lastpush_hash;
968         }
969     } elsif ($dsc) {
970         $hash = generate_commit_from_dsc();
971     } elsif ($lastpush_hash) {
972         # only in git, not in the archive yet
973         $hash = $lastpush_hash;
974         print STDERR <<END or die $!;
975
976 Package not found in the archive, but has allegedly been pushed using dgit.
977 $later_warning_msg
978 END
979     } else {
980         printdebug "nothing found!\n";
981         if (defined $skew_warning_vsn) {
982             print STDERR <<END or die $!;
983
984 Warning: relevant archive skew detected.
985 Archive allegedly contains $skew_warning_vsn
986 But we were not able to obtain any version from the archive or git.
987
988 END
989         }
990         return 0;
991     }
992     printdebug "current hash=$hash\n";
993     if ($lastpush_hash) {
994         fail "not fast forward on last upload branch!".
995             " (archive's version left in DGIT_ARCHIVE)"
996             unless is_fast_fwd($lastpush_hash, $hash);
997     }
998     if (defined $skew_warning_vsn) {
999         mkpath '.git/dgit';
1000         printdebug "SKEW CHECK WANT $skew_warning_vsn\n";
1001         my $clogf = ".git/dgit/changelog.tmp";
1002         runcmd shell_cmd "exec >$clogf",
1003             @git, qw(cat-file blob), "$hash:debian/changelog";
1004         my $gotclogp = parsechangelog("-l$clogf");
1005         my $got_vsn = getfield $gotclogp, 'Version';
1006         printdebug "SKEW CHECK GOT $got_vsn\n";
1007         if (version_compare_string($got_vsn, $skew_warning_vsn) < 0) {
1008             print STDERR <<END or die $!;
1009
1010 Warning: archive skew detected.  Using the available version:
1011 Archive allegedly contains    $skew_warning_vsn
1012 We were able to obtain only   $got_vsn
1013
1014 END
1015         }
1016     }
1017     if ($lastpush_hash ne $hash) {
1018         my @upd_cmd = (@git, qw(update-ref -m), 'dgit fetch', lrref(), $hash);
1019         if (act_local()) {
1020             cmdoutput @upd_cmd;
1021         } else {
1022             dryrun_report @upd_cmd;
1023         }
1024     }
1025     return 1;
1026 }
1027
1028 sub clone ($) {
1029     my ($dstdir) = @_;
1030     canonicalise_suite();
1031     badusage "dry run makes no sense with clone" unless act_local();
1032     mkdir $dstdir or die "$dstdir $!";
1033     changedir $dstdir;
1034     runcmd @git, qw(init -q);
1035     runcmd @git, qw(config), "remote.$remotename.fetch", fetchspec();
1036     open H, "> .git/HEAD" or die $!;
1037     print H "ref: ".lref()."\n" or die $!;
1038     close H or die $!;
1039     runcmd @git, qw(remote add), 'origin', access_giturl();
1040     if (check_for_git()) {
1041         progress "fetching existing git history";
1042         git_fetch_us();
1043         runcmd_ordryrun_local @git, qw(fetch origin);
1044     } else {
1045         progress "starting new git history";
1046     }
1047     fetch_from_archive() or no_such_package;
1048     runcmd @git, qw(reset --hard), lrref();
1049     printdone "ready for work in $dstdir";
1050 }
1051
1052 sub fetch () {
1053     if (check_for_git()) {
1054         git_fetch_us();
1055     }
1056     fetch_from_archive() or no_such_package();
1057     printdone "fetched into ".lrref();
1058 }
1059
1060 sub pull () {
1061     fetch();
1062     runcmd_ordryrun_local @git, qw(merge -m),"Merge from $csuite [dgit]",
1063         lrref();
1064     printdone "fetched to ".lrref()." and merged into HEAD";
1065 }
1066
1067 sub check_not_dirty () {
1068     return if $ignoredirty;
1069     my @cmd = (@git, qw(diff --quiet HEAD));
1070     printcmd(\*DEBUG,$debugprefix."+",@cmd) if $debug>0;
1071     $!=0; $?=0; system @cmd;
1072     return if !$! && !$?;
1073     if (!$! && $?==256) {
1074         fail "working tree is dirty (does not match HEAD)";
1075     } else {
1076         failedcmd @cmd;
1077     }
1078 }
1079
1080 sub commit_quilty_patch () {
1081     my $output = cmdoutput @git, qw(status --porcelain);
1082     my %adds;
1083     foreach my $l (split /\n/, $output) {
1084         next unless $l =~ m/\S/;
1085         if ($l =~ m{^(?:\?\?| M) (.pc|debian/patches)}) {
1086             $adds{$1}++;
1087         }
1088     }
1089     if (!%adds) {
1090         progress "nothing quilty to commit, ok.";
1091         return;
1092     }
1093     runcmd_ordryrun_local @git, qw(add), sort keys %adds;
1094     my $m = "Commit Debian 3.0 (quilt) metadata";
1095     progress "$m";
1096     runcmd_ordryrun_local @git, qw(commit -m), $m;
1097 }
1098
1099 sub madformat ($) {
1100     my ($format) = @_;
1101     return 0 unless $format eq '3.0 (quilt)';
1102     progress "Format \`$format', urgh";
1103     if ($noquilt) {
1104         progress "Not doing any fixup of \`$format' due to --no-quilt-fixup";
1105         return 0;
1106     }
1107     return 1;
1108 }
1109
1110 sub push_parse_changelog ($) {
1111     my ($clogpfn) = @_;
1112
1113     my $clogp = Dpkg::Control::Hash->new();
1114     $clogp->load($clogpfn) or die;
1115
1116     $package = getfield $clogp, 'Source';
1117     my $cversion = getfield $clogp, 'Version';
1118     my $tag = debiantag($cversion);
1119     runcmd @git, qw(check-ref-format), $tag;
1120
1121     my $dscfn = dscfn($cversion);
1122
1123     return ($clogp, $cversion, $tag, $dscfn);
1124 }
1125
1126 sub push_parse_dsc ($$$) {
1127     my ($dscfn,$dscfnwhat, $cversion) = @_;
1128     $dsc = parsecontrol($dscfn,$dscfnwhat);
1129     my $dversion = getfield $dsc, 'Version';
1130     my $dscpackage = getfield $dsc, 'Source';
1131     ($dscpackage eq $package && $dversion eq $cversion) or
1132         fail "$dsc is for $dscpackage $dversion".
1133             " but debian/changelog is for $package $cversion";
1134 }
1135
1136 sub push_mktag ($$$$$$$$) {
1137     my ($head,$clogp,$tag,
1138         $dsc,$dscfn,
1139         $changesfile,$changesfilewhat,
1140         $tfn) = @_;
1141
1142     $dsc->{$ourdscfield[0]} = $head;
1143     $dsc->save("$dscfn.tmp") or die $!;
1144
1145     my $changes = parsecontrol($changesfile,$changesfilewhat);
1146     foreach my $field (qw(Source Distribution Version)) {
1147         $changes->{$field} eq $clogp->{$field} or
1148             fail "changes field $field \`$changes->{$field}'".
1149                 " does not match changelog \`$clogp->{$field}'";
1150     }
1151
1152     my $cversion = getfield $clogp, 'Version';
1153
1154     # We make the git tag by hand because (a) that makes it easier
1155     # to control the "tagger" (b) we can do remote signing
1156     my $authline = clogp_authline $clogp;
1157     open TO, '>', $tfn->('.tmp') or die $!;
1158     print TO <<END or die $!;
1159 object $head
1160 type commit
1161 tag $tag
1162 tagger $authline
1163
1164 $package release $cversion for $csuite [dgit]
1165 END
1166     close TO or die $!;
1167
1168     my $tagobjfn = $tfn->('.tmp');
1169     if ($sign) {
1170         if (!defined $keyid) {
1171             $keyid = access_cfg('keyid','RETURN-UNDEF');
1172         }
1173         unlink $tfn->('.tmp.asc') or $!==&ENOENT or die $!;
1174         my @sign_cmd = (@gpg, qw(--detach-sign --armor));
1175         push @sign_cmd, qw(-u),$keyid if defined $keyid;
1176         push @sign_cmd, $tfn->('.tmp');
1177         runcmd_ordryrun @sign_cmd;
1178         if (act_scary()) {
1179             $tagobjfn = $tfn->('.signed.tmp');
1180             runcmd shell_cmd "exec >$tagobjfn", qw(cat --),
1181                 $tfn->('.tmp'), $tfn->('.tmp.asc');
1182         }
1183     }
1184
1185     return ($tagobjfn);
1186 }
1187
1188 sub sign_changes ($) {
1189     my ($changesfile) = @_;
1190     if ($sign) {
1191         my @debsign_cmd = @debsign;
1192         push @debsign_cmd, "-k$keyid" if defined $keyid;
1193         push @debsign_cmd, "-p$gpg[0]" if $gpg[0] ne 'gpg';
1194         push @debsign_cmd, $changesfile;
1195         runcmd_ordryrun @debsign_cmd;
1196     }
1197 }
1198
1199 sub dopush () {
1200     printdebug "actually entering push\n";
1201     prep_ud();
1202
1203     my $clogpfn = ".git/dgit/changelog.822.tmp";
1204     runcmd shell_cmd "exec >$clogpfn", qw(dpkg-parsechangelog);
1205
1206     responder_send_file('parsed-changelog', $clogpfn);
1207
1208     my ($clogp, $cversion, $tag, $dscfn) =
1209         push_parse_changelog("$clogpfn");
1210
1211     stat "../$dscfn" or
1212         fail "looked for .dsc $dscfn, but $!;".
1213             " maybe you forgot to build";
1214
1215     responder_send_file('dsc', "../$dscfn");
1216
1217     push_parse_dsc("../$dscfn", $dscfn, $cversion);
1218
1219     my $format = getfield $dsc, 'Format';
1220     printdebug "format $format\n";
1221     if (madformat($format)) {
1222         commit_quilty_patch();
1223     }
1224     check_not_dirty();
1225     changedir $ud;
1226     progress "checking that $dscfn corresponds to HEAD";
1227     runcmd qw(dpkg-source -x --), "../../../../$dscfn";
1228     my ($tree,$dir) = mktree_in_ud_from_only_subdir();
1229     changedir '../../../..';
1230     my @diffcmd = (@git, qw(diff --exit-code), $tree);
1231     printcmd \*DEBUG,$debugprefix."+",@diffcmd;
1232     $!=0; $?=0;
1233     if (system @diffcmd) {
1234         if ($! && $?==256) {
1235             fail "$dscfn specifies a different tree to your HEAD commit;".
1236                 " perhaps you forgot to build";
1237         } else {
1238             failedcmd @diffcmd;
1239         }
1240     }
1241 #fetch from alioth
1242 #do fast forward check and maybe fake merge
1243 #    if (!is_fast_fwd(mainbranch
1244 #    runcmd @git, qw(fetch -p ), "$alioth_git/$package.git",
1245 #        map { lref($_).":".rref($_) }
1246 #        (uploadbranch());
1247     my $head = rev_parse('HEAD');
1248     if (!$changesfile) {
1249         my $multi = "../${package}_".(stripepoch $cversion)."_multi.changes";
1250         if (stat "$multi") {
1251             $changesfile = $multi;
1252         } else {
1253             $!==&ENOENT or die "$multi: $!";
1254             my $pat = "${package}_".(stripepoch $cversion)."_*.changes";
1255             my @cs = glob "../$pat";
1256             fail "failed to find unique changes file".
1257                 " (looked for $pat in .., or $multi);".
1258                 " perhaps you need to use dgit -C"
1259                 unless @cs==1;
1260             ($changesfile) = @cs;
1261         }
1262     }
1263
1264     responder_send_file('changes',$changesfile);
1265     responder_send_command("param head $head");
1266
1267     my $tfn = sub { ".git/dgit/tag$_[0]"; };
1268     my ($tagobjfn) =
1269         $we_are_responder
1270         ? responder_receive_files('signed-tag', $tfn->('.signed.tmp'))
1271         : push_mktag($head,$clogp,$tag,
1272                      $dsc,"../$dscfn",
1273                      $changesfile,$changesfile,
1274                                  $tfn);
1275
1276     my $tag_obj_hash = cmdoutput @git, qw(hash-object -w -t tag), $tagobjfn;
1277     runcmd_ordryrun @git, qw(verify-tag), $tag_obj_hash;
1278     runcmd_ordryrun_local @git, qw(update-ref), "refs/tags/$tag", $tag_obj_hash;
1279     runcmd_ordryrun @git, qw(tag -v --), $tag;
1280
1281     if (!check_for_git()) {
1282         create_remote_git_repo();
1283     }
1284     runcmd_ordryrun @git, qw(push),access_giturl(),"HEAD:".rrref();
1285     runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), 'HEAD';
1286
1287     if (!$we_are_responder) {
1288         if (act_local()) {
1289             rename "../$dscfn.tmp","../$dscfn" or die "$dscfn $!";
1290         } else {
1291             progress "[new .dsc left in $dscfn.tmp]";
1292         }
1293     }
1294
1295     if ($we_are_responder) {
1296         my $dryrunsuffix = act_local() ? "" : ".tmp";
1297         responder_receive_files('signed-dsc-changes',
1298                                 "../$dscfn$dryrunsuffix",
1299                                 "$changesfile$dryrunsuffix");
1300     } else {
1301         sign_changes $changesfile;
1302     }
1303
1304     runcmd_ordryrun @git, qw(push),access_giturl(),"refs/tags/$tag";
1305     my $host = access_cfg('upload-host','RETURN-UNDEF');
1306     my @hostarg = defined($host) ? ($host,) : ();
1307     runcmd_ordryrun @dput, @hostarg, $changesfile;
1308     printdone "pushed and uploaded $cversion";
1309
1310     responder_send_command("complete");
1311 }
1312
1313 sub cmd_clone {
1314     parseopts();
1315     my $dstdir;
1316     badusage "-p is not allowed with clone; specify as argument instead"
1317         if defined $package;
1318     if (@ARGV==1) {
1319         ($package) = @ARGV;
1320     } elsif (@ARGV==2 && $ARGV[1] =~ m#^\w#) {
1321         ($package,$isuite) = @ARGV;
1322     } elsif (@ARGV==2 && $ARGV[1] =~ m#^[./]#) {
1323         ($package,$dstdir) = @ARGV;
1324     } elsif (@ARGV==3) {
1325         ($package,$isuite,$dstdir) = @ARGV;
1326     } else {
1327         badusage "incorrect arguments to dgit clone";
1328     }
1329     $dstdir ||= "$package";
1330     clone($dstdir);
1331 }
1332
1333 sub branchsuite () {
1334     my $branch = cmdoutput_errok @git, qw(symbolic-ref HEAD);
1335     if ($branch =~ m#$lbranch_re#o) {
1336         return $1;
1337     } else {
1338         return undef;
1339     }
1340 }
1341
1342 sub fetchpullargs () {
1343     if (!defined $package) {
1344         my $sourcep = parsecontrol('debian/control','debian/control');
1345         $package = getfield $sourcep, 'Source';
1346     }
1347     if (@ARGV==0) {
1348 #       $isuite = branchsuite();  # this doesn't work because dak hates canons
1349         if (!$isuite) {
1350             my $clogp = parsechangelog();
1351             $isuite = getfield $clogp, 'Distribution';
1352         }
1353         canonicalise_suite();
1354         progress "fetching from suite $csuite";
1355     } elsif (@ARGV==1) {
1356         ($isuite) = @ARGV;
1357         canonicalise_suite();
1358     } else {
1359         badusage "incorrect arguments to dgit fetch or dgit pull";
1360     }
1361 }
1362
1363 sub cmd_fetch {
1364     parseopts();
1365     fetchpullargs();
1366     fetch();
1367 }
1368
1369 sub cmd_pull {
1370     parseopts();
1371     fetchpullargs();
1372     pull();
1373 }
1374
1375 sub cmd_push {
1376     parseopts();
1377     badusage "-p is not allowed with dgit push" if defined $package;
1378     check_not_dirty();
1379     my $clogp = parsechangelog();
1380     $package = getfield $clogp, 'Source';
1381     my $specsuite;
1382     if (@ARGV==0) {
1383     } elsif (@ARGV==1) {
1384         ($specsuite) = (@ARGV);
1385     } else {
1386         badusage "incorrect arguments to dgit push";
1387     }
1388     $isuite = getfield $clogp, 'Distribution';
1389     if ($new_package) {
1390         local ($package) = $existing_package; # this is a hack
1391         canonicalise_suite();
1392     }
1393     if (defined $specsuite && $specsuite ne $isuite) {
1394         canonicalise_suite();
1395         $csuite eq $specsuite or
1396             fail "dgit push: changelog specifies $isuite ($csuite)".
1397                 " but command line specifies $specsuite";
1398     }
1399     if (check_for_git()) {
1400         git_fetch_us();
1401     }
1402     if (fetch_from_archive()) {
1403         is_fast_fwd(lrref(), 'HEAD') or
1404             fail "dgit push: HEAD is not a descendant".
1405                 " of the archive's version.\n".
1406                 "$us: To overwrite it, use git-merge -s ours ".lrref().".";
1407     } else {
1408         $new_package or
1409             fail "package appears to be new in this suite;".
1410                 " if this is intentional, use --new";
1411     }
1412     dopush();
1413 }
1414
1415 #---------- remote commands' implementation ----------
1416
1417 sub cmd_remote_push_responder {
1418     my ($nrargs) = shift @ARGV;
1419     my (@rargs) = @ARGV[0..$nrargs-1];
1420     @ARGV = @ARGV[$nrargs..$#ARGV];
1421     die unless @rargs;
1422     my ($dir) = @rargs;
1423     $debugprefix = ' ';
1424     $we_are_responder = 1;
1425
1426     open PI, "<&STDIN" or die $!;
1427     open STDIN, "/dev/null" or die $!;
1428     open PO, ">&STDOUT" or die $!;
1429     autoflush PO 1;
1430     open STDOUT, ">&STDERR" or die $!;
1431     autoflush STDOUT 1;
1432
1433     responder_send_command("dgit-remote-push-ready");
1434
1435     changedir $dir;
1436     &cmd_push;
1437 }
1438
1439 our $i_tmp;
1440 our $i_child_pid;
1441
1442 sub i_cleanup {
1443     local ($@);
1444     if ($i_child_pid) {
1445         printdebug "(killing remote child $i_child_pid)\n";
1446         kill 15, $i_child_pid;
1447     }
1448     if (defined $i_tmp && !defined $initiator_tempdir) {
1449         changedir "/";
1450         eval { rmtree $i_tmp; };
1451     }
1452 }
1453
1454 END { i_cleanup(); }
1455
1456 sub i_method {
1457     my ($base,$selector,@args) = @_;
1458     $selector =~ s/\-/_/g;
1459     { no strict qw(refs); &{"${base}_${selector}"}(@args); }
1460 }
1461
1462 sub cmd_rpush {
1463     my $host = nextarg;
1464     my $dir;
1465     if ($host =~ m/^((?:[^][]|\[[^][]*\])*)\:/) {
1466         $host = $1;
1467         $dir = $'; #';
1468     } else {
1469         $dir = nextarg;
1470     }
1471     $dir =~ s{^-}{./-};
1472     my @rargs = ($dir);
1473     my @rdgit;
1474     push @rdgit, @dgit;
1475     push @rdgit, @ropts;
1476     push @rdgit, qw(remote-push-responder), (scalar @rargs), @rargs;
1477     push @rdgit, @ARGV;
1478     my @cmd = (@ssh, $host, shellquote @rdgit);
1479     printcmd \*DEBUG,$debugprefix."+",@cmd;
1480
1481     if (defined $initiator_tempdir) {
1482         rmtree $initiator_tempdir;
1483         mkdir $initiator_tempdir, 0700 or die "$initiator_tempdir: $!";
1484         $i_tmp = $initiator_tempdir;
1485     } else {
1486         $i_tmp = tempdir();
1487     }
1488     $i_child_pid = open2(\*RO, \*RI, @cmd);
1489     changedir $i_tmp;
1490     initiator_expect { m/^dgit-remote-push-ready/ };
1491     for (;;) {
1492         my ($icmd,$iargs) = initiator_expect {
1493             m/^(\S+)(?: (.*))?$/;
1494             ($1,$2);
1495         };
1496         i_method "i_resp", $icmd, $iargs;
1497     }
1498
1499     my $pid = $i_child_pid;
1500     $i_child_pid = undef; # prevents killing some other process with same pid
1501     printdebug "waiting for remote child $pid...";
1502     my $got = waitpid $pid, 0;
1503     die $! unless $got == $pid;
1504     die "remote child failed $?" if $?;
1505
1506     i_cleanup();
1507     exit 0;
1508 }
1509
1510 sub i_resp_progress ($) {
1511     my ($rhs) = @_;
1512     my $msg = protocol_read_bytes \*RO, $rhs;
1513     progress $msg;
1514 }
1515
1516 sub i_resp_complete {
1517     i_cleanup();
1518     exit 0;
1519 }
1520
1521 sub i_resp_file ($) {
1522     my ($keyword) = @_;
1523     my $localname = i_method "i_localname", $keyword;
1524     my $localpath = "$i_tmp/$localname";
1525     stat $localpath and badproto \*RO, "file $keyword ($localpath) twice";
1526     protocol_receive_file \*RO, $localpath;
1527 }
1528
1529 our %i_param;
1530
1531 sub i_resp_param ($) {
1532     $_[0] =~ m/^(\S+) (.*)$/ or badproto \*RO, "bad param spec";
1533     $i_param{$1} = $2;
1534 }
1535
1536 our %i_wanted;
1537
1538 sub i_resp_want ($) {
1539     my ($keyword) = @_;
1540     die "$keyword ?" if $i_wanted{$keyword}++;
1541     my @localpaths = i_method "i_want", $keyword;
1542     printdebug "]]  $keyword @localpaths\n";
1543     foreach my $localpath (@localpaths) {
1544         protocol_send_file \*RI, $localpath;
1545     }
1546     print RI "end-files\n" or die $!;
1547 }
1548
1549 our ($i_clogp, $i_version, $i_tag, $i_dscfn);
1550
1551 sub i_localname_parsed_changelog { return "remote-changelog.822"; }
1552 sub i_localname_changes { return "remote.changes"; }
1553 sub i_localname_dsc {
1554     ($i_clogp, $i_version, $i_tag, $i_dscfn) =
1555         push_parse_changelog "$i_tmp/remote-changelog.822";
1556     die if $i_dscfn =~ m#/|^\W#;
1557     return $i_dscfn;
1558 }
1559
1560 sub i_want_signed_tag {
1561     printdebug Dumper(\%i_param, $i_dscfn);
1562     defined $i_param{'head'} && defined $i_dscfn
1563         or badproto \*RO, "sequencing error";
1564     my $head = $i_param{'head'};
1565     die if $head =~ m/[^0-9a-f]/ || $head !~ m/^../;
1566
1567     push_parse_dsc $i_dscfn, 'remote dsc', 
1568
1569     push_mktag $head, $i_clogp, $i_tag,
1570         $dsc, $i_dscfn,
1571         'remote.changes', 'remote changes',
1572         'tag.tag';
1573
1574     return 'tag.tag';
1575 }
1576
1577 sub i_want_signed_dsc_changes {
1578     rename "$i_dscfn.tmp","$i_dscfn" or die "$i_dscfn $!";
1579     sign_changes 'remote.changes';
1580     return ($i_dscfn, 'remote.changes');
1581 }
1582
1583 #---------- building etc. ----------
1584
1585 our $version;
1586 our $sourcechanges;
1587 our $dscfn;
1588
1589 our $fakeeditorenv = 'DGIT_FAKE_EDITOR_QUILT';
1590
1591 sub build_maybe_quilt_fixup () {
1592     if (!open F, "debian/source/format") {
1593         die $! unless $!==&ENOENT;
1594         return;
1595     }
1596     $_ = <F>;
1597     F->error and die $!;
1598     chomp;
1599     return unless madformat($_);
1600     # sigh
1601     my $clogp = parsechangelog();
1602     my $version = getfield $clogp, 'Version';
1603     my $author = getfield $clogp, 'Maintainer';
1604     my $headref = rev_parse('HEAD');
1605     my $time = time;
1606     my $ncommits = 3;
1607     my $patchname = "auto-$version-$headref-$time";
1608     my $msg = cmdoutput @git, qw(log), "-n$ncommits";
1609     mkpath '.git/dgit';
1610     my $descfn = ".git/dgit/quilt-description.tmp";
1611     open O, '>', $descfn or die "$descfn: $!";
1612     $msg =~ s/\n/\n /g;
1613     $msg =~ s/^\s+$/ ./mg;
1614     print O <<END or die $!;
1615 Description: Automatically generated patch ($clogp->{Version})
1616  Last (up to) $ncommits git changes, FYI:
1617  .
1618  $msg
1619 Author: $author
1620
1621 ---
1622
1623 END
1624     close O or die $!;
1625     {
1626         local $ENV{'EDITOR'} = cmdoutput qw(realpath --), $0;
1627         local $ENV{'VISUAL'} = $ENV{'EDITOR'};
1628         local $ENV{$fakeeditorenv} = cmdoutput qw(realpath --), $descfn;
1629         runcmd_ordryrun_local @dpkgsource, qw(--commit .), $patchname;
1630     }
1631
1632     if (!open P, '>>', ".pc/applied-patches") {
1633         $!==&ENOENT or die $!;
1634     } else {
1635         close P;
1636     }
1637
1638     commit_quilty_patch();
1639 }
1640
1641 sub quilt_fixup_editor () {
1642     my $descfn = $ENV{$fakeeditorenv};
1643     my $editing = $ARGV[$#ARGV];
1644     open I1, '<', $descfn or die "$descfn: $!";
1645     open I2, '<', $editing or die "$editing: $!";
1646     unlink $editing or die "$editing: $!";
1647     open O, '>', $editing or die "$editing: $!";
1648     while (<I1>) { print O or die $!; } I1->error and die $!;
1649     my $copying = 0;
1650     while (<I2>) {
1651         $copying ||= m/^\-\-\- /;
1652         next unless $copying;
1653         print O or die $!;
1654     }
1655     I2->error and die $!;
1656     close O or die $1;
1657     exit 0;
1658 }
1659
1660 sub build_prep () {
1661     badusage "-p is not allowed when building" if defined $package;
1662     check_not_dirty();
1663     my $clogp = parsechangelog();
1664     $isuite = getfield $clogp, 'Distribution';
1665     $package = getfield $clogp, 'Source';
1666     $version = getfield $clogp, 'Version';
1667     build_maybe_quilt_fixup();
1668 }
1669
1670 sub cmd_build {
1671     badusage "dgit build implies --clean=dpkg-source"
1672         if $cleanmode ne 'dpkg-source';
1673     build_prep();
1674     runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc), changesopts(), @ARGV;
1675     printdone "build successful\n";
1676 }
1677
1678 sub cmd_git_build {
1679     badusage "dgit git-build implies --clean=dpkg-source"
1680         if $cleanmode ne 'dpkg-source';
1681     build_prep();
1682     my @cmd =
1683         (qw(git-buildpackage -us -uc --git-no-sign-tags),
1684          "--git-builder=@dpkgbuildpackage");
1685     unless (grep { m/^--git-debian-branch|^--git-ignore-branch/ } @ARGV) {
1686         canonicalise_suite();
1687         push @cmd, "--git-debian-branch=".lbranch();
1688     }
1689     push @cmd, changesopts();
1690     runcmd_ordryrun_local @cmd, @ARGV;
1691     printdone "build successful\n";
1692 }
1693
1694 sub build_source {
1695     build_prep();
1696     $sourcechanges = "${package}_".(stripepoch $version)."_source.changes";
1697     $dscfn = dscfn($version);
1698     if ($cleanmode eq 'dpkg-source') {
1699         runcmd_ordryrun_local (@dpkgbuildpackage, qw(-us -uc -S)),
1700             changesopts();
1701     } else {
1702         if ($cleanmode eq 'git') {
1703             runcmd_ordryrun_local @git, qw(clean -xdf);
1704         } elsif ($cleanmode eq 'none') {
1705         } else {
1706             die "$cleanmode ?";
1707         }
1708         my $pwd = cmdoutput qw(env - pwd);
1709         my $leafdir = basename $pwd;
1710         changedir "..";
1711         runcmd_ordryrun_local @dpkgsource, qw(-b --), $leafdir;
1712         changedir $pwd;
1713         runcmd_ordryrun_local qw(sh -ec),
1714             'exec >$1; shift; exec "$@"','x',
1715             "../$sourcechanges",
1716             @dpkggenchanges, qw(-S), changesopts();
1717     }
1718 }
1719
1720 sub cmd_build_source {
1721     badusage "build-source takes no additional arguments" if @ARGV;
1722     build_source();
1723     printdone "source built, results in $dscfn and $sourcechanges";
1724 }
1725
1726 sub cmd_sbuild {
1727     build_source();
1728     changedir "..";
1729     my $pat = "${package}_".(stripepoch $version)."_*.changes";
1730     if (act_local()) {
1731         stat $dscfn or fail "$dscfn (in parent directory): $!";
1732         stat $sourcechanges or fail "$sourcechanges (in parent directory): $!";
1733         foreach my $cf (glob $pat) {
1734             next if $cf eq $sourcechanges;
1735             unlink $cf or fail "remove $cf: $!";
1736         }
1737     }
1738     runcmd_ordryrun_local @sbuild, @ARGV, qw(-d), $isuite, $dscfn;
1739     runcmd_ordryrun_local @mergechanges, glob $pat;
1740     my $multichanges = "${package}_".(stripepoch $version)."_multi.changes";
1741     if (act_local()) {
1742         stat $multichanges or fail "$multichanges: $!";
1743     }
1744     printdone "build successful, results in $multichanges\n" or die $!;
1745 }    
1746
1747 sub cmd_quilt_fixup {
1748     badusage "incorrect arguments to dgit quilt-fixup" if @ARGV;
1749     my $clogp = parsechangelog();
1750     $version = getfield $clogp, 'Version';
1751     build_maybe_quilt_fixup();
1752 }
1753
1754 #---------- argument parsing and main program ----------
1755
1756 sub cmd_version {
1757     print "dgit version $our_version\n" or die $!;
1758     exit 0;
1759 }
1760
1761 sub parseopts () {
1762     my $om;
1763
1764     if (defined $ENV{'DGIT_SSH'}) {
1765         @ssh = string_to_ssh $ENV{'DGIT_SSH'};
1766     } elsif (defined $ENV{'GIT_SSH'}) {
1767         @ssh = ($ENV{'GIT_SSH'});
1768     }
1769
1770     while (@ARGV) {
1771         last unless $ARGV[0] =~ m/^-/;
1772         $_ = shift @ARGV;
1773         last if m/^--?$/;
1774         if (m/^--/) {
1775             if (m/^--dry-run$/) {
1776                 push @ropts, $_;
1777                 $dryrun_level=2;
1778             } elsif (m/^--damp-run$/) {
1779                 push @ropts, $_;
1780                 $dryrun_level=1;
1781             } elsif (m/^--no-sign$/) {
1782                 push @ropts, $_;
1783                 $sign=0;
1784             } elsif (m/^--help$/) {
1785                 cmd_help();
1786             } elsif (m/^--version$/) {
1787                 cmd_version();
1788             } elsif (m/^--new$/) {
1789                 push @ropts, $_;
1790                 $new_package=1;
1791             } elsif (m/^--(\w+)=(.*)/s &&
1792                      ($om = $opts_opt_map{$1}) &&
1793                      length $om->[0]) {
1794                 push @ropts, $_;
1795                 $om->[0] = $2;
1796             } elsif (m/^--(\w+):(.*)/s &&
1797                      !$opts_opt_cmdonly{$1} &&
1798                      ($om = $opts_opt_map{$1})) {
1799                 push @ropts, $_;
1800                 push @$om, $2;
1801             } elsif (m/^--existing-package=(.*)/s) {
1802                 push @ropts, $_;
1803                 $existing_package = $1;
1804             } elsif (m/^--initiator-tempdir=(.*)/s) {
1805                 $initiator_tempdir = $1;
1806                 $initiator_tempdir =~ m#^/# or
1807                     badusage "--initiator-tempdir must be used specify an".
1808                         " absolute, not relative, directory."
1809             } elsif (m/^--distro=(.*)/s) {
1810                 push @ropts, $_;
1811                 $idistro = $1;
1812             } elsif (m/^--clean=(dpkg-source|git|none)$/s) {
1813                 push @ropts, $_;
1814                 $cleanmode = $1;
1815             } elsif (m/^--clean=(.*)$/s) {
1816                 badusage "unknown cleaning mode \`$1'";
1817             } elsif (m/^--ignore-dirty$/s) {
1818                 push @ropts, $_;
1819                 $ignoredirty = 1;
1820             } elsif (m/^--no-quilt-fixup$/s) {
1821                 push @ropts, $_;
1822                 $noquilt = 1;
1823             } else {
1824                 badusage "unknown long option \`$_'";
1825             }
1826         } else {
1827             while (m/^-./s) {
1828                 if (s/^-n/-/) {
1829                     push @ropts, $&;
1830                     $dryrun_level=2;
1831                 } elsif (s/^-L/-/) {
1832                     push @ropts, $&;
1833                     $dryrun_level=1;
1834                 } elsif (s/^-h/-/) {
1835                     cmd_help();
1836                 } elsif (s/^-D/-/) {
1837                     push @ropts, $&;
1838                     open DEBUG, ">&STDERR" or die $!;
1839                     autoflush DEBUG 1;
1840                     $debug++;
1841                 } elsif (s/^-N/-/) {
1842                     push @ropts, $&;
1843                     $new_package=1;
1844                 } elsif (m/^-[vm]/) {
1845                     push @ropts, $&;
1846                     push @changesopts, $_;
1847                     $_ = '';
1848                 } elsif (s/^-c(.*=.*)//s) {
1849                     push @ropts, $&;
1850                     push @git, '-c', $1;
1851                 } elsif (s/^-d(.*)//s) {
1852                     push @ropts, $&;
1853                     $idistro = $1;
1854                 } elsif (s/^-C(.*)//s) {
1855                     push @ropts, $&;
1856                     $changesfile = $1;
1857                 } elsif (s/^-k(.*)//s) {
1858                     $keyid=$1;
1859                 } elsif (s/^-wn//s) {
1860                     push @ropts, $&;
1861                     $cleanmode = 'none';
1862                 } elsif (s/^-wg//s) {
1863                     push @ropts, $&;
1864                     $cleanmode = 'git';
1865                 } elsif (s/^-wd//s) {
1866                     push @ropts, $&;
1867                     $cleanmode = 'dpkg-source';
1868                 } else {
1869                     badusage "unknown short option \`$_'";
1870                 }
1871             }
1872         }
1873     }
1874 }
1875
1876 if ($ENV{$fakeeditorenv}) {
1877     quilt_fixup_editor();
1878 }
1879
1880 delete $ENV{'DGET_UNPACK'};
1881
1882 parseopts();
1883 print STDERR "DRY RUN ONLY\n" if $dryrun_level > 1;
1884 print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n"
1885     if $dryrun_level == 1;
1886 if (!@ARGV) {
1887     print STDERR $helpmsg or die $!;
1888     exit 8;
1889 }
1890 my $cmd = shift @ARGV;
1891 $cmd =~ y/-/_/;
1892 { no strict qw(refs); &{"cmd_$cmd"}(); }