X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?p=dgit.git;a=blobdiff_plain;f=dgit;h=42f9f457332655cae0832baec4c03c4029fcad50;hp=3c311400afeaec6ef64dccc64cbc5a4129e59c66;hb=0c838feda384f115a115ddd609eca9b8c6ece162;hpb=1c920a62c188ff676127973a548855fa7b02841c diff --git a/dgit b/dgit index 3c311400..42f9f457 100755 --- a/dgit +++ b/dgit @@ -18,6 +18,7 @@ # along with this program. If not, see . use strict; +$SIG{__WARN__} = sub { die $_[0]; }; use IO::Handle; use Data::Dumper; @@ -31,7 +32,8 @@ use POSIX; use IPC::Open2; use Digest::SHA; use Digest::MD5; -use Config; + +use Debian::Dgit; our $our_version = 'UNRELEASED'; ###substituted### @@ -48,11 +50,14 @@ our $changesfile; our $buildproductsdir = '..'; our $new_package = 0; our $ignoredirty = 0; -our $noquilt = 0; our $rmonerror = 1; +our @deliberatelies; +our %supersedes; our $existing_package = 'dpkg'; our $cleanmode = 'dpkg-source'; our $changes_since_version; +our $quilt_mode; +our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck'; our $we_are_responder; our $initiator_tempdir; @@ -93,26 +98,18 @@ our %opts_opt_cmdonly = ('gpg' => 1); our $keyid; -our $debug = 0; -open DEBUG, ">/dev/null" or die $!; - autoflush STDOUT 1; our $remotename = 'dgit'; our @ourdscfield = qw(Dgit Vcs-Dgit-Master); -our $branchprefix = 'dgit'; our $csuite; +our $instead_distro; sub lbranch () { return "$branchprefix/$csuite"; } my $lbranch_re = '^refs/heads/'.$branchprefix.'/([^/.]+)$'; sub lref () { return "refs/heads/".lbranch(); } -sub lrref () { return "refs/remotes/$remotename/$branchprefix/$csuite"; } -sub rrref () { return "refs/$branchprefix/$csuite"; } -sub debiantag ($) { - my ($v) = @_; - $v =~ y/~:/_%/; - return "debian/$v"; -} +sub lrref () { return "refs/remotes/$remotename/".server_branch($csuite); } +sub rrref () { return server_ref($csuite); } sub stripepoch ($) { my ($vsn) = @_; @@ -131,7 +128,7 @@ sub dscfn ($) { } our $us = 'dgit'; -our $debugprefix = ''; +initdebug(''); our @end; END { @@ -142,29 +139,6 @@ END { } }; -our @signames = split / /, $Config{sig_name}; - -sub waitstatusmsg () { - if (!$?) { - return "terminated, reporting successful completion"; - } elsif (!($? & 255)) { - return "failed with error exit status ".WEXITSTATUS($?); - } elsif (WIFSIGNALED($?)) { - my $signum=WTERMSIG($?); - return "died due to fatal signal ". - ($signames[$signum] // "number $signum"). - ($? & 128 ? " (core dumped)" : ""); # POSIX(3pm) has no WCOREDUMP - } else { - return "failed with unknown wait status ".$?; - } -} - -sub printdebug { print DEBUG $debugprefix, @_ or die $!; } - -sub fail { - die $us.($we_are_responder ? " (build host)" : "").": @_\n"; -} - sub badcfg { print STDERR "$us: invalid configuration: @_\n"; exit 12; } sub no_such_package () { @@ -183,11 +157,9 @@ sub changedir ($) { chdir $newdir or die "chdir: $newdir: $!"; } -sub stat_exists ($) { - my ($f) = @_; - return 1 if stat $f; - return 0 if $!==&ENOENT; - die "stat $f: $!"; +sub deliberately ($) { + my ($enquiry) = @_; + return !!grep { $_ eq "--deliberately-$enquiry" } @deliberatelies; } #---------- remote protocol support, common ---------- @@ -378,42 +350,8 @@ sub url_get { our ($dscdata,$dscurl,$dsc,$dsc_checked,$skew_warning_vsn); -sub shellquote { - my @out; - local $_; - foreach my $a (@_) { - $_ = $a; - if (m{[^-=_./0-9a-z]}i) { - s{['\\]}{'\\$&'}g; - push @out, "'$_'"; - } else { - push @out, $_; - } - } - return join ' ', @out; -} - -sub printcmd { - my $fh = shift @_; - my $intro = shift @_; - print $fh $intro," " or die $!; - print $fh shellquote @_ or die $!; - print $fh "\n" or die $!; -} - -sub failedcmd { - { local ($!); printcmd \*STDERR, "$us: failed command:", @_ or die $!; }; - if ($!) { - fail "failed to fork/exec: $!"; - } elsif ($?) { - fail "subprocess ".waitstatusmsg(); - } else { - fail "subprocess produced invalid output"; - } -} - sub runcmd { - printcmd(\*DEBUG,$debugprefix."+",@_) if $debug>0; + debugcmd "+",@_; $!=0; $?=0; failedcmd @_ if system @_; } @@ -429,27 +367,6 @@ sub printdone { } } -sub cmdoutput_errok { - die Dumper(\@_)." ?" if grep { !defined } @_; - printcmd(\*DEBUG,$debugprefix."|",@_) if $debug>0; - open P, "-|", @_ or die $!; - my $d; - $!=0; $?=0; - { local $/ = undef; $d =

; } - die $! if P->error; - if (!close P) { printdebug "=>!$?\n" if $debug>0; return undef; } - chomp $d; - $d =~ m/^.*/; - printdebug "=> \`$&'",(length $' ? '...' : ''),"\n" if $debug>0; #'; - return $d; -} - -sub cmdoutput { - my $d = cmdoutput_errok @_; - defined $d or failedcmd @_; - return $d; -} - sub dryrun_report { printcmd(\*STDERR,$debugprefix."#",@_); } @@ -518,13 +435,21 @@ our %defcfg = ('dgit.default.distro' => 'debian', 'dgit.default.ssh' => 'ssh', 'dgit.default.archive-query' => 'madison:', 'dgit.default.sshpsql-dbname' => 'service=projectb', - 'dgit-distro.debian.archive-query' => 'sshpsql:', - 'dgit-distro.debian.git-host' => 'git.debian.org', + 'dgit-distro.debian.archive-query' => 'ftpmasterapi:', + 'dgit-distro.debian.git-host' => 'dgit-git.debian.net', + 'dgit-distro.debian.git-user-force' => 'dgit', 'dgit-distro.debian.git-proto' => 'git+ssh://', - 'dgit-distro.debian.git-path' => '/git/dgit-repos/repos', + 'dgit-distro.debian.git-path' => '/dgit/debian/repos', 'dgit-distro.debian.git-check' => 'ssh-cmd', - 'dgit-distro.debian.git-create' => 'ssh-cmd', - 'dgit-distro.debian.sshpsql-host' => 'mirror.ftp-master.debian.org', + 'dgit-distro.debian.archive-query-url', 'https://api.ftp-master.debian.org/', + 'dgit-distro.debian.archive-query-tls-key', + '/etc/ssl/certs/%HOST%.pem:/etc/dgit/%HOST%.pem', + 'dgit-distro.debian.diverts.alioth' => '/alioth', + 'dgit-distro.debian/alioth.git-host' => 'git.debian.org', + 'dgit-distro.debian/alioth.git-user-force' => '', + 'dgit-distro.debian/alioth.git-proto' => 'git+ssh://', + 'dgit-distro.debian/alioth.git-path' => '/git/dgit-repos/repos', + 'dgit-distro.debian/alioth.git-create' => 'ssh-cmd', 'dgit-distro.debian.upload-host' => 'ftp-master', # for dput 'dgit-distro.debian.mirror' => 'http://ftp.debian.org/debian/', 'dgit-distro.debian.backports-quirk' => '(squeeze)-backports*', @@ -538,7 +463,8 @@ our %defcfg = ('dgit.default.distro' => 'debian', 'dgit-distro.test-dummy.git-url' => "$td/git", 'dgit-distro.test-dummy.git-host' => "git", 'dgit-distro.test-dummy.git-path' => "$td/git", - 'dgit-distro.test-dummy.archive-query' => "dummycat:$td/aq", + 'dgit-distro.test-dummy.archive-query' => "ftpmasterapi:", + 'dgit-distro.test-dummy.archive-query-url' => "file://$td/aq/", 'dgit-distro.test-dummy.mirror' => "file://$td/mirror/", 'dgit-distro.test-dummy.upload-host' => 'test-dummy', ); @@ -549,7 +475,7 @@ sub cfg { my @cmd = (@git, qw(config --), $c); my $v; { - local ($debug) = $debug-1; + local ($debuglevel) = $debuglevel-2; $v = cmdoutput_errok @cmd; }; if ($?==0) { @@ -595,15 +521,16 @@ sub access_distros () { # Returns list of distros to try, in order # # We want to try: + # 0. `instead of' distro name(s) we have been pointed to # 1. the access_quirk distro, if any # 2a. the user's specified distro, or failing that } basedistro # 2b. the distro calculated from the suite } my @l = access_basedistro(); my (undef,$quirkdistro) = access_quirk(); - unshift @l, $quirkdistro if defined $quirkdistro; - - return @l; + unshift @l, $quirkdistro; + unshift @l, $instead_distro; + return grep { defined } @l; } sub access_cfg (@) { @@ -655,9 +582,16 @@ sub access_cfg_ssh () { } } +sub access_runeinfo ($) { + my ($info) = @_; + return ": dgit ".access_basedistro()." $info ;"; +} + sub access_someuserhost ($) { my ($some) = @_; - my $user = access_cfg("$some-user",'username'); + my $user = access_cfg("$some-user-force", 'RETURN-UNDEF'); + defined($user) && length($user) or + $user = access_cfg("$some-user",'username'); my $host = access_cfg("$some-host"); return length($user) ? "$user\@$host" : $host; } @@ -733,23 +667,6 @@ sub parsechangelog { return $c; } -sub git_get_ref ($) { - my ($refname) = @_; - my $got = cmdoutput_errok @git, qw(show-ref --), $refname; - if (!defined $got) { - $?==256 or fail "git show-ref failed (status $?)"; - printdebug "ref $refname= [show-ref exited 1]\n"; - return ''; - } - if ($got =~ m/^(\w+) \Q$refname\E$/m) { - printdebug "ref $refname=$1\n"; - return $1; - } else { - printdebug "ref $refname= [no match]\n"; - return ''; - } -} - sub must_getcwd () { my $d = getcwd(); defined $d or fail "getcwd failed: $!"; @@ -773,6 +690,92 @@ sub pool_dsc_subpath ($$) { return "/pool/$component/$prefix/$package/".dscfn($vsn); } +#---------- `ftpmasterapi' archive query method (nascent) ---------- + +sub archive_api_query_cmd ($) { + my ($subpath) = @_; + my @cmd = qw(curl -sS); + my $url = access_cfg('archive-query-url'); + if ($url =~ m#^https://([-.0-9a-z]+)/#) { + my $host = $1; + my $keys = access_cfg('archive-query-tls-key','RETURN-UNDEF'); + foreach my $key (split /\:/, $keys) { + $key =~ s/\%HOST\%/$host/g; + if (!stat $key) { + fail "for $url: stat $key: $!" unless $!==ENOENT; + next; + } + push @cmd, "--ca-certificate=$key", "--ca-directory=/dev/enoent"; + last; + } + } + push @cmd, $url.$subpath; + return @cmd; +} + +sub api_query ($$) { + use JSON; + my ($data, $subpath) = @_; + badcfg "ftpmasterapi archive query method takes no data part" + if length $data; + my @cmd = archive_api_query_cmd($subpath); + my $json = cmdoutput @cmd; + return decode_json($json); +} + +sub canonicalise_suite_ftpmasterapi () { + my ($proto,$data) = @_; + my $suites = api_query($data, 'suites'); + my @matched; + foreach my $entry (@$suites) { + next unless grep { + my $v = $entry->{$_}; + defined $v && $v eq $isuite; + } qw(codename name); + push @matched, $entry; + } + fail "unknown suite $isuite" unless @matched; + my $cn; + eval { + @matched==1 or die "multiple matches for suite $isuite\n"; + $cn = "$matched[0]{codename}"; + defined $cn or die "suite $isuite info has no codename\n"; + $cn =~ m/^$suite_re$/ or die "suite $isuite maps to bad codename\n"; + }; + die "bad ftpmaster api response: $@\n".Dumper(\@matched) + if length $@; + return $cn; +} + +sub archive_query_ftpmasterapi () { + my ($proto,$data) = @_; + my $info = api_query($data, "dsc_in_suite/$isuite/$package"); + my @rows; + my $digester = Digest::SHA->new(256); + foreach my $entry (@$info) { + eval { + my $vsn = "$entry->{version}"; + my ($ok,$msg) = version_check $vsn; + die "bad version: $msg\n" unless $ok; + my $component = "$entry->{component}"; + $component =~ m/^$component_re$/ or die "bad component"; + my $filename = "$entry->{filename}"; + $filename && $filename !~ m#[^-+:._~0-9a-zA-Z/]|^[/.]|/[/.]# + or die "bad filename"; + my $sha256sum = "$entry->{sha256sum}"; + $sha256sum =~ m/^[0-9a-f]+$/ or die "bad sha256sum"; + push @rows, [ $vsn, "/pool/$component/$filename", + $digester, $sha256sum ]; + }; + die "bad ftpmaster api response: $@\n".Dumper($entry) + if length $@; + } + @rows = sort { -version_compare($a->[0],$b->[0]) } @rows; + return @rows; +} + +#---------- `madison' archive query method ---------- + sub archive_query_madison { return map { [ @$_[0..1] ] } madison_get_parse(@_); } @@ -819,8 +822,10 @@ sub canonicalise_suite_madison { return $r[0][2]; } -sub sshpsql ($$) { - my ($data,$sql) = @_; +#---------- `sshpsql' archive query method ---------- + +sub sshpsql ($$$) { + my ($data,$runeinfo,$sql) = @_; if (!length $data) { $data= access_someuserhost('sshpsql').':'. access_cfg('sshpsql-dbname'); @@ -829,8 +834,10 @@ sub sshpsql ($$) { my ($userhost,$dbname) = ($`,$'); #'; my @rows; my @cmd = (access_cfg_ssh, $userhost, - "export LANG=C; ".shellquote qw(psql -A), $dbname, qw(-c), $sql); - printcmd(\*DEBUG,$debugprefix."|",@cmd) if $debug>0; + access_runeinfo("ssh-psql $runeinfo"). + " export LC_MESSAGES=C; export LC_CTYPE=C;". + " ".shellquote qw(psql -A), $dbname, qw(-c), $sql); + debugcmd "|",@cmd; open P, "-|", @cmd or die $!; while (

) { chomp or die; @@ -849,13 +856,13 @@ sub sshpsql ($$) { } sub sql_injection_check { - foreach (@_) { die "$_ $& ?" if m/[']/; } + foreach (@_) { die "$_ $& ?" if m{[^-+=:_.,/0-9a-zA-Z]}; } } sub archive_query_sshpsql ($$) { my ($proto,$data) = @_; sql_injection_check $isuite, $package; - my @rows = sshpsql($data, <[0],$b->[0]); } @rows; } +#---------- archive query entrypoints and rest of program ---------- + sub canonicalise_suite () { return if defined $csuite; fail "cannot operate on $isuite suite" if $isuite eq 'UNRELEASED'; @@ -958,9 +969,9 @@ sub get_archive_dsc () { " archive told us to expect $digest"; } my $dscfh = new IO::File \$dscdata, '<' or die $!; - printdebug Dumper($dscdata) if $debug>1; + printdebug Dumper($dscdata) if $debuglevel>1; $dsc = parsecontrolfh($dscfh,$dscurl,1); - printdebug Dumper($dsc) if $debug>1; + printdebug Dumper($dsc) if $debuglevel>1; my $fmt = getfield $dsc, 'Format'; fail "unsupported source format $fmt, sorry" unless $format_ok{$fmt}; $dsc_checked = !!$digester; @@ -969,15 +980,25 @@ sub get_archive_dsc () { $dsc = undef; } +sub check_for_git (); sub check_for_git () { # returns 0 or 1 my $how = access_cfg('git-check'); if ($how eq 'ssh-cmd') { my @cmd = (access_cfg_ssh, access_gituserhost(), + access_runeinfo("git-check $package"). " set -e; cd ".access_cfg('git-path').";". " if test -d $package.git; then echo 1; else echo 0; fi"); my $r= cmdoutput @cmd; + if ($r =~ m/^divert (\w+)$/) { + my $divert=$1; + my ($usedistro,) = access_distros(); + $instead_distro= cfg("dgit-distro.$usedistro.diverts.$divert"); + $instead_distro =~ s{^/}{ access_basedistro()."/" }e; + printdebug "diverting $divert so using distro $instead_distro\n"; + return check_for_git(); + } failedcmd @cmd unless $r =~ m/^[01]$/; return $r+0; } elsif ($how eq 'true') { @@ -994,6 +1015,7 @@ sub create_remote_git_repo () { if ($how eq 'ssh-cmd') { runcmd_ordryrun (access_cfg_ssh, access_gituserhost(), + access_runeinfo("git-create $package"). "set -e; cd ".access_cfg('git-path').";". " cp -a _template $package.git"); } elsif ($how eq 'true') { @@ -1019,6 +1041,12 @@ sub mktree_in_ud_here () { symlink '../../../../objects','.git/objects' or die $!; } +sub git_write_tree () { + my $tree = cmdoutput @git, qw(write-tree); + $tree =~ m/^\w+$/ or die "$tree ?"; + return $tree; +} + sub mktree_in_ud_from_only_subdir () { # changes into the subdir my (@dirs) = <*/.>; @@ -1033,8 +1061,7 @@ sub mktree_in_ud_from_only_subdir () { rmtree '.pc'; } runcmd @git, qw(add -Af); - my $tree = cmdoutput @git, qw(write-tree); - $tree =~ m/^\w+$/ or die "$tree ?"; + my $tree=git_write_tree(); return ($tree,$dir); } @@ -1227,22 +1254,6 @@ sub ensure_we_have_orig () { } } -sub rev_parse ($) { - return cmdoutput @git, qw(rev-parse), "$_[0]~0"; -} - -sub is_fast_fwd ($$) { - my ($ancestor,$child) = @_; - my @cmd = (@git, qw(merge-base), $ancestor, $child); - my $mb = cmdoutput_errok @cmd; - if (defined $mb) { - return rev_parse($mb) eq rev_parse($ancestor); - } else { - $?==256 or failedcmd @cmd; - return 0; - } -} - sub git_fetch_us () { runcmd_ordryrun_local @git, qw(fetch),access_giturl(),fetchspec(); } @@ -1289,8 +1300,8 @@ $later_warning_msg END $hash = $lastpush_hash; } else { - fail "archive's .dsc refers to ".$dsc_hash. - " but this is an ancestor of ".$lastpush_hash; + fail "git head (".lrref()."=$lastpush_hash) is not a ". + "descendant of archive's .dsc hash ($dsc_hash)"; } } elsif ($dsc) { $hash = generate_commit_from_dsc(); @@ -1355,6 +1366,7 @@ sub clone ($) { my ($dstdir) = @_; canonicalise_suite(); badusage "dry run makes no sense with clone" unless act_local(); + my $hasgit = check_for_git(); mkdir $dstdir or die "$dstdir $!"; changedir $dstdir; runcmd @git, qw(init -q); @@ -1366,7 +1378,7 @@ sub clone ($) { close H or die $!; runcmd @git, qw(remote add), 'origin', $giturl; } - if (check_for_git()) { + if ($hasgit) { progress "fetching existing git history"; git_fetch_us(); runcmd_ordryrun_local @git, qw(fetch origin); @@ -1376,6 +1388,7 @@ sub clone ($) { fetch_from_archive() or no_such_package; my $vcsgiturl = $dsc->{'Vcs-Git'}; if (length $vcsgiturl) { + $vcsgiturl =~ s/\s+-b\s+\S+//g; runcmd @git, qw(remote add vcs-git), $vcsgiturl; } runcmd @git, qw(reset --hard), lrref(); @@ -1400,7 +1413,7 @@ sub pull () { sub check_not_dirty () { return if $ignoredirty; my @cmd = (@git, qw(diff --quiet HEAD)); - printcmd(\*DEBUG,$debugprefix."+",@cmd) if $debug>0; + debugcmd "+",@cmd; $!=0; $?=0; system @cmd; return if !$! && !$?; if (!$! && $?==256) { @@ -1448,7 +1461,7 @@ sub get_source_format () { sub madformat ($) { my ($format) = @_; return 0 unless $format eq '3.0 (quilt)'; - if ($noquilt) { + if ($quilt_mode eq 'nocheck') { progress "Not doing any fixup of \`$format' due to --no-quilt-fixup"; return 0; } @@ -1504,6 +1517,8 @@ sub push_mktag ($$$$$$$) { # We make the git tag by hand because (a) that makes it easier # to control the "tagger" (b) we can do remote signing my $authline = clogp_authline $clogp; + my $delibs = join(" ", "",@deliberatelies); + my $declaredistro = access_basedistro(); open TO, '>', $tfn->('.tmp') or die $!; print TO <('.tmp'); @@ -1546,7 +1568,8 @@ sub sign_changes ($) { } } -sub dopush () { +sub dopush ($) { + my ($forceflag) = @_; printdebug "actually entering push\n"; prep_ud(); @@ -1581,9 +1604,9 @@ sub dopush () { $dscpath =~ m#^/# ? $dscpath : "../../../$dscpath"; my ($tree,$dir) = mktree_in_ud_from_only_subdir(); changedir '../../../..'; - my $diffopt = $debug>0 ? '--exit-code' : '--quiet'; + my $diffopt = $debuglevel>0 ? '--exit-code' : '--quiet'; my @diffcmd = (@git, qw(diff), $diffopt, $tree); - printcmd \*DEBUG,$debugprefix."+",@diffcmd; + debugcmd "+",@diffcmd; $!=0; $?=0; my $r = system @diffcmd; if ($r) { @@ -1602,7 +1625,7 @@ sub dopush () { # runcmd @git, qw(fetch -p ), "$alioth_git/$package.git", # map { lref($_).":".rref($_) } # (uploadbranch()); - my $head = rev_parse('HEAD'); + my $head = git_rev_parse('HEAD'); if (!$changesfile) { my $multi = "$buildproductsdir/". "${package}_".(stripepoch $cversion)."_multi.changes"; @@ -1625,6 +1648,14 @@ sub dopush () { responder_send_command("param head $head"); responder_send_command("param csuite $csuite"); + if ($forceflag && defined $lastpush_hash) { + git_for_each_tag_referring($lastpush_hash, sub { + my ($objid,$refobjid,$fullrefname,$tagname) = @_; + responder_send_command("supersedes $fullrefname=$objid"); + $supersedes{$fullrefname} = $objid; + }); + } + my $tfn = sub { ".git/dgit/tag$_[0]"; }; my $tagobjfn; @@ -1648,7 +1679,7 @@ sub dopush () { create_remote_git_repo(); } runcmd_ordryrun @git, qw(push),access_giturl(), - "HEAD:".rrref(), "refs/tags/$tag"; + $forceflag."HEAD:".rrref(), "refs/tags/$tag"; runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), 'HEAD'; if ($we_are_responder) { @@ -1781,17 +1812,27 @@ sub cmd_push { if (check_for_git()) { git_fetch_us(); } + my $forceflag = ''; if (fetch_from_archive()) { - is_fast_fwd(lrref(), 'HEAD') or + if (is_fast_fwd(lrref(), 'HEAD')) { + # ok + } elsif (deliberately('not-fast-forward') || + deliberately('TEST-not-fast-forward-dgit-only')) { + $forceflag = '+'; + } else { fail "dgit push: HEAD is not a descendant". " of the archive's version.\n". - "$us: To overwrite it, use git merge -s ours ".lrref()."."; + "dgit: To overwrite its contents,". + " use git merge -s ours ".lrref().".\n". + "dgit: To rewind history, if permitted by the archive,". + " use --deliberately-not-fast-forward"; + } } else { $new_package or fail "package appears to be new in this suite;". " if this is intentional, use --new"; } - dopush(); + dopush($forceflag); } #---------- remote commands' implementation ---------- @@ -1807,6 +1848,7 @@ sub cmd_remote_push_build_host { # offered several) $debugprefix = ' '; $we_are_responder = 1; + $us .= " (build host)"; open PI, "<&STDIN" or die $!; open STDIN, "/dev/null" or die $!; @@ -1872,7 +1914,7 @@ sub cmd_rpush { push @rdgit, qw(remote-push-build-host), (scalar @rargs), @rargs; push @rdgit, @ARGV; my @cmd = (@ssh, $host, shellquote @rdgit); - printcmd \*DEBUG,$debugprefix."+",@cmd; + debugcmd "+",@cmd; if (defined $initiator_tempdir) { rmtree $initiator_tempdir; @@ -1929,6 +1971,14 @@ sub i_resp_param ($) { $i_param{$1} = $2; } +sub i_resp_supersedes ($) { + $_[0] =~ m#^(refs/tags/\S+)=(\w+)$# + or badproto \*RO, "bad supersedes spec"; + my $r = system qw(git check-ref-format), $1; + die "bad supersedes ref spec ($r)" if $r; + $supersedes{$1} = $2; +} + our %i_wanted; sub i_resp_want ($) { @@ -2000,8 +2050,276 @@ our $version; our $sourcechanges; our $dscfn; +#----- `3.0 (quilt)' handling ----- + our $fakeeditorenv = 'DGIT_FAKE_EDITOR_QUILT'; +sub quiltify_dpkg_commit ($$$;$) { + my ($patchname,$author,$msg, $xinfo) = @_; + $xinfo //= ''; + + mkpath '.git/dgit'; + my $descfn = ".git/dgit/quilt-description.tmp"; + open O, '>', $descfn or die "$descfn: $!"; + $msg =~ s/\s+$//g; + $msg =~ s/\n/\n /g; + $msg =~ s/^\s+$/ ./mg; + print O < $git_commit_id, + # Child => $c, # or undef if P=T + # Whynot => $reason_edge_PC_unsuitable, # in @nots only + # Nontrivial => true iff $p..$c has relevant changes + # }; + + my @todo; + my @nots; + my $sref_S; + my $max_work=100; + my %considered; # saves being exponential on some weird graphs + + my $t_sentinels = quiltify_tree_sentinelfiles $target; + + my $not = sub { + my ($search,$whynot) = @_; + printdebug " search NOT $search->{Commit} $whynot\n"; + $search->{Whynot} = $whynot; + push @nots, $search; + no warnings qw(exiting); + next; + }; + + push @todo, { + Commit => $target, + }; + + while (@todo) { + my $c = shift @todo; + next if $considered{$c->{Commit}}++; + + $not->($c, "maximum search space exceeded") if --$max_work <= 0; + + printdebug "quiltify investigate $c->{Commit}\n"; + + # are we done? + if (!quiltify_trees_differ $c->{Commit}, $oldtiptree) { + printdebug " search finished hooray!\n"; + $sref_S = $c; + last; + } + + if ($quilt_mode eq 'nofix') { + fail "quilt fixup required but quilt mode is \`nofix'\n". + "HEAD commit $c->{Commit} differs from tree implied by ". + " debian/patches (tree object $oldtiptree)"; + } + if ($quilt_mode eq 'smash') { + printdebug " search quitting smash\n"; + last; + } + + my $c_sentinels = quiltify_tree_sentinelfiles $c->{Commit}; + $not->($c, "has $c_sentinels not $t_sentinels") + if $c_sentinels ne $t_sentinels; + + my $commitdata = cmdoutput @git, qw(cat-file commit), $c->{Commit}; + $commitdata =~ m/\n\n/; + $commitdata =~ $`; + my @parents = ($commitdata =~ m/^parent (\w+)$/gm); + @parents = map { { Commit => $_, Child => $c } } @parents; + + $not->($c, "root commit") if !@parents; + + foreach my $p (@parents) { + $p->{Nontrivial}= quiltify_trees_differ $p->{Commit},$c->{Commit}; + } + my $ndiffers = grep { $_->{Nontrivial} } @parents; + $not->($c, "merge ($ndiffers nontrivial parents)") if $ndiffers > 1; + + foreach my $p (@parents) { + printdebug "considering C=$c->{Commit} P=$p->{Commit}\n"; + + my @cmd= (@git, qw(diff-tree -r --name-only), + $p->{Commit},$c->{Commit}, qw(-- debian/patches .pc)); + my $patchstackchange = cmdoutput @cmd; + if (length $patchstackchange) { + $patchstackchange =~ s/\n/,/g; + $not->($p, "changed $patchstackchange"); + } + + printdebug " search queue P=$p->{Commit} ", + ($p->{Nontrivial} ? "NT" : "triv"),"\n"; + push @todo, $p; + } + } + + if (!$sref_S) { + printdebug "quiltify want to smash\n"; + + my $abbrev = sub { + my $x = $_[0]{Commit}; + $x =~ s/(.*?[0-9a-z]{8})[0-9a-z]*$/$1/; + return $; + }; + my $reportnot = sub { + my ($notp) = @_; + my $s = $abbrev->($notp); + my $c = $notp->{Child}; + $s .= "..".$abbrev->($c) if $c; + $s .= ": ".$c->{Whynot}; + return $s; + }; + if ($quilt_mode eq 'linear') { + print STDERR "$us: quilt fixup cannot be linear. Stopped at:\n"; + foreach my $notp (@nots) { + print STDERR "$us: ", $reportnot->($notp), "\n"; + } + fail "quilt fixup naive history linearisation failed.\n". + "Use dpkg-source --commit by hand; or, --quilt=smash for one ugly patch"; + } elsif ($quilt_mode eq 'smash') { + } elsif ($quilt_mode eq 'auto') { + progress "quilt fixup cannot be linear, smashing..."; + } else { + die "$quilt_mode ?"; + } + + my $time = time; + my $ncommits = 3; + my $msg = cmdoutput @git, qw(log), "-n$ncommits"; + + quiltify_dpkg_commit "auto-$version-$target-$time", + (getfield $clogp, 'Maintainer'), + "Automatically generated patch ($clogp->{Version})\n". + "Last (up to) $ncommits git changes, FYI:\n\n". $msg; + return; + } + + progress "quiltify linearisation planning successful, executing..."; + + for (my $p = $sref_S; + my $c = $p->{Child}; + $p = $p->{Child}) { + printdebug "quiltify traverse $p->{Commit}..$c->{Commit}\n"; + next unless $p->{Nontrivial}; + + my $cc = $c->{Commit}; + + my $commitdata = cmdoutput @git, qw(cat-file commit), $cc; + $commitdata =~ m/\n\n/ or die "$c ?"; + $commitdata = $`; + my $msg = $'; #'; + $commitdata =~ m/^author (.*) \d+ [-+0-9]+$/m or die "$cc ?"; + my $author = $1; + + $msg =~ s/^(.*)\n*/$1\n/ or die "$cc $msg ?"; + + my $title = $1; + my $patchname = $title; + $patchname =~ s/[.:]$//; + $patchname =~ y/ A-Z/-a-z/; + $patchname =~ y/-a-z0-9_.+=~//cd; + $patchname =~ s/^\W/x-$&/; + $patchname = substr($patchname,0,40); + my $index; + for ($index=''; + stat "debian/patches/$patchname$index"; + $index++) { } + $!==ENOENT or die "$patchname$index $!"; + + runcmd @git, qw(checkout -q), $cc; + + # We use the tip's changelog so that dpkg-source doesn't + # produce complaining messages from dpkg-parsechangelog. None + # of the information dpkg-source gets from the changelog is + # actually relevant - it gets put into the original message + # which dpkg-source provides our stunt editor, and then + # overwritten. + runcmd @git, qw(checkout -q), $target, qw(debian/changelog); + + quiltify_dpkg_commit "$patchname$index", $author, $msg, + "X-Dgit-Generated: $clogp->{Version} $cc\n"; + + runcmd @git, qw(checkout -q), $cc, qw(debian/changelog); + } + + runcmd @git, qw(checkout -q master); +} + sub build_maybe_quilt_fixup () { my $format=get_source_format; return unless madformat $format; @@ -2052,7 +2370,7 @@ sub build_maybe_quilt_fixup () { # 6. Back in the main tree, fast forward to the new HEAD my $clogp = parsechangelog(); - my $headref = rev_parse('HEAD'); + my $headref = git_rev_parse('HEAD'); prep_ud(); changedir $ud; @@ -2120,33 +2438,7 @@ END rename '../fake/.pc','.pc' or die $!; } - my $author = getfield $clogp, 'Maintainer'; - my $time = time; - my $ncommits = 3; - my $patchname = "auto-$version-$headref-$time"; - my $msg = cmdoutput @git, qw(log), "-n$ncommits"; - mkpath '.git/dgit'; - my $descfn = ".git/dgit/quilt-description.tmp"; - open O, '>', $descfn or die "$descfn: $!"; - $msg =~ s/\n/\n /g; - $msg =~ s/^\s+$/ ./mg; - print O <{Version}) - Last (up to) $ncommits git changes, FYI: - . - $msg -Author: $author - ---- - -END - close O or die $!; - { - local $ENV{'EDITOR'} = cmdoutput qw(realpath --), $0; - local $ENV{'VISUAL'} = $ENV{'EDITOR'}; - local $ENV{$fakeeditorenv} = cmdoutput qw(realpath --), $descfn; - runcmd_ordryrun_local @dpkgsource, qw(--commit .), $patchname; - } + quiltify($clogp,$headref); if (!open P, '>>', ".pc/applied-patches") { $!==&ENOENT or die $!; @@ -2184,6 +2476,8 @@ sub quilt_fixup_editor () { exit 0; } +#----- other building ----- + sub clean_tree () { if ($cleanmode eq 'dpkg-source') { runcmd_ordryrun_local @dpkgbuildpackage, qw(-T clean); @@ -2321,6 +2615,14 @@ sub cmd_quilt_fixup { build_maybe_quilt_fixup(); } +sub cmd_archive_api_query { + badusage "need only 1 subpath argument" unless @ARGV==1; + my ($subpath) = @ARGV; + my @cmd = archive_api_query_cmd($subpath); + debugcmd ">",@cmd; + exec @cmd or fail "exec curl: $!\n"; +} + #---------- argument parsing and main program ---------- sub cmd_version { @@ -2390,15 +2692,23 @@ sub parseopts () { $cleanmode = $1; } elsif (m/^--clean=(.*)$/s) { badusage "unknown cleaning mode \`$1'"; + } elsif (m/^--quilt=($quilt_modes_re)$/s) { + push @ropts, $_; + $quilt_mode = $1; + } elsif (m/^--quilt=(.*)$/s) { + badusage "unknown quilt fixup mode \`$1'"; } elsif (m/^--ignore-dirty$/s) { push @ropts, $_; $ignoredirty = 1; } elsif (m/^--no-quilt-fixup$/s) { push @ropts, $_; - $noquilt = 1; + $quilt_mode = 'nocheck'; } elsif (m/^--no-rm-on-error$/s) { push @ropts, $_; $rmonerror = 0; + } elsif (m/^--deliberately-($deliberately_re)$/s) { + push @ropts, $_; + push @deliberatelies, $&; } else { badusage "unknown long option \`$_'"; } @@ -2414,9 +2724,8 @@ sub parseopts () { cmd_help(); } elsif (s/^-D/-/) { push @ropts, $&; - open DEBUG, ">&STDERR" or die $!; - autoflush DEBUG 1; - $debug++; + $debuglevel++; + enabledebug(); } elsif (s/^-N/-/) { push @ropts, $&; $new_package=1; @@ -2430,24 +2739,27 @@ sub parseopts () { } elsif (s/^-c(.*=.*)//s) { push @ropts, $&; push @git, '-c', $1; - } elsif (s/^-d(.*)//s) { + } elsif (s/^-d(.+)//s) { push @ropts, $&; $idistro = $1; - } elsif (s/^-C(.*)//s) { + } elsif (s/^-C(.+)//s) { push @ropts, $&; $changesfile = $1; if ($changesfile =~ s#^(.*)/##) { $buildproductsdir = $1; } - } elsif (s/^-k(.*)//s) { + } elsif (s/^-k(.+)//s) { $keyid=$1; - } elsif (s/^-wn//s) { + } elsif (m/^-[vdCk]$/) { + badusage + "option \`$_' requires an argument (and no space before the argument)"; + } elsif (s/^-wn$//s) { push @ropts, $&; $cleanmode = 'none'; - } elsif (s/^-wg//s) { + } elsif (s/^-wg$//s) { push @ropts, $&; $cleanmode = 'git'; - } elsif (s/^-wd//s) { + } elsif (s/^-wd$//s) { push @ropts, $&; $cleanmode = 'dpkg-source'; } else { @@ -2473,6 +2785,15 @@ if (!@ARGV) { my $cmd = shift @ARGV; $cmd =~ y/-/_/; +if (!defined $quilt_mode) { + $quilt_mode = cfg('dgit.force.quilt-mode', 'RETURN-UNDEF') + // access_cfg('quilt-mode', 'RETURN-UNDEF') + // 'linear'; + $quilt_mode =~ m/^($quilt_modes_re)$/ + or badcfg "unknown quilt-mode \`$quilt_mode'"; + $quilt_mode = $1; +} + my $fn = ${*::}{"cmd_$cmd"}; $fn or badusage "unknown operation $cmd"; $fn->();