3 # Integration between git and Debian-style archives
5 # Copyright (C)2013-2015 Ian Jackson
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.
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.
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/>.
28 use Dpkg::Control::Hash;
30 use File::Temp qw(tempdir);
40 our $our_version = 'UNRELEASED'; ###substituted###
42 our @rpushprotovsn_support = qw(3 2);
45 our $isuite = 'unstable';
51 our $dryrun_level = 0;
53 our $buildproductsdir = '..';
59 our $existing_package = 'dpkg';
61 our $changes_since_version;
64 our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck|gbp|unapplied';
65 our $we_are_responder;
66 our $initiator_tempdir;
68 our %format_ok = map { $_=>1 } ("1.0","3.0 (native)","3.0 (quilt)");
70 our $suite_re = '[-+.0-9a-z]+';
71 our $cleanmode_re = 'dpkg-source(?:-d)?|git|git-ff|check|none';
74 our (@dget) = qw(dget);
75 our (@curl) = qw(curl -f);
76 our (@dput) = qw(dput);
77 our (@debsign) = qw(debsign);
79 our (@sbuild) = qw(sbuild);
81 our (@dgit) = qw(dgit);
82 our (@dpkgbuildpackage) = qw(dpkg-buildpackage -i\.git/ -I.git);
83 our (@dpkgsource) = qw(dpkg-source -i\.git/ -I.git);
84 our (@dpkggenchanges) = qw(dpkg-genchanges);
85 our (@mergechanges) = qw(mergechanges -f);
86 our (@gbppq) = qw(gbp-pq);
87 our (@changesopts) = ('');
89 our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
92 'debsign' => \@debsign,
98 'dpkg-source' => \@dpkgsource,
99 'dpkg-buildpackage' => \@dpkgbuildpackage,
100 'dpkg-genchanges' => \@dpkggenchanges,
101 'ch' => \@changesopts,
102 'mergechanges' => \@mergechanges);
104 our %opts_opt_cmdonly = ('gpg' => 1, 'git' => 1);
105 our %opts_cfg_insertpos = map {
107 scalar @{ $opts_opt_map{$_} }
108 } keys %opts_opt_map;
110 sub finalise_opts_opts();
116 our $supplementary_message = '';
117 our $need_split_build_invocation = 0;
118 our $split_brain = 0;
122 print STDERR "! $_\n" foreach $supplementary_message =~ m/^.+$/mg;
125 our $remotename = 'dgit';
126 our @ourdscfield = qw(Dgit Vcs-Dgit-Master);
130 sub lbranch () { return "$branchprefix/$csuite"; }
131 my $lbranch_re = '^refs/heads/'.$branchprefix.'/([^/.]+)$';
132 sub lref () { return "refs/heads/".lbranch(); }
133 sub lrref () { return "refs/remotes/$remotename/".server_branch($csuite); }
134 sub rrref () { return server_ref($csuite); }
136 sub lrfetchrefs () { return "refs/dgit-fetch/$csuite"; }
146 return "${package}_".(stripepoch $vsn).$sfx
151 return srcfn($vsn,".dsc");
154 sub changespat ($;$) {
155 my ($vsn, $arch) = @_;
156 return "${package}_".(stripepoch $vsn)."_".($arch//'*').".changes";
165 foreach my $f (@end) {
167 print STDERR "$us: cleanup: $@" if length $@;
171 sub badcfg { print STDERR "$us: invalid configuration: @_\n"; exit 12; }
173 sub no_such_package () {
174 print STDERR "$us: package $package does not exist in suite $isuite\n";
180 return "+".rrref().":".lrref();
185 printdebug "CD $newdir\n";
186 chdir $newdir or die "chdir: $newdir: $!";
189 sub deliberately ($) {
191 return !!grep { $_ eq "--deliberately-$enquiry" } @deliberatelies;
194 sub deliberately_not_fast_forward () {
195 foreach (qw(not-fast-forward fresh-repo)) {
196 return 1 if deliberately($_) || deliberately("TEST-dgit-only-$_");
200 #---------- remote protocol support, common ----------
202 # remote push initiator/responder protocol:
203 # $ dgit remote-push-build-host <n-rargs> <rargs>... <push-args>...
204 # where <rargs> is <push-host-dir> <supported-proto-vsn>,... ...
205 # < dgit-remote-push-ready <actual-proto-vsn>
207 # > file parsed-changelog
208 # [indicates that output of dpkg-parsechangelog follows]
209 # > data-block NBYTES
210 # > [NBYTES bytes of data (no newline)]
211 # [maybe some more blocks]
223 # [indicates that signed tag is wanted]
224 # < data-block NBYTES
225 # < [NBYTES bytes of data (no newline)]
226 # [maybe some more blocks]
230 # > want signed-dsc-changes
231 # < data-block NBYTES [transfer of signed dsc]
233 # < data-block NBYTES [transfer of signed changes]
241 sub i_child_report () {
242 # Sees if our child has died, and reap it if so. Returns a string
243 # describing how it died if it failed, or undef otherwise.
244 return undef unless $i_child_pid;
245 my $got = waitpid $i_child_pid, WNOHANG;
246 return undef if $got <= 0;
247 die unless $got == $i_child_pid;
248 $i_child_pid = undef;
249 return undef unless $?;
250 return "build host child ".waitstatusmsg();
255 fail "connection lost: $!" if $fh->error;
256 fail "protocol violation; $m not expected";
259 sub badproto_badread ($$) {
261 fail "connection lost: $!" if $!;
262 my $report = i_child_report();
263 fail $report if defined $report;
264 badproto $fh, "eof (reading $wh)";
267 sub protocol_expect (&$) {
268 my ($match, $fh) = @_;
271 defined && chomp or badproto_badread $fh, "protocol message";
279 badproto $fh, "\`$_'";
282 sub protocol_send_file ($$) {
283 my ($fh, $ourfn) = @_;
284 open PF, "<", $ourfn or die "$ourfn: $!";
287 my $got = read PF, $d, 65536;
288 die "$ourfn: $!" unless defined $got;
290 print $fh "data-block ".length($d)."\n" or die $!;
291 print $fh $d or die $!;
293 PF->error and die "$ourfn $!";
294 print $fh "data-end\n" or die $!;
298 sub protocol_read_bytes ($$) {
299 my ($fh, $nbytes) = @_;
300 $nbytes =~ m/^[1-9]\d{0,5}$|^0$/ or badproto \*RO, "bad byte count";
302 my $got = read $fh, $d, $nbytes;
303 $got==$nbytes or badproto_badread $fh, "data block";
307 sub protocol_receive_file ($$) {
308 my ($fh, $ourfn) = @_;
309 printdebug "() $ourfn\n";
310 open PF, ">", $ourfn or die "$ourfn: $!";
312 my ($y,$l) = protocol_expect {
313 m/^data-block (.*)$/ ? (1,$1) :
314 m/^data-end$/ ? (0,) :
318 my $d = protocol_read_bytes $fh, $l;
319 print PF $d or die $!;
324 #---------- remote protocol support, responder ----------
326 sub responder_send_command ($) {
328 return unless $we_are_responder;
329 # called even without $we_are_responder
330 printdebug ">> $command\n";
331 print PO $command, "\n" or die $!;
334 sub responder_send_file ($$) {
335 my ($keyword, $ourfn) = @_;
336 return unless $we_are_responder;
337 printdebug "]] $keyword $ourfn\n";
338 responder_send_command "file $keyword";
339 protocol_send_file \*PO, $ourfn;
342 sub responder_receive_files ($@) {
343 my ($keyword, @ourfns) = @_;
344 die unless $we_are_responder;
345 printdebug "[[ $keyword @ourfns\n";
346 responder_send_command "want $keyword";
347 foreach my $fn (@ourfns) {
348 protocol_receive_file \*PI, $fn;
351 protocol_expect { m/^files-end$/ } \*PI;
354 #---------- remote protocol support, initiator ----------
356 sub initiator_expect (&) {
358 protocol_expect { &$match } \*RO;
361 #---------- end remote code ----------
364 if ($we_are_responder) {
366 responder_send_command "progress ".length($m) or die $!;
367 print PO $m or die $!;
377 $ua = LWP::UserAgent->new();
381 progress "downloading $what...";
382 my $r = $ua->get(@_) or die $!;
383 return undef if $r->code == 404;
384 $r->is_success or fail "failed to fetch $what: ".$r->status_line;
385 return $r->decoded_content(charset => 'none');
388 our ($dscdata,$dscurl,$dsc,$dsc_checked,$skew_warning_vsn);
393 failedcmd @_ if system @_;
396 sub act_local () { return $dryrun_level <= 1; }
397 sub act_scary () { return !$dryrun_level; }
400 if (!$dryrun_level) {
401 progress "dgit ok: @_";
403 progress "would be ok: @_ (but dry run only)";
408 printcmd(\*STDERR,$debugprefix."#",@_);
411 sub runcmd_ordryrun {
419 sub runcmd_ordryrun_local {
428 my ($first_shell, @cmd) = @_;
429 return qw(sh -ec), $first_shell.'; exec "$@"', 'x', @cmd;
432 our $helpmsg = <<END;
434 dgit [dgit-opts] clone [dgit-opts] package [suite] [./dir|/dir]
435 dgit [dgit-opts] fetch|pull [dgit-opts] [suite]
436 dgit [dgit-opts] build [dpkg-buildpackage-opts]
437 dgit [dgit-opts] sbuild [sbuild-opts]
438 dgit [dgit-opts] push [dgit-opts] [suite]
439 dgit [dgit-opts] rpush build-host:build-dir ...
440 important dgit options:
441 -k<keyid> sign tag and package with <keyid> instead of default
442 --dry-run -n do not change anything, but go through the motions
443 --damp-run -L like --dry-run but make local changes, without signing
444 --new -N allow introducing a new package
445 --debug -D increase debug level
446 -c<name>=<value> set git config option (used directly by dgit too)
449 our $later_warning_msg = <<END;
450 Perhaps the upload is stuck in incoming. Using the version from git.
454 print STDERR "$us: @_\n", $helpmsg or die $!;
459 @ARGV or badusage "too few arguments";
460 return scalar shift @ARGV;
464 print $helpmsg or die $!;
468 our $td = $ENV{DGIT_TEST_DUMMY_DIR} || "DGIT_TEST_DUMMY_DIR-unset";
470 our %defcfg = ('dgit.default.distro' => 'debian',
471 'dgit.default.username' => '',
472 'dgit.default.archive-query-default-component' => 'main',
473 'dgit.default.ssh' => 'ssh',
474 'dgit.default.archive-query' => 'madison:',
475 'dgit.default.sshpsql-dbname' => 'service=projectb',
476 'dgit-distro.debian.archive-query' => 'ftpmasterapi:',
477 'dgit-distro.debian.git-check' => 'url',
478 'dgit-distro.debian.git-check-suffix' => '/info/refs',
479 'dgit-distro.debian.new-private-pushers' => 't',
480 'dgit-distro.debian/push.git-url' => '',
481 'dgit-distro.debian/push.git-host' => 'push.dgit.debian.org',
482 'dgit-distro.debian/push.git-user-force' => 'dgit',
483 'dgit-distro.debian/push.git-proto' => 'git+ssh://',
484 'dgit-distro.debian/push.git-path' => '/dgit/debian/repos',
485 'dgit-distro.debian/push.git-create' => 'true',
486 'dgit-distro.debian/push.git-check' => 'ssh-cmd',
487 'dgit-distro.debian.archive-query-url', 'https://api.ftp-master.debian.org/',
488 # 'dgit-distro.debian.archive-query-tls-key',
489 # '/etc/ssl/certs/%HOST%.pem:/etc/dgit/%HOST%.pem',
490 # ^ this does not work because curl is broken nowadays
491 # Fixing #790093 properly will involve providing providing the key
492 # in some pacagke and maybe updating these paths.
494 # 'dgit-distro.debian.archive-query-tls-curl-args',
495 # '--ca-path=/etc/ssl/ca-debian',
496 # ^ this is a workaround but works (only) on DSA-administered machines
497 'dgit-distro.debian.git-url' => 'https://git.dgit.debian.org',
498 'dgit-distro.debian.git-url-suffix' => '',
499 'dgit-distro.debian.upload-host' => 'ftp-master', # for dput
500 'dgit-distro.debian.mirror' => 'http://ftp.debian.org/debian/',
501 'dgit-distro.debian.backports-quirk' => '(squeeze)-backports*',
502 'dgit-distro.debian-backports.mirror' => 'http://backports.debian.org/debian-backports/',
503 'dgit-distro.ubuntu.git-check' => 'false',
504 'dgit-distro.ubuntu.mirror' => 'http://archive.ubuntu.com/ubuntu',
505 'dgit-distro.test-dummy.ssh' => "$td/ssh",
506 'dgit-distro.test-dummy.username' => "alice",
507 'dgit-distro.test-dummy.git-check' => "ssh-cmd",
508 'dgit-distro.test-dummy.git-create' => "ssh-cmd",
509 'dgit-distro.test-dummy.git-url' => "$td/git",
510 'dgit-distro.test-dummy.git-host' => "git",
511 'dgit-distro.test-dummy.git-path' => "$td/git",
512 'dgit-distro.test-dummy.archive-query' => "ftpmasterapi:",
513 'dgit-distro.test-dummy.archive-query-url' => "file://$td/aq/",
514 'dgit-distro.test-dummy.mirror' => "file://$td/mirror/",
515 'dgit-distro.test-dummy.upload-host' => 'test-dummy',
520 sub git_slurp_config () {
521 local ($debuglevel) = $debuglevel-2;
524 my @cmd = (@git, qw(config -z --get-regexp .*));
527 open GITS, "-|", @cmd or failedcmd @cmd;
530 printdebug "=> ", (messagequote $_), "\n";
532 push @{ $gitcfg{$`} }, $'; #';
536 or ($!==0 && $?==256)
540 sub git_get_config ($) {
543 printdebug"C $c ".(defined $l ? messagequote "'$l'" : "undef")."\n"
546 @$l==1 or badcfg "multiple values for $c" if @$l > 1;
552 return undef if $c =~ /RETURN-UNDEF/;
553 my $v = git_get_config($c);
554 return $v if defined $v;
555 my $dv = $defcfg{$c};
556 return $dv if defined $dv;
558 badcfg "need value for one of: @_\n".
559 "$us: distro or suite appears not to be (properly) supported";
562 sub access_basedistro () {
563 if (defined $idistro) {
566 return cfg("dgit-suite.$isuite.distro",
567 "dgit.default.distro");
571 sub access_quirk () {
572 # returns (quirk name, distro to use instead or undef, quirk-specific info)
573 my $basedistro = access_basedistro();
574 my $backports_quirk = cfg("dgit-distro.$basedistro.backports-quirk",
576 if (defined $backports_quirk) {
577 my $re = $backports_quirk;
578 $re =~ s/[^-0-9a-z_\%*()]/\\$&/ig;
580 $re =~ s/\%/([-0-9a-z_]+)/
581 or $re =~ m/[()]/ or badcfg "backports-quirk needs \% or ( )";
582 if ($isuite =~ m/^$re$/) {
583 return ('backports',"$basedistro-backports",$1);
586 return ('none',undef);
591 sub parse_cfg_bool ($$$) {
592 my ($what,$def,$v) = @_;
595 $v =~ m/^[ty1]/ ? 1 :
596 $v =~ m/^[fn0]/ ? 0 :
597 badcfg "$what needs t (true, y, 1) or f (false, n, 0) not \`$v'";
600 sub access_forpush_config () {
601 my $d = access_basedistro();
605 parse_cfg_bool('new-private-pushers', 0,
606 cfg("dgit-distro.$d.new-private-pushers",
609 my $v = cfg("dgit-distro.$d.readonly", 'RETURN-UNDEF');
612 $v =~ m/^[ty1]/ ? 0 : # force readonly, forpush = 0
613 $v =~ m/^[fn0]/ ? 1 : # force nonreadonly, forpush = 1
614 $v =~ m/^[a]/ ? '' : # auto, forpush = ''
615 badcfg "readonly needs t (true, y, 1) or f (false, n, 0) or a (auto)";
618 sub access_forpush () {
619 $access_forpush //= access_forpush_config();
620 return $access_forpush;
624 die "$access_forpush ?" if ($access_forpush // 1) ne 1;
625 badcfg "pushing but distro is configured readonly"
626 if access_forpush_config() eq '0';
628 $supplementary_message = <<'END' unless $we_are_responder;
629 Push failed, before we got started.
630 You can retry the push, after fixing the problem, if you like.
632 finalise_opts_opts();
636 finalise_opts_opts();
639 sub supplementary_message ($) {
641 if (!$we_are_responder) {
642 $supplementary_message = $msg;
644 } elsif ($protovsn >= 3) {
645 responder_send_command "supplementary-message ".length($msg)
647 print PO $msg or die $!;
651 sub access_distros () {
652 # Returns list of distros to try, in order
655 # 0. `instead of' distro name(s) we have been pointed to
656 # 1. the access_quirk distro, if any
657 # 2a. the user's specified distro, or failing that } basedistro
658 # 2b. the distro calculated from the suite }
659 my @l = access_basedistro();
661 my (undef,$quirkdistro) = access_quirk();
662 unshift @l, $quirkdistro;
663 unshift @l, $instead_distro;
664 @l = grep { defined } @l;
666 if (access_forpush()) {
667 @l = map { ("$_/push", $_) } @l;
672 sub access_cfg_cfgs (@) {
675 # The nesting of these loops determines the search order. We put
676 # the key loop on the outside so that we search all the distros
677 # for each key, before going on to the next key. That means that
678 # if access_cfg is called with a more specific, and then a less
679 # specific, key, an earlier distro can override the less specific
680 # without necessarily overriding any more specific keys. (If the
681 # distro wants to override the more specific keys it can simply do
682 # so; whereas if we did the loop the other way around, it would be
683 # impossible to for an earlier distro to override a less specific
684 # key but not the more specific ones without restating the unknown
685 # values of the more specific keys.
688 # We have to deal with RETURN-UNDEF specially, so that we don't
689 # terminate the search prematurely.
691 if (m/RETURN-UNDEF/) { push @rundef, $_; last; }
694 foreach my $d (access_distros()) {
695 push @cfgs, map { "dgit-distro.$d.$_" } @realkeys;
697 push @cfgs, map { "dgit.default.$_" } @realkeys;
704 my (@cfgs) = access_cfg_cfgs(@keys);
705 my $value = cfg(@cfgs);
709 sub access_cfg_bool ($$) {
710 my ($def, @keys) = @_;
711 parse_cfg_bool($keys[0], $def, access_cfg(@keys, 'RETURN-UNDEF'));
714 sub string_to_ssh ($) {
716 if ($spec =~ m/\s/) {
717 return qw(sh -ec), 'exec '.$spec.' "$@"', 'x';
723 sub access_cfg_ssh () {
724 my $gitssh = access_cfg('ssh', 'RETURN-UNDEF');
725 if (!defined $gitssh) {
728 return string_to_ssh $gitssh;
732 sub access_runeinfo ($) {
734 return ": dgit ".access_basedistro()." $info ;";
737 sub access_someuserhost ($) {
739 my $user = access_cfg("$some-user-force", 'RETURN-UNDEF');
740 defined($user) && length($user) or
741 $user = access_cfg("$some-user",'username');
742 my $host = access_cfg("$some-host");
743 return length($user) ? "$user\@$host" : $host;
746 sub access_gituserhost () {
747 return access_someuserhost('git');
750 sub access_giturl (;$) {
752 my $url = access_cfg('git-url','RETURN-UNDEF');
755 my $proto = access_cfg('git-proto', 'RETURN-UNDEF');
756 return undef unless defined $proto;
759 access_gituserhost().
760 access_cfg('git-path');
762 $suffix = access_cfg('git-url-suffix','RETURN-UNDEF');
765 return "$url/$package$suffix";
768 sub parsecontrolfh ($$;$) {
769 my ($fh, $desc, $allowsigned) = @_;
770 our $dpkgcontrolhash_noissigned;
773 my %opts = ('name' => $desc);
774 $opts{allow_pgp}= $allowsigned || !$dpkgcontrolhash_noissigned;
775 $c = Dpkg::Control::Hash->new(%opts);
776 $c->parse($fh,$desc) or die "parsing of $desc failed";
777 last if $allowsigned;
778 last if $dpkgcontrolhash_noissigned;
779 my $issigned= $c->get_option('is_pgp_signed');
780 if (!defined $issigned) {
781 $dpkgcontrolhash_noissigned= 1;
782 seek $fh, 0,0 or die "seek $desc: $!";
783 } elsif ($issigned) {
784 fail "control file $desc is (already) PGP-signed. ".
785 " Note that dgit push needs to modify the .dsc and then".
786 " do the signature itself";
795 my ($file, $desc) = @_;
796 my $fh = new IO::Handle;
797 open $fh, '<', $file or die "$file: $!";
798 my $c = parsecontrolfh($fh,$desc);
799 $fh->error and die $!;
805 my ($dctrl,$field) = @_;
806 my $v = $dctrl->{$field};
807 return $v if defined $v;
808 fail "missing field $field in ".$v->get_option('name');
812 my $c = Dpkg::Control::Hash->new();
813 my $p = new IO::Handle;
814 my @cmd = (qw(dpkg-parsechangelog), @_);
815 open $p, '-|', @cmd or die $!;
817 $?=0; $!=0; close $p or failedcmd @cmd;
823 defined $d or fail "getcwd failed: $!";
829 sub archive_query ($) {
831 my $query = access_cfg('archive-query','RETURN-UNDEF');
832 $query =~ s/^(\w+):// or badcfg "invalid archive-query method \`$query'";
835 { no strict qw(refs); &{"${method}_${proto}"}($proto,$data); }
838 sub pool_dsc_subpath ($$) {
839 my ($vsn,$component) = @_; # $package is implict arg
840 my $prefix = substr($package, 0, $package =~ m/^l/ ? 4 : 1);
841 return "/pool/$component/$prefix/$package/".dscfn($vsn);
844 #---------- `ftpmasterapi' archive query method (nascent) ----------
846 sub archive_api_query_cmd ($) {
848 my @cmd = qw(curl -sS);
849 my $url = access_cfg('archive-query-url');
850 if ($url =~ m#^https://([-.0-9a-z]+)/#) {
852 my $keys = access_cfg('archive-query-tls-key','RETURN-UNDEF') //'';
853 foreach my $key (split /\:/, $keys) {
854 $key =~ s/\%HOST\%/$host/g;
856 fail "for $url: stat $key: $!" unless $!==ENOENT;
859 fail "config requested specific TLS key but do not know".
860 " how to get curl to use exactly that EE key ($key)";
861 # push @cmd, "--cacert", $key, "--capath", "/dev/enoent";
862 # # Sadly the above line does not work because of changes
863 # # to gnutls. The real fix for #790093 may involve
864 # # new curl options.
867 # Fixing #790093 properly will involve providing a value
868 # for this on clients.
869 my $kargs = access_cfg('archive-query-tls-curl-ca-args','RETURN-UNDEF');
870 push @cmd, split / /, $kargs if defined $kargs;
872 push @cmd, $url.$subpath;
878 my ($data, $subpath) = @_;
879 badcfg "ftpmasterapi archive query method takes no data part"
881 my @cmd = archive_api_query_cmd($subpath);
882 my $json = cmdoutput @cmd;
883 return decode_json($json);
886 sub canonicalise_suite_ftpmasterapi () {
887 my ($proto,$data) = @_;
888 my $suites = api_query($data, 'suites');
890 foreach my $entry (@$suites) {
892 my $v = $entry->{$_};
893 defined $v && $v eq $isuite;
895 push @matched, $entry;
897 fail "unknown suite $isuite" unless @matched;
900 @matched==1 or die "multiple matches for suite $isuite\n";
901 $cn = "$matched[0]{codename}";
902 defined $cn or die "suite $isuite info has no codename\n";
903 $cn =~ m/^$suite_re$/ or die "suite $isuite maps to bad codename\n";
905 die "bad ftpmaster api response: $@\n".Dumper(\@matched)
910 sub archive_query_ftpmasterapi () {
911 my ($proto,$data) = @_;
912 my $info = api_query($data, "dsc_in_suite/$isuite/$package");
914 my $digester = Digest::SHA->new(256);
915 foreach my $entry (@$info) {
917 my $vsn = "$entry->{version}";
918 my ($ok,$msg) = version_check $vsn;
919 die "bad version: $msg\n" unless $ok;
920 my $component = "$entry->{component}";
921 $component =~ m/^$component_re$/ or die "bad component";
922 my $filename = "$entry->{filename}";
923 $filename && $filename !~ m#[^-+:._~0-9a-zA-Z/]|^[/.]|/[/.]#
924 or die "bad filename";
925 my $sha256sum = "$entry->{sha256sum}";
926 $sha256sum =~ m/^[0-9a-f]+$/ or die "bad sha256sum";
927 push @rows, [ $vsn, "/pool/$component/$filename",
928 $digester, $sha256sum ];
930 die "bad ftpmaster api response: $@\n".Dumper($entry)
933 @rows = sort { -version_compare($a->[0],$b->[0]) } @rows;
937 #---------- `madison' archive query method ----------
939 sub archive_query_madison {
940 return map { [ @$_[0..1] ] } madison_get_parse(@_);
943 sub madison_get_parse {
944 my ($proto,$data) = @_;
945 die unless $proto eq 'madison';
947 $data= access_cfg('madison-distro','RETURN-UNDEF');
948 $data //= access_basedistro();
950 $rmad{$proto,$data,$package} ||= cmdoutput
951 qw(rmadison -asource),"-s$isuite","-u$data",$package;
952 my $rmad = $rmad{$proto,$data,$package};
955 foreach my $l (split /\n/, $rmad) {
956 $l =~ m{^ \s*( [^ \t|]+ )\s* \|
957 \s*( [^ \t|]+ )\s* \|
958 \s*( [^ \t|/]+ )(?:/([^ \t|/]+))? \s* \|
959 \s*( [^ \t|]+ )\s* }x or die "$rmad ?";
960 $1 eq $package or die "$rmad $package ?";
967 $component = access_cfg('archive-query-default-component');
969 $5 eq 'source' or die "$rmad ?";
970 push @out, [$vsn,pool_dsc_subpath($vsn,$component),$newsuite];
972 return sort { -version_compare($a->[0],$b->[0]); } @out;
975 sub canonicalise_suite_madison {
976 # madison canonicalises for us
977 my @r = madison_get_parse(@_);
979 "unable to canonicalise suite using package $package".
980 " which does not appear to exist in suite $isuite;".
981 " --existing-package may help";
985 #---------- `sshpsql' archive query method ----------
988 my ($data,$runeinfo,$sql) = @_;
990 $data= access_someuserhost('sshpsql').':'.
991 access_cfg('sshpsql-dbname');
993 $data =~ m/:/ or badcfg "invalid sshpsql method string \`$data'";
994 my ($userhost,$dbname) = ($`,$'); #';
996 my @cmd = (access_cfg_ssh, $userhost,
997 access_runeinfo("ssh-psql $runeinfo").
998 " export LC_MESSAGES=C; export LC_CTYPE=C;".
999 " ".shellquote qw(psql -A), $dbname, qw(-c), $sql);
1001 open P, "-|", @cmd or die $!;
1004 printdebug(">|$_|\n");
1007 $!=0; $?=0; close P or failedcmd @cmd;
1009 my $nrows = pop @rows;
1010 $nrows =~ s/^\((\d+) rows?\)$/$1/ or die "$nrows ?";
1011 @rows == $nrows+1 or die "$nrows ".(scalar @rows)." ?";
1012 @rows = map { [ split /\|/, $_ ] } @rows;
1013 my $ncols = scalar @{ shift @rows };
1014 die if grep { scalar @$_ != $ncols } @rows;
1018 sub sql_injection_check {
1019 foreach (@_) { die "$_ $& ?" if m{[^-+=:_.,/0-9a-zA-Z]}; }
1022 sub archive_query_sshpsql ($$) {
1023 my ($proto,$data) = @_;
1024 sql_injection_check $isuite, $package;
1025 my @rows = sshpsql($data, "archive-query $isuite $package", <<END);
1026 SELECT source.version, component.name, files.filename, files.sha256sum
1028 JOIN src_associations ON source.id = src_associations.source
1029 JOIN suite ON suite.id = src_associations.suite
1030 JOIN dsc_files ON dsc_files.source = source.id
1031 JOIN files_archive_map ON files_archive_map.file_id = dsc_files.file
1032 JOIN component ON component.id = files_archive_map.component_id
1033 JOIN files ON files.id = dsc_files.file
1034 WHERE ( suite.suite_name='$isuite' OR suite.codename='$isuite' )
1035 AND source.source='$package'
1036 AND files.filename LIKE '%.dsc';
1038 @rows = sort { -version_compare($a->[0],$b->[0]) } @rows;
1039 my $digester = Digest::SHA->new(256);
1041 my ($vsn,$component,$filename,$sha256sum) = @$_;
1042 [ $vsn, "/pool/$component/$filename",$digester,$sha256sum ];
1047 sub canonicalise_suite_sshpsql ($$) {
1048 my ($proto,$data) = @_;
1049 sql_injection_check $isuite;
1050 my @rows = sshpsql($data, "canonicalise-suite $isuite", <<END);
1051 SELECT suite.codename
1052 FROM suite where suite_name='$isuite' or codename='$isuite';
1054 @rows = map { $_->[0] } @rows;
1055 fail "unknown suite $isuite" unless @rows;
1056 die "ambiguous $isuite: @rows ?" if @rows>1;
1060 #---------- `dummycat' archive query method ----------
1062 sub canonicalise_suite_dummycat ($$) {
1063 my ($proto,$data) = @_;
1064 my $dpath = "$data/suite.$isuite";
1065 if (!open C, "<", $dpath) {
1066 $!==ENOENT or die "$dpath: $!";
1067 printdebug "dummycat canonicalise_suite $isuite $dpath ENOENT\n";
1071 chomp or die "$dpath: $!";
1073 printdebug "dummycat canonicalise_suite $isuite $dpath = $_\n";
1077 sub archive_query_dummycat ($$) {
1078 my ($proto,$data) = @_;
1079 canonicalise_suite();
1080 my $dpath = "$data/package.$csuite.$package";
1081 if (!open C, "<", $dpath) {
1082 $!==ENOENT or die "$dpath: $!";
1083 printdebug "dummycat query $csuite $package $dpath ENOENT\n";
1091 printdebug "dummycat query $csuite $package $dpath | $_\n";
1092 my @row = split /\s+/, $_;
1093 @row==2 or die "$dpath: $_ ?";
1096 C->error and die "$dpath: $!";
1098 return sort { -version_compare($a->[0],$b->[0]); } @rows;
1101 #---------- archive query entrypoints and rest of program ----------
1103 sub canonicalise_suite () {
1104 return if defined $csuite;
1105 fail "cannot operate on $isuite suite" if $isuite eq 'UNRELEASED';
1106 $csuite = archive_query('canonicalise_suite');
1107 if ($isuite ne $csuite) {
1108 progress "canonical suite name for $isuite is $csuite";
1112 sub get_archive_dsc () {
1113 canonicalise_suite();
1114 my @vsns = archive_query('archive_query');
1115 foreach my $vinfo (@vsns) {
1116 my ($vsn,$subpath,$digester,$digest) = @$vinfo;
1117 $dscurl = access_cfg('mirror').$subpath;
1118 $dscdata = url_get($dscurl);
1120 $skew_warning_vsn = $vsn if !defined $skew_warning_vsn;
1125 $digester->add($dscdata);
1126 my $got = $digester->hexdigest();
1128 fail "$dscurl has hash $got but".
1129 " archive told us to expect $digest";
1131 my $dscfh = new IO::File \$dscdata, '<' or die $!;
1132 printdebug Dumper($dscdata) if $debuglevel>1;
1133 $dsc = parsecontrolfh($dscfh,$dscurl,1);
1134 printdebug Dumper($dsc) if $debuglevel>1;
1135 my $fmt = getfield $dsc, 'Format';
1136 fail "unsupported source format $fmt, sorry" unless $format_ok{$fmt};
1137 $dsc_checked = !!$digester;
1143 sub check_for_git ();
1144 sub check_for_git () {
1146 my $how = access_cfg('git-check');
1147 if ($how eq 'ssh-cmd') {
1149 (access_cfg_ssh, access_gituserhost(),
1150 access_runeinfo("git-check $package").
1151 " set -e; cd ".access_cfg('git-path').";".
1152 " if test -d $package.git; then echo 1; else echo 0; fi");
1153 my $r= cmdoutput @cmd;
1154 if ($r =~ m/^divert (\w+)$/) {
1156 my ($usedistro,) = access_distros();
1157 # NB that if we are pushing, $usedistro will be $distro/push
1158 $instead_distro= cfg("dgit-distro.$usedistro.diverts.$divert");
1159 $instead_distro =~ s{^/}{ access_basedistro()."/" }e;
1160 progress "diverting to $divert (using config for $instead_distro)";
1161 return check_for_git();
1163 failedcmd @cmd unless $r =~ m/^[01]$/;
1165 } elsif ($how eq 'url') {
1166 my $prefix = access_cfg('git-check-url','git-url');
1167 my $suffix = access_cfg('git-check-suffix','git-suffix',
1168 'RETURN-UNDEF') // '.git';
1169 my $url = "$prefix/$package$suffix";
1170 my @cmd = (qw(curl -sS -I), $url);
1171 my $result = cmdoutput @cmd;
1172 $result =~ s/^\S+ 200 .*\n\r?\n//;
1173 # curl -sS -I with https_proxy prints
1174 # HTTP/1.0 200 Connection established
1175 $result =~ m/^\S+ (404|200) /s or
1176 fail "unexpected results from git check query - ".
1177 Dumper($prefix, $result);
1179 if ($code eq '404') {
1181 } elsif ($code eq '200') {
1186 } elsif ($how eq 'true') {
1188 } elsif ($how eq 'false') {
1191 badcfg "unknown git-check \`$how'";
1195 sub create_remote_git_repo () {
1196 my $how = access_cfg('git-create');
1197 if ($how eq 'ssh-cmd') {
1199 (access_cfg_ssh, access_gituserhost(),
1200 access_runeinfo("git-create $package").
1201 "set -e; cd ".access_cfg('git-path').";".
1202 " cp -a _template $package.git");
1203 } elsif ($how eq 'true') {
1206 badcfg "unknown git-create \`$how'";
1210 our ($dsc_hash,$lastpush_hash);
1212 our $ud = '.git/dgit/unpack';
1222 sub mktree_in_ud_here () {
1223 runcmd qw(git init -q);
1224 rmtree('.git/objects');
1225 symlink '../../../../objects','.git/objects' or die $!;
1228 sub git_write_tree () {
1229 my $tree = cmdoutput @git, qw(write-tree);
1230 $tree =~ m/^\w+$/ or die "$tree ?";
1234 sub remove_stray_gits () {
1235 my @gitscmd = qw(find -name .git -prune -print0);
1236 debugcmd "|",@gitscmd;
1237 open GITS, "-|", @gitscmd or failedcmd @gitscmd;
1242 print STDERR "$us: warning: removing from source package: ",
1243 (messagequote $_), "\n";
1247 $!=0; $?=0; close GITS or failedcmd @gitscmd;
1250 sub mktree_in_ud_from_only_subdir () {
1251 # changes into the subdir
1253 die unless @dirs==1;
1254 $dirs[0] =~ m#^([^/]+)/\.$# or die;
1258 remove_stray_gits();
1259 mktree_in_ud_here();
1260 my ($format, $fopts) = get_source_format();
1261 if (madformat($format)) {
1264 runcmd @git, qw(add -Af);
1265 my $tree=git_write_tree();
1266 return ($tree,$dir);
1269 sub dsc_files_info () {
1270 foreach my $csumi (['Checksums-Sha256','Digest::SHA', 'new(256)'],
1271 ['Checksums-Sha1', 'Digest::SHA', 'new(1)'],
1272 ['Files', 'Digest::MD5', 'new()']) {
1273 my ($fname, $module, $method) = @$csumi;
1274 my $field = $dsc->{$fname};
1275 next unless defined $field;
1276 eval "use $module; 1;" or die $@;
1278 foreach (split /\n/, $field) {
1280 m/^(\w+) (\d+) (\S+)$/ or
1281 fail "could not parse .dsc $fname line \`$_'";
1282 my $digester = eval "$module"."->$method;" or die $@;
1287 Digester => $digester,
1292 fail "missing any supported Checksums-* or Files field in ".
1293 $dsc->get_option('name');
1297 map { $_->{Filename} } dsc_files_info();
1300 sub is_orig_file ($;$) {
1303 m/\.orig(?:-\w+)?\.tar\.\w+$/ or return 0;
1304 defined $base or return 1;
1308 sub make_commit ($) {
1310 return cmdoutput @git, qw(hash-object -w -t commit), $file;
1313 sub clogp_authline ($) {
1315 my $author = getfield $clogp, 'Maintainer';
1316 $author =~ s#,.*##ms;
1317 my $date = cmdoutput qw(date), '+%s %z', qw(-d), getfield($clogp,'Date');
1318 my $authline = "$author $date";
1319 $authline =~ m/^[^<>]+ \<\S+\> \d+ [-+]\d+$/ or
1320 fail "unexpected commit author line format \`$authline'".
1321 " (was generated from changelog Maintainer field)";
1325 sub vendor_patches_distro ($$) {
1326 my ($checkdistro, $what) = @_;
1327 return unless defined $checkdistro;
1329 my $series = "debian/patches/\L$checkdistro\E.series";
1330 printdebug "checking for vendor-specific $series ($what)\n";
1332 if (!open SERIES, "<", $series) {
1333 die "$series $!" unless $!==ENOENT;
1342 Unfortunately, this source package uses a feature of dpkg-source where
1343 the same source package unpacks to different source code on different
1344 distros. dgit cannot safely operate on such packages on affected
1345 distros, because the meaning of source packages is not stable.
1347 Please ask the distro/maintainer to remove the distro-specific series
1348 files and use a different technique (if necessary, uploading actually
1349 different packages, if different distros are supposed to have
1353 fail "Found active distro-specific series file for".
1354 " $checkdistro ($what): $series, cannot continue";
1356 die "$series $!" if SERIES->error;
1360 sub check_for_vendor_patches () {
1361 # This dpkg-source feature doesn't seem to be documented anywhere!
1362 # But it can be found in the changelog (reformatted):
1364 # commit 4fa01b70df1dc4458daee306cfa1f987b69da58c
1365 # Author: Raphael Hertzog <hertzog@debian.org>
1366 # Date: Sun Oct 3 09:36:48 2010 +0200
1368 # dpkg-source: correctly create .pc/.quilt_series with alternate
1371 # If you have debian/patches/ubuntu.series and you were
1372 # unpacking the source package on ubuntu, quilt was still
1373 # directed to debian/patches/series instead of
1374 # debian/patches/ubuntu.series.
1376 # debian/changelog | 3 +++
1377 # scripts/Dpkg/Source/Package/V3/quilt.pm | 4 +++-
1378 # 2 files changed, 6 insertions(+), 1 deletion(-)
1381 vendor_patches_distro($ENV{DEB_VENDOR}, "DEB_VENDOR");
1382 vendor_patches_distro(Dpkg::Vendor::get_current_vendor(),
1383 "Dpkg::Vendor \`current vendor'");
1384 vendor_patches_distro(access_basedistro(),
1385 "distro being accessed");
1388 sub generate_commit_from_dsc () {
1392 foreach my $fi (dsc_files_info()) {
1393 my $f = $fi->{Filename};
1394 die "$f ?" if $f =~ m#/|^\.|\.dsc$|\.tmp$#;
1396 link_ltarget "../../../$f", $f
1400 complete_file_from_dsc('.', $fi)
1403 if (is_orig_file($f)) {
1404 link $f, "../../../../$f"
1410 my $dscfn = "$package.dsc";
1412 open D, ">", $dscfn or die "$dscfn: $!";
1413 print D $dscdata or die "$dscfn: $!";
1414 close D or die "$dscfn: $!";
1415 my @cmd = qw(dpkg-source);
1416 push @cmd, '--no-check' if $dsc_checked;
1417 push @cmd, qw(-x --), $dscfn;
1420 my ($tree,$dir) = mktree_in_ud_from_only_subdir();
1421 check_for_vendor_patches() if madformat($dsc->{format});
1422 runcmd qw(sh -ec), 'dpkg-parsechangelog >../changelog.tmp';
1423 my $clogp = parsecontrol('../changelog.tmp',"commit's changelog");
1424 my $authline = clogp_authline $clogp;
1425 my $changes = getfield $clogp, 'Changes';
1426 open C, ">../commit.tmp" or die $!;
1427 print C <<END or die $!;
1434 # imported from the archive
1437 my $outputhash = make_commit qw(../commit.tmp);
1438 my $cversion = getfield $clogp, 'Version';
1439 progress "synthesised git commit from .dsc $cversion";
1440 if ($lastpush_hash) {
1441 runcmd @git, qw(reset --hard), $lastpush_hash;
1442 runcmd qw(sh -ec), 'dpkg-parsechangelog >>../changelogold.tmp';
1443 my $oldclogp = parsecontrol('../changelogold.tmp','previous changelog');
1444 my $oversion = getfield $oldclogp, 'Version';
1446 version_compare($oversion, $cversion);
1448 # git upload/ is earlier vsn than archive, use archive
1449 open C, ">../commit2.tmp" or die $!;
1450 print C <<END or die $!;
1452 parent $lastpush_hash
1457 Record $package ($cversion) in archive suite $csuite
1459 $outputhash = make_commit qw(../commit2.tmp);
1460 } elsif ($vcmp > 0) {
1461 print STDERR <<END or die $!;
1463 Version actually in archive: $cversion (older)
1464 Last allegedly pushed/uploaded: $oversion (newer or same)
1467 $outputhash = $lastpush_hash;
1469 $outputhash = $lastpush_hash;
1472 changedir '../../../..';
1473 runcmd @git, qw(update-ref -m),"dgit fetch import $cversion",
1474 'DGIT_ARCHIVE', $outputhash;
1475 cmdoutput @git, qw(log -n2), $outputhash;
1476 # ... gives git a chance to complain if our commit is malformed
1481 sub complete_file_from_dsc ($$) {
1482 our ($dstdir, $fi) = @_;
1483 # Ensures that we have, in $dir, the file $fi, with the correct
1484 # contents. (Downloading it from alongside $dscurl if necessary.)
1486 my $f = $fi->{Filename};
1487 my $tf = "$dstdir/$f";
1490 if (stat_exists $tf) {
1491 progress "using existing $f";
1494 $furl =~ s{/[^/]+$}{};
1496 die "$f ?" unless $f =~ m/^\Q${package}\E_/;
1497 die "$f ?" if $f =~ m#/#;
1498 runcmd_ordryrun_local @curl,qw(-o),$tf,'--',"$furl";
1499 return 0 if !act_local();
1503 open F, "<", "$tf" or die "$tf: $!";
1504 $fi->{Digester}->reset();
1505 $fi->{Digester}->addfile(*F);
1506 F->error and die $!;
1507 my $got = $fi->{Digester}->hexdigest();
1508 $got eq $fi->{Hash} or
1509 fail "file $f has hash $got but .dsc".
1510 " demands hash $fi->{Hash} ".
1511 ($downloaded ? "(got wrong file from archive!)"
1512 : "(perhaps you should delete this file?)");
1517 sub ensure_we_have_orig () {
1518 foreach my $fi (dsc_files_info()) {
1519 my $f = $fi->{Filename};
1520 next unless is_orig_file($f);
1521 complete_file_from_dsc('..', $fi)
1526 sub git_fetch_us () {
1527 my @specs = (fetchspec());
1529 map { "+refs/$_/*:".lrfetchrefs."/$_/*" }
1531 runcmd_ordryrun_local @git, qw(fetch -p -n -q), access_giturl(), @specs;
1534 my $tagpat = debiantag('*',access_basedistro);
1536 git_for_each_ref("refs/tags/".$tagpat, sub {
1537 my ($objid,$objtype,$fullrefname,$reftail) = @_;
1538 printdebug "currently $fullrefname=$objid\n";
1539 $here{$fullrefname} = $objid;
1541 git_for_each_ref(lrfetchrefs."/tags/".$tagpat, sub {
1542 my ($objid,$objtype,$fullrefname,$reftail) = @_;
1543 my $lref = "refs".substr($fullrefname, length lrfetchrefs);
1544 printdebug "offered $lref=$objid\n";
1545 if (!defined $here{$lref}) {
1546 my @upd = (@git, qw(update-ref), $lref, $objid, '');
1547 runcmd_ordryrun_local @upd;
1548 } elsif ($here{$lref} eq $objid) {
1551 "Not updateting $lref from $here{$lref} to $objid.\n";
1556 sub fetch_from_archive () {
1557 # ensures that lrref() is what is actually in the archive,
1558 # one way or another
1562 foreach my $field (@ourdscfield) {
1563 $dsc_hash = $dsc->{$field};
1564 last if defined $dsc_hash;
1566 if (defined $dsc_hash) {
1567 $dsc_hash =~ m/\w+/ or fail "invalid hash in .dsc \`$dsc_hash'";
1569 progress "last upload to archive specified git hash";
1571 progress "last upload to archive has NO git hash";
1574 progress "no version available from the archive";
1577 $lastpush_hash = git_get_ref(lrref());
1578 printdebug "previous reference hash=$lastpush_hash\n";
1580 if (defined $dsc_hash) {
1581 fail "missing remote git history even though dsc has hash -".
1582 " could not find ref ".lrref().
1583 " (should have been fetched from ".access_giturl()."#".rrref().")"
1584 unless $lastpush_hash;
1586 ensure_we_have_orig();
1587 if ($dsc_hash eq $lastpush_hash) {
1588 } elsif (is_fast_fwd($dsc_hash,$lastpush_hash)) {
1589 print STDERR <<END or die $!;
1591 Git commit in archive is behind the last version allegedly pushed/uploaded.
1592 Commit referred to by archive: $dsc_hash
1593 Last allegedly pushed/uploaded: $lastpush_hash
1596 $hash = $lastpush_hash;
1598 fail "git head (".lrref()."=$lastpush_hash) is not a ".
1599 "descendant of archive's .dsc hash ($dsc_hash)";
1602 $hash = generate_commit_from_dsc();
1603 } elsif ($lastpush_hash) {
1604 # only in git, not in the archive yet
1605 $hash = $lastpush_hash;
1606 print STDERR <<END or die $!;
1608 Package not found in the archive, but has allegedly been pushed using dgit.
1612 printdebug "nothing found!\n";
1613 if (defined $skew_warning_vsn) {
1614 print STDERR <<END or die $!;
1616 Warning: relevant archive skew detected.
1617 Archive allegedly contains $skew_warning_vsn
1618 But we were not able to obtain any version from the archive or git.
1624 printdebug "current hash=$hash\n";
1625 if ($lastpush_hash) {
1626 fail "not fast forward on last upload branch!".
1627 " (archive's version left in DGIT_ARCHIVE)"
1628 unless is_fast_fwd($lastpush_hash, $hash);
1630 if (defined $skew_warning_vsn) {
1632 printdebug "SKEW CHECK WANT $skew_warning_vsn\n";
1633 my $clogf = ".git/dgit/changelog.tmp";
1634 runcmd shell_cmd "exec >$clogf",
1635 @git, qw(cat-file blob), "$hash:debian/changelog";
1636 my $gotclogp = parsechangelog("-l$clogf");
1637 my $got_vsn = getfield $gotclogp, 'Version';
1638 printdebug "SKEW CHECK GOT $got_vsn\n";
1639 if (version_compare($got_vsn, $skew_warning_vsn) < 0) {
1640 print STDERR <<END or die $!;
1642 Warning: archive skew detected. Using the available version:
1643 Archive allegedly contains $skew_warning_vsn
1644 We were able to obtain only $got_vsn
1649 if ($lastpush_hash ne $hash) {
1650 my @upd_cmd = (@git, qw(update-ref -m), 'dgit fetch', lrref(), $hash);
1654 dryrun_report @upd_cmd;
1660 sub set_local_git_config ($$) {
1662 runcmd @git, qw(config), $k, $v;
1665 sub setup_mergechangelogs (;$) {
1667 return unless $always || access_cfg_bool(1, 'setup-mergechangelogs');
1669 my $driver = 'dpkg-mergechangelogs';
1670 my $cb = "merge.$driver";
1671 my $attrs = '.git/info/attributes';
1672 ensuredir '.git/info';
1674 open NATTRS, ">", "$attrs.new" or die "$attrs.new $!";
1675 if (!open ATTRS, "<", $attrs) {
1676 $!==ENOENT or die "$attrs: $!";
1680 next if m{^debian/changelog\s};
1681 print NATTRS $_, "\n" or die $!;
1683 ATTRS->error and die $!;
1686 print NATTRS "debian/changelog merge=$driver\n" or die $!;
1689 set_local_git_config "$cb.name", 'debian/changelog merge driver';
1690 set_local_git_config "$cb.driver", 'dpkg-mergechangelogs -m %O %A %B %A';
1692 rename "$attrs.new", "$attrs" or die "$attrs: $!";
1695 sub setup_useremail (;$) {
1697 return unless $always || access_cfg_bool(1, 'setup-useremail');
1700 my ($k, $envvar) = @_;
1701 my $v = access_cfg("user-$k", 'RETURN-UNDEF') // $ENV{$envvar};
1702 return unless defined $v;
1703 set_local_git_config "user.$k", $v;
1706 $setup->('email', 'DEBEMAIL');
1707 $setup->('name', 'DEBFULLNAME');
1710 sub setup_new_tree () {
1711 setup_mergechangelogs();
1717 canonicalise_suite();
1718 badusage "dry run makes no sense with clone" unless act_local();
1719 my $hasgit = check_for_git();
1720 mkdir $dstdir or fail "create \`$dstdir': $!";
1722 runcmd @git, qw(init -q);
1723 my $giturl = access_giturl(1);
1724 if (defined $giturl) {
1725 set_local_git_config "remote.$remotename.fetch", fetchspec();
1726 open H, "> .git/HEAD" or die $!;
1727 print H "ref: ".lref()."\n" or die $!;
1729 runcmd @git, qw(remote add), 'origin', $giturl;
1732 progress "fetching existing git history";
1734 runcmd_ordryrun_local @git, qw(fetch origin);
1736 progress "starting new git history";
1738 fetch_from_archive() or no_such_package;
1739 my $vcsgiturl = $dsc->{'Vcs-Git'};
1740 if (length $vcsgiturl) {
1741 $vcsgiturl =~ s/\s+-b\s+\S+//g;
1742 runcmd @git, qw(remote add vcs-git), $vcsgiturl;
1745 runcmd @git, qw(reset --hard), lrref();
1746 printdone "ready for work in $dstdir";
1750 if (check_for_git()) {
1753 fetch_from_archive() or no_such_package();
1754 printdone "fetched into ".lrref();
1759 runcmd_ordryrun_local @git, qw(merge -m),"Merge from $csuite [dgit]",
1761 printdone "fetched to ".lrref()." and merged into HEAD";
1764 sub check_not_dirty () {
1765 foreach my $f (qw(local-options local-patch-header)) {
1766 if (stat_exists "debian/source/$f") {
1767 fail "git tree contains debian/source/$f";
1771 return if $ignoredirty;
1773 my @cmd = (@git, qw(diff --quiet HEAD));
1775 $!=0; $?=0; system @cmd;
1776 return if !$! && !$?;
1777 if (!$! && $?==256) {
1778 fail "working tree is dirty (does not match HEAD)";
1784 sub commit_admin ($) {
1787 runcmd_ordryrun_local @git, qw(commit -m), $m;
1790 sub commit_quilty_patch () {
1791 my $output = cmdoutput @git, qw(status --porcelain);
1793 foreach my $l (split /\n/, $output) {
1794 next unless $l =~ m/\S/;
1795 if ($l =~ m{^(?:\?\?| M) (.pc|debian/patches)}) {
1799 delete $adds{'.pc'}; # if there wasn't one before, don't add it
1801 progress "nothing quilty to commit, ok.";
1804 my @adds = map { s/[][*?\\]/\\$&/g; $_; } sort keys %adds;
1805 runcmd_ordryrun_local @git, qw(add -f), @adds;
1806 commit_admin "Commit Debian 3.0 (quilt) metadata";
1809 sub get_source_format () {
1811 if (open F, "debian/source/options") {
1815 s/\s+$//; # ignore missing final newline
1817 my ($k, $v) = ($`, $'); #');
1818 $v =~ s/^"(.*)"$/$1/;
1824 F->error and die $!;
1827 die $! unless $!==&ENOENT;
1830 if (!open F, "debian/source/format") {
1831 die $! unless $!==&ENOENT;
1835 F->error and die $!;
1837 return ($_, \%options);
1842 return 0 unless $format eq '3.0 (quilt)';
1843 if ($quilt_mode eq 'nocheck') {
1844 progress "Not doing any fixup of \`$format' due to --no-quilt-fixup";
1847 progress "Format \`$format', checking/updating patch stack";
1851 sub push_parse_changelog ($) {
1854 my $clogp = Dpkg::Control::Hash->new();
1855 $clogp->load($clogpfn) or die;
1857 $package = getfield $clogp, 'Source';
1858 my $cversion = getfield $clogp, 'Version';
1859 my $tag = debiantag($cversion, access_basedistro);
1860 runcmd @git, qw(check-ref-format), $tag;
1862 my $dscfn = dscfn($cversion);
1864 return ($clogp, $cversion, $tag, $dscfn);
1867 sub push_parse_dsc ($$$) {
1868 my ($dscfn,$dscfnwhat, $cversion) = @_;
1869 $dsc = parsecontrol($dscfn,$dscfnwhat);
1870 my $dversion = getfield $dsc, 'Version';
1871 my $dscpackage = getfield $dsc, 'Source';
1872 ($dscpackage eq $package && $dversion eq $cversion) or
1873 fail "$dscfn is for $dscpackage $dversion".
1874 " but debian/changelog is for $package $cversion";
1877 sub push_mktag ($$$$$$$) {
1878 my ($head,$clogp,$tag,
1880 $changesfile,$changesfilewhat,
1883 $dsc->{$ourdscfield[0]} = $head;
1884 $dsc->save("$dscfn.tmp") or die $!;
1886 my $changes = parsecontrol($changesfile,$changesfilewhat);
1887 foreach my $field (qw(Source Distribution Version)) {
1888 $changes->{$field} eq $clogp->{$field} or
1889 fail "changes field $field \`$changes->{$field}'".
1890 " does not match changelog \`$clogp->{$field}'";
1893 my $cversion = getfield $clogp, 'Version';
1894 my $clogsuite = getfield $clogp, 'Distribution';
1896 # We make the git tag by hand because (a) that makes it easier
1897 # to control the "tagger" (b) we can do remote signing
1898 my $authline = clogp_authline $clogp;
1899 my $delibs = join(" ", "",@deliberatelies);
1900 my $declaredistro = access_basedistro();
1901 open TO, '>', $tfn->('.tmp') or die $!;
1902 print TO <<END or die $!;
1908 $package release $cversion for $clogsuite ($csuite) [dgit]
1909 [dgit distro=$declaredistro$delibs]
1911 foreach my $ref (sort keys %previously) {
1912 print TO <<END or die $!;
1913 [dgit previously:$ref=$previously{$ref}]
1919 my $tagobjfn = $tfn->('.tmp');
1921 if (!defined $keyid) {
1922 $keyid = access_cfg('keyid','RETURN-UNDEF');
1924 if (!defined $keyid) {
1925 $keyid = getfield $clogp, 'Maintainer';
1927 unlink $tfn->('.tmp.asc') or $!==&ENOENT or die $!;
1928 my @sign_cmd = (@gpg, qw(--detach-sign --armor));
1929 push @sign_cmd, qw(-u),$keyid if defined $keyid;
1930 push @sign_cmd, $tfn->('.tmp');
1931 runcmd_ordryrun @sign_cmd;
1933 $tagobjfn = $tfn->('.signed.tmp');
1934 runcmd shell_cmd "exec >$tagobjfn", qw(cat --),
1935 $tfn->('.tmp'), $tfn->('.tmp.asc');
1942 sub sign_changes ($) {
1943 my ($changesfile) = @_;
1945 my @debsign_cmd = @debsign;
1946 push @debsign_cmd, "-k$keyid" if defined $keyid;
1947 push @debsign_cmd, "-p$gpg[0]" if $gpg[0] ne 'gpg';
1948 push @debsign_cmd, $changesfile;
1949 runcmd_ordryrun @debsign_cmd;
1954 my ($forceflag) = @_;
1955 printdebug "actually entering push\n";
1956 supplementary_message(<<'END');
1957 Push failed, while preparing your push.
1958 You can retry the push, after fixing the problem, if you like.
1962 access_giturl(); # check that success is vaguely likely
1964 my $clogpfn = ".git/dgit/changelog.822.tmp";
1965 runcmd shell_cmd "exec >$clogpfn", qw(dpkg-parsechangelog);
1967 responder_send_file('parsed-changelog', $clogpfn);
1969 my ($clogp, $cversion, $tag, $dscfn) =
1970 push_parse_changelog("$clogpfn");
1972 my $dscpath = "$buildproductsdir/$dscfn";
1973 stat_exists $dscpath or
1974 fail "looked for .dsc $dscfn, but $!;".
1975 " maybe you forgot to build";
1977 responder_send_file('dsc', $dscpath);
1979 push_parse_dsc($dscpath, $dscfn, $cversion);
1981 my $format = getfield $dsc, 'Format';
1982 printdebug "format $format\n";
1983 if (madformat($format)) {
1984 # user might have not used dgit build, so maybe do this now:
1985 commit_quilty_patch();
1989 progress "checking that $dscfn corresponds to HEAD";
1990 runcmd qw(dpkg-source -x --),
1991 $dscpath =~ m#^/# ? $dscpath : "../../../$dscpath";
1992 my ($tree,$dir) = mktree_in_ud_from_only_subdir();
1993 check_for_vendor_patches() if madformat($dsc->{format});
1994 changedir '../../../..';
1995 my $diffopt = $debuglevel>0 ? '--exit-code' : '--quiet';
1996 my @diffcmd = (@git, qw(diff), $diffopt, $tree);
1997 debugcmd "+",@diffcmd;
1999 my $r = system @diffcmd;
2002 fail "$dscfn specifies a different tree to your HEAD commit;".
2003 " perhaps you forgot to build".
2004 ($diffopt eq '--exit-code' ? "" :
2005 " (run with -D to see full diff output)");
2010 my $head = git_rev_parse('HEAD');
2011 if (!$changesfile) {
2012 my $pat = changespat $cversion;
2013 my @cs = glob "$buildproductsdir/$pat";
2014 fail "failed to find unique changes file".
2015 " (looked for $pat in $buildproductsdir);".
2016 " perhaps you need to use dgit -C"
2018 ($changesfile) = @cs;
2020 $changesfile = "$buildproductsdir/$changesfile";
2023 responder_send_file('changes',$changesfile);
2024 responder_send_command("param head $head");
2025 responder_send_command("param csuite $csuite");
2027 if (deliberately_not_fast_forward) {
2028 git_for_each_ref(lrfetchrefs, sub {
2029 my ($objid,$objtype,$lrfetchrefname,$reftail) = @_;
2030 my $rrefname= substr($lrfetchrefname, length(lrfetchrefs) + 1);
2031 responder_send_command("previously $rrefname=$objid");
2032 $previously{$rrefname} = $objid;
2036 my $tfn = sub { ".git/dgit/tag$_[0]"; };
2039 supplementary_message(<<'END');
2040 Push failed, while signing the tag.
2041 You can retry the push, after fixing the problem, if you like.
2043 # If we manage to sign but fail to record it anywhere, it's fine.
2044 if ($we_are_responder) {
2045 $tagobjfn = $tfn->('.signed.tmp');
2046 responder_receive_files('signed-tag', $tagobjfn);
2049 push_mktag($head,$clogp,$tag,
2051 $changesfile,$changesfile,
2054 supplementary_message(<<'END');
2055 Push failed, *after* signing the tag.
2056 If you want to try again, you should use a new version number.
2059 my $tag_obj_hash = cmdoutput @git, qw(hash-object -w -t tag), $tagobjfn;
2060 runcmd_ordryrun @git, qw(verify-tag), $tag_obj_hash;
2061 runcmd_ordryrun_local @git, qw(update-ref), "refs/tags/$tag", $tag_obj_hash;
2063 supplementary_message(<<'END');
2064 Push failed, while updating the remote git repository - see messages above.
2065 If you want to try again, you should use a new version number.
2067 if (!check_for_git()) {
2068 create_remote_git_repo();
2070 runcmd_ordryrun @git, qw(push),access_giturl(),
2071 $forceflag."HEAD:".rrref(), $forceflag."refs/tags/$tag";
2072 runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), 'HEAD';
2074 supplementary_message(<<'END');
2075 Push failed, after updating the remote git repository.
2076 If you want to try again, you must use a new version number.
2078 if ($we_are_responder) {
2079 my $dryrunsuffix = act_local() ? "" : ".tmp";
2080 responder_receive_files('signed-dsc-changes',
2081 "$dscpath$dryrunsuffix",
2082 "$changesfile$dryrunsuffix");
2085 rename "$dscpath.tmp",$dscpath or die "$dscfn $!";
2087 progress "[new .dsc left in $dscpath.tmp]";
2089 sign_changes $changesfile;
2092 supplementary_message(<<END);
2093 Push failed, while uploading package(s) to the archive server.
2094 You can retry the upload of exactly these same files with dput of:
2096 If that .changes file is broken, you will need to use a new version
2097 number for your next attempt at the upload.
2099 my $host = access_cfg('upload-host','RETURN-UNDEF');
2100 my @hostarg = defined($host) ? ($host,) : ();
2101 runcmd_ordryrun @dput, @hostarg, $changesfile;
2102 printdone "pushed and uploaded $cversion";
2104 supplementary_message('');
2105 responder_send_command("complete");
2112 badusage "-p is not allowed with clone; specify as argument instead"
2113 if defined $package;
2116 } elsif (@ARGV==2 && $ARGV[1] =~ m#^\w#) {
2117 ($package,$isuite) = @ARGV;
2118 } elsif (@ARGV==2 && $ARGV[1] =~ m#^[./]#) {
2119 ($package,$dstdir) = @ARGV;
2120 } elsif (@ARGV==3) {
2121 ($package,$isuite,$dstdir) = @ARGV;
2123 badusage "incorrect arguments to dgit clone";
2125 $dstdir ||= "$package";
2127 if (stat_exists $dstdir) {
2128 fail "$dstdir already exists";
2132 if ($rmonerror && !$dryrun_level) {
2133 $cwd_remove= getcwd();
2135 return unless defined $cwd_remove;
2136 if (!chdir "$cwd_remove") {
2137 return if $!==&ENOENT;
2138 die "chdir $cwd_remove: $!";
2141 rmtree($dstdir) or die "remove $dstdir: $!\n";
2142 } elsif (!grep { $! == $_ }
2143 (ENOENT, ENOTDIR, EACCES, EPERM, ELOOP)) {
2145 print STDERR "check whether to remove $dstdir: $!\n";
2151 $cwd_remove = undef;
2154 sub branchsuite () {
2155 my $branch = cmdoutput_errok @git, qw(symbolic-ref HEAD);
2156 if ($branch =~ m#$lbranch_re#o) {
2163 sub fetchpullargs () {
2165 if (!defined $package) {
2166 my $sourcep = parsecontrol('debian/control','debian/control');
2167 $package = getfield $sourcep, 'Source';
2170 # $isuite = branchsuite(); # this doesn't work because dak hates canons
2172 my $clogp = parsechangelog();
2173 $isuite = getfield $clogp, 'Distribution';
2175 canonicalise_suite();
2176 progress "fetching from suite $csuite";
2177 } elsif (@ARGV==1) {
2179 canonicalise_suite();
2181 badusage "incorrect arguments to dgit fetch or dgit pull";
2200 badusage "-p is not allowed with dgit push" if defined $package;
2202 my $clogp = parsechangelog();
2203 $package = getfield $clogp, 'Source';
2206 } elsif (@ARGV==1) {
2207 ($specsuite) = (@ARGV);
2209 badusage "incorrect arguments to dgit push";
2211 $isuite = getfield $clogp, 'Distribution';
2213 local ($package) = $existing_package; # this is a hack
2214 canonicalise_suite();
2216 canonicalise_suite();
2218 if (defined $specsuite &&
2219 $specsuite ne $isuite &&
2220 $specsuite ne $csuite) {
2221 fail "dgit push: changelog specifies $isuite ($csuite)".
2222 " but command line specifies $specsuite";
2224 supplementary_message(<<'END');
2225 Push failed, while checking state of the archive.
2226 You can retry the push, after fixing the problem, if you like.
2228 if (check_for_git()) {
2232 if (fetch_from_archive()) {
2233 if (is_fast_fwd(lrref(), 'HEAD')) {
2235 } elsif (deliberately_not_fast_forward) {
2238 fail "dgit push: HEAD is not a descendant".
2239 " of the archive's version.\n".
2240 "dgit: To overwrite its contents,".
2241 " use git merge -s ours ".lrref().".\n".
2242 "dgit: To rewind history, if permitted by the archive,".
2243 " use --deliberately-not-fast-forward";
2247 fail "package appears to be new in this suite;".
2248 " if this is intentional, use --new";
2253 #---------- remote commands' implementation ----------
2255 sub cmd_remote_push_build_host {
2256 my ($nrargs) = shift @ARGV;
2257 my (@rargs) = @ARGV[0..$nrargs-1];
2258 @ARGV = @ARGV[$nrargs..$#ARGV];
2260 my ($dir,$vsnwant) = @rargs;
2261 # vsnwant is a comma-separated list; we report which we have
2262 # chosen in our ready response (so other end can tell if they
2265 $we_are_responder = 1;
2266 $us .= " (build host)";
2270 open PI, "<&STDIN" or die $!;
2271 open STDIN, "/dev/null" or die $!;
2272 open PO, ">&STDOUT" or die $!;
2274 open STDOUT, ">&STDERR" or die $!;
2278 ($protovsn) = grep {
2279 $vsnwant =~ m{^(?:.*,)?$_(?:,.*)?$}
2280 } @rpushprotovsn_support;
2282 fail "build host has dgit rpush protocol versions ".
2283 (join ",", @rpushprotovsn_support).
2284 " but invocation host has $vsnwant"
2285 unless defined $protovsn;
2287 responder_send_command("dgit-remote-push-ready $protovsn");
2293 sub cmd_remote_push_responder { cmd_remote_push_build_host(); }
2294 # ... for compatibility with proto vsn.1 dgit (just so that user gets
2295 # a good error message)
2301 my $report = i_child_report();
2302 if (defined $report) {
2303 printdebug "($report)\n";
2304 } elsif ($i_child_pid) {
2305 printdebug "(killing build host child $i_child_pid)\n";
2306 kill 15, $i_child_pid;
2308 if (defined $i_tmp && !defined $initiator_tempdir) {
2310 eval { rmtree $i_tmp; };
2314 END { i_cleanup(); }
2317 my ($base,$selector,@args) = @_;
2318 $selector =~ s/\-/_/g;
2319 { no strict qw(refs); &{"${base}_${selector}"}(@args); }
2326 if ($host =~ m/^((?:[^][]|\[[^][]*\])*)\:/) {
2334 push @rargs, join ",", @rpushprotovsn_support;
2337 push @rdgit, @ropts;
2338 push @rdgit, qw(remote-push-build-host), (scalar @rargs), @rargs;
2340 my @cmd = (@ssh, $host, shellquote @rdgit);
2343 if (defined $initiator_tempdir) {
2344 rmtree $initiator_tempdir;
2345 mkdir $initiator_tempdir, 0700 or die "$initiator_tempdir: $!";
2346 $i_tmp = $initiator_tempdir;
2350 $i_child_pid = open2(\*RO, \*RI, @cmd);
2352 ($protovsn) = initiator_expect { m/^dgit-remote-push-ready (\S+)/ };
2353 die "$protovsn ?" unless grep { $_ eq $protovsn } @rpushprotovsn_support;
2354 $supplementary_message = '' unless $protovsn >= 3;
2356 my ($icmd,$iargs) = initiator_expect {
2357 m/^(\S+)(?: (.*))?$/;
2360 i_method "i_resp", $icmd, $iargs;
2364 sub i_resp_progress ($) {
2366 my $msg = protocol_read_bytes \*RO, $rhs;
2370 sub i_resp_supplementary_message ($) {
2372 $supplementary_message = protocol_read_bytes \*RO, $rhs;
2375 sub i_resp_complete {
2376 my $pid = $i_child_pid;
2377 $i_child_pid = undef; # prevents killing some other process with same pid
2378 printdebug "waiting for build host child $pid...\n";
2379 my $got = waitpid $pid, 0;
2380 die $! unless $got == $pid;
2381 die "build host child failed $?" if $?;
2384 printdebug "all done\n";
2388 sub i_resp_file ($) {
2390 my $localname = i_method "i_localname", $keyword;
2391 my $localpath = "$i_tmp/$localname";
2392 stat_exists $localpath and
2393 badproto \*RO, "file $keyword ($localpath) twice";
2394 protocol_receive_file \*RO, $localpath;
2395 i_method "i_file", $keyword;
2400 sub i_resp_param ($) {
2401 $_[0] =~ m/^(\S+) (.*)$/ or badproto \*RO, "bad param spec";
2405 sub i_resp_previously ($) {
2406 $_[0] =~ m#^(refs/tags/\S+)=(\w+)$#
2407 or badproto \*RO, "bad previously spec";
2408 my $r = system qw(git check-ref-format), $1;
2409 die "bad previously ref spec ($r)" if $r;
2410 $previously{$1} = $2;
2415 sub i_resp_want ($) {
2417 die "$keyword ?" if $i_wanted{$keyword}++;
2418 my @localpaths = i_method "i_want", $keyword;
2419 printdebug "[[ $keyword @localpaths\n";
2420 foreach my $localpath (@localpaths) {
2421 protocol_send_file \*RI, $localpath;
2423 print RI "files-end\n" or die $!;
2426 our ($i_clogp, $i_version, $i_tag, $i_dscfn, $i_changesfn);
2428 sub i_localname_parsed_changelog {
2429 return "remote-changelog.822";
2431 sub i_file_parsed_changelog {
2432 ($i_clogp, $i_version, $i_tag, $i_dscfn) =
2433 push_parse_changelog "$i_tmp/remote-changelog.822";
2434 die if $i_dscfn =~ m#/|^\W#;
2437 sub i_localname_dsc {
2438 defined $i_dscfn or badproto \*RO, "dsc (before parsed-changelog)";
2443 sub i_localname_changes {
2444 defined $i_dscfn or badproto \*RO, "dsc (before parsed-changelog)";
2445 $i_changesfn = $i_dscfn;
2446 $i_changesfn =~ s/\.dsc$/_dgit.changes/ or die;
2447 return $i_changesfn;
2449 sub i_file_changes { }
2451 sub i_want_signed_tag {
2452 printdebug Dumper(\%i_param, $i_dscfn);
2453 defined $i_param{'head'} && defined $i_dscfn && defined $i_clogp
2454 && defined $i_param{'csuite'}
2455 or badproto \*RO, "premature desire for signed-tag";
2456 my $head = $i_param{'head'};
2457 die if $head =~ m/[^0-9a-f]/ || $head !~ m/^../;
2459 die unless $i_param{'csuite'} =~ m/^$suite_re$/;
2461 push_parse_dsc $i_dscfn, 'remote dsc', $i_version;
2464 push_mktag $head, $i_clogp, $i_tag,
2466 $i_changesfn, 'remote changes',
2467 sub { "tag$_[0]"; };
2472 sub i_want_signed_dsc_changes {
2473 rename "$i_dscfn.tmp","$i_dscfn" or die "$i_dscfn $!";
2474 sign_changes $i_changesfn;
2475 return ($i_dscfn, $i_changesfn);
2478 #---------- building etc. ----------
2484 #----- `3.0 (quilt)' handling -----
2486 our $fakeeditorenv = 'DGIT_FAKE_EDITOR_QUILT';
2488 sub quiltify_dpkg_commit ($$$;$) {
2489 my ($patchname,$author,$msg, $xinfo) = @_;
2493 my $descfn = ".git/dgit/quilt-description.tmp";
2494 open O, '>', $descfn or die "$descfn: $!";
2497 $msg =~ s/^\s+$/ ./mg;
2498 print O <<END or die $!;
2508 local $ENV{'EDITOR'} = cmdoutput qw(realpath --), $0;
2509 local $ENV{'VISUAL'} = $ENV{'EDITOR'};
2510 local $ENV{$fakeeditorenv} = cmdoutput qw(realpath --), $descfn;
2511 runcmd @dpkgsource, qw(--commit .), $patchname;
2515 sub quiltify_trees_differ ($$;$) {
2516 my ($x,$y,$finegrained) = @_;
2517 # returns true iff the two tree objects differ other than in debian/
2518 # with $finegrained,
2519 # returns bitmask 01 - differ in upstream files except .gitignore
2520 # 02 - differ in .gitignore
2522 my @cmd = (@git, qw(diff-tree --name-only -z));
2523 push @cmd, qw(-r) if $finegrained;
2525 my $diffs= cmdoutput @cmd;
2527 foreach my $f (split /\0/, $diffs) {
2528 next if $f =~ m#^debian(?:/.*)?$#s;
2529 $r |= ($f =~ m#^(?:.*/)?.gitignore$#s) ? 02 : 01;
2531 printdebug "quiltify_trees_differ $x $y => $r\n";
2535 sub quiltify_tree_sentinelfiles ($) {
2536 # lists the `sentinel' files present in the tree
2538 my $r = cmdoutput @git, qw(ls-tree --name-only), $x,
2539 qw(-- debian/rules debian/control);
2544 sub quiltify_splitbrain_needed () {
2545 if (!$split_brain) {
2546 progress "creating dgit view";
2547 runcmd @git, qw(checkout -q -b dgit-view);
2552 sub quiltify_splitbrain ($) {
2553 my ($diffbits) = @_;
2554 if ($quilt_mode !~ m/gbp|dpm/) {
2555 # treat .gitignore just like any other upstream file
2556 $diffbits = { %$diffbits };
2557 $_ = !!$_ foreach values %$diffbits;
2559 if ($quilt_mode =~ m/gbp|unapplied/ &&
2560 ($diffbits->{O2A} & 01) && # some patches
2561 !($diffbits->{H2O} & 01)) { # but HEAD is like orig
2562 quiltify_splitbrain_needed();
2563 runcmd @gbppq, qw(import);
2566 die 'xxx gitignore';
2567 die 'xxx memoisation via git-reflog';
2568 die 'xxx fast forward';
2571 sub quiltify ($$$$) {
2572 my ($clogp,$target,$oldtiptree,$failsuggestion) = @_;
2574 # Quilt patchification algorithm
2576 # We search backwards through the history of the main tree's HEAD
2577 # (T) looking for a start commit S whose tree object is identical
2578 # to to the patch tip tree (ie the tree corresponding to the
2579 # current dpkg-committed patch series). For these purposes
2580 # `identical' disregards anything in debian/ - this wrinkle is
2581 # necessary because dpkg-source treates debian/ specially.
2583 # We can only traverse edges where at most one of the ancestors'
2584 # trees differs (in changes outside in debian/). And we cannot
2585 # handle edges which change .pc/ or debian/patches. To avoid
2586 # going down a rathole we avoid traversing edges which introduce
2587 # debian/rules or debian/control. And we set a limit on the
2588 # number of edges we are willing to look at.
2590 # If we succeed, we walk forwards again. For each traversed edge
2591 # PC (with P parent, C child) (starting with P=S and ending with
2592 # C=T) to we do this:
2594 # - dpkg-source --commit with a patch name and message derived from C
2595 # After traversing PT, we git commit the changes which
2596 # should be contained within debian/patches.
2598 # The search for the path S..T is breadth-first. We maintain a
2599 # todo list containing search nodes. A search node identifies a
2600 # commit, and looks something like this:
2602 # Commit => $git_commit_id,
2603 # Child => $c, # or undef if P=T
2604 # Whynot => $reason_edge_PC_unsuitable, # in @nots only
2605 # Nontrivial => true iff $p..$c has relevant changes
2612 my %considered; # saves being exponential on some weird graphs
2614 my $t_sentinels = quiltify_tree_sentinelfiles $target;
2617 my ($search,$whynot) = @_;
2618 printdebug " search NOT $search->{Commit} $whynot\n";
2619 $search->{Whynot} = $whynot;
2620 push @nots, $search;
2621 no warnings qw(exiting);
2630 my $c = shift @todo;
2631 next if $considered{$c->{Commit}}++;
2633 $not->($c, "maximum search space exceeded") if --$max_work <= 0;
2635 printdebug "quiltify investigate $c->{Commit}\n";
2638 if (!quiltify_trees_differ $c->{Commit}, $oldtiptree) {
2639 printdebug " search finished hooray!\n";
2644 if ($quilt_mode eq 'nofix') {
2645 fail "quilt fixup required but quilt mode is \`nofix'\n".
2646 "HEAD commit $c->{Commit} differs from tree implied by ".
2647 " debian/patches (tree object $oldtiptree)";
2649 if ($quilt_mode eq 'smash') {
2650 printdebug " search quitting smash\n";
2654 my $c_sentinels = quiltify_tree_sentinelfiles $c->{Commit};
2655 $not->($c, "has $c_sentinels not $t_sentinels")
2656 if $c_sentinels ne $t_sentinels;
2658 my $commitdata = cmdoutput @git, qw(cat-file commit), $c->{Commit};
2659 $commitdata =~ m/\n\n/;
2661 my @parents = ($commitdata =~ m/^parent (\w+)$/gm);
2662 @parents = map { { Commit => $_, Child => $c } } @parents;
2664 $not->($c, "root commit") if !@parents;
2666 foreach my $p (@parents) {
2667 $p->{Nontrivial}= quiltify_trees_differ $p->{Commit},$c->{Commit};
2669 my $ndiffers = grep { $_->{Nontrivial} } @parents;
2670 $not->($c, "merge ($ndiffers nontrivial parents)") if $ndiffers > 1;
2672 foreach my $p (@parents) {
2673 printdebug "considering C=$c->{Commit} P=$p->{Commit}\n";
2675 my @cmd= (@git, qw(diff-tree -r --name-only),
2676 $p->{Commit},$c->{Commit}, qw(-- debian/patches .pc));
2677 my $patchstackchange = cmdoutput @cmd;
2678 if (length $patchstackchange) {
2679 $patchstackchange =~ s/\n/,/g;
2680 $not->($p, "changed $patchstackchange");
2683 printdebug " search queue P=$p->{Commit} ",
2684 ($p->{Nontrivial} ? "NT" : "triv"),"\n";
2690 printdebug "quiltify want to smash\n";
2693 my $x = $_[0]{Commit};
2694 $x =~ s/(.*?[0-9a-z]{8})[0-9a-z]*$/$1/;
2697 my $reportnot = sub {
2699 my $s = $abbrev->($notp);
2700 my $c = $notp->{Child};
2701 $s .= "..".$abbrev->($c) if $c;
2702 $s .= ": ".$notp->{Whynot};
2705 if ($quilt_mode eq 'linear') {
2706 print STDERR "$us: quilt fixup cannot be linear. Stopped at:\n";
2707 foreach my $notp (@nots) {
2708 print STDERR "$us: ", $reportnot->($notp), "\n";
2710 print STDERR "$us: $_\n" foreach @$failsuggestion;
2711 fail "quilt fixup naive history linearisation failed.\n".
2712 "Use dpkg-source --commit by hand; or, --quilt=smash for one ugly patch";
2713 } elsif ($quilt_mode eq 'smash') {
2714 } elsif ($quilt_mode eq 'auto') {
2715 progress "quilt fixup cannot be linear, smashing...";
2717 die "$quilt_mode ?";
2722 my $msg = cmdoutput @git, qw(log), "-n$ncommits";
2724 quiltify_dpkg_commit "auto-$version-$target-$time",
2725 (getfield $clogp, 'Maintainer'),
2726 "Automatically generated patch ($clogp->{Version})\n".
2727 "Last (up to) $ncommits git changes, FYI:\n\n". $msg;
2731 progress "quiltify linearisation planning successful, executing...";
2733 for (my $p = $sref_S;
2734 my $c = $p->{Child};
2736 printdebug "quiltify traverse $p->{Commit}..$c->{Commit}\n";
2737 next unless $p->{Nontrivial};
2739 my $cc = $c->{Commit};
2741 my $commitdata = cmdoutput @git, qw(cat-file commit), $cc;
2742 $commitdata =~ m/\n\n/ or die "$c ?";
2745 $commitdata =~ m/^author (.*) \d+ [-+0-9]+$/m or die "$cc ?";
2748 $msg =~ s/^(.*)\n*/$1\n/ or die "$cc $msg ?";
2751 my $patchname = $title;
2752 $patchname =~ s/[.:]$//;
2753 $patchname =~ y/ A-Z/-a-z/;
2754 $patchname =~ y/-a-z0-9_.+=~//cd;
2755 $patchname =~ s/^\W/x-$&/;
2756 $patchname = substr($patchname,0,40);
2759 stat "debian/patches/$patchname$index";
2761 $!==ENOENT or die "$patchname$index $!";
2763 runcmd @git, qw(checkout -q), $cc;
2765 # We use the tip's changelog so that dpkg-source doesn't
2766 # produce complaining messages from dpkg-parsechangelog. None
2767 # of the information dpkg-source gets from the changelog is
2768 # actually relevant - it gets put into the original message
2769 # which dpkg-source provides our stunt editor, and then
2771 runcmd @git, qw(checkout -q), $target, qw(debian/changelog);
2773 quiltify_dpkg_commit "$patchname$index", $author, $msg,
2774 "X-Dgit-Generated: $clogp->{Version} $cc\n";
2776 runcmd @git, qw(checkout -q), $cc, qw(debian/changelog);
2779 runcmd @git, qw(checkout -q master);
2782 sub build_maybe_quilt_fixup () {
2783 my ($format,$fopts) = get_source_format;
2784 return unless madformat $format;
2787 check_for_vendor_patches();
2789 my $clogp = parsechangelog();
2790 my $headref = git_rev_parse('HEAD');
2795 my $upstreamversion=$version;
2796 $upstreamversion =~ s/-[^-]*$//;
2798 if ($fopts->{'single-debian-patch'}) {
2799 quilt_fixup_singlepatch($clogp, $headref, $upstreamversion);
2801 quilt_fixup_multipatch($clogp, $headref, $upstreamversion);
2804 changedir '../../../..';
2805 runcmd_ordryrun_local
2806 @git, qw(pull --ff-only -q .git/dgit/unpack/work master);
2809 sub quilt_fixup_mkwork ($) {
2812 mkdir "work" or die $!;
2814 mktree_in_ud_here();
2815 runcmd @git, qw(reset -q --hard), $headref;
2818 sub quilt_fixup_linkorigs ($$) {
2819 my ($upstreamversion, $fn) = @_;
2820 # calls $fn->($leafname);
2822 foreach my $f (<../../../../*>) { #/){
2823 my $b=$f; $b =~ s{.*/}{};
2825 local ($debuglevel) = $debuglevel-1;
2826 printdebug "QF linkorigs $b, $f ?\n";
2828 next unless is_orig_file $b, srcfn $upstreamversion,'';
2829 printdebug "QF linkorigs $b, $f Y\n";
2830 link_ltarget $f, $b or die "$b $!";
2835 sub quilt_fixup_delete_pc () {
2836 runcmd @git, qw(rm -rqf .pc);
2837 commit_admin "Commit removal of .pc (quilt series tracking data)";
2840 sub quilt_fixup_singlepatch ($$$) {
2841 my ($clogp, $headref, $upstreamversion) = @_;
2843 progress "starting quiltify (single-debian-patch)";
2845 # dpkg-source --commit generates new patches even if
2846 # single-debian-patch is in debian/source/options. In order to
2847 # get it to generate debian/patches/debian-changes, it is
2848 # necessary to build the source package.
2850 quilt_fixup_linkorigs($upstreamversion, sub { });
2851 quilt_fixup_mkwork($headref);
2853 rmtree("debian/patches");
2855 runcmd @dpkgsource, qw(-b .);
2857 runcmd @dpkgsource, qw(-x), (srcfn $version, ".dsc");
2858 rename srcfn("$upstreamversion", "/debian/patches"),
2859 "work/debian/patches";
2862 commit_quilty_patch();
2867 sub quilt_fixup_multipatch ($$$) {
2868 my ($clogp, $headref, $upstreamversion) = @_;
2870 progress "starting quiltify (multiple patches, $quilt_mode mode)";
2873 # - honour any existing .pc in case it has any strangeness
2874 # - determine the git commit corresponding to the tip of
2875 # the patch stack (if there is one)
2876 # - if there is such a git commit, convert each subsequent
2877 # git commit into a quilt patch with dpkg-source --commit
2878 # - otherwise convert all the differences in the tree into
2879 # a single git commit
2883 # Our git tree doesn't necessarily contain .pc. (Some versions of
2884 # dgit would include the .pc in the git tree.) If there isn't
2885 # one, we need to generate one by unpacking the patches that we
2888 # We first look for a .pc in the git tree. If there is one, we
2889 # will use it. (This is not the normal case.)
2891 # Otherwise need to regenerate .pc so that dpkg-source --commit
2892 # can work. We do this as follows:
2893 # 1. Collect all relevant .orig from parent directory
2894 # 2. Generate a debian.tar.gz out of
2895 # debian/{patches,rules,source/format,source/options}
2896 # 3. Generate a fake .dsc containing just these fields:
2897 # Format Source Version Files
2898 # 4. Extract the fake .dsc
2899 # Now the fake .dsc has a .pc directory.
2900 # (In fact we do this in every case, because in future we will
2901 # want to search for a good base commit for generating patches.)
2903 # Then we can actually do the dpkg-source --commit
2904 # 1. Make a new working tree with the same object
2905 # store as our main tree and check out the main
2907 # 2. Copy .pc from the fake's extraction, if necessary
2908 # 3. Run dpkg-source --commit
2909 # 4. If the result has changes to debian/, then
2910 # - git-add them them
2911 # - git-add .pc if we had a .pc in-tree
2913 # 5. If we had a .pc in-tree, delete it, and git-commit
2914 # 6. Back in the main tree, fast forward to the new HEAD
2916 # Another situation we may have to cope with is gbp-style
2917 # patches-unapplied trees.
2919 # We would want to detect these, so we know to escape into
2920 # quilt_fixup_gbp. However, this is in general not possible.
2921 # Consider a package with a one patch which the dgit user reverts
2922 # (with git-revert or the moral equivalent).
2924 # That is indistinguishable in contents from a patches-unapplied
2925 # tree. And looking at the history to distinguish them is not
2926 # useful because the user might have made a confusing-looking git
2927 # history structure (which ought to produce an error if dgit can't
2928 # cope, not a silent reintroduction of an unwanted patch).
2930 # So gbp users will have to pass an option. But we can usually
2931 # detect their failure to do so: if the tree is not a clean
2932 # patches-applied tree, quilt linearisation fails, but the tree
2933 # _is_ a clean patches-unapplied tree, we can suggest that maybe
2934 # they want --quilt=unapplied.
2936 # To help detect this, when we are extracting the fake dsc, we
2937 # first extract it with --skip-patches, and then apply the patches
2938 # afterwards with dpkg-source --before-build. That lets us save a
2939 # tree object corresponding to .origs.
2941 my $fakeversion="$upstreamversion-~~DGITFAKE";
2943 my $fakedsc=new IO::File 'fake.dsc', '>' or die $!;
2944 print $fakedsc <<END or die $!;
2947 Version: $fakeversion
2951 my $dscaddfile=sub {
2954 my $md = new Digest::MD5;
2956 my $fh = new IO::File $b, '<' or die "$b $!";
2961 print $fakedsc " ".$md->hexdigest." $size $b\n" or die $!;
2964 quilt_fixup_linkorigs($upstreamversion, $dscaddfile);
2966 my @files=qw(debian/source/format debian/rules
2967 debian/control debian/changelog);
2968 foreach my $maybe (qw(debian/patches debian/source/options
2969 debian/tests/control)) {
2970 next unless stat_exists "../../../$maybe";
2971 push @files, $maybe;
2974 my $debtar= srcfn $fakeversion,'.debian.tar.gz';
2975 runcmd qw(env GZIP=-1 tar -zcf), "./$debtar", qw(-C ../../..), @files;
2977 $dscaddfile->($debtar);
2978 close $fakedsc or die $!;
2981 'exec dpkg-source --no-check --skip-patches -x fake.dsc >/dev/null';
2983 my $fakexdir= $package.'-'.(stripepoch $upstreamversion);
2984 rename $fakexdir, "fake" or die "$fakexdir $!";
2988 remove_stray_gits();
2989 mktree_in_ud_here();
2993 runcmd @git, qw(add -Af .);
2994 my $unapplied=git_write_tree();
2995 printdebug "fake orig tree object $unapplied\n";
3000 'exec dpkg-source --before-build . >/dev/null';
3004 quilt_fixup_mkwork($headref);
3007 if (stat_exists ".pc") {
3009 progress "Tree already contains .pc - will use it then delete it.";
3012 rename '../fake/.pc','.pc' or die $!;
3015 changedir '../fake';
3017 runcmd @git, qw(add -Af .);
3018 my $oldtiptree=git_write_tree();
3019 changedir '../work';
3022 # We calculate some guesswork now about what kind of tree this might
3023 # be. This is mostly for error reporting.
3027 # O = orig, without patches applied
3028 # A = "applied", ie orig with H's debian/patches applied
3029 H2O => quiltify_trees_differ($headref, $unapplied, 1),
3030 H2A => quiltify_trees_differ($headref, $oldtiptree,1),
3031 O2A => quiltify_trees_differ($unapplied,$oldtiptree,1),
3035 foreach my $b (qw(01 02)) {
3036 foreach my $v (qw(H2O O2A H2A)) {
3037 push @dl, ($diffbits->{$v} & $b) ? '##' : '==';
3040 printdebug "differences \@dl @dl.\n";
3043 "$us: quilt differences: src: %s orig %s gitignores: %s orig %s\n".
3044 "$us: quilt differences: HEAD %s o+d/p HEAD %s o+d/p",
3045 $dl[0], $dl[1], $dl[3], $dl[4],
3049 if (!($diffbits->{H2O} & $diffbits->{O2A})) {
3050 push @failsuggestion, "This might be a patches-unapplied branch.";
3051 } elsif (!($diffbits->{H2A} & $diffbits->{O2A})) {
3052 push @failsuggestion, "This might be a patches-applied branch.";
3054 push @failsuggestion, "Maybe you need to specify one of".
3055 " --quilt=gbp --quilt=dpm --quilt=unapplied ?";
3057 if ($quilt_mode =~ m/gbp|dpm|unapplied/) {
3058 quiltify_splitbrain($diffbits);
3062 quiltify($clogp,$headref,$oldtiptree,\@failsuggestion);
3064 if (!open P, '>>', ".pc/applied-patches") {
3065 $!==&ENOENT or die $!;
3070 commit_quilty_patch();
3072 if ($mustdeletepc) {
3073 quilt_fixup_delete_pc();
3077 sub quilt_fixup_editor () {
3078 my $descfn = $ENV{$fakeeditorenv};
3079 my $editing = $ARGV[$#ARGV];
3080 open I1, '<', $descfn or die "$descfn: $!";
3081 open I2, '<', $editing or die "$editing: $!";
3082 unlink $editing or die "$editing: $!";
3083 open O, '>', $editing or die "$editing: $!";
3084 while (<I1>) { print O or die $!; } I1->error and die $!;
3087 $copying ||= m/^\-\-\- /;
3088 next unless $copying;
3091 I2->error and die $!;
3096 #----- other building -----
3098 our $suppress_clean;
3101 return if $suppress_clean;
3102 if ($cleanmode eq 'dpkg-source') {
3103 runcmd_ordryrun_local @dpkgbuildpackage, qw(-T clean);
3104 } elsif ($cleanmode eq 'dpkg-source-d') {
3105 runcmd_ordryrun_local @dpkgbuildpackage, qw(-d -T clean);
3106 } elsif ($cleanmode eq 'git') {
3107 runcmd_ordryrun_local @git, qw(clean -xdf);
3108 } elsif ($cleanmode eq 'git-ff') {
3109 runcmd_ordryrun_local @git, qw(clean -xdff);
3110 } elsif ($cleanmode eq 'check') {
3111 my $leftovers = cmdoutput @git, qw(clean -xdn);
3112 if (length $leftovers) {
3113 print STDERR $leftovers, "\n" or die $!;
3114 fail "tree contains uncommitted files and --clean=check specified";
3116 } elsif ($cleanmode eq 'none') {
3123 badusage "clean takes no additional arguments" if @ARGV;
3130 badusage "-p is not allowed when building" if defined $package;
3133 my $clogp = parsechangelog();
3134 $isuite = getfield $clogp, 'Distribution';
3135 $package = getfield $clogp, 'Source';
3136 $version = getfield $clogp, 'Version';
3137 build_maybe_quilt_fixup();
3139 my $pat = changespat $version;
3140 foreach my $f (glob "$buildproductsdir/$pat") {
3142 unlink $f or fail "remove old changes file $f: $!";
3144 progress "would remove $f";
3150 sub changesopts_initial () {
3151 my @opts =@changesopts[1..$#changesopts];
3154 sub changesopts_version () {
3155 if (!defined $changes_since_version) {
3156 my @vsns = archive_query('archive_query');
3157 my @quirk = access_quirk();
3158 if ($quirk[0] eq 'backports') {
3159 local $isuite = $quirk[2];
3161 canonicalise_suite();
3162 push @vsns, archive_query('archive_query');
3165 @vsns = map { $_->[0] } @vsns;
3166 @vsns = sort { -version_compare($a, $b) } @vsns;
3167 $changes_since_version = $vsns[0];
3168 progress "changelog will contain changes since $vsns[0]";
3170 $changes_since_version = '_';
3171 progress "package seems new, not specifying -v<version>";
3174 if ($changes_since_version ne '_') {
3175 return ("-v$changes_since_version");
3181 sub changesopts () {
3182 return (changesopts_initial(), changesopts_version());
3185 sub massage_dbp_args ($;$) {
3186 my ($cmd,$xargs) = @_;
3189 # - if we're going to split the source build out so we can
3190 # do strange things to it, massage the arguments to dpkg-buildpackage
3191 # so that the main build doessn't build source (or add an argument
3192 # to stop it building source by default).
3194 # - add -nc to stop dpkg-source cleaning the source tree,
3195 # unless we're not doing a split build and want dpkg-source
3196 # as cleanmode, in which case we can do nothing
3199 # 0 - source will NOT need to be built separately by caller
3200 # +1 - source will need to be built separately by caller
3201 # +2 - source will need to be built separately by caller AND
3202 # dpkg-buildpackage should not in fact be run at all!
3203 debugcmd '#massaging#', @$cmd if $debuglevel>1;
3204 #print STDERR "MASS0 ",Dumper($cmd, $xargs, $need_split_build_invocation);
3205 if ($cleanmode eq 'dpkg-source' && !$need_split_build_invocation) {
3206 $suppress_clean = 1;
3209 # -nc has the side effect of specifying -b if nothing else specified
3210 # and some combinations of -S, -b, et al, are errors, rather than
3211 # later simply overriding earlie. So we need to:
3212 # - search the command line for these options
3213 # - pick the last one
3214 # - perhaps add our own as a default
3215 # - perhaps adjust it to the corresponding non-source-building version
3217 foreach my $l ($cmd, $xargs) {
3219 @$l = grep { !(m/^-[SgGFABb]$/s and $dmode=$_) } @$l;
3222 #print STDERR "MASS1 ",Dumper($cmd, $xargs, $dmode);
3224 if ($need_split_build_invocation) {
3225 $r = $dmode =~ m/[S]/ ? +2 :
3226 $dmode =~ y/gGF/ABb/ ? +1 :
3227 $dmode =~ m/[ABb]/ ? 0 :
3231 #print STDERR "MASS2 ",Dumper($cmd, $xargs, $r);
3236 my @dbp = (@dpkgbuildpackage, qw(-us -uc), changesopts_initial(), @ARGV);
3237 my $wantsrc = massage_dbp_args \@dbp;
3244 push @dbp, changesopts_version();
3245 runcmd_ordryrun_local @dbp;
3247 printdone "build successful\n";
3251 my @dbp = @dpkgbuildpackage;
3253 my $wantsrc = massage_dbp_args \@dbp, \@ARGV;
3256 if (length executable_on_path('git-buildpackage')) {
3257 @cmd = qw(git-buildpackage);
3259 @cmd = qw(gbp buildpackage);
3261 push @cmd, (qw(-us -uc --git-no-sign-tags), "--git-builder=@dbp");
3266 if (!$suppress_clean) {
3267 push @cmd, '--git-cleaner=true';
3272 unless (grep { m/^--git-debian-branch|^--git-ignore-branch/ } @ARGV) {
3273 canonicalise_suite();
3274 push @cmd, "--git-debian-branch=".lbranch();
3276 push @cmd, changesopts();
3277 runcmd_ordryrun_local @cmd, @ARGV;
3279 printdone "build successful\n";
3281 sub cmd_git_build { cmd_gbp_build(); } # compatibility with <= 1.0
3284 if ($cleanmode =~ m/^dpkg-source/) {
3285 # dpkg-source will clean, so we shouldn't
3286 $suppress_clean = 1;
3289 $sourcechanges = changespat $version,'source';
3291 unlink "../$sourcechanges" or $!==ENOENT
3292 or fail "remove $sourcechanges: $!";
3294 $dscfn = dscfn($version);
3295 if ($cleanmode eq 'dpkg-source') {
3296 runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc -S),
3298 } elsif ($cleanmode eq 'dpkg-source-d') {
3299 runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc -S -d),
3302 my $pwd = must_getcwd();
3303 my $leafdir = basename $pwd;
3305 runcmd_ordryrun_local @dpkgsource, qw(-b --), $leafdir;
3307 runcmd_ordryrun_local qw(sh -ec),
3308 'exec >$1; shift; exec "$@"','x',
3309 "../$sourcechanges",
3310 @dpkggenchanges, qw(-S), changesopts();
3314 sub cmd_build_source {
3315 badusage "build-source takes no additional arguments" if @ARGV;
3317 printdone "source built, results in $dscfn and $sourcechanges";
3322 my $pat = changespat $version;
3324 my @unwanted = map { s#^\.\./##; $_; } glob "../$pat";
3325 @unwanted = grep { $_ ne changespat $version,'source' } @unwanted;
3326 fail "changes files other than source matching $pat".
3327 " already present (@unwanted);".
3328 " building would result in ambiguity about the intended results"
3333 stat_exists $dscfn or fail "$dscfn (in parent directory): $!";
3334 stat_exists $sourcechanges
3335 or fail "$sourcechanges (in parent directory): $!";
3337 runcmd_ordryrun_local @sbuild, qw(-d), $isuite, @ARGV, $dscfn;
3338 my @changesfiles = glob $pat;
3339 @changesfiles = sort {
3340 ($b =~ m/_source\.changes$/ <=> $a =~ m/_source\.changes$/)
3343 fail "wrong number of different changes files (@changesfiles)"
3344 unless @changesfiles==2;
3345 my $binchanges = parsecontrol($changesfiles[1], "binary changes file");
3346 foreach my $l (split /\n/, getfield $binchanges, 'Files') {
3347 fail "$l found in binaries changes file $binchanges"
3350 runcmd_ordryrun_local @mergechanges, @changesfiles;
3351 my $multichanges = changespat $version,'multi';
3353 stat_exists $multichanges or fail "$multichanges: $!";
3354 foreach my $cf (glob $pat) {
3355 next if $cf eq $multichanges;
3356 rename "$cf", "$cf.inmulti" or fail "$cf\{,.inmulti}: $!";
3359 printdone "build successful, results in $multichanges\n" or die $!;
3362 sub cmd_quilt_fixup {
3363 badusage "incorrect arguments to dgit quilt-fixup" if @ARGV;
3364 my $clogp = parsechangelog();
3365 $version = getfield $clogp, 'Version';
3366 $package = getfield $clogp, 'Source';
3369 build_maybe_quilt_fixup();
3372 sub cmd_archive_api_query {
3373 badusage "need only 1 subpath argument" unless @ARGV==1;
3374 my ($subpath) = @ARGV;
3375 my @cmd = archive_api_query_cmd($subpath);
3377 exec @cmd or fail "exec curl: $!\n";
3380 sub cmd_clone_dgit_repos_server {
3381 badusage "need destination argument" unless @ARGV==1;
3382 my ($destdir) = @ARGV;
3383 $package = '_dgit-repos-server';
3384 my @cmd = (@git, qw(clone), access_giturl(), $destdir);
3386 exec @cmd or fail "exec git clone: $!\n";
3389 sub cmd_setup_mergechangelogs {
3390 badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
3391 setup_mergechangelogs(1);
3394 sub cmd_setup_useremail {
3395 badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
3399 sub cmd_setup_new_tree {
3400 badusage "no arguments allowed to dgit setup-tree" if @ARGV;
3404 #---------- argument parsing and main program ----------
3407 print "dgit version $our_version\n" or die $!;
3411 our (%valopts_long, %valopts_short);
3414 sub defvalopt ($$$$) {
3415 my ($long,$short,$val_re,$how) = @_;
3416 my $oi = { Long => $long, Short => $short, Re => $val_re, How => $how };
3417 $valopts_long{$long} = $oi;
3418 $valopts_short{$short} = $oi;
3419 # $how subref should:
3420 # do whatever assignemnt or thing it likes with $_[0]
3421 # if the option should not be passed on to remote, @rvalopts=()
3422 # or $how can be a scalar ref, meaning simply assign the value
3425 defvalopt '--since-version', '-v', '[^_]+|_', \$changes_since_version;
3426 defvalopt '--distro', '-d', '.+', \$idistro;
3427 defvalopt '', '-k', '.+', \$keyid;
3428 defvalopt '--existing-package','', '.*', \$existing_package;
3429 defvalopt '--build-products-dir','','.*', \$buildproductsdir;
3430 defvalopt '--clean', '', $cleanmode_re, \$cleanmode;
3431 defvalopt '--quilt', '', $quilt_modes_re, \$quilt_mode;
3433 defvalopt '', '-c', '.*=.*', sub { push @git, '-c', @_; };
3435 defvalopt '', '-C', '.+', sub {
3436 ($changesfile) = (@_);
3437 if ($changesfile =~ s#^(.*)/##) {
3438 $buildproductsdir = $1;
3442 defvalopt '--initiator-tempdir','','.*', sub {
3443 ($initiator_tempdir) = (@_);
3444 $initiator_tempdir =~ m#^/# or
3445 badusage "--initiator-tempdir must be used specify an".
3446 " absolute, not relative, directory."
3452 if (defined $ENV{'DGIT_SSH'}) {
3453 @ssh = string_to_ssh $ENV{'DGIT_SSH'};
3454 } elsif (defined $ENV{'GIT_SSH'}) {
3455 @ssh = ($ENV{'GIT_SSH'});
3463 if (!defined $val) {
3464 badusage "$what needs a value" unless @ARGV;
3466 push @rvalopts, $val;
3468 badusage "bad value \`$val' for $what" unless
3469 $val =~ m/^$oi->{Re}$(?!\n)/s;
3470 my $how = $oi->{How};
3471 if (ref($how) eq 'SCALAR') {
3476 push @ropts, @rvalopts;
3480 last unless $ARGV[0] =~ m/^-/;
3484 if (m/^--dry-run$/) {
3487 } elsif (m/^--damp-run$/) {
3490 } elsif (m/^--no-sign$/) {
3493 } elsif (m/^--help$/) {
3495 } elsif (m/^--version$/) {
3497 } elsif (m/^--new$/) {
3500 } elsif (m/^--([-0-9a-z]+)=(.+)/s &&
3501 ($om = $opts_opt_map{$1}) &&
3505 } elsif (m/^--([-0-9a-z]+):(.*)/s &&
3506 !$opts_opt_cmdonly{$1} &&
3507 ($om = $opts_opt_map{$1})) {
3510 } elsif (m/^--ignore-dirty$/s) {
3513 } elsif (m/^--no-quilt-fixup$/s) {
3515 $quilt_mode = 'nocheck';
3516 } elsif (m/^--no-rm-on-error$/s) {
3519 } elsif (m/^--(no-)?rm-old-changes$/s) {
3522 } elsif (m/^--deliberately-($deliberately_re)$/s) {
3524 push @deliberatelies, $&;
3525 } elsif (m/^--always-split-source-build$/s) {
3526 # undocumented, for testing
3528 $need_split_build_invocation = 1;
3529 } elsif (m/^(--[-0-9a-z]+)(=|$)/ && ($oi = $valopts_long{$1})) {
3530 $val = $2 ? $' : undef; #';
3531 $valopt->($oi->{Long});
3533 badusage "unknown long option \`$_'";
3540 } elsif (s/^-L/-/) {
3543 } elsif (s/^-h/-/) {
3545 } elsif (s/^-D/-/) {
3549 } elsif (s/^-N/-/) {
3554 push @changesopts, $_;
3556 } elsif (s/^-wn$//s) {
3558 $cleanmode = 'none';
3559 } elsif (s/^-wg$//s) {
3562 } elsif (s/^-wgf$//s) {
3564 $cleanmode = 'git-ff';
3565 } elsif (s/^-wd$//s) {
3567 $cleanmode = 'dpkg-source';
3568 } elsif (s/^-wdd$//s) {
3570 $cleanmode = 'dpkg-source-d';
3571 } elsif (s/^-wc$//s) {
3573 $cleanmode = 'check';
3574 } elsif (m/^-[a-zA-Z]/ && ($oi = $valopts_short{$&})) {
3576 $val = undef unless length $val;
3577 $valopt->($oi->{Short});
3580 badusage "unknown short option \`$_'";
3587 sub finalise_opts_opts () {
3588 foreach my $k (keys %opts_opt_map) {
3589 my $om = $opts_opt_map{$k};
3591 my $v = access_cfg("cmd-$k", 'RETURN-UNDEF');
3593 badcfg "cannot set command for $k"
3594 unless length $om->[0];
3598 foreach my $c (access_cfg_cfgs("opts-$k")) {
3599 my $vl = $gitcfg{$c};
3600 printdebug "CL $c ",
3601 ($vl ? join " ", map { shellquote } @$vl : ""),
3602 "\n" if $debuglevel >= 4;
3604 badcfg "cannot configure options for $k"
3605 if $opts_opt_cmdonly{$k};
3606 my $insertpos = $opts_cfg_insertpos{$k};
3607 @$om = ( @$om[0..$insertpos-1],
3609 @$om[$insertpos..$#$om] );
3614 if ($ENV{$fakeeditorenv}) {
3616 quilt_fixup_editor();
3622 print STDERR "DRY RUN ONLY\n" if $dryrun_level > 1;
3623 print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n"
3624 if $dryrun_level == 1;
3626 print STDERR $helpmsg or die $!;
3629 my $cmd = shift @ARGV;
3632 if (!defined $rmchanges) {
3633 local $access_forpush;
3634 $rmchanges = access_cfg_bool(0, 'rm-old-changes');
3637 if (!defined $quilt_mode) {
3638 local $access_forpush;
3639 $quilt_mode = cfg('dgit.force.quilt-mode', 'RETURN-UNDEF')
3640 // access_cfg('quilt-mode', 'RETURN-UNDEF')
3642 $quilt_mode =~ m/^($quilt_modes_re)$/
3643 or badcfg "unknown quilt-mode \`$quilt_mode'";
3647 if (!defined $cleanmode) {
3648 local $access_forpush;
3649 $cleanmode = access_cfg('clean-mode', 'RETURN-UNDEF');
3650 $cleanmode //= 'dpkg-source';
3652 badcfg "unknown clean-mode \`$cleanmode'" unless
3653 $cleanmode =~ m/^($cleanmode_re)$(?!\n)/s;
3656 my $fn = ${*::}{"cmd_$cmd"};
3657 $fn or badusage "unknown operation $cmd";