chiark / gitweb /
Tests: break out some things into lib-core (nfc)
[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 $SIG{__WARN__} = sub { die $_[0]; };
22
23 use IO::Handle;
24 use Data::Dumper;
25 use LWP::UserAgent;
26 use Dpkg::Control::Hash;
27 use File::Path;
28 use File::Temp qw(tempdir);
29 use File::Basename;
30 use Dpkg::Version;
31 use POSIX;
32 use IPC::Open2;
33 use Digest::SHA;
34 use Digest::MD5;
35 use Config;
36
37 use Debian::Dgit;
38
39 our $our_version = 'UNRELEASED'; ###substituted###
40
41 our $rpushprotovsn = 2;
42
43 our $isuite = 'unstable';
44 our $idistro;
45 our $package;
46 our @ropts;
47
48 our $sign = 1;
49 our $dryrun_level = 0;
50 our $changesfile;
51 our $buildproductsdir = '..';
52 our $new_package = 0;
53 our $ignoredirty = 0;
54 our $rmonerror = 1;
55 our @deliberatelies;
56 our %supersedes;
57 our $existing_package = 'dpkg';
58 our $cleanmode = 'dpkg-source';
59 our $changes_since_version;
60 our $quilt_mode;
61 our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck';
62 our $we_are_responder;
63 our $initiator_tempdir;
64
65 our %format_ok = map { $_=>1 } ("1.0","3.0 (native)","3.0 (quilt)");
66
67 our $suite_re = '[-+.0-9a-z]+';
68
69 our (@git) = qw(git);
70 our (@dget) = qw(dget);
71 our (@curl) = qw(curl -f);
72 our (@dput) = qw(dput);
73 our (@debsign) = qw(debsign);
74 our (@gpg) = qw(gpg);
75 our (@sbuild) = qw(sbuild -A);
76 our (@ssh) = 'ssh';
77 our (@dgit) = qw(dgit);
78 our (@dpkgbuildpackage) = qw(dpkg-buildpackage -i\.git/ -I.git);
79 our (@dpkgsource) = qw(dpkg-source -i\.git/ -I.git);
80 our (@dpkggenchanges) = qw(dpkg-genchanges);
81 our (@mergechanges) = qw(mergechanges -f);
82 our (@changesopts) = ('');
83
84 our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
85                      'curl' => \@curl,
86                      'dput' => \@dput,
87                      'debsign' => \@debsign,
88                      'gpg' => \@gpg,
89                      'sbuild' => \@sbuild,
90                      'ssh' => \@ssh,
91                      'dgit' => \@dgit,
92                      'dpkg-source' => \@dpkgsource,
93                      'dpkg-buildpackage' => \@dpkgbuildpackage,
94                      'dpkg-genchanges' => \@dpkggenchanges,
95                      'ch' => \@changesopts,
96                      'mergechanges' => \@mergechanges);
97
98 our %opts_opt_cmdonly = ('gpg' => 1);
99
100 our $keyid;
101
102 autoflush STDOUT 1;
103
104 our $remotename = 'dgit';
105 our @ourdscfield = qw(Dgit Vcs-Dgit-Master);
106 our $csuite;
107 our $instead_distro;
108
109 sub lbranch () { return "$branchprefix/$csuite"; }
110 my $lbranch_re = '^refs/heads/'.$branchprefix.'/([^/.]+)$';
111 sub lref () { return "refs/heads/".lbranch(); }
112 sub lrref () { return "refs/remotes/$remotename/".server_branch($csuite); }
113 sub rrref () { return server_ref($csuite); }
114
115 sub stripepoch ($) {
116     my ($vsn) = @_;
117     $vsn =~ s/^\d+\://;
118     return $vsn;
119 }
120
121 sub srcfn ($$) {
122     my ($vsn,$sfx) = @_;
123     return "${package}_".(stripepoch $vsn).$sfx
124 }
125
126 sub dscfn ($) {
127     my ($vsn) = @_;
128     return srcfn($vsn,".dsc");
129 }
130
131 our $us = 'dgit';
132 initdebug('');
133
134 our @end;
135 END { 
136     local ($?);
137     foreach my $f (@end) {
138         eval { $f->(); };
139         warn "$us: cleanup: $@" if length $@;
140     }
141 };
142
143 our @signames = split / /, $Config{sig_name};
144
145 sub waitstatusmsg () {
146     if (!$?) {
147         return "terminated, reporting successful completion";
148     } elsif (!($? & 255)) {
149         return "failed with error exit status ".WEXITSTATUS($?);
150     } elsif (WIFSIGNALED($?)) {
151         my $signum=WTERMSIG($?);
152         return "died due to fatal signal ".
153             ($signames[$signum] // "number $signum").
154             ($? & 128 ? " (core dumped)" : ""); # POSIX(3pm) has no WCOREDUMP
155     } else {
156         return "failed with unknown wait status ".$?;
157     }
158 }
159
160 sub fail { 
161     my $s = "@_\n";
162     my $prefix = $us.($we_are_responder ? " (build host)" : "").": ";
163     $s =~ s/^/$prefix/gm;
164     die $s;
165 }
166
167 sub badcfg { print STDERR "$us: invalid configuration: @_\n"; exit 12; }
168
169 sub no_such_package () {
170     print STDERR "$us: package $package does not exist in suite $isuite\n";
171     exit 4;
172 }
173
174 sub fetchspec () {
175     local $csuite = '*';
176     return  "+".rrref().":".lrref();
177 }
178
179 sub changedir ($) {
180     my ($newdir) = @_;
181     printdebug "CD $newdir\n";
182     chdir $newdir or die "chdir: $newdir: $!";
183 }
184
185 sub deliberately ($) {
186     return !!grep { $_[0] eq $_ } @deliberatelies;
187 }
188
189 #---------- remote protocol support, common ----------
190
191 # remote push initiator/responder protocol:
192 #  < dgit-remote-push-ready [optional extra info ignored by old initiators]
193 #
194 #  > file parsed-changelog
195 #  [indicates that output of dpkg-parsechangelog follows]
196 #  > data-block NBYTES
197 #  > [NBYTES bytes of data (no newline)]
198 #  [maybe some more blocks]
199 #  > data-end
200 #
201 #  > file dsc
202 #  [etc]
203 #
204 #  > file changes
205 #  [etc]
206 #
207 #  > param head HEAD
208 #
209 #  > want signed-tag
210 #  [indicates that signed tag is wanted]
211 #  < data-block NBYTES
212 #  < [NBYTES bytes of data (no newline)]
213 #  [maybe some more blocks]
214 #  < data-end
215 #  < files-end
216 #
217 #  > want signed-dsc-changes
218 #  < data-block NBYTES    [transfer of signed dsc]
219 #  [etc]
220 #  < data-block NBYTES    [transfer of signed changes]
221 #  [etc]
222 #  < files-end
223 #
224 #  > complete
225
226 our $i_child_pid;
227
228 sub i_child_report () {
229     # Sees if our child has died, and reap it if so.  Returns a string
230     # describing how it died if it failed, or undef otherwise.
231     return undef unless $i_child_pid;
232     my $got = waitpid $i_child_pid, WNOHANG;
233     return undef if $got <= 0;
234     die unless $got == $i_child_pid;
235     $i_child_pid = undef;
236     return undef unless $?;
237     return "build host child ".waitstatusmsg();
238 }
239
240 sub badproto ($$) {
241     my ($fh, $m) = @_;
242     fail "connection lost: $!" if $fh->error;
243     fail "protocol violation; $m not expected";
244 }
245
246 sub badproto_badread ($$) {
247     my ($fh, $wh) = @_;
248     fail "connection lost: $!" if $!;
249     my $report = i_child_report();
250     fail $report if defined $report;
251     badproto $fh, "eof (reading $wh)";
252 }
253
254 sub protocol_expect (&$) {
255     my ($match, $fh) = @_;
256     local $_;
257     $_ = <$fh>;
258     defined && chomp or badproto_badread $fh, "protocol message";
259     if (wantarray) {
260         my @r = &$match;
261         return @r if @r;
262     } else {
263         my $r = &$match;
264         return $r if $r;
265     }
266     badproto $fh, "\`$_'";
267 }
268
269 sub protocol_send_file ($$) {
270     my ($fh, $ourfn) = @_;
271     open PF, "<", $ourfn or die "$ourfn: $!";
272     for (;;) {
273         my $d;
274         my $got = read PF, $d, 65536;
275         die "$ourfn: $!" unless defined $got;
276         last if !$got;
277         print $fh "data-block ".length($d)."\n" or die $!;
278         print $fh $d or die $!;
279     }
280     PF->error and die "$ourfn $!";
281     print $fh "data-end\n" or die $!;
282     close PF;
283 }
284
285 sub protocol_read_bytes ($$) {
286     my ($fh, $nbytes) = @_;
287     $nbytes =~ m/^[1-9]\d{0,5}$/ or badproto \*RO, "bad byte count";
288     my $d;
289     my $got = read $fh, $d, $nbytes;
290     $got==$nbytes or badproto_badread $fh, "data block";
291     return $d;
292 }
293
294 sub protocol_receive_file ($$) {
295     my ($fh, $ourfn) = @_;
296     printdebug "() $ourfn\n";
297     open PF, ">", $ourfn or die "$ourfn: $!";
298     for (;;) {
299         my ($y,$l) = protocol_expect {
300             m/^data-block (.*)$/ ? (1,$1) :
301             m/^data-end$/ ? (0,) :
302             ();
303         } $fh;
304         last unless $y;
305         my $d = protocol_read_bytes $fh, $l;
306         print PF $d or die $!;
307     }
308     close PF or die $!;
309 }
310
311 #---------- remote protocol support, responder ----------
312
313 sub responder_send_command ($) {
314     my ($command) = @_;
315     return unless $we_are_responder;
316     # called even without $we_are_responder
317     printdebug ">> $command\n";
318     print PO $command, "\n" or die $!;
319 }    
320
321 sub responder_send_file ($$) {
322     my ($keyword, $ourfn) = @_;
323     return unless $we_are_responder;
324     printdebug "]] $keyword $ourfn\n";
325     responder_send_command "file $keyword";
326     protocol_send_file \*PO, $ourfn;
327 }
328
329 sub responder_receive_files ($@) {
330     my ($keyword, @ourfns) = @_;
331     die unless $we_are_responder;
332     printdebug "[[ $keyword @ourfns\n";
333     responder_send_command "want $keyword";
334     foreach my $fn (@ourfns) {
335         protocol_receive_file \*PI, $fn;
336     }
337     printdebug "[[\$\n";
338     protocol_expect { m/^files-end$/ } \*PI;
339 }
340
341 #---------- remote protocol support, initiator ----------
342
343 sub initiator_expect (&) {
344     my ($match) = @_;
345     protocol_expect { &$match } \*RO;
346 }
347
348 #---------- end remote code ----------
349
350 sub progress {
351     if ($we_are_responder) {
352         my $m = join '', @_;
353         responder_send_command "progress ".length($m) or die $!;
354         print PO $m or die $!;
355     } else {
356         print @_, "\n";
357     }
358 }
359
360 our $ua;
361
362 sub url_get {
363     if (!$ua) {
364         $ua = LWP::UserAgent->new();
365         $ua->env_proxy;
366     }
367     my $what = $_[$#_];
368     progress "downloading $what...";
369     my $r = $ua->get(@_) or die $!;
370     return undef if $r->code == 404;
371     $r->is_success or fail "failed to fetch $what: ".$r->status_line;
372     return $r->decoded_content(charset => 'none');
373 }
374
375 our ($dscdata,$dscurl,$dsc,$dsc_checked,$skew_warning_vsn);
376
377 sub failedcmd {
378     { local ($!); printcmd \*STDERR, "$us: failed command:", @_ or die $!; };
379     if ($!) {
380         fail "failed to fork/exec: $!";
381     } elsif ($?) {
382         fail "subprocess ".waitstatusmsg();
383     } else {
384         fail "subprocess produced invalid output";
385     }
386 }
387
388 sub runcmd {
389     debugcmd "+",@_;
390     $!=0; $?=0;
391     failedcmd @_ if system @_;
392 }
393
394 sub act_local () { return $dryrun_level <= 1; }
395 sub act_scary () { return !$dryrun_level; }
396
397 sub printdone {
398     if (!$dryrun_level) {
399         progress "dgit ok: @_";
400     } else {
401         progress "would be ok: @_ (but dry run only)";
402     }
403 }
404
405 sub cmdoutput_errok {
406     die Dumper(\@_)." ?" if grep { !defined } @_;
407     debugcmd "|",@_;
408     open P, "-|", @_ or die $!;
409     my $d;
410     $!=0; $?=0;
411     { local $/ = undef; $d = <P>; }
412     die $! if P->error;
413     if (!close P) { printdebug "=>!$?\n"; return undef; }
414     chomp $d;
415     $d =~ m/^.*/;
416     printdebug "=> \`$&'",(length $' ? '...' : ''),"\n" if $debuglevel>0; #';
417     return $d;
418 }
419
420 sub cmdoutput {
421     my $d = cmdoutput_errok @_;
422     defined $d or failedcmd @_;
423     return $d;
424 }
425
426 sub dryrun_report {
427     printcmd(\*STDERR,$debugprefix."#",@_);
428 }
429
430 sub runcmd_ordryrun {
431     if (act_scary()) {
432         runcmd @_;
433     } else {
434         dryrun_report @_;
435     }
436 }
437
438 sub runcmd_ordryrun_local {
439     if (act_local()) {
440         runcmd @_;
441     } else {
442         dryrun_report @_;
443     }
444 }
445
446 sub shell_cmd {
447     my ($first_shell, @cmd) = @_;
448     return qw(sh -ec), $first_shell.'; exec "$@"', 'x', @cmd;
449 }
450
451 our $helpmsg = <<END;
452 main usages:
453   dgit [dgit-opts] clone [dgit-opts] package [suite] [./dir|/dir]
454   dgit [dgit-opts] fetch|pull [dgit-opts] [suite]
455   dgit [dgit-opts] build [git-buildpackage-opts|dpkg-buildpackage-opts]
456   dgit [dgit-opts] push [dgit-opts] [suite]
457   dgit [dgit-opts] rpush build-host:build-dir ...
458 important dgit options:
459   -k<keyid>           sign tag and package with <keyid> instead of default
460   --dry-run -n        do not change anything, but go through the motions
461   --damp-run -L       like --dry-run but make local changes, without signing
462   --new -N            allow introducing a new package
463   --debug -D          increase debug level
464   -c<name>=<value>    set git config option (used directly by dgit too)
465 END
466
467 our $later_warning_msg = <<END;
468 Perhaps the upload is stuck in incoming.  Using the version from git.
469 END
470
471 sub badusage {
472     print STDERR "$us: @_\n", $helpmsg or die $!;
473     exit 8;
474 }
475
476 sub nextarg {
477     @ARGV or badusage "too few arguments";
478     return scalar shift @ARGV;
479 }
480
481 sub cmd_help () {
482     print $helpmsg or die $!;
483     exit 0;
484 }
485
486 our $td = $ENV{DGIT_TEST_DUMMY_DIR} || "DGIT_TEST_DUMMY_DIR-unset";
487
488 our %defcfg = ('dgit.default.distro' => 'debian',
489                'dgit.default.username' => '',
490                'dgit.default.archive-query-default-component' => 'main',
491                'dgit.default.ssh' => 'ssh',
492                'dgit.default.archive-query' => 'madison:',
493                'dgit.default.sshpsql-dbname' => 'service=projectb',
494                'dgit-distro.debian.archive-query' => 'ftpmasterapi:',
495                'dgit-distro.debian.git-host' => 'dgit-git.debian.net',
496                'dgit-distro.debian.git-user-force' => 'dgit',
497                'dgit-distro.debian.git-proto' => 'git+ssh://',
498                'dgit-distro.debian.git-path' => '/dgit/debian/repos',
499                'dgit-distro.debian.git-check' => 'ssh-cmd',
500  'dgit-distro.debian.archive-query-url', 'https://api.ftp-master.debian.org/',
501  'dgit-distro.debian.archive-query-tls-key',
502     '/etc/ssl/certs/%HOST%.pem:/etc/dgit/%HOST%.pem',
503                'dgit-distro.debian.diverts.alioth' => '/alioth',
504                'dgit-distro.debian/alioth.git-host' => 'git.debian.org',
505                'dgit-distro.debian/alioth.git-user-force' => '',
506                'dgit-distro.debian/alioth.git-proto' => 'git+ssh://',
507                'dgit-distro.debian/alioth.git-path' => '/git/dgit-repos/repos',
508                'dgit-distro.debian/alioth.git-create' => 'ssh-cmd',
509                'dgit-distro.debian.upload-host' => 'ftp-master', # for dput
510                'dgit-distro.debian.mirror' => 'http://ftp.debian.org/debian/',
511  'dgit-distro.debian.backports-quirk' => '(squeeze)-backports*',
512  'dgit-distro.debian-backports.mirror' => 'http://backports.debian.org/debian-backports/',
513                'dgit-distro.ubuntu.git-check' => 'false',
514  'dgit-distro.ubuntu.mirror' => 'http://archive.ubuntu.com/ubuntu',
515                'dgit-distro.test-dummy.ssh' => "$td/ssh",
516                'dgit-distro.test-dummy.username' => "alice",
517                'dgit-distro.test-dummy.git-check' => "ssh-cmd",
518                'dgit-distro.test-dummy.git-create' => "ssh-cmd",
519                'dgit-distro.test-dummy.git-url' => "$td/git",
520                'dgit-distro.test-dummy.git-host' => "git",
521                'dgit-distro.test-dummy.git-path' => "$td/git",
522                'dgit-distro.test-dummy.archive-query' => "ftpmasterapi:",
523                'dgit-distro.test-dummy.archive-query-url' => "file://$td/aq/",
524                'dgit-distro.test-dummy.mirror' => "file://$td/mirror/",
525                'dgit-distro.test-dummy.upload-host' => 'test-dummy',
526                );
527
528 sub cfg {
529     foreach my $c (@_) {
530         return undef if $c =~ /RETURN-UNDEF/;
531         my @cmd = (@git, qw(config --), $c);
532         my $v;
533         {
534             local ($debuglevel) = $debuglevel-1;
535             $v = cmdoutput_errok @cmd;
536         };
537         if ($?==0) {
538             return $v;
539         } elsif ($?!=256) {
540             failedcmd @cmd;
541         }
542         my $dv = $defcfg{$c};
543         return $dv if defined $dv;
544     }
545     badcfg "need value for one of: @_\n".
546         "$us: distro or suite appears not to be (properly) supported";
547 }
548
549 sub access_basedistro () {
550     if (defined $idistro) {
551         return $idistro;
552     } else {    
553         return cfg("dgit-suite.$isuite.distro",
554                    "dgit.default.distro");
555     }
556 }
557
558 sub access_quirk () {
559     # returns (quirk name, distro to use instead or undef, quirk-specific info)
560     my $basedistro = access_basedistro();
561     my $backports_quirk = cfg("dgit-distro.$basedistro.backports-quirk",
562                               'RETURN-UNDEF');
563     if (defined $backports_quirk) {
564         my $re = $backports_quirk;
565         $re =~ s/[^-0-9a-z_\%*()]/\\$&/ig;
566         $re =~ s/\*/.*/g;
567         $re =~ s/\%/([-0-9a-z_]+)/
568             or $re =~ m/[()]/ or badcfg "backports-quirk needs \% or ( )";
569         if ($isuite =~ m/^$re$/) {
570             return ('backports',"$basedistro-backports",$1);
571         }
572     }
573     return ('none',undef);
574 }
575
576 sub access_distros () {
577     # Returns list of distros to try, in order
578     #
579     # We want to try:
580     #    0. `instead of' distro name(s) we have been pointed to
581     #    1. the access_quirk distro, if any
582     #    2a. the user's specified distro, or failing that  } basedistro
583     #    2b. the distro calculated from the suite          }
584     my @l = access_basedistro();
585
586     my (undef,$quirkdistro) = access_quirk();
587     unshift @l, $quirkdistro;
588     unshift @l, $instead_distro;
589     return grep { defined } @l;
590 }
591
592 sub access_cfg (@) {
593     my (@keys) = @_;
594     my @cfgs;
595     # The nesting of these loops determines the search order.  We put
596     # the key loop on the outside so that we search all the distros
597     # for each key, before going on to the next key.  That means that
598     # if access_cfg is called with a more specific, and then a less
599     # specific, key, an earlier distro can override the less specific
600     # without necessarily overriding any more specific keys.  (If the
601     # distro wants to override the more specific keys it can simply do
602     # so; whereas if we did the loop the other way around, it would be
603     # impossible to for an earlier distro to override a less specific
604     # key but not the more specific ones without restating the unknown
605     # values of the more specific keys.
606     my @realkeys;
607     my @rundef;
608     # We have to deal with RETURN-UNDEF specially, so that we don't
609     # terminate the search prematurely.
610     foreach (@keys) {
611         if (m/RETURN-UNDEF/) { push @rundef, $_; last; }
612         push @realkeys, $_
613     }
614     foreach my $d (access_distros()) {
615         push @cfgs, map { "dgit-distro.$d.$_" } @realkeys;
616     }
617     push @cfgs, map { "dgit.default.$_" } @realkeys;
618     push @cfgs, @rundef;
619     my $value = cfg(@cfgs);
620     return $value;
621 }
622
623 sub string_to_ssh ($) {
624     my ($spec) = @_;
625     if ($spec =~ m/\s/) {
626         return qw(sh -ec), 'exec '.$spec.' "$@"', 'x';
627     } else {
628         return ($spec);
629     }
630 }
631
632 sub access_cfg_ssh () {
633     my $gitssh = access_cfg('ssh', 'RETURN-UNDEF');
634     if (!defined $gitssh) {
635         return @ssh;
636     } else {
637         return string_to_ssh $gitssh;
638     }
639 }
640
641 sub access_runeinfo ($) {
642     my ($info) = @_;
643     return ": dgit ".access_basedistro()." $info ;";
644 }
645
646 sub access_someuserhost ($) {
647     my ($some) = @_;
648     my $user = access_cfg("$some-user-force", 'RETURN-UNDEF');
649     defined($user) && length($user) or
650         $user = access_cfg("$some-user",'username');
651     my $host = access_cfg("$some-host");
652     return length($user) ? "$user\@$host" : $host;
653 }
654
655 sub access_gituserhost () {
656     return access_someuserhost('git');
657 }
658
659 sub access_giturl (;$) {
660     my ($optional) = @_;
661     my $url = access_cfg('git-url','RETURN-UNDEF');
662     if (!defined $url) {
663         my $proto = access_cfg('git-proto', 'RETURN-UNDEF');
664         return undef unless defined $proto;
665         $url =
666             $proto.
667             access_gituserhost().
668             access_cfg('git-path');
669     }
670     return "$url/$package.git";
671 }              
672
673 sub parsecontrolfh ($$;$) {
674     my ($fh, $desc, $allowsigned) = @_;
675     our $dpkgcontrolhash_noissigned;
676     my $c;
677     for (;;) {
678         my %opts = ('name' => $desc);
679         $opts{allow_pgp}= $allowsigned || !$dpkgcontrolhash_noissigned;
680         $c = Dpkg::Control::Hash->new(%opts);
681         $c->parse($fh,$desc) or die "parsing of $desc failed";
682         last if $allowsigned;
683         last if $dpkgcontrolhash_noissigned;
684         my $issigned= $c->get_option('is_pgp_signed');
685         if (!defined $issigned) {
686             $dpkgcontrolhash_noissigned= 1;
687             seek $fh, 0,0 or die "seek $desc: $!";
688         } elsif ($issigned) {
689             fail "control file $desc is (already) PGP-signed. ".
690                 " Note that dgit push needs to modify the .dsc and then".
691                 " do the signature itself";
692         } else {
693             last;
694         }
695     }
696     return $c;
697 }
698
699 sub parsecontrol {
700     my ($file, $desc) = @_;
701     my $fh = new IO::Handle;
702     open $fh, '<', $file or die "$file: $!";
703     my $c = parsecontrolfh($fh,$desc);
704     $fh->error and die $!;
705     close $fh;
706     return $c;
707 }
708
709 sub getfield ($$) {
710     my ($dctrl,$field) = @_;
711     my $v = $dctrl->{$field};
712     return $v if defined $v;
713     fail "missing field $field in ".$v->get_option('name');
714 }
715
716 sub parsechangelog {
717     my $c = Dpkg::Control::Hash->new();
718     my $p = new IO::Handle;
719     my @cmd = (qw(dpkg-parsechangelog), @_);
720     open $p, '-|', @cmd or die $!;
721     $c->parse($p);
722     $?=0; $!=0; close $p or failedcmd @cmd;
723     return $c;
724 }
725
726 sub git_get_ref ($) {
727     my ($refname) = @_;
728     my $got = cmdoutput_errok @git, qw(show-ref --), $refname;
729     if (!defined $got) {
730         $?==256 or fail "git show-ref failed (status $?)";
731         printdebug "ref $refname= [show-ref exited 1]\n";
732         return '';
733     }
734     if ($got =~ m/^(\w+) \Q$refname\E$/m) {
735         printdebug "ref $refname=$1\n";
736         return $1;
737     } else {
738         printdebug "ref $refname= [no match]\n";
739         return '';
740     }
741 }
742
743 sub must_getcwd () {
744     my $d = getcwd();
745     defined $d or fail "getcwd failed: $!";
746     return $d;
747 }
748
749 our %rmad;
750
751 sub archive_query ($) {
752     my ($method) = @_;
753     my $query = access_cfg('archive-query','RETURN-UNDEF');
754     $query =~ s/^(\w+):// or badcfg "invalid archive-query method \`$query'";
755     my $proto = $1;
756     my $data = $'; #';
757     { no strict qw(refs); &{"${method}_${proto}"}($proto,$data); }
758 }
759
760 sub pool_dsc_subpath ($$) {
761     my ($vsn,$component) = @_; # $package is implict arg
762     my $prefix = substr($package, 0, $package =~ m/^l/ ? 4 : 1);
763     return "/pool/$component/$prefix/$package/".dscfn($vsn);
764 }
765
766 #---------- `ftpmasterapi' archive query method (nascent) ----------
767
768 sub archive_api_query_cmd ($) {
769     my ($subpath) = @_;
770     my @cmd = qw(curl -sS);
771     my $url = access_cfg('archive-query-url');
772     if ($url =~ m#^https://([-.0-9a-z]+)/#) {
773         my $host = $1;
774         my $keys = access_cfg('archive-query-tls-key','RETURN-UNDEF');
775         foreach my $key (split /\:/, $keys) {
776             $key =~ s/\%HOST\%/$host/g;
777             if (!stat $key) {
778                 fail "for $url: stat $key: $!" unless $!==ENOENT;
779                 next;
780             }
781             push @cmd, "--ca-certificate=$key", "--ca-directory=/dev/enoent";
782             last;
783         }
784     }
785     push @cmd, $url.$subpath;
786     return @cmd;
787 }
788
789 sub api_query ($$) {
790     use JSON;
791     my ($data, $subpath) = @_;
792     badcfg "ftpmasterapi archive query method takes no data part"
793         if length $data;
794     my @cmd = archive_api_query_cmd($subpath);
795     my $json = cmdoutput @cmd;
796     return decode_json($json);
797 }
798
799 sub canonicalise_suite_ftpmasterapi () {
800     my ($proto,$data) = @_;
801     my $suites = api_query($data, 'suites');
802     my @matched;
803     foreach my $entry (@$suites) {
804         next unless grep { 
805             my $v = $entry->{$_};
806             defined $v && $v eq $isuite;
807         } qw(codename name);
808         push @matched, $entry;
809     }
810     fail "unknown suite $isuite" unless @matched;
811     my $cn;
812     eval {
813         @matched==1 or die "multiple matches for suite $isuite\n";
814         $cn = "$matched[0]{codename}";
815         defined $cn or die "suite $isuite info has no codename\n";
816         $cn =~ m/^$suite_re$/ or die "suite $isuite maps to bad codename\n";
817     };
818     die "bad ftpmaster api response: $@\n".Dumper(\@matched)
819         if length $@;
820     return $cn;
821 }
822
823 sub archive_query_ftpmasterapi () {
824     my ($proto,$data) = @_;
825     my $info = api_query($data, "dsc_in_suite/$isuite/$package");
826     my @rows;
827     my $digester = Digest::SHA->new(256);
828     foreach my $entry (@$info) {
829         eval {
830             my $vsn = "$entry->{version}";
831             my ($ok,$msg) = version_check $vsn;
832             die "bad version: $msg\n" unless $ok;
833             my $component = "$entry->{component}";
834             $component =~ m/^$component_re$/ or die "bad component";
835             my $filename = "$entry->{filename}";
836             $filename && $filename !~ m#[^-+:._~0-9a-zA-Z/]|^[/.]|/[/.]#
837                 or die "bad filename";
838             my $sha256sum = "$entry->{sha256sum}";
839             $sha256sum =~ m/^[0-9a-f]+$/ or die "bad sha256sum";
840             push @rows, [ $vsn, "/pool/$component/$filename",
841                           $digester, $sha256sum ];
842         };
843         die "bad ftpmaster api response: $@\n".Dumper($entry)
844             if length $@;
845     }
846     @rows = sort { -version_compare($a->[0],$b->[0]) } @rows;
847     return @rows;
848 }
849
850 #---------- `madison' archive query method ----------
851
852 sub archive_query_madison {
853     return map { [ @$_[0..1] ] } madison_get_parse(@_);
854 }
855
856 sub madison_get_parse {
857     my ($proto,$data) = @_;
858     die unless $proto eq 'madison';
859     if (!length $data) {
860         $data= access_cfg('madison-distro','RETURN-UNDEF');
861         $data //= access_basedistro();
862     }
863     $rmad{$proto,$data,$package} ||= cmdoutput
864         qw(rmadison -asource),"-s$isuite","-u$data",$package;
865     my $rmad = $rmad{$proto,$data,$package};
866
867     my @out;
868     foreach my $l (split /\n/, $rmad) {
869         $l =~ m{^ \s*( [^ \t|]+ )\s* \|
870                   \s*( [^ \t|]+ )\s* \|
871                   \s*( [^ \t|/]+ )(?:/([^ \t|/]+))? \s* \|
872                   \s*( [^ \t|]+ )\s* }x or die "$rmad ?";
873         $1 eq $package or die "$rmad $package ?";
874         my $vsn = $2;
875         my $newsuite = $3;
876         my $component;
877         if (defined $4) {
878             $component = $4;
879         } else {
880             $component = access_cfg('archive-query-default-component');
881         }
882         $5 eq 'source' or die "$rmad ?";
883         push @out, [$vsn,pool_dsc_subpath($vsn,$component),$newsuite];
884     }
885     return sort { -version_compare($a->[0],$b->[0]); } @out;
886 }
887
888 sub canonicalise_suite_madison {
889     # madison canonicalises for us
890     my @r = madison_get_parse(@_);
891     @r or fail
892         "unable to canonicalise suite using package $package".
893         " which does not appear to exist in suite $isuite;".
894         " --existing-package may help";
895     return $r[0][2];
896 }
897
898 #---------- `sshpsql' archive query method ----------
899
900 sub sshpsql ($$$) {
901     my ($data,$runeinfo,$sql) = @_;
902     if (!length $data) {
903         $data= access_someuserhost('sshpsql').':'.
904             access_cfg('sshpsql-dbname');
905     }
906     $data =~ m/:/ or badcfg "invalid sshpsql method string \`$data'";
907     my ($userhost,$dbname) = ($`,$'); #';
908     my @rows;
909     my @cmd = (access_cfg_ssh, $userhost,
910                access_runeinfo("ssh-psql $runeinfo").
911                " export LC_MESSAGES=C; export LC_CTYPE=C;".
912                " ".shellquote qw(psql -A), $dbname, qw(-c), $sql);
913     debugcmd "|",@cmd;
914     open P, "-|", @cmd or die $!;
915     while (<P>) {
916         chomp or die;
917         printdebug("$debugprefix>|$_|\n");
918         push @rows, $_;
919     }
920     $!=0; $?=0; close P or failedcmd @cmd;
921     @rows or die;
922     my $nrows = pop @rows;
923     $nrows =~ s/^\((\d+) rows?\)$/$1/ or die "$nrows ?";
924     @rows == $nrows+1 or die "$nrows ".(scalar @rows)." ?";
925     @rows = map { [ split /\|/, $_ ] } @rows;
926     my $ncols = scalar @{ shift @rows };
927     die if grep { scalar @$_ != $ncols } @rows;
928     return @rows;
929 }
930
931 sub sql_injection_check {
932     foreach (@_) { die "$_ $& ?" if m{[^-+=:_.,/0-9a-zA-Z]}; }
933 }
934
935 sub archive_query_sshpsql ($$) {
936     my ($proto,$data) = @_;
937     sql_injection_check $isuite, $package;
938     my @rows = sshpsql($data, "archive-query $isuite $package", <<END);
939         SELECT source.version, component.name, files.filename, files.sha256sum
940           FROM source
941           JOIN src_associations ON source.id = src_associations.source
942           JOIN suite ON suite.id = src_associations.suite
943           JOIN dsc_files ON dsc_files.source = source.id
944           JOIN files_archive_map ON files_archive_map.file_id = dsc_files.file
945           JOIN component ON component.id = files_archive_map.component_id
946           JOIN files ON files.id = dsc_files.file
947          WHERE ( suite.suite_name='$isuite' OR suite.codename='$isuite' )
948            AND source.source='$package'
949            AND files.filename LIKE '%.dsc';
950 END
951     @rows = sort { -version_compare($a->[0],$b->[0]) } @rows;
952     my $digester = Digest::SHA->new(256);
953     @rows = map {
954         my ($vsn,$component,$filename,$sha256sum) = @$_;
955         [ $vsn, "/pool/$component/$filename",$digester,$sha256sum ];
956     } @rows;
957     return @rows;
958 }
959
960 sub canonicalise_suite_sshpsql ($$) {
961     my ($proto,$data) = @_;
962     sql_injection_check $isuite;
963     my @rows = sshpsql($data, "canonicalise-suite $isuite", <<END);
964         SELECT suite.codename
965           FROM suite where suite_name='$isuite' or codename='$isuite';
966 END
967     @rows = map { $_->[0] } @rows;
968     fail "unknown suite $isuite" unless @rows;
969     die "ambiguous $isuite: @rows ?" if @rows>1;
970     return $rows[0];
971 }
972
973 #---------- `dummycat' archive query method ----------
974
975 sub canonicalise_suite_dummycat ($$) {
976     my ($proto,$data) = @_;
977     my $dpath = "$data/suite.$isuite";
978     if (!open C, "<", $dpath) {
979         $!==ENOENT or die "$dpath: $!";
980         printdebug "dummycat canonicalise_suite $isuite $dpath ENOENT\n";
981         return $isuite;
982     }
983     $!=0; $_ = <C>;
984     chomp or die "$dpath: $!";
985     close C;
986     printdebug "dummycat canonicalise_suite $isuite $dpath = $_\n";
987     return $_;
988 }
989
990 sub archive_query_dummycat ($$) {
991     my ($proto,$data) = @_;
992     canonicalise_suite();
993     my $dpath = "$data/package.$csuite.$package";
994     if (!open C, "<", $dpath) {
995         $!==ENOENT or die "$dpath: $!";
996         printdebug "dummycat query $csuite $package $dpath ENOENT\n";
997         return ();
998     }
999     my @rows;
1000     while (<C>) {
1001         next if m/^\#/;
1002         next unless m/\S/;
1003         die unless chomp;
1004         printdebug "dummycat query $csuite $package $dpath | $_\n";
1005         my @row = split /\s+/, $_;
1006         @row==2 or die "$dpath: $_ ?";
1007         push @rows, \@row;
1008     }
1009     C->error and die "$dpath: $!";
1010     close C;
1011     return sort { -version_compare($a->[0],$b->[0]); } @rows;
1012 }
1013
1014 #---------- archive query entrypoints and rest of program ----------
1015
1016 sub canonicalise_suite () {
1017     return if defined $csuite;
1018     fail "cannot operate on $isuite suite" if $isuite eq 'UNRELEASED';
1019     $csuite = archive_query('canonicalise_suite');
1020     if ($isuite ne $csuite) {
1021         progress "canonical suite name for $isuite is $csuite";
1022     }
1023 }
1024
1025 sub get_archive_dsc () {
1026     canonicalise_suite();
1027     my @vsns = archive_query('archive_query');
1028     foreach my $vinfo (@vsns) {
1029         my ($vsn,$subpath,$digester,$digest) = @$vinfo;
1030         $dscurl = access_cfg('mirror').$subpath;
1031         $dscdata = url_get($dscurl);
1032         if (!$dscdata) {
1033             $skew_warning_vsn = $vsn if !defined $skew_warning_vsn;
1034             next;
1035         }
1036         if ($digester) {
1037             $digester->reset();
1038             $digester->add($dscdata);
1039             my $got = $digester->hexdigest();
1040             $got eq $digest or
1041                 fail "$dscurl has hash $got but".
1042                     " archive told us to expect $digest";
1043         }
1044         my $dscfh = new IO::File \$dscdata, '<' or die $!;
1045         printdebug Dumper($dscdata) if $debuglevel>1;
1046         $dsc = parsecontrolfh($dscfh,$dscurl,1);
1047         printdebug Dumper($dsc) if $debuglevel>1;
1048         my $fmt = getfield $dsc, 'Format';
1049         fail "unsupported source format $fmt, sorry" unless $format_ok{$fmt};
1050         $dsc_checked = !!$digester;
1051         return;
1052     }
1053     $dsc = undef;
1054 }
1055
1056 sub check_for_git ();
1057 sub check_for_git () {
1058     # returns 0 or 1
1059     my $how = access_cfg('git-check');
1060     if ($how eq 'ssh-cmd') {
1061         my @cmd =
1062             (access_cfg_ssh, access_gituserhost(),
1063              access_runeinfo("git-check $package").
1064              " set -e; cd ".access_cfg('git-path').";".
1065              " if test -d $package.git; then echo 1; else echo 0; fi");
1066         my $r= cmdoutput @cmd;
1067         if ($r =~ m/^divert (\w+)$/) {
1068             my $divert=$1;
1069             my ($usedistro,) = access_distros();
1070             $instead_distro= cfg("dgit-distro.$usedistro.diverts.$divert");
1071             $instead_distro =~ s{^/}{ access_basedistro()."/" }e;
1072             printdebug "diverting $divert so using distro $instead_distro\n";
1073             return check_for_git();
1074         }
1075         failedcmd @cmd unless $r =~ m/^[01]$/;
1076         return $r+0;
1077     } elsif ($how eq 'true') {
1078         return 1;
1079     } elsif ($how eq 'false') {
1080         return 0;
1081     } else {
1082         badcfg "unknown git-check \`$how'";
1083     }
1084 }
1085
1086 sub create_remote_git_repo () {
1087     my $how = access_cfg('git-create');
1088     if ($how eq 'ssh-cmd') {
1089         runcmd_ordryrun
1090             (access_cfg_ssh, access_gituserhost(),
1091              access_runeinfo("git-create $package").
1092              "set -e; cd ".access_cfg('git-path').";".
1093              " cp -a _template $package.git");
1094     } elsif ($how eq 'true') {
1095         # nothing to do
1096     } else {
1097         badcfg "unknown git-create \`$how'";
1098     }
1099 }
1100
1101 our ($dsc_hash,$lastpush_hash);
1102
1103 our $ud = '.git/dgit/unpack';
1104
1105 sub prep_ud () {
1106     rmtree($ud);
1107     mkpath '.git/dgit';
1108     mkdir $ud or die $!;
1109 }
1110
1111 sub mktree_in_ud_here () {
1112     runcmd qw(git init -q);
1113     rmtree('.git/objects');
1114     symlink '../../../../objects','.git/objects' or die $!;
1115 }
1116
1117 sub git_write_tree () {
1118     my $tree = cmdoutput @git, qw(write-tree);
1119     $tree =~ m/^\w+$/ or die "$tree ?";
1120     return $tree;
1121 }
1122
1123 sub mktree_in_ud_from_only_subdir () {
1124     # changes into the subdir
1125     my (@dirs) = <*/.>;
1126     die unless @dirs==1;
1127     $dirs[0] =~ m#^([^/]+)/\.$# or die;
1128     my $dir = $1;
1129     changedir $dir;
1130     fail "source package contains .git directory" if stat_exists '.git';
1131     mktree_in_ud_here();
1132     my $format=get_source_format();
1133     if (madformat($format)) {
1134         rmtree '.pc';
1135     }
1136     runcmd @git, qw(add -Af);
1137     my $tree=git_write_tree();
1138     return ($tree,$dir);
1139 }
1140
1141 sub dsc_files_info () {
1142     foreach my $csumi (['Checksums-Sha256','Digest::SHA', 'new(256)'],
1143                        ['Checksums-Sha1',  'Digest::SHA', 'new(1)'],
1144                        ['Files',           'Digest::MD5', 'new()']) {
1145         my ($fname, $module, $method) = @$csumi;
1146         my $field = $dsc->{$fname};
1147         next unless defined $field;
1148         eval "use $module; 1;" or die $@;
1149         my @out;
1150         foreach (split /\n/, $field) {
1151             next unless m/\S/;
1152             m/^(\w+) (\d+) (\S+)$/ or
1153                 fail "could not parse .dsc $fname line \`$_'";
1154             my $digester = eval "$module"."->$method;" or die $@;
1155             push @out, {
1156                 Hash => $1,
1157                 Bytes => $2,
1158                 Filename => $3,
1159                 Digester => $digester,
1160             };
1161         }
1162         return @out;
1163     }
1164     fail "missing any supported Checksums-* or Files field in ".
1165         $dsc->get_option('name');
1166 }
1167
1168 sub dsc_files () {
1169     map { $_->{Filename} } dsc_files_info();
1170 }
1171
1172 sub is_orig_file ($;$) {
1173     local ($_) = $_[0];
1174     my $base = $_[1];
1175     m/\.orig(?:-\w+)?\.tar\.\w+$/ or return 0;
1176     defined $base or return 1;
1177     return $` eq $base;
1178 }
1179
1180 sub make_commit ($) {
1181     my ($file) = @_;
1182     return cmdoutput @git, qw(hash-object -w -t commit), $file;
1183 }
1184
1185 sub clogp_authline ($) {
1186     my ($clogp) = @_;
1187     my $author = getfield $clogp, 'Maintainer';
1188     $author =~ s#,.*##ms;
1189     my $date = cmdoutput qw(date), '+%s %z', qw(-d), getfield($clogp,'Date');
1190     my $authline = "$author $date";
1191     $authline =~ m/^[^<>]+ \<\S+\> \d+ [-+]\d+$/ or
1192         fail "unexpected commit author line format \`$authline'".
1193         " (was generated from changelog Maintainer field)";
1194     return $authline;
1195 }
1196
1197 sub generate_commit_from_dsc () {
1198     prep_ud();
1199     changedir $ud;
1200
1201     foreach my $fi (dsc_files_info()) {
1202         my $f = $fi->{Filename};
1203         die "$f ?" if $f =~ m#/|^\.|\.dsc$|\.tmp$#;
1204
1205         link "../../../$f", $f
1206             or $!==&ENOENT
1207             or die "$f $!";
1208
1209         complete_file_from_dsc('.', $fi);
1210
1211         if (is_orig_file($f)) {
1212             link $f, "../../../../$f"
1213                 or $!==&EEXIST
1214                 or die "$f $!";
1215         }
1216     }
1217
1218     my $dscfn = "$package.dsc";
1219
1220     open D, ">", $dscfn or die "$dscfn: $!";
1221     print D $dscdata or die "$dscfn: $!";
1222     close D or die "$dscfn: $!";
1223     my @cmd = qw(dpkg-source);
1224     push @cmd, '--no-check' if $dsc_checked;
1225     push @cmd, qw(-x --), $dscfn;
1226     runcmd @cmd;
1227
1228     my ($tree,$dir) = mktree_in_ud_from_only_subdir();
1229     runcmd qw(sh -ec), 'dpkg-parsechangelog >../changelog.tmp';
1230     my $clogp = parsecontrol('../changelog.tmp',"commit's changelog");
1231     my $authline = clogp_authline $clogp;
1232     my $changes = getfield $clogp, 'Changes';
1233     open C, ">../commit.tmp" or die $!;
1234     print C <<END or die $!;
1235 tree $tree
1236 author $authline
1237 committer $authline
1238
1239 $changes
1240
1241 # imported from the archive
1242 END
1243     close C or die $!;
1244     my $outputhash = make_commit qw(../commit.tmp);
1245     my $cversion = getfield $clogp, 'Version';
1246     progress "synthesised git commit from .dsc $cversion";
1247     if ($lastpush_hash) {
1248         runcmd @git, qw(reset --hard), $lastpush_hash;
1249         runcmd qw(sh -ec), 'dpkg-parsechangelog >>../changelogold.tmp';
1250         my $oldclogp = parsecontrol('../changelogold.tmp','previous changelog');
1251         my $oversion = getfield $oldclogp, 'Version';
1252         my $vcmp =
1253             version_compare($oversion, $cversion);
1254         if ($vcmp < 0) {
1255             # git upload/ is earlier vsn than archive, use archive
1256             open C, ">../commit2.tmp" or die $!;
1257             print C <<END or die $!;
1258 tree $tree
1259 parent $lastpush_hash
1260 parent $outputhash
1261 author $authline
1262 committer $authline
1263
1264 Record $package ($cversion) in archive suite $csuite
1265 END
1266             $outputhash = make_commit qw(../commit2.tmp);
1267         } elsif ($vcmp > 0) {
1268             print STDERR <<END or die $!;
1269
1270 Version actually in archive:    $cversion (older)
1271 Last allegedly pushed/uploaded: $oversion (newer or same)
1272 $later_warning_msg
1273 END
1274             $outputhash = $lastpush_hash;
1275         } else {
1276             $outputhash = $lastpush_hash;
1277         }
1278     }
1279     changedir '../../../..';
1280     runcmd @git, qw(update-ref -m),"dgit fetch import $cversion",
1281             'DGIT_ARCHIVE', $outputhash;
1282     cmdoutput @git, qw(log -n2), $outputhash;
1283     # ... gives git a chance to complain if our commit is malformed
1284     rmtree($ud);
1285     return $outputhash;
1286 }
1287
1288 sub complete_file_from_dsc ($$) {
1289     our ($dstdir, $fi) = @_;
1290     # Ensures that we have, in $dir, the file $fi, with the correct
1291     # contents.  (Downloading it from alongside $dscurl if necessary.)
1292
1293     my $f = $fi->{Filename};
1294     my $tf = "$dstdir/$f";
1295     my $downloaded = 0;
1296
1297     if (stat_exists $tf) {
1298         progress "using existing $f";
1299     } else {
1300         my $furl = $dscurl;
1301         $furl =~ s{/[^/]+$}{};
1302         $furl .= "/$f";
1303         die "$f ?" unless $f =~ m/^${package}_/;
1304         die "$f ?" if $f =~ m#/#;
1305         runcmd_ordryrun_local @curl,qw(-o),$tf,'--',"$furl";
1306         next if !act_local();
1307         $downloaded = 1;
1308     }
1309
1310     open F, "<", "$tf" or die "$tf: $!";
1311     $fi->{Digester}->reset();
1312     $fi->{Digester}->addfile(*F);
1313     F->error and die $!;
1314     my $got = $fi->{Digester}->hexdigest();
1315     $got eq $fi->{Hash} or
1316         fail "file $f has hash $got but .dsc".
1317             " demands hash $fi->{Hash} ".
1318             ($downloaded ? "(got wrong file from archive!)"
1319              : "(perhaps you should delete this file?)");
1320 }
1321
1322 sub ensure_we_have_orig () {
1323     foreach my $fi (dsc_files_info()) {
1324         my $f = $fi->{Filename};
1325         next unless is_orig_file($f);
1326         complete_file_from_dsc('..', $fi);
1327     }
1328 }
1329
1330 sub rev_parse ($) {
1331     return cmdoutput @git, qw(rev-parse), "$_[0]~0";
1332 }
1333
1334 sub is_fast_fwd ($$) {
1335     my ($ancestor,$child) = @_;
1336     my @cmd = (@git, qw(merge-base), $ancestor, $child);
1337     my $mb = cmdoutput_errok @cmd;
1338     if (defined $mb) {
1339         return rev_parse($mb) eq rev_parse($ancestor);
1340     } else {
1341         $?==256 or failedcmd @cmd;
1342         return 0;
1343     }
1344 }
1345
1346 sub git_fetch_us () {
1347     runcmd_ordryrun_local @git, qw(fetch),access_giturl(),fetchspec();
1348 }
1349
1350 sub fetch_from_archive () {
1351     # ensures that lrref() is what is actually in the archive,
1352     #  one way or another
1353     get_archive_dsc();
1354
1355     if ($dsc) {
1356         foreach my $field (@ourdscfield) {
1357             $dsc_hash = $dsc->{$field};
1358             last if defined $dsc_hash;
1359         }
1360         if (defined $dsc_hash) {
1361             $dsc_hash =~ m/\w+/ or fail "invalid hash in .dsc \`$dsc_hash'";
1362             $dsc_hash = $&;
1363             progress "last upload to archive specified git hash";
1364         } else {
1365             progress "last upload to archive has NO git hash";
1366         }
1367     } else {
1368         progress "no version available from the archive";
1369     }
1370
1371     $lastpush_hash = git_get_ref(lrref());
1372     printdebug "previous reference hash=$lastpush_hash\n";
1373     my $hash;
1374     if (defined $dsc_hash) {
1375         fail "missing remote git history even though dsc has hash -".
1376             " could not find ref ".lrref().
1377             " (should have been fetched from ".access_giturl()."#".rrref().")"
1378             unless $lastpush_hash;
1379         $hash = $dsc_hash;
1380         ensure_we_have_orig();
1381         if ($dsc_hash eq $lastpush_hash) {
1382         } elsif (is_fast_fwd($dsc_hash,$lastpush_hash)) {
1383             print STDERR <<END or die $!;
1384
1385 Git commit in archive is behind the last version allegedly pushed/uploaded.
1386 Commit referred to by archive:  $dsc_hash
1387 Last allegedly pushed/uploaded: $lastpush_hash
1388 $later_warning_msg
1389 END
1390             $hash = $lastpush_hash;
1391         } else {
1392             fail "git head (".lrref()."=$lastpush_hash) is not a ".
1393                 "descendant of archive's .dsc hash ($dsc_hash)";
1394         }
1395     } elsif ($dsc) {
1396         $hash = generate_commit_from_dsc();
1397     } elsif ($lastpush_hash) {
1398         # only in git, not in the archive yet
1399         $hash = $lastpush_hash;
1400         print STDERR <<END or die $!;
1401
1402 Package not found in the archive, but has allegedly been pushed using dgit.
1403 $later_warning_msg
1404 END
1405     } else {
1406         printdebug "nothing found!\n";
1407         if (defined $skew_warning_vsn) {
1408             print STDERR <<END or die $!;
1409
1410 Warning: relevant archive skew detected.
1411 Archive allegedly contains $skew_warning_vsn
1412 But we were not able to obtain any version from the archive or git.
1413
1414 END
1415         }
1416         return 0;
1417     }
1418     printdebug "current hash=$hash\n";
1419     if ($lastpush_hash) {
1420         fail "not fast forward on last upload branch!".
1421             " (archive's version left in DGIT_ARCHIVE)"
1422             unless is_fast_fwd($lastpush_hash, $hash);
1423     }
1424     if (defined $skew_warning_vsn) {
1425         mkpath '.git/dgit';
1426         printdebug "SKEW CHECK WANT $skew_warning_vsn\n";
1427         my $clogf = ".git/dgit/changelog.tmp";
1428         runcmd shell_cmd "exec >$clogf",
1429             @git, qw(cat-file blob), "$hash:debian/changelog";
1430         my $gotclogp = parsechangelog("-l$clogf");
1431         my $got_vsn = getfield $gotclogp, 'Version';
1432         printdebug "SKEW CHECK GOT $got_vsn\n";
1433         if (version_compare($got_vsn, $skew_warning_vsn) < 0) {
1434             print STDERR <<END or die $!;
1435
1436 Warning: archive skew detected.  Using the available version:
1437 Archive allegedly contains    $skew_warning_vsn
1438 We were able to obtain only   $got_vsn
1439
1440 END
1441         }
1442     }
1443     if ($lastpush_hash ne $hash) {
1444         my @upd_cmd = (@git, qw(update-ref -m), 'dgit fetch', lrref(), $hash);
1445         if (act_local()) {
1446             cmdoutput @upd_cmd;
1447         } else {
1448             dryrun_report @upd_cmd;
1449         }
1450     }
1451     return 1;
1452 }
1453
1454 sub clone ($) {
1455     my ($dstdir) = @_;
1456     canonicalise_suite();
1457     badusage "dry run makes no sense with clone" unless act_local();
1458     my $hasgit = check_for_git();
1459     mkdir $dstdir or die "$dstdir $!";
1460     changedir $dstdir;
1461     runcmd @git, qw(init -q);
1462     my $giturl = access_giturl(1);
1463     if (defined $giturl) {
1464         runcmd @git, qw(config), "remote.$remotename.fetch", fetchspec();
1465         open H, "> .git/HEAD" or die $!;
1466         print H "ref: ".lref()."\n" or die $!;
1467         close H or die $!;
1468         runcmd @git, qw(remote add), 'origin', $giturl;
1469     }
1470     if ($hasgit) {
1471         progress "fetching existing git history";
1472         git_fetch_us();
1473         runcmd_ordryrun_local @git, qw(fetch origin);
1474     } else {
1475         progress "starting new git history";
1476     }
1477     fetch_from_archive() or no_such_package;
1478     my $vcsgiturl = $dsc->{'Vcs-Git'};
1479     if (length $vcsgiturl) {
1480         $vcsgiturl =~ s/\s+-b\s+\S+//g;
1481         runcmd @git, qw(remote add vcs-git), $vcsgiturl;
1482     }
1483     runcmd @git, qw(reset --hard), lrref();
1484     printdone "ready for work in $dstdir";
1485 }
1486
1487 sub fetch () {
1488     if (check_for_git()) {
1489         git_fetch_us();
1490     }
1491     fetch_from_archive() or no_such_package();
1492     printdone "fetched into ".lrref();
1493 }
1494
1495 sub pull () {
1496     fetch();
1497     runcmd_ordryrun_local @git, qw(merge -m),"Merge from $csuite [dgit]",
1498         lrref();
1499     printdone "fetched to ".lrref()." and merged into HEAD";
1500 }
1501
1502 sub check_not_dirty () {
1503     return if $ignoredirty;
1504     my @cmd = (@git, qw(diff --quiet HEAD));
1505     debugcmd "+",@cmd;
1506     $!=0; $?=0; system @cmd;
1507     return if !$! && !$?;
1508     if (!$! && $?==256) {
1509         fail "working tree is dirty (does not match HEAD)";
1510     } else {
1511         failedcmd @cmd;
1512     }
1513 }
1514
1515 sub commit_admin ($) {
1516     my ($m) = @_;
1517     progress "$m";
1518     runcmd_ordryrun_local @git, qw(commit -m), $m;
1519 }
1520
1521 sub commit_quilty_patch () {
1522     my $output = cmdoutput @git, qw(status --porcelain);
1523     my %adds;
1524     foreach my $l (split /\n/, $output) {
1525         next unless $l =~ m/\S/;
1526         if ($l =~ m{^(?:\?\?| M) (.pc|debian/patches)}) {
1527             $adds{$1}++;
1528         }
1529     }
1530     delete $adds{'.pc'}; # if there wasn't one before, don't add it
1531     if (!%adds) {
1532         progress "nothing quilty to commit, ok.";
1533         return;
1534     }
1535     runcmd_ordryrun_local @git, qw(add), sort keys %adds;
1536     commit_admin "Commit Debian 3.0 (quilt) metadata";
1537 }
1538
1539 sub get_source_format () {
1540     if (!open F, "debian/source/format") {
1541         die $! unless $!==&ENOENT;
1542         return '';
1543     }
1544     $_ = <F>;
1545     F->error and die $!;
1546     chomp;
1547     return $_;
1548 }
1549
1550 sub madformat ($) {
1551     my ($format) = @_;
1552     return 0 unless $format eq '3.0 (quilt)';
1553     if ($quilt_mode eq 'nocheck') {
1554         progress "Not doing any fixup of \`$format' due to --no-quilt-fixup";
1555         return 0;
1556     }
1557     progress "Format \`$format', checking/updating patch stack";
1558     return 1;
1559 }
1560
1561 sub push_parse_changelog ($) {
1562     my ($clogpfn) = @_;
1563
1564     my $clogp = Dpkg::Control::Hash->new();
1565     $clogp->load($clogpfn) or die;
1566
1567     $package = getfield $clogp, 'Source';
1568     my $cversion = getfield $clogp, 'Version';
1569     my $tag = debiantag($cversion);
1570     runcmd @git, qw(check-ref-format), $tag;
1571
1572     my $dscfn = dscfn($cversion);
1573
1574     return ($clogp, $cversion, $tag, $dscfn);
1575 }
1576
1577 sub push_parse_dsc ($$$) {
1578     my ($dscfn,$dscfnwhat, $cversion) = @_;
1579     $dsc = parsecontrol($dscfn,$dscfnwhat);
1580     my $dversion = getfield $dsc, 'Version';
1581     my $dscpackage = getfield $dsc, 'Source';
1582     ($dscpackage eq $package && $dversion eq $cversion) or
1583         fail "$dscfn is for $dscpackage $dversion".
1584             " but debian/changelog is for $package $cversion";
1585 }
1586
1587 sub push_mktag ($$$$$$$) {
1588     my ($head,$clogp,$tag,
1589         $dscfn,
1590         $changesfile,$changesfilewhat,
1591         $tfn) = @_;
1592
1593     $dsc->{$ourdscfield[0]} = $head;
1594     $dsc->save("$dscfn.tmp") or die $!;
1595
1596     my $changes = parsecontrol($changesfile,$changesfilewhat);
1597     foreach my $field (qw(Source Distribution Version)) {
1598         $changes->{$field} eq $clogp->{$field} or
1599             fail "changes field $field \`$changes->{$field}'".
1600                 " does not match changelog \`$clogp->{$field}'";
1601     }
1602
1603     my $cversion = getfield $clogp, 'Version';
1604     my $clogsuite = getfield $clogp, 'Distribution';
1605
1606     # We make the git tag by hand because (a) that makes it easier
1607     # to control the "tagger" (b) we can do remote signing
1608     my $authline = clogp_authline $clogp;
1609     my $delibs = join(" ", "",@deliberatelies);
1610     my $declaredistro = access_basedistro();
1611     open TO, '>', $tfn->('.tmp') or die $!;
1612     print TO <<END or die $!;
1613 object $head
1614 type commit
1615 tag $tag
1616 tagger $authline
1617
1618 $package release $cversion for $clogsuite ($csuite) [dgit]
1619 [dgit distro=$declaredistro$delibs]
1620 END
1621     foreach my $ref (sort keys %supersedes) {
1622                     print TO <<END or die $!;
1623 [dgit supersede:$ref=$supersedes{$ref}]
1624 END
1625     }
1626
1627     close TO or die $!;
1628
1629     my $tagobjfn = $tfn->('.tmp');
1630     if ($sign) {
1631         if (!defined $keyid) {
1632             $keyid = access_cfg('keyid','RETURN-UNDEF');
1633         }
1634         unlink $tfn->('.tmp.asc') or $!==&ENOENT or die $!;
1635         my @sign_cmd = (@gpg, qw(--detach-sign --armor));
1636         push @sign_cmd, qw(-u),$keyid if defined $keyid;
1637         push @sign_cmd, $tfn->('.tmp');
1638         runcmd_ordryrun @sign_cmd;
1639         if (act_scary()) {
1640             $tagobjfn = $tfn->('.signed.tmp');
1641             runcmd shell_cmd "exec >$tagobjfn", qw(cat --),
1642                 $tfn->('.tmp'), $tfn->('.tmp.asc');
1643         }
1644     }
1645
1646     return ($tagobjfn);
1647 }
1648
1649 sub sign_changes ($) {
1650     my ($changesfile) = @_;
1651     if ($sign) {
1652         my @debsign_cmd = @debsign;
1653         push @debsign_cmd, "-k$keyid" if defined $keyid;
1654         push @debsign_cmd, "-p$gpg[0]" if $gpg[0] ne 'gpg';
1655         push @debsign_cmd, $changesfile;
1656         runcmd_ordryrun @debsign_cmd;
1657     }
1658 }
1659
1660 sub dopush () {
1661     printdebug "actually entering push\n";
1662     prep_ud();
1663
1664     access_giturl(); # check that success is vaguely likely
1665
1666     my $clogpfn = ".git/dgit/changelog.822.tmp";
1667     runcmd shell_cmd "exec >$clogpfn", qw(dpkg-parsechangelog);
1668
1669     responder_send_file('parsed-changelog', $clogpfn);
1670
1671     my ($clogp, $cversion, $tag, $dscfn) =
1672         push_parse_changelog("$clogpfn");
1673
1674     my $dscpath = "$buildproductsdir/$dscfn";
1675     stat_exists $dscpath or
1676         fail "looked for .dsc $dscfn, but $!;".
1677             " maybe you forgot to build";
1678
1679     responder_send_file('dsc', $dscpath);
1680
1681     push_parse_dsc($dscpath, $dscfn, $cversion);
1682
1683     my $format = getfield $dsc, 'Format';
1684     printdebug "format $format\n";
1685     if (madformat($format)) {
1686         commit_quilty_patch();
1687     }
1688     check_not_dirty();
1689     changedir $ud;
1690     progress "checking that $dscfn corresponds to HEAD";
1691     runcmd qw(dpkg-source -x --),
1692         $dscpath =~ m#^/# ? $dscpath : "../../../$dscpath";
1693     my ($tree,$dir) = mktree_in_ud_from_only_subdir();
1694     changedir '../../../..';
1695     my $diffopt = $debuglevel>0 ? '--exit-code' : '--quiet';
1696     my @diffcmd = (@git, qw(diff), $diffopt, $tree);
1697     debugcmd "+",@diffcmd;
1698     $!=0; $?=0;
1699     my $r = system @diffcmd;
1700     if ($r) {
1701         if ($r==256) {
1702             fail "$dscfn specifies a different tree to your HEAD commit;".
1703                 " perhaps you forgot to build".
1704                 ($diffopt eq '--exit-code' ? "" :
1705                  " (run with -D to see full diff output)");
1706         } else {
1707             failedcmd @diffcmd;
1708         }
1709     }
1710 #fetch from alioth
1711 #do fast forward check and maybe fake merge
1712 #    if (!is_fast_fwd(mainbranch
1713 #    runcmd @git, qw(fetch -p ), "$alioth_git/$package.git",
1714 #        map { lref($_).":".rref($_) }
1715 #        (uploadbranch());
1716     my $head = rev_parse('HEAD');
1717     if (!$changesfile) {
1718         my $multi = "$buildproductsdir/".
1719             "${package}_".(stripepoch $cversion)."_multi.changes";
1720         if (stat_exists "$multi") {
1721             $changesfile = $multi;
1722         } else {
1723             my $pat = "${package}_".(stripepoch $cversion)."_*.changes";
1724             my @cs = glob "$buildproductsdir/$pat";
1725             fail "failed to find unique changes file".
1726                 " (looked for $pat in $buildproductsdir, or $multi);".
1727                 " perhaps you need to use dgit -C"
1728                 unless @cs==1;
1729             ($changesfile) = @cs;
1730         }
1731     } else {
1732         $changesfile = "$buildproductsdir/$changesfile";
1733     }
1734
1735     responder_send_file('changes',$changesfile);
1736     responder_send_command("param head $head");
1737     responder_send_command("param csuite $csuite");
1738
1739     my $forceflag = deliberately('not-fast-forward') ? '+' : '';
1740     if ($forceflag && defined $lastpush_hash) {
1741         git_for_each_tag_referring($lastpush_hash, sub {
1742             my ($objid,$fullrefname,$tagname) = @_;
1743             responder_send_command("supersedes $fullrefname=$objid");
1744             $supersedes{$fullrefname} = $objid;
1745         });
1746     }
1747
1748     my $tfn = sub { ".git/dgit/tag$_[0]"; };
1749     my $tagobjfn;
1750
1751     if ($we_are_responder) {
1752         $tagobjfn = $tfn->('.signed.tmp');
1753         responder_receive_files('signed-tag', $tagobjfn);
1754     } else {
1755         $tagobjfn =
1756             push_mktag($head,$clogp,$tag,
1757                        $dscpath,
1758                        $changesfile,$changesfile,
1759                        $tfn);
1760     }
1761
1762     my $tag_obj_hash = cmdoutput @git, qw(hash-object -w -t tag), $tagobjfn;
1763     runcmd_ordryrun @git, qw(verify-tag), $tag_obj_hash;
1764     runcmd_ordryrun_local @git, qw(update-ref), "refs/tags/$tag", $tag_obj_hash;
1765     runcmd_ordryrun @git, qw(tag -v --), $tag;
1766
1767     if (!check_for_git()) {
1768         create_remote_git_repo();
1769     }
1770     runcmd_ordryrun @git, qw(push),access_giturl(),
1771         $forceflag."HEAD:".rrref(), "refs/tags/$tag";
1772     runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), 'HEAD';
1773
1774     if ($we_are_responder) {
1775         my $dryrunsuffix = act_local() ? "" : ".tmp";
1776         responder_receive_files('signed-dsc-changes',
1777                                 "$dscpath$dryrunsuffix",
1778                                 "$changesfile$dryrunsuffix");
1779     } else {
1780         if (act_local()) {
1781             rename "$dscpath.tmp",$dscpath or die "$dscfn $!";
1782         } else {
1783             progress "[new .dsc left in $dscpath.tmp]";
1784         }
1785         sign_changes $changesfile;
1786     }
1787
1788     my $host = access_cfg('upload-host','RETURN-UNDEF');
1789     my @hostarg = defined($host) ? ($host,) : ();
1790     runcmd_ordryrun @dput, @hostarg, $changesfile;
1791     printdone "pushed and uploaded $cversion";
1792
1793     responder_send_command("complete");
1794 }
1795
1796 sub cmd_clone {
1797     parseopts();
1798     my $dstdir;
1799     badusage "-p is not allowed with clone; specify as argument instead"
1800         if defined $package;
1801     if (@ARGV==1) {
1802         ($package) = @ARGV;
1803     } elsif (@ARGV==2 && $ARGV[1] =~ m#^\w#) {
1804         ($package,$isuite) = @ARGV;
1805     } elsif (@ARGV==2 && $ARGV[1] =~ m#^[./]#) {
1806         ($package,$dstdir) = @ARGV;
1807     } elsif (@ARGV==3) {
1808         ($package,$isuite,$dstdir) = @ARGV;
1809     } else {
1810         badusage "incorrect arguments to dgit clone";
1811     }
1812     $dstdir ||= "$package";
1813
1814     if (stat_exists $dstdir) {
1815         fail "$dstdir already exists";
1816     }
1817
1818     my $cwd_remove;
1819     if ($rmonerror && !$dryrun_level) {
1820         $cwd_remove= getcwd();
1821         unshift @end, sub { 
1822             return unless defined $cwd_remove;
1823             if (!chdir "$cwd_remove") {
1824                 return if $!==&ENOENT;
1825                 die "chdir $cwd_remove: $!";
1826             }
1827             rmtree($dstdir) or die "remove $dstdir: $!\n";
1828         };
1829     }
1830
1831     clone($dstdir);
1832     $cwd_remove = undef;
1833 }
1834
1835 sub branchsuite () {
1836     my $branch = cmdoutput_errok @git, qw(symbolic-ref HEAD);
1837     if ($branch =~ m#$lbranch_re#o) {
1838         return $1;
1839     } else {
1840         return undef;
1841     }
1842 }
1843
1844 sub fetchpullargs () {
1845     if (!defined $package) {
1846         my $sourcep = parsecontrol('debian/control','debian/control');
1847         $package = getfield $sourcep, 'Source';
1848     }
1849     if (@ARGV==0) {
1850 #       $isuite = branchsuite();  # this doesn't work because dak hates canons
1851         if (!$isuite) {
1852             my $clogp = parsechangelog();
1853             $isuite = getfield $clogp, 'Distribution';
1854         }
1855         canonicalise_suite();
1856         progress "fetching from suite $csuite";
1857     } elsif (@ARGV==1) {
1858         ($isuite) = @ARGV;
1859         canonicalise_suite();
1860     } else {
1861         badusage "incorrect arguments to dgit fetch or dgit pull";
1862     }
1863 }
1864
1865 sub cmd_fetch {
1866     parseopts();
1867     fetchpullargs();
1868     fetch();
1869 }
1870
1871 sub cmd_pull {
1872     parseopts();
1873     fetchpullargs();
1874     pull();
1875 }
1876
1877 sub cmd_push {
1878     parseopts();
1879     badusage "-p is not allowed with dgit push" if defined $package;
1880     check_not_dirty();
1881     my $clogp = parsechangelog();
1882     $package = getfield $clogp, 'Source';
1883     my $specsuite;
1884     if (@ARGV==0) {
1885     } elsif (@ARGV==1) {
1886         ($specsuite) = (@ARGV);
1887     } else {
1888         badusage "incorrect arguments to dgit push";
1889     }
1890     $isuite = getfield $clogp, 'Distribution';
1891     if ($new_package) {
1892         local ($package) = $existing_package; # this is a hack
1893         canonicalise_suite();
1894     }
1895     if (defined $specsuite && $specsuite ne $isuite) {
1896         canonicalise_suite();
1897         $csuite eq $specsuite or
1898             fail "dgit push: changelog specifies $isuite ($csuite)".
1899                 " but command line specifies $specsuite";
1900     }
1901     if (check_for_git()) {
1902         git_fetch_us();
1903     }
1904     if (fetch_from_archive()) {
1905         is_fast_fwd(lrref(), 'HEAD') or
1906             fail "dgit push: HEAD is not a descendant".
1907                 " of the archive's version.\n".
1908                 "$us: To overwrite it, use git merge -s ours ".lrref().".";
1909     } else {
1910         $new_package or
1911             fail "package appears to be new in this suite;".
1912                 " if this is intentional, use --new";
1913     }
1914     dopush();
1915 }
1916
1917 #---------- remote commands' implementation ----------
1918
1919 sub cmd_remote_push_build_host {
1920     my ($nrargs) = shift @ARGV;
1921     my (@rargs) = @ARGV[0..$nrargs-1];
1922     @ARGV = @ARGV[$nrargs..$#ARGV];
1923     die unless @rargs;
1924     my ($dir,$vsnwant) = @rargs;
1925     # vsnwant is a comma-separated list; we report which we have
1926     # chosen in our ready response (so other end can tell if they
1927     # offered several)
1928     $debugprefix = ' ';
1929     $we_are_responder = 1;
1930
1931     open PI, "<&STDIN" or die $!;
1932     open STDIN, "/dev/null" or die $!;
1933     open PO, ">&STDOUT" or die $!;
1934     autoflush PO 1;
1935     open STDOUT, ">&STDERR" or die $!;
1936     autoflush STDOUT 1;
1937
1938     $vsnwant //= 1;
1939     fail "build host has dgit rpush protocol version".
1940         " $rpushprotovsn but invocation host has $vsnwant"
1941         unless grep { $rpushprotovsn eq $_ } split /,/, $vsnwant;
1942
1943     responder_send_command("dgit-remote-push-ready $rpushprotovsn");
1944
1945     changedir $dir;
1946     &cmd_push;
1947 }
1948
1949 sub cmd_remote_push_responder { cmd_remote_push_build_host(); }
1950 # ... for compatibility with proto vsn.1 dgit (just so that user gets
1951 #     a good error message)
1952
1953 our $i_tmp;
1954
1955 sub i_cleanup {
1956     local ($@, $?);
1957     my $report = i_child_report();
1958     if (defined $report) {
1959         printdebug "($report)\n";
1960     } elsif ($i_child_pid) {
1961         printdebug "(killing build host child $i_child_pid)\n";
1962         kill 15, $i_child_pid;
1963     }
1964     if (defined $i_tmp && !defined $initiator_tempdir) {
1965         changedir "/";
1966         eval { rmtree $i_tmp; };
1967     }
1968 }
1969
1970 END { i_cleanup(); }
1971
1972 sub i_method {
1973     my ($base,$selector,@args) = @_;
1974     $selector =~ s/\-/_/g;
1975     { no strict qw(refs); &{"${base}_${selector}"}(@args); }
1976 }
1977
1978 sub cmd_rpush {
1979     my $host = nextarg;
1980     my $dir;
1981     if ($host =~ m/^((?:[^][]|\[[^][]*\])*)\:/) {
1982         $host = $1;
1983         $dir = $'; #';
1984     } else {
1985         $dir = nextarg;
1986     }
1987     $dir =~ s{^-}{./-};
1988     my @rargs = ($dir,$rpushprotovsn);
1989     my @rdgit;
1990     push @rdgit, @dgit;
1991     push @rdgit, @ropts;
1992     push @rdgit, qw(remote-push-build-host), (scalar @rargs), @rargs;
1993     push @rdgit, @ARGV;
1994     my @cmd = (@ssh, $host, shellquote @rdgit);
1995     debugcmd "+",@cmd;
1996
1997     if (defined $initiator_tempdir) {
1998         rmtree $initiator_tempdir;
1999         mkdir $initiator_tempdir, 0700 or die "$initiator_tempdir: $!";
2000         $i_tmp = $initiator_tempdir;
2001     } else {
2002         $i_tmp = tempdir();
2003     }
2004     $i_child_pid = open2(\*RO, \*RI, @cmd);
2005     changedir $i_tmp;
2006     initiator_expect { m/^dgit-remote-push-ready/ };
2007     for (;;) {
2008         my ($icmd,$iargs) = initiator_expect {
2009             m/^(\S+)(?: (.*))?$/;
2010             ($1,$2);
2011         };
2012         i_method "i_resp", $icmd, $iargs;
2013     }
2014 }
2015
2016 sub i_resp_progress ($) {
2017     my ($rhs) = @_;
2018     my $msg = protocol_read_bytes \*RO, $rhs;
2019     progress $msg;
2020 }
2021
2022 sub i_resp_complete {
2023     my $pid = $i_child_pid;
2024     $i_child_pid = undef; # prevents killing some other process with same pid
2025     printdebug "waiting for build host child $pid...\n";
2026     my $got = waitpid $pid, 0;
2027     die $! unless $got == $pid;
2028     die "build host child failed $?" if $?;
2029
2030     i_cleanup();
2031     printdebug "all done\n";
2032     exit 0;
2033 }
2034
2035 sub i_resp_file ($) {
2036     my ($keyword) = @_;
2037     my $localname = i_method "i_localname", $keyword;
2038     my $localpath = "$i_tmp/$localname";
2039     stat_exists $localpath and
2040         badproto \*RO, "file $keyword ($localpath) twice";
2041     protocol_receive_file \*RO, $localpath;
2042     i_method "i_file", $keyword;
2043 }
2044
2045 our %i_param;
2046
2047 sub i_resp_param ($) {
2048     $_[0] =~ m/^(\S+) (.*)$/ or badproto \*RO, "bad param spec";
2049     $i_param{$1} = $2;
2050 }
2051
2052 sub i_resp_supersedes ($) {
2053     $_[0] =~ m#^(refs/tags/\S+)=(\w+)$#
2054         or badproto \*RO, "bad supersedes spec";
2055     my $r = system qw(git check-ref-format), $1;
2056     die "bad supersedes ref spec ($r)" if $r;
2057     $supersedes{$1} = $2;
2058 }
2059
2060 our %i_wanted;
2061
2062 sub i_resp_want ($) {
2063     my ($keyword) = @_;
2064     die "$keyword ?" if $i_wanted{$keyword}++;
2065     my @localpaths = i_method "i_want", $keyword;
2066     printdebug "[[  $keyword @localpaths\n";
2067     foreach my $localpath (@localpaths) {
2068         protocol_send_file \*RI, $localpath;
2069     }
2070     print RI "files-end\n" or die $!;
2071 }
2072
2073 our ($i_clogp, $i_version, $i_tag, $i_dscfn, $i_changesfn);
2074
2075 sub i_localname_parsed_changelog {
2076     return "remote-changelog.822";
2077 }
2078 sub i_file_parsed_changelog {
2079     ($i_clogp, $i_version, $i_tag, $i_dscfn) =
2080         push_parse_changelog "$i_tmp/remote-changelog.822";
2081     die if $i_dscfn =~ m#/|^\W#;
2082 }
2083
2084 sub i_localname_dsc {
2085     defined $i_dscfn or badproto \*RO, "dsc (before parsed-changelog)";
2086     return $i_dscfn;
2087 }
2088 sub i_file_dsc { }
2089
2090 sub i_localname_changes {
2091     defined $i_dscfn or badproto \*RO, "dsc (before parsed-changelog)";
2092     $i_changesfn = $i_dscfn;
2093     $i_changesfn =~ s/\.dsc$/_dgit.changes/ or die;
2094     return $i_changesfn;
2095 }
2096 sub i_file_changes { }
2097
2098 sub i_want_signed_tag {
2099     printdebug Dumper(\%i_param, $i_dscfn);
2100     defined $i_param{'head'} && defined $i_dscfn && defined $i_clogp
2101         && defined $i_param{'csuite'}
2102         or badproto \*RO, "premature desire for signed-tag";
2103     my $head = $i_param{'head'};
2104     die if $head =~ m/[^0-9a-f]/ || $head !~ m/^../;
2105
2106     die unless $i_param{'csuite'} =~ m/^$suite_re$/;
2107     $csuite = $&;
2108     push_parse_dsc $i_dscfn, 'remote dsc', $i_version;
2109
2110     my $tagobjfn =
2111         push_mktag $head, $i_clogp, $i_tag,
2112             $i_dscfn,
2113             $i_changesfn, 'remote changes',
2114             sub { "tag$_[0]"; };
2115
2116     return $tagobjfn;
2117 }
2118
2119 sub i_want_signed_dsc_changes {
2120     rename "$i_dscfn.tmp","$i_dscfn" or die "$i_dscfn $!";
2121     sign_changes $i_changesfn;
2122     return ($i_dscfn, $i_changesfn);
2123 }
2124
2125 #---------- building etc. ----------
2126
2127 our $version;
2128 our $sourcechanges;
2129 our $dscfn;
2130
2131 #----- `3.0 (quilt)' handling -----
2132
2133 our $fakeeditorenv = 'DGIT_FAKE_EDITOR_QUILT';
2134
2135 sub quiltify_dpkg_commit ($$$;$) {
2136     my ($patchname,$author,$msg, $xinfo) = @_;
2137     $xinfo //= '';
2138
2139     mkpath '.git/dgit';
2140     my $descfn = ".git/dgit/quilt-description.tmp";
2141     open O, '>', $descfn or die "$descfn: $!";
2142     $msg =~ s/\s+$//g;
2143     $msg =~ s/\n/\n /g;
2144     $msg =~ s/^\s+$/ ./mg;
2145     print O <<END or die $!;
2146 Description: $msg
2147 Author: $author
2148 $xinfo
2149 ---
2150
2151 END
2152     close O or die $!;
2153
2154     {
2155         local $ENV{'EDITOR'} = cmdoutput qw(realpath --), $0;
2156         local $ENV{'VISUAL'} = $ENV{'EDITOR'};
2157         local $ENV{$fakeeditorenv} = cmdoutput qw(realpath --), $descfn;
2158         runcmd_ordryrun_local @dpkgsource, qw(--commit .), $patchname;
2159     }
2160 }
2161
2162 sub quiltify_trees_differ ($$) {
2163     my ($x,$y) = @_;
2164     # returns 1 iff the two tree objects differ other than in debian/
2165     local $/=undef;
2166     my @cmd = (@git, qw(diff-tree --name-only -z), $x, $y);
2167     my $diffs= cmdoutput @cmd;
2168     foreach my $f (split /\0/, $diffs) {
2169         next if $f eq 'debian';
2170         return 1;
2171     }
2172     return 0;
2173 }
2174
2175 sub quiltify_tree_sentinelfiles ($) {
2176     # lists the `sentinel' files present in the tree
2177     my ($x) = @_;
2178     my $r = cmdoutput @git, qw(ls-tree --name-only), $x,
2179         qw(-- debian/rules debian/control);
2180     $r =~ s/\n/,/g;
2181     return $r;
2182 }
2183
2184 sub quiltify ($$) {
2185     my ($clogp,$target) = @_;
2186
2187     # Quilt patchification algorithm
2188     #
2189     # We search backwards through the history of the main tree's HEAD
2190     # (T) looking for a start commit S whose tree object is identical
2191     # to to the patch tip tree (ie the tree corresponding to the
2192     # current dpkg-committed patch series).  For these purposes
2193     # `identical' disregards anything in debian/ - this wrinkle is
2194     # necessary because dpkg-source treates debian/ specially.
2195     #
2196     # We can only traverse edges where at most one of the ancestors'
2197     # trees differs (in changes outside in debian/).  And we cannot
2198     # handle edges which change .pc/ or debian/patches.  To avoid
2199     # going down a rathole we avoid traversing edges which introduce
2200     # debian/rules or debian/control.  And we set a limit on the
2201     # number of edges we are willing to look at.
2202     #
2203     # If we succeed, we walk forwards again.  For each traversed edge
2204     # PC (with P parent, C child) (starting with P=S and ending with
2205     # C=T) to we do this:
2206     #  - git checkout C
2207     #  - dpkg-source --commit with a patch name and message derived from C
2208     # After traversing PT, we git commit the changes which
2209     # should be contained within debian/patches.
2210
2211     changedir '../fake';
2212     mktree_in_ud_here();
2213     rmtree '.pc';
2214     runcmd @git, 'add', '.';
2215     my $oldtiptree=git_write_tree();
2216     changedir '../work';
2217
2218     # The search for the path S..T is breadth-first.  We maintain a
2219     # todo list containing search nodes.  A search node identifies a
2220     # commit, and looks something like this:
2221     #  $p = {
2222     #      Commit => $git_commit_id,
2223     #      Child => $c,                          # or undef if P=T
2224     #      Whynot => $reason_edge_PC_unsuitable, # in @nots only
2225     #      Nontrivial => true iff $p..$c has relevant changes
2226     #  };
2227
2228     my @todo;
2229     my @nots;
2230     my $sref_S;
2231     my $max_work=100;
2232     my %considered; # saves being exponential on some weird graphs
2233
2234     my $t_sentinels = quiltify_tree_sentinelfiles $target;
2235
2236     my $not = sub {
2237         my ($search,$whynot) = @_;
2238         printdebug " search NOT $search->{Commit} $whynot\n";
2239         $search->{Whynot} = $whynot;
2240         push @nots, $search;
2241         no warnings qw(exiting);
2242         next;
2243     };
2244
2245     push @todo, {
2246         Commit => $target,
2247     };
2248
2249     while (@todo) {
2250         my $c = shift @todo;
2251         next if $considered{$c->{Commit}}++;
2252
2253         $not->($c, "maximum search space exceeded") if --$max_work <= 0;
2254
2255         printdebug "quiltify investigate $c->{Commit}\n";
2256
2257         # are we done?
2258         if (!quiltify_trees_differ $c->{Commit}, $oldtiptree) {
2259             printdebug " search finished hooray!\n";
2260             $sref_S = $c;
2261             last;
2262         }
2263
2264         if ($quilt_mode eq 'nofix') {
2265             fail "quilt fixup required but quilt mode is \`nofix'\n".
2266                 "HEAD commit $c->{Commit} differs from tree implied by ".
2267                 " debian/patches (tree object $oldtiptree)";
2268         }
2269         if ($quilt_mode eq 'smash') {
2270             printdebug " search quitting smash\n";
2271             last;
2272         }
2273
2274         my $c_sentinels = quiltify_tree_sentinelfiles $c->{Commit};
2275         $not->($c, "has $c_sentinels not $t_sentinels")
2276             if $c_sentinels ne $t_sentinels;
2277
2278         my $commitdata = cmdoutput @git, qw(cat-file commit), $c->{Commit};
2279         $commitdata =~ m/\n\n/;
2280         $commitdata =~ $`;
2281         my @parents = ($commitdata =~ m/^parent (\w+)$/gm);
2282         @parents = map { { Commit => $_, Child => $c } } @parents;
2283
2284         $not->($c, "root commit") if !@parents;
2285
2286         foreach my $p (@parents) {
2287             $p->{Nontrivial}= quiltify_trees_differ $p->{Commit},$c->{Commit};
2288         }
2289         my $ndiffers = grep { $_->{Nontrivial} } @parents;
2290         $not->($c, "merge ($ndiffers nontrivial parents)") if $ndiffers > 1;
2291
2292         foreach my $p (@parents) {
2293             printdebug "considering C=$c->{Commit} P=$p->{Commit}\n";
2294
2295             my @cmd= (@git, qw(diff-tree -r --name-only),
2296                       $p->{Commit},$c->{Commit}, qw(-- debian/patches .pc));
2297             my $patchstackchange = cmdoutput @cmd;
2298             if (length $patchstackchange) {
2299                 $patchstackchange =~ s/\n/,/g;
2300                 $not->($p, "changed $patchstackchange");
2301             }
2302
2303             printdebug " search queue P=$p->{Commit} ",
2304                 ($p->{Nontrivial} ? "NT" : "triv"),"\n";
2305             push @todo, $p;
2306         }
2307     }
2308
2309     if (!$sref_S) {
2310         printdebug "quiltify want to smash\n";
2311
2312         my $abbrev = sub {
2313             my $x = $_[0]{Commit};
2314             $x =~ s/(.*?[0-9a-z]{8})[0-9a-z]*$/$1/;
2315             return $;
2316         };
2317         my $reportnot = sub {
2318             my ($notp) = @_;
2319             my $s = $abbrev->($notp);
2320             my $c = $notp->{Child};
2321             $s .= "..".$abbrev->($c) if $c;
2322             $s .= ": ".$c->{Whynot};
2323             return $s;
2324         };
2325         if ($quilt_mode eq 'linear') {
2326             print STDERR "$us: quilt fixup cannot be linear.  Stopped at:\n";
2327             foreach my $notp (@nots) {
2328                 print STDERR "$us:  ", $reportnot->($notp), "\n";
2329             }
2330             fail "quilt fixup naive history linearisation failed.\n".
2331  "Use dpkg-source --commit by hand; or, --quilt=smash for one ugly patch";
2332         } elsif ($quilt_mode eq 'smash') {
2333         } elsif ($quilt_mode eq 'auto') {
2334             progress "quilt fixup cannot be linear, smashing...";
2335         } else {
2336             die "$quilt_mode ?";
2337         }
2338
2339         my $time = time;
2340         my $ncommits = 3;
2341         my $msg = cmdoutput @git, qw(log), "-n$ncommits";
2342
2343         quiltify_dpkg_commit "auto-$version-$target-$time",
2344             (getfield $clogp, 'Maintainer'),
2345             "Automatically generated patch ($clogp->{Version})\n".
2346             "Last (up to) $ncommits git changes, FYI:\n\n". $msg;
2347         return;
2348     }
2349
2350     progress "quiltify linearisation planning successful, executing...";
2351
2352     for (my $p = $sref_S;
2353          my $c = $p->{Child};
2354          $p = $p->{Child}) {
2355         printdebug "quiltify traverse $p->{Commit}..$c->{Commit}\n";
2356         next unless $p->{Nontrivial};
2357
2358         my $cc = $c->{Commit};
2359
2360         my $commitdata = cmdoutput @git, qw(cat-file commit), $cc;
2361         $commitdata =~ m/\n\n/ or die "$c ?";
2362         $commitdata = $`;
2363         my $msg = $'; #';
2364         $commitdata =~ m/^author (.*) \d+ [-+0-9]+$/m or die "$cc ?";
2365         my $author = $1;
2366
2367         $msg =~ s/^(.*)\n*/$1\n/ or die "$cc $msg ?";
2368
2369         my $title = $1;
2370         my $patchname = $title;
2371         $patchname =~ s/[.:]$//;
2372         $patchname =~ y/ A-Z/-a-z/;
2373         $patchname =~ y/-a-z0-9_.+=~//cd;
2374         $patchname =~ s/^\W/x-$&/;
2375         $patchname = substr($patchname,0,40);
2376         my $index;
2377         for ($index='';
2378              stat "debian/patches/$patchname$index";
2379              $index++) { }
2380         $!==ENOENT or die "$patchname$index $!";
2381
2382         runcmd @git, qw(checkout -q), $cc;
2383
2384         # We use the tip's changelog so that dpkg-source doesn't
2385         # produce complaining messages from dpkg-parsechangelog.  None
2386         # of the information dpkg-source gets from the changelog is
2387         # actually relevant - it gets put into the original message
2388         # which dpkg-source provides our stunt editor, and then
2389         # overwritten.
2390         runcmd @git, qw(checkout -q), $target, qw(debian/changelog);
2391
2392         quiltify_dpkg_commit "$patchname$index", $author, $msg,
2393             "X-Dgit-Generated: $clogp->{Version} $cc\n";
2394
2395         runcmd @git, qw(checkout -q), $cc, qw(debian/changelog);
2396     }
2397
2398     runcmd @git, qw(checkout -q master);
2399 }
2400
2401 sub build_maybe_quilt_fixup () {
2402     my $format=get_source_format;
2403     return unless madformat $format;
2404     # sigh
2405
2406     # Our objective is:
2407     #  - honour any existing .pc in case it has any strangeness
2408     #  - determine the git commit corresponding to the tip of
2409     #    the patch stack (if there is one)
2410     #  - if there is such a git commit, convert each subsequent
2411     #    git commit into a quilt patch with dpkg-source --commit
2412     #  - otherwise convert all the differences in the tree into
2413     #    a single git commit
2414     #
2415     # To do this we:
2416
2417     # Our git tree doesn't necessarily contain .pc.  (Some versions of
2418     # dgit would include the .pc in the git tree.)  If there isn't
2419     # one, we need to generate one by unpacking the patches that we
2420     # have.
2421     #
2422     # We first look for a .pc in the git tree.  If there is one, we
2423     # will use it.  (This is not the normal case.)
2424     #
2425     # Otherwise need to regenerate .pc so that dpkg-source --commit
2426     # can work.  We do this as follows:
2427     #     1. Collect all relevant .orig from parent directory
2428     #     2. Generate a debian.tar.gz out of
2429     #         debian/{patches,rules,source/format}
2430     #     3. Generate a fake .dsc containing just these fields:
2431     #          Format Source Version Files
2432     #     4. Extract the fake .dsc
2433     #        Now the fake .dsc has a .pc directory.
2434     # (In fact we do this in every case, because in future we will
2435     # want to search for a good base commit for generating patches.)
2436     #
2437     # Then we can actually do the dpkg-source --commit
2438     #     1. Make a new working tree with the same object
2439     #        store as our main tree and check out the main
2440     #        tree's HEAD.
2441     #     2. Copy .pc from the fake's extraction, if necessary
2442     #     3. Run dpkg-source --commit
2443     #     4. If the result has changes to debian/, then
2444     #          - git-add them them
2445     #          - git-add .pc if we had a .pc in-tree
2446     #          - git-commit
2447     #     5. If we had a .pc in-tree, delete it, and git-commit
2448     #     6. Back in the main tree, fast forward to the new HEAD
2449
2450     my $clogp = parsechangelog();
2451     my $headref = rev_parse('HEAD');
2452
2453     prep_ud();
2454     changedir $ud;
2455
2456     my $upstreamversion=$version;
2457     $upstreamversion =~ s/-[^-]*$//;
2458
2459     my $fakeversion="$upstreamversion-~~DGITFAKE";
2460
2461     my $fakedsc=new IO::File 'fake.dsc', '>' or die $!;
2462     print $fakedsc <<END or die $!;
2463 Format: 3.0 (quilt)
2464 Source: $package
2465 Version: $fakeversion
2466 Files:
2467 END
2468
2469     my $dscaddfile=sub {
2470         my ($b) = @_;
2471         
2472         my $md = new Digest::MD5;
2473
2474         my $fh = new IO::File $b, '<' or die "$b $!";
2475         stat $fh or die $!;
2476         my $size = -s _;
2477
2478         $md->addfile($fh);
2479         print $fakedsc " ".$md->hexdigest." $size $b\n" or die $!;
2480     };
2481
2482     foreach my $f (<../../../../*>) { #/){
2483         my $b=$f; $b =~ s{.*/}{};
2484         next unless is_orig_file $b, srcfn $upstreamversion,'';
2485         link $f, $b or die "$b $!";
2486         $dscaddfile->($b);
2487     }
2488
2489     my @files=qw(debian/source/format debian/rules);
2490     if (stat_exists '../../../debian/patches') {
2491         push @files, 'debian/patches';
2492     }
2493
2494     my $debtar= srcfn $fakeversion,'.debian.tar.gz';
2495     runcmd qw(env GZIP=-1 tar -zcf), "./$debtar", qw(-C ../../..), @files;
2496
2497     $dscaddfile->($debtar);
2498     close $fakedsc or die $!;
2499
2500     runcmd qw(sh -ec), 'exec dpkg-source --no-check -x fake.dsc >/dev/null';
2501
2502     my $fakexdir= $package.'-'.(stripepoch $upstreamversion);
2503     rename $fakexdir, "fake" or die "$fakexdir $!";
2504
2505     mkdir "work" or die $!;
2506     changedir "work";
2507     mktree_in_ud_here();
2508     runcmd @git, qw(reset --hard), $headref;
2509
2510     my $mustdeletepc=0;
2511     if (stat_exists ".pc") {
2512         -d _ or die;
2513         progress "Tree already contains .pc - will use it then delete it.";
2514         $mustdeletepc=1;
2515     } else {
2516         rename '../fake/.pc','.pc' or die $!;
2517     }
2518
2519     quiltify($clogp,$headref);
2520
2521     if (!open P, '>>', ".pc/applied-patches") {
2522         $!==&ENOENT or die $!;
2523     } else {
2524         close P;
2525     }
2526
2527     commit_quilty_patch();
2528
2529     if ($mustdeletepc) {
2530         runcmd @git, qw(rm -rq .pc);
2531         commit_admin "Commit removal of .pc (quilt series tracking data)";
2532     }
2533
2534     changedir '../../../..';
2535     runcmd @git, qw(pull --ff-only -q .git/dgit/unpack/work master);
2536 }
2537
2538 sub quilt_fixup_editor () {
2539     my $descfn = $ENV{$fakeeditorenv};
2540     my $editing = $ARGV[$#ARGV];
2541     open I1, '<', $descfn or die "$descfn: $!";
2542     open I2, '<', $editing or die "$editing: $!";
2543     unlink $editing or die "$editing: $!";
2544     open O, '>', $editing or die "$editing: $!";
2545     while (<I1>) { print O or die $!; } I1->error and die $!;
2546     my $copying = 0;
2547     while (<I2>) {
2548         $copying ||= m/^\-\-\- /;
2549         next unless $copying;
2550         print O or die $!;
2551     }
2552     I2->error and die $!;
2553     close O or die $1;
2554     exit 0;
2555 }
2556
2557 #----- other building -----
2558
2559 sub clean_tree () {
2560     if ($cleanmode eq 'dpkg-source') {
2561         runcmd_ordryrun_local @dpkgbuildpackage, qw(-T clean);
2562     } elsif ($cleanmode eq 'git') {
2563         runcmd_ordryrun_local @git, qw(clean -xdf);
2564     } elsif ($cleanmode eq 'none') {
2565     } else {
2566         die "$cleanmode ?";
2567     }
2568 }
2569
2570 sub cmd_clean () {
2571     badusage "clean takes no additional arguments" if @ARGV;
2572     clean_tree();
2573 }
2574
2575 sub build_prep () {
2576     badusage "-p is not allowed when building" if defined $package;
2577     check_not_dirty();
2578     clean_tree();
2579     my $clogp = parsechangelog();
2580     $isuite = getfield $clogp, 'Distribution';
2581     $package = getfield $clogp, 'Source';
2582     $version = getfield $clogp, 'Version';
2583     build_maybe_quilt_fixup();
2584 }
2585
2586 sub changesopts () {
2587     my @opts =@changesopts[1..$#changesopts];
2588     if (!defined $changes_since_version) {
2589         my @vsns = archive_query('archive_query');
2590         my @quirk = access_quirk();
2591         if ($quirk[0] eq 'backports') {
2592             local $isuite = $quirk[2];
2593             local $csuite;
2594             canonicalise_suite();
2595             push @vsns, archive_query('archive_query');
2596         }
2597         if (@vsns) {
2598             @vsns = map { $_->[0] } @vsns;
2599             @vsns = sort { -version_compare($a, $b) } @vsns;
2600             $changes_since_version = $vsns[0];
2601             progress "changelog will contain changes since $vsns[0]";
2602         } else {
2603             $changes_since_version = '_';
2604             progress "package seems new, not specifying -v<version>";
2605         }
2606     }
2607     if ($changes_since_version ne '_') {
2608         unshift @opts, "-v$changes_since_version";
2609     }
2610     return @opts;
2611 }
2612
2613 sub cmd_build {
2614     build_prep();
2615     runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc), changesopts(), @ARGV;
2616     printdone "build successful\n";
2617 }
2618
2619 sub cmd_git_build {
2620     build_prep();
2621     my @cmd =
2622         (qw(git-buildpackage -us -uc --git-no-sign-tags),
2623          "--git-builder=@dpkgbuildpackage");
2624     unless (grep { m/^--git-debian-branch|^--git-ignore-branch/ } @ARGV) {
2625         canonicalise_suite();
2626         push @cmd, "--git-debian-branch=".lbranch();
2627     }
2628     push @cmd, changesopts();
2629     runcmd_ordryrun_local @cmd, @ARGV;
2630     printdone "build successful\n";
2631 }
2632
2633 sub build_source {
2634     build_prep();
2635     $sourcechanges = "${package}_".(stripepoch $version)."_source.changes";
2636     $dscfn = dscfn($version);
2637     if ($cleanmode eq 'dpkg-source') {
2638         runcmd_ordryrun_local (@dpkgbuildpackage, qw(-us -uc -S)),
2639             changesopts();
2640     } else {
2641         my $pwd = must_getcwd();
2642         my $leafdir = basename $pwd;
2643         changedir "..";
2644         runcmd_ordryrun_local @dpkgsource, qw(-b --), $leafdir;
2645         changedir $pwd;
2646         runcmd_ordryrun_local qw(sh -ec),
2647             'exec >$1; shift; exec "$@"','x',
2648             "../$sourcechanges",
2649             @dpkggenchanges, qw(-S), changesopts();
2650     }
2651 }
2652
2653 sub cmd_build_source {
2654     badusage "build-source takes no additional arguments" if @ARGV;
2655     build_source();
2656     printdone "source built, results in $dscfn and $sourcechanges";
2657 }
2658
2659 sub cmd_sbuild {
2660     build_source();
2661     changedir "..";
2662     my $pat = "${package}_".(stripepoch $version)."_*.changes";
2663     if (act_local()) {
2664         stat_exist $dscfn or fail "$dscfn (in parent directory): $!";
2665         stat_exists $sourcechanges
2666             or fail "$sourcechanges (in parent directory): $!";
2667         foreach my $cf (glob $pat) {
2668             next if $cf eq $sourcechanges;
2669             unlink $cf or fail "remove $cf: $!";
2670         }
2671     }
2672     runcmd_ordryrun_local @sbuild, @ARGV, qw(-d), $isuite, $dscfn;
2673     my @changesfiles = glob $pat;
2674     @changesfiles = sort {
2675         ($b =~ m/_source\.changes$/ <=> $a =~ m/_source\.changes$/)
2676             or $a cmp $b
2677     } @changesfiles;
2678     fail "wrong number of different changes files (@changesfiles)"
2679         unless @changesfiles;
2680     runcmd_ordryrun_local @mergechanges, @changesfiles;
2681     my $multichanges = "${package}_".(stripepoch $version)."_multi.changes";
2682     if (act_local()) {
2683         stat_exists $multichanges or fail "$multichanges: $!";
2684     }
2685     printdone "build successful, results in $multichanges\n" or die $!;
2686 }    
2687
2688 sub cmd_quilt_fixup {
2689     badusage "incorrect arguments to dgit quilt-fixup" if @ARGV;
2690     my $clogp = parsechangelog();
2691     $version = getfield $clogp, 'Version';
2692     $package = getfield $clogp, 'Source';
2693     build_maybe_quilt_fixup();
2694 }
2695
2696 sub cmd_archive_api_query {
2697     badusage "need only 1 subpath argument" unless @ARGV==1;
2698     my ($subpath) = @ARGV;
2699     my @cmd = archive_api_query_cmd($subpath);
2700     exec @cmd or fail "exec curl: $!\n";
2701 }
2702
2703 #---------- argument parsing and main program ----------
2704
2705 sub cmd_version {
2706     print "dgit version $our_version\n" or die $!;
2707     exit 0;
2708 }
2709
2710 sub parseopts () {
2711     my $om;
2712
2713     if (defined $ENV{'DGIT_SSH'}) {
2714         @ssh = string_to_ssh $ENV{'DGIT_SSH'};
2715     } elsif (defined $ENV{'GIT_SSH'}) {
2716         @ssh = ($ENV{'GIT_SSH'});
2717     }
2718
2719     while (@ARGV) {
2720         last unless $ARGV[0] =~ m/^-/;
2721         $_ = shift @ARGV;
2722         last if m/^--?$/;
2723         if (m/^--/) {
2724             if (m/^--dry-run$/) {
2725                 push @ropts, $_;
2726                 $dryrun_level=2;
2727             } elsif (m/^--damp-run$/) {
2728                 push @ropts, $_;
2729                 $dryrun_level=1;
2730             } elsif (m/^--no-sign$/) {
2731                 push @ropts, $_;
2732                 $sign=0;
2733             } elsif (m/^--help$/) {
2734                 cmd_help();
2735             } elsif (m/^--version$/) {
2736                 cmd_version();
2737             } elsif (m/^--new$/) {
2738                 push @ropts, $_;
2739                 $new_package=1;
2740             } elsif (m/^--since-version=([^_]+|_)$/) {
2741                 push @ropts, $_;
2742                 $changes_since_version = $1;
2743             } elsif (m/^--([-0-9a-z]+)=(.*)/s &&
2744                      ($om = $opts_opt_map{$1}) &&
2745                      length $om->[0]) {
2746                 push @ropts, $_;
2747                 $om->[0] = $2;
2748             } elsif (m/^--([-0-9a-z]+):(.*)/s &&
2749                      !$opts_opt_cmdonly{$1} &&
2750                      ($om = $opts_opt_map{$1})) {
2751                 push @ropts, $_;
2752                 push @$om, $2;
2753             } elsif (m/^--existing-package=(.*)/s) {
2754                 push @ropts, $_;
2755                 $existing_package = $1;
2756             } elsif (m/^--initiator-tempdir=(.*)/s) {
2757                 $initiator_tempdir = $1;
2758                 $initiator_tempdir =~ m#^/# or
2759                     badusage "--initiator-tempdir must be used specify an".
2760                         " absolute, not relative, directory."
2761             } elsif (m/^--distro=(.*)/s) {
2762                 push @ropts, $_;
2763                 $idistro = $1;
2764             } elsif (m/^--build-products-dir=(.*)/s) {
2765                 push @ropts, $_;
2766                 $buildproductsdir = $1;
2767             } elsif (m/^--clean=(dpkg-source|git|none)$/s) {
2768                 push @ropts, $_;
2769                 $cleanmode = $1;
2770             } elsif (m/^--clean=(.*)$/s) {
2771                 badusage "unknown cleaning mode \`$1'";
2772             } elsif (m/^--quilt=($quilt_modes_re)$/s) {
2773                 push @ropts, $_;
2774                 $quilt_mode = $1;
2775             } elsif (m/^--quilt=(.*)$/s) {
2776                 badusage "unknown quilt fixup mode \`$1'";
2777             } elsif (m/^--ignore-dirty$/s) {
2778                 push @ropts, $_;
2779                 $ignoredirty = 1;
2780             } elsif (m/^--no-quilt-fixup$/s) {
2781                 push @ropts, $_;
2782                 $quilt_mode = 'nocheck';
2783             } elsif (m/^--no-rm-on-error$/s) {
2784                 push @ropts, $_;
2785                 $rmonerror = 0;
2786             } elsif (m/^--deliberately-($suite_re)$/s) {
2787                 push @ropts, $_;
2788                 push @deliberatelies, $&;
2789             } else {
2790                 badusage "unknown long option \`$_'";
2791             }
2792         } else {
2793             while (m/^-./s) {
2794                 if (s/^-n/-/) {
2795                     push @ropts, $&;
2796                     $dryrun_level=2;
2797                 } elsif (s/^-L/-/) {
2798                     push @ropts, $&;
2799                     $dryrun_level=1;
2800                 } elsif (s/^-h/-/) {
2801                     cmd_help();
2802                 } elsif (s/^-D/-/) {
2803                     push @ropts, $&;
2804                     $debuglevel++;
2805                     enabledebug();
2806                 } elsif (s/^-N/-/) {
2807                     push @ropts, $&;
2808                     $new_package=1;
2809                 } elsif (s/^-v([^_]+|_)$//s) {
2810                     push @ropts, $&;
2811                     $changes_since_version = $1;
2812                 } elsif (m/^-m/) {
2813                     push @ropts, $&;
2814                     push @changesopts, $_;
2815                     $_ = '';
2816                 } elsif (s/^-c(.*=.*)//s) {
2817                     push @ropts, $&;
2818                     push @git, '-c', $1;
2819                 } elsif (s/^-d(.+)//s) {
2820                     push @ropts, $&;
2821                     $idistro = $1;
2822                 } elsif (s/^-C(.+)//s) {
2823                     push @ropts, $&;
2824                     $changesfile = $1;
2825                     if ($changesfile =~ s#^(.*)/##) {
2826                         $buildproductsdir = $1;
2827                     }
2828                 } elsif (s/^-k(.+)//s) {
2829                     $keyid=$1;
2830                 } elsif (m/^-[vdCk]$/) {
2831                     badusage
2832  "option \`$_' requires an argument (and no space before the argument)";
2833                 } elsif (s/^-wn$//s) {
2834                     push @ropts, $&;
2835                     $cleanmode = 'none';
2836                 } elsif (s/^-wg$//s) {
2837                     push @ropts, $&;
2838                     $cleanmode = 'git';
2839                 } elsif (s/^-wd$//s) {
2840                     push @ropts, $&;
2841                     $cleanmode = 'dpkg-source';
2842                 } else {
2843                     badusage "unknown short option \`$_'";
2844                 }
2845             }
2846         }
2847     }
2848 }
2849
2850 if ($ENV{$fakeeditorenv}) {
2851     quilt_fixup_editor();
2852 }
2853
2854 parseopts();
2855 print STDERR "DRY RUN ONLY\n" if $dryrun_level > 1;
2856 print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n"
2857     if $dryrun_level == 1;
2858 if (!@ARGV) {
2859     print STDERR $helpmsg or die $!;
2860     exit 8;
2861 }
2862 my $cmd = shift @ARGV;
2863 $cmd =~ y/-/_/;
2864
2865 if (!defined $quilt_mode) {
2866     $quilt_mode = cfg('dgit.force.quilt-mode', 'RETURN-UNDEF')
2867         // access_cfg('quilt-mode', 'RETURN-UNDEF')
2868         // 'linear';
2869     $quilt_mode =~ m/^($quilt_modes_re)$/ 
2870         or badcfg "unknown quilt-mode \`$quilt_mode'";
2871     $quilt_mode = $1;
2872 }
2873
2874 my $fn = ${*::}{"cmd_$cmd"};
2875 $fn or badusage "unknown operation $cmd";
2876 $fn->();