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