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