X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?p=dgit.git;a=blobdiff_plain;f=dgit;h=38b02e4e1b4ca07f8163b9e0ad440a2df0d1b24b;hp=cb4e497778697d8f432b1680f72040e4df8b3cf0;hb=b2930de82f96119b5e4c3ac8f7c855477dfc2e6b;hpb=b92d6ac923182f1227146c9857f78c971e913585 diff --git a/dgit b/dgit index cb4e4977..38b02e4e 100755 --- a/dgit +++ b/dgit @@ -29,9 +29,14 @@ use File::Basename; use Dpkg::Version; use POSIX; use IPC::Open2; +use Digest::SHA; +use Digest::MD5; +use Config; our $our_version = 'UNRELEASED'; ###substituted### +our $rpushprotovsn = 2; + our $isuite = 'unstable'; our $idistro; our $package; @@ -40,18 +45,24 @@ our @ropts; our $sign = 1; our $dryrun_level = 0; our $changesfile; +our $buildproductsdir = '..'; our $new_package = 0; our $ignoredirty = 0; our $noquilt = 0; +our $rmonerror = 1; our $existing_package = 'dpkg'; our $cleanmode = 'dpkg-source'; +our $changes_since_version; our $we_are_responder; our $initiator_tempdir; our %format_ok = map { $_=>1 } ("1.0","3.0 (native)","3.0 (quilt)"); +our $suite_re = '[-+.0-9a-z]+'; + our (@git) = qw(git); our (@dget) = qw(dget); +our (@curl) = qw(curl -f); our (@dput) = qw(dput); our (@debsign) = qw(debsign); our (@gpg) = qw(gpg); @@ -64,7 +75,8 @@ our (@dpkggenchanges) = qw(dpkg-genchanges); our (@mergechanges) = qw(mergechanges -f); our (@changesopts) = (''); -our %opts_opt_map = ('dget' => \@dget, +our %opts_opt_map = ('dget' => \@dget, # accept for compatibility + 'curl' => \@curl, 'dput' => \@dput, 'debsign' => \@debsign, 'gpg' => \@gpg, @@ -90,6 +102,7 @@ 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.'/([^/.]+)$'; @@ -108,16 +121,45 @@ sub stripepoch ($) { return $vsn; } +sub srcfn ($$) { + my ($vsn,$sfx) = @_; + return "${package}_".(stripepoch $vsn).$sfx +} + sub dscfn ($) { my ($vsn) = @_; - return "${package}_".(stripepoch $vsn).".dsc"; + return srcfn($vsn,".dsc"); } -sub changesopts () { return @changesopts[1..$#changesopts]; } - our $us = 'dgit'; our $debugprefix = ''; +our @end; +END { + local ($?); + foreach my $f (@end) { + eval { $f->(); }; + warn "$us: cleanup: $@" if length $@; + } +}; + +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 { @@ -142,6 +184,13 @@ 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: $!"; +} + #---------- remote protocol support, common ---------- # remote push initiator/responder protocol: @@ -179,17 +228,39 @@ sub changedir ($) { # # > complete +our $i_child_pid; + +sub i_child_report () { + # Sees if our child has died, and reap it if so. Returns a string + # describing how it died if it failed, or undef otherwise. + return undef unless $i_child_pid; + my $got = waitpid $i_child_pid, WNOHANG; + return undef if $got <= 0; + die unless $got == $i_child_pid; + $i_child_pid = undef; + return undef unless $?; + return "build host child ".waitstatusmsg(); +} + sub badproto ($$) { my ($fh, $m) = @_; fail "connection lost: $!" if $fh->error; fail "protocol violation; $m not expected"; } +sub badproto_badread ($$) { + my ($fh, $wh) = @_; + fail "connection lost: $!" if $!; + my $report = i_child_report(); + fail $report if defined $report; + badproto $fh, "eof (reading $wh)"; +} + sub protocol_expect (&$) { my ($match, $fh) = @_; local $_; $_ = <$fh>; - defined && chomp or badproto $fh, "eof"; + defined && chomp or badproto_badread $fh, "protocol message"; if (wantarray) { my @r = &$match; return @r if @r; @@ -221,7 +292,7 @@ sub protocol_read_bytes ($$) { $nbytes =~ m/^[1-9]\d{0,5}$/ or badproto \*RO, "bad byte count"; my $d; my $got = read $fh, $d, $nbytes; - $got==$nbytes or badproto $fh, "eof during data block"; + $got==$nbytes or badproto_badread $fh, "data block"; return $d; } @@ -303,17 +374,18 @@ sub url_get { my $r = $ua->get(@_) or die $!; return undef if $r->code == 404; $r->is_success or fail "failed to fetch $what: ".$r->status_line; - return $r->decoded_content(); + return $r->decoded_content(charset => 'none'); } -our ($dscdata,$dscurl,$dsc,$skew_warning_vsn); +our ($dscdata,$dscurl,$dsc,$dsc_checked,$skew_warning_vsn); sub shellquote { my @out; local $_; foreach my $a (@_) { $_ = $a; - if (s{['\\]}{\\$&}g || m{\s} || m{[^-_./0-9a-z]}i) { + if (m{[^-=_./0-9a-z]}i) { + s{['\\]}{'\\$&'}g; push @out, "'$_'"; } else { push @out, $_; @@ -331,13 +403,11 @@ sub printcmd { } sub failedcmd { - { local ($!); printcmd \*STDERR, "$_[0]: failed command:", @_ or die $!; }; + { local ($!); printcmd \*STDERR, "$us: failed command:", @_ or die $!; }; if ($!) { fail "failed to fork/exec: $!"; - } elsif (!($? & 0xff)) { - fail "subprocess failed with error exit status ".($?>>8); } elsif ($?) { - fail "subprocess crashed (wait status $?)"; + fail "subprocess ".waitstatusmsg(); } else { fail "subprocess produced invalid output"; } @@ -441,20 +511,38 @@ sub cmd_help () { exit 0; } +our $td = $ENV{DGIT_TEST_DUMMY_DIR} || "DGIT_TEST_DUMMY_DIR-unset"; + our %defcfg = ('dgit.default.distro' => 'debian', 'dgit.default.username' => '', 'dgit.default.archive-query-default-component' => 'main', '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.git-proto' => 'git+ssh://', 'dgit-distro.debian.git-path' => '/git/dgit-repos/repos', 'dgit-distro.debian.git-check' => 'ssh-cmd', 'dgit-distro.debian.git-create' => 'ssh-cmd', - 'dgit-distro.debian.sshdakls-host' => 'coccia.debian.org', - 'dgit-distro.debian.sshdakls-dir' => - '/srv/ftp-master.debian.org/ftp/dists', + 'dgit-distro.debian.sshpsql-host' => 'mirror.ftp-master.debian.org', 'dgit-distro.debian.upload-host' => 'ftp-master', # for dput - 'dgit-distro.debian.mirror' => 'http://ftp.debian.org/debian/'); + 'dgit-distro.debian.mirror' => 'http://ftp.debian.org/debian/', + 'dgit-distro.debian.backports-quirk' => '(squeeze)-backports*', + 'dgit-distro.debian-backports.mirror' => 'http://backports.debian.org/debian-backports/', + 'dgit-distro.ubuntu.git-check' => 'false', + 'dgit-distro.ubuntu.mirror' => 'http://archive.ubuntu.com/ubuntu', + 'dgit-distro.test-dummy.ssh' => "$td/ssh", + 'dgit-distro.test-dummy.username' => "alice", + 'dgit-distro.test-dummy.git-check' => "ssh-cmd", + 'dgit-distro.test-dummy.git-create' => "ssh-cmd", + '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.mirror' => "file://$td/mirror/", + 'dgit-distro.test-dummy.upload-host' => 'test-dummy', + ); sub cfg { foreach my $c (@_) { @@ -473,19 +561,81 @@ sub cfg { my $dv = $defcfg{$c}; return $dv if defined $dv; } - badcfg "need value for one of: @_"; + badcfg "need value for one of: @_\n". + "$us: distro or suite appears not to be (properly) supported"; +} + +sub access_basedistro () { + if (defined $idistro) { + return $idistro; + } else { + return cfg("dgit-suite.$isuite.distro", + "dgit.default.distro"); + } +} + +sub access_quirk () { + # returns (quirk name, distro to use instead or undef, quirk-specific info) + my $basedistro = access_basedistro(); + my $backports_quirk = cfg("dgit-distro.$basedistro.backports-quirk", + 'RETURN-UNDEF'); + if (defined $backports_quirk) { + my $re = $backports_quirk; + $re =~ s/[^-0-9a-z_\%*()]/\\$&/ig; + $re =~ s/\*/.*/g; + $re =~ s/\%/([-0-9a-z_]+)/ + or $re =~ m/[()]/ or badcfg "backports-quirk needs \% or ( )"; + if ($isuite =~ m/^$re$/) { + return ('backports',"$basedistro-backports",$1); + } + } + return ('none',undef); } -sub access_distro () { - return cfg("dgit-suite.$isuite.distro", - "dgit.default.distro"); +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; + unshift @l, $instead_distro; + return grep { defined } @l; } sub access_cfg (@) { my (@keys) = @_; - my $distro = $idistro || access_distro(); - my $value = cfg(map { ("dgit-distro.$distro.$_", - "dgit.default.$_") } @keys); + my @cfgs; + # The nesting of these loops determines the search order. We put + # the key loop on the outside so that we search all the distros + # for each key, before going on to the next key. That means that + # if access_cfg is called with a more specific, and then a less + # specific, key, an earlier distro can override the less specific + # without necessarily overriding any more specific keys. (If the + # distro wants to override the more specific keys it can simply do + # so; whereas if we did the loop the other way around, it would be + # impossible to for an earlier distro to override a less specific + # key but not the more specific ones without restating the unknown + # values of the more specific keys. + my @realkeys; + my @rundef; + # We have to deal with RETURN-UNDEF specially, so that we don't + # terminate the search prematurely. + foreach (@keys) { + if (m/RETURN-UNDEF/) { push @rundef, $_; last; } + push @realkeys, $_ + } + foreach my $d (access_distros()) { + push @cfgs, map { "dgit-distro.$d.$_" } @realkeys; + } + push @cfgs, map { "dgit.default.$_" } @realkeys; + push @cfgs, @rundef; + my $value = cfg(@cfgs); return $value; } @@ -507,9 +657,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; } @@ -518,22 +675,43 @@ sub access_gituserhost () { return access_someuserhost('git'); } -sub access_giturl () { +sub access_giturl (;$) { + my ($optional) = @_; my $url = access_cfg('git-url','RETURN-UNDEF'); if (!defined $url) { + my $proto = access_cfg('git-proto', 'RETURN-UNDEF'); + return undef unless defined $proto; $url = - access_cfg('git-proto'). + $proto. access_gituserhost(). access_cfg('git-path'); } return "$url/$package.git"; } -sub parsecontrolfh ($$@) { - my ($fh, $desc, @opts) = @_; - my %opts = ('name' => $desc, @opts); - my $c = Dpkg::Control::Hash->new(%opts); - $c->parse($fh) or die "parsing of $desc failed"; +sub parsecontrolfh ($$;$) { + my ($fh, $desc, $allowsigned) = @_; + our $dpkgcontrolhash_noissigned; + my $c; + for (;;) { + my %opts = ('name' => $desc); + $opts{allow_pgp}= $allowsigned || !$dpkgcontrolhash_noissigned; + $c = Dpkg::Control::Hash->new(%opts); + $c->parse($fh,$desc) or die "parsing of $desc failed"; + last if $allowsigned; + last if $dpkgcontrolhash_noissigned; + my $issigned= $c->get_option('is_pgp_signed'); + if (!defined $issigned) { + $dpkgcontrolhash_noissigned= 1; + seek $fh, 0,0 or die "seek $desc: $!"; + } elsif ($issigned) { + fail "control file $desc is (already) PGP-signed. ". + " Note that dgit push needs to modify the .dsc and then". + " do the signature itself"; + } else { + last; + } + } return $c; } @@ -564,66 +742,67 @@ 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: $!"; + return $d; +} + our %rmad; sub archive_query ($) { my ($method) = @_; my $query = access_cfg('archive-query','RETURN-UNDEF'); - if (!defined $query) { - my $distro = access_distro(); - if ($distro eq 'debian') { - $query = "sshdakls:". - access_someuserhost('sshdakls').':'. - access_cfg('sshdakls-dir'); - } else { - $query = "madison:$distro"; - } - } $query =~ s/^(\w+):// or badcfg "invalid archive-query method \`$query'"; my $proto = $1; my $data = $'; #'; { no strict qw(refs); &{"${method}_${proto}"}($proto,$data); } } -sub archive_query_madison ($$) { - my ($proto,$data) = @_; - die unless $proto eq 'madison'; - $rmad{$package} ||= cmdoutput - qw(rmadison -asource),"-s$isuite","-u$data",$package; - my $rmad = $rmad{$package}; - return madison_parse($rmad); +sub pool_dsc_subpath ($$) { + my ($vsn,$component) = @_; # $package is implict arg + my $prefix = substr($package, 0, $package =~ m/^l/ ? 4 : 1); + return "/pool/$component/$prefix/$package/".dscfn($vsn); } -sub archive_query_sshdakls ($$) { - my ($proto,$data) = @_; - $data =~ s/:.*// or badcfg "invalid sshdakls method string \`$data'"; - my $dakls = cmdoutput - access_cfg_ssh, $data, qw(dak ls -asource),"-s$isuite",$package; - return madison_parse($dakls); +sub archive_query_madison { + return map { [ @$_[0..1] ] } madison_get_parse(@_); } -sub canonicalise_suite_sshdakls ($$) { +sub madison_get_parse { my ($proto,$data) = @_; - $data =~ m/:/ or badcfg "invalid sshdakls method string \`$data'"; - my @cmd = - (access_cfg_ssh, $`, - "set -e; cd $';". - " if test -h $isuite; then readlink $isuite; exit 0; fi;". - " if test -d $isuite; then echo $isuite; exit 0; fi;". - " exit 1"); - my $dakls = cmdoutput @cmd; - failedcmd @cmd unless $dakls =~ m/^\w/; - return $dakls; -} - -sub madison_parse ($) { - my ($rmad) = @_; + die unless $proto eq 'madison'; + if (!length $data) { + $data= access_cfg('madison-distro','RETURN-UNDEF'); + $data //= access_basedistro(); + } + $rmad{$proto,$data,$package} ||= cmdoutput + qw(rmadison -asource),"-s$isuite","-u$data",$package; + my $rmad = $rmad{$proto,$data,$package}; + my @out; foreach my $l (split /\n/, $rmad) { $l =~ m{^ \s*( [^ \t|]+ )\s* \| \s*( [^ \t|]+ )\s* \| \s*( [^ \t|/]+ )(?:/([^ \t|/]+))? \s* \| - \s*( [^ \t|]+ )\s* }x or die "$rmad $?"; + \s*( [^ \t|]+ )\s* }x or die "$rmad ?"; $1 eq $package or die "$rmad $package ?"; my $vsn = $2; my $newsuite = $3; @@ -634,15 +813,14 @@ sub madison_parse ($) { $component = access_cfg('archive-query-default-component'); } $5 eq 'source' or die "$rmad ?"; - my $prefix = substr($package, 0, $package =~ m/^l/ ? 4 : 1); - my $subpath = "/pool/$component/$prefix/$package/".dscfn($vsn); - push @out, [$vsn,$subpath,$newsuite]; + push @out, [$vsn,pool_dsc_subpath($vsn,$component),$newsuite]; } - return sort { -version_compare_string($a->[0],$b->[0]); } @out; + return sort { -version_compare($a->[0],$b->[0]); } @out; } -sub canonicalise_suite_madison ($$) { - my @r = archive_query_madison($_[0],$_[1]); +sub canonicalise_suite_madison { + # madison canonicalises for us + my @r = madison_get_parse(@_); @r or fail "unable to canonicalise suite using package $package". " which does not appear to exist in suite $isuite;". @@ -650,12 +828,123 @@ sub canonicalise_suite_madison ($$) { return $r[0][2]; } +sub sshpsql ($$$) { + my ($data,$runeinfo,$sql) = @_; + if (!length $data) { + $data= access_someuserhost('sshpsql').':'. + access_cfg('sshpsql-dbname'); + } + $data =~ m/:/ or badcfg "invalid sshpsql method string \`$data'"; + my ($userhost,$dbname) = ($`,$'); #'; + my @rows; + my @cmd = (access_cfg_ssh, $userhost, + access_runeinfo("ssh-psql $runeinfo"). + " export LANG=C;". + " ".shellquote qw(psql -A), $dbname, qw(-c), $sql); + printcmd(\*DEBUG,$debugprefix."|",@cmd) if $debug>0; + open P, "-|", @cmd or die $!; + while (

) { + chomp or die; + printdebug("$debugprefix>|$_|\n"); + push @rows, $_; + } + $!=0; $?=0; close P or failedcmd @cmd; + @rows or die; + my $nrows = pop @rows; + $nrows =~ s/^\((\d+) rows?\)$/$1/ or die "$nrows ?"; + @rows == $nrows+1 or die "$nrows ".(scalar @rows)." ?"; + @rows = map { [ split /\|/, $_ ] } @rows; + my $ncols = scalar @{ shift @rows }; + die if grep { scalar @$_ != $ncols } @rows; + return @rows; +} + +sub sql_injection_check { + foreach (@_) { die "$_ $& ?" if m{[^-+=:_.,/0-9a-zA-Z]}; } +} + +sub archive_query_sshpsql ($$) { + my ($proto,$data) = @_; + sql_injection_check $isuite, $package; + my @rows = sshpsql($data, "archive-query $isuite $package", <[0],$b->[0]) } @rows; + my $digester = Digest::SHA->new(256); + @rows = map { + my ($vsn,$component,$filename,$sha256sum) = @$_; + [ $vsn, "/pool/$component/$filename",$digester,$sha256sum ]; + } @rows; + return @rows; +} + +sub canonicalise_suite_sshpsql ($$) { + my ($proto,$data) = @_; + sql_injection_check $isuite; + my @rows = sshpsql($data, "canonicalise-suite $isuite", <[0] } @rows; + fail "unknown suite $isuite" unless @rows; + die "ambiguous $isuite: @rows ?" if @rows>1; + return $rows[0]; +} + +sub canonicalise_suite_dummycat ($$) { + my ($proto,$data) = @_; + my $dpath = "$data/suite.$isuite"; + if (!open C, "<", $dpath) { + $!==ENOENT or die "$dpath: $!"; + printdebug "dummycat canonicalise_suite $isuite $dpath ENOENT\n"; + return $isuite; + } + $!=0; $_ = ; + chomp or die "$dpath: $!"; + close C; + printdebug "dummycat canonicalise_suite $isuite $dpath = $_\n"; + return $_; +} + +sub archive_query_dummycat ($$) { + my ($proto,$data) = @_; + canonicalise_suite(); + my $dpath = "$data/package.$csuite.$package"; + if (!open C, "<", $dpath) { + $!==ENOENT or die "$dpath: $!"; + printdebug "dummycat query $csuite $package $dpath ENOENT\n"; + return (); + } + my @rows; + while () { + next if m/^\#/; + next unless m/\S/; + die unless chomp; + printdebug "dummycat query $csuite $package $dpath | $_\n"; + my @row = split /\s+/, $_; + @row==2 or die "$dpath: $_ ?"; + push @rows, \@row; + } + C->error and die "$dpath: $!"; + close C; + return sort { -version_compare($a->[0],$b->[0]); } @rows; +} + sub canonicalise_suite () { return if defined $csuite; fail "cannot operate on $isuite suite" if $isuite eq 'UNRELEASED'; $csuite = archive_query('canonicalise_suite'); if ($isuite ne $csuite) { - # madison canonicalises for us progress "canonical suite name for $isuite is $csuite"; } } @@ -664,35 +953,58 @@ sub get_archive_dsc () { canonicalise_suite(); my @vsns = archive_query('archive_query'); foreach my $vinfo (@vsns) { - my ($vsn,$subpath) = @$vinfo; + my ($vsn,$subpath,$digester,$digest) = @$vinfo; $dscurl = access_cfg('mirror').$subpath; $dscdata = url_get($dscurl); if (!$dscdata) { $skew_warning_vsn = $vsn if !defined $skew_warning_vsn; next; } + if ($digester) { + $digester->reset(); + $digester->add($dscdata); + my $got = $digester->hexdigest(); + $got eq $digest or + fail "$dscurl has hash $got but". + " archive told us to expect $digest"; + } my $dscfh = new IO::File \$dscdata, '<' or die $!; printdebug Dumper($dscdata) if $debug>1; - $dsc = parsecontrolfh($dscfh,$dscurl, allow_pgp=>1); + $dsc = parsecontrolfh($dscfh,$dscurl,1); printdebug Dumper($dsc) if $debug>1; my $fmt = getfield $dsc, 'Format'; fail "unsupported source format $fmt, sorry" unless $format_ok{$fmt}; + $dsc_checked = !!$digester; return; } $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') { + return 1; + } elsif ($how eq 'false') { + return 0; } else { badcfg "unknown git-check \`$how'"; } @@ -703,8 +1015,11 @@ 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') { + # nothing to do } else { badcfg "unknown git-create \`$how'"; } @@ -720,6 +1035,12 @@ sub prep_ud () { mkdir $ud or die $!; } +sub mktree_in_ud_here () { + runcmd qw(git init -q); + rmtree('.git/objects'); + symlink '../../../../objects','.git/objects' or die $!; +} + sub mktree_in_ud_from_only_subdir () { # changes into the subdir my (@dirs) = <*/.>; @@ -727,11 +1048,12 @@ sub mktree_in_ud_from_only_subdir () { $dirs[0] =~ m#^([^/]+)/\.$# or die; my $dir = $1; changedir $dir; - fail "source package contains .git directory" if stat '.git'; - die $! unless $!==&ENOENT; - runcmd qw(git init -q); - rmtree('.git/objects'); - symlink '../../../../objects','.git/objects' or die $!; + fail "source package contains .git directory" if stat_exists '.git'; + mktree_in_ud_here(); + my $format=get_source_format(); + if (madformat($format)) { + rmtree '.pc'; + } runcmd @git, qw(add -Af); my $tree = cmdoutput @git, qw(write-tree); $tree =~ m/^\w+$/ or die "$tree ?"; @@ -769,9 +1091,12 @@ sub dsc_files () { map { $_->{Filename} } dsc_files_info(); } -sub is_orig_file ($) { - local ($_) = @_; - m/\.orig(?:-\w+)?\.tar\.\w+$/; +sub is_orig_file ($;$) { + local ($_) = $_[0]; + my $base = $_[1]; + m/\.orig(?:-\w+)?\.tar\.\w+$/ or return 0; + defined $base or return 1; + return $` eq $base; } sub make_commit ($) { @@ -794,20 +1119,34 @@ sub clogp_authline ($) { sub generate_commit_from_dsc () { prep_ud(); changedir $ud; - my @files; - foreach my $f (dsc_files()) { + + foreach my $fi (dsc_files_info()) { + my $f = $fi->{Filename}; die "$f ?" if $f =~ m#/|^\.|\.dsc$|\.tmp$#; - push @files, $f; + link "../../../$f", $f or $!==&ENOENT or die "$f $!"; + + complete_file_from_dsc('.', $fi); + + if (is_orig_file($f)) { + link $f, "../../../../$f" + or $!==&EEXIST + or die "$f $!"; + } } - runcmd @dget, qw(--), $dscurl; - foreach my $f (grep { is_orig_file($_) } @files) { - link $f, "../../../../$f" - or $!==&EEXIST - or die "$f $!"; - } + + my $dscfn = "$package.dsc"; + + open D, ">", $dscfn or die "$dscfn: $!"; + print D $dscdata or die "$dscfn: $!"; + close D or die "$dscfn: $!"; + my @cmd = qw(dpkg-source); + push @cmd, '--no-check' if $dsc_checked; + push @cmd, qw(-x --), $dscfn; + runcmd @cmd; + my ($tree,$dir) = mktree_in_ud_from_only_subdir(); runcmd qw(sh -ec), 'dpkg-parsechangelog >../changelog.tmp'; my $clogp = parsecontrol('../changelog.tmp',"commit's changelog"); @@ -833,7 +1172,7 @@ END my $oldclogp = parsecontrol('../changelogold.tmp','previous changelog'); my $oversion = getfield $oldclogp, 'Version'; my $vcmp = - version_compare_string($oversion, $cversion); + version_compare($oversion, $cversion); if ($vcmp < 0) { # git upload/ is earlier vsn than archive, use archive open C, ">../commit2.tmp" or die $!; @@ -868,30 +1207,45 @@ END return $outputhash; } +sub complete_file_from_dsc ($$) { + our ($dstdir, $fi) = @_; + # Ensures that we have, in $dir, the file $fi, with the correct + # contents. (Downloading it from alongside $dscurl if necessary.) + + my $f = $fi->{Filename}; + my $tf = "$dstdir/$f"; + my $downloaded = 0; + + if (stat_exists $tf) { + progress "using existing $f"; + } else { + my $furl = $dscurl; + $furl =~ s{/[^/]+$}{}; + $furl .= "/$f"; + die "$f ?" unless $f =~ m/^${package}_/; + die "$f ?" if $f =~ m#/#; + runcmd_ordryrun_local @curl,qw(-o),$tf,'--',"$furl"; + next if !act_local(); + $downloaded = 1; + } + + open F, "<", "$tf" or die "$tf: $!"; + $fi->{Digester}->reset(); + $fi->{Digester}->addfile(*F); + F->error and die $!; + my $got = $fi->{Digester}->hexdigest(); + $got eq $fi->{Hash} or + fail "file $f has hash $got but .dsc". + " demands hash $fi->{Hash} ". + ($downloaded ? "(got wrong file from archive!)" + : "(perhaps you should delete this file?)"); +} + sub ensure_we_have_orig () { foreach my $fi (dsc_files_info()) { my $f = $fi->{Filename}; next unless is_orig_file($f); - if (open F, "<", "../$f") { - $fi->{Digester}->reset(); - $fi->{Digester}->addfile(*F); - F->error and die $!; - my $got = $fi->{Digester}->hexdigest(); - $got eq $fi->{Hash} or - fail "existing file $f has hash $got but .dsc". - " demands hash $fi->{Hash}". - " (perhaps you should delete this file?)"; - progress "using existing $f"; - next; - } else { - die "$f $!" unless $!==&ENOENT; - } - my $origurl = $dscurl; - $origurl =~ s{/[^/]+$}{}; - $origurl .= "/$f"; - die "$f ?" unless $f =~ m/^${package}_/; - die "$f ?" if $f =~ m#/#; - runcmd_ordryrun_local shell_cmd 'cd ..', @dget,'--',$origurl; + complete_file_from_dsc('..', $fi); } } @@ -936,22 +1290,13 @@ sub fetch_from_archive () { progress "no version available from the archive"; } - my $lrref_fn = ".git/".lrref(); - if (open H, $lrref_fn) { - $lastpush_hash = ; - chomp $lastpush_hash; - die "$lrref_fn $lastpush_hash ?" unless $lastpush_hash =~ m/^\w+$/; - } elsif ($! == &ENOENT) { - $lastpush_hash = ''; - } else { - die "$lrref_fn $!"; - } + $lastpush_hash = git_get_ref(lrref()); printdebug "previous reference hash=$lastpush_hash\n"; my $hash; if (defined $dsc_hash) { - fail "missing git history even though dsc has hash -". - " could not find commit $dsc_hash". - " (should be in ".access_giturl()."#".rrref().")" + fail "missing remote git history even though dsc has hash -". + " could not find ref ".lrref(). + " (should have been fetched from ".access_giturl()."#".rrref().")" unless $lastpush_hash; $hash = $dsc_hash; ensure_we_have_orig(); @@ -1007,7 +1352,7 @@ END my $gotclogp = parsechangelog("-l$clogf"); my $got_vsn = getfield $gotclogp, 'Version'; printdebug "SKEW CHECK GOT $got_vsn\n"; - if (version_compare_string($got_vsn, $skew_warning_vsn) < 0) { + if (version_compare($got_vsn, $skew_warning_vsn) < 0) { print STDERR < .git/HEAD" or die $!; - print H "ref: ".lref()."\n" or die $!; - close H or die $!; - runcmd @git, qw(remote add), 'origin', access_giturl(); + my $giturl = access_giturl(1); + if (defined $giturl) { + runcmd @git, qw(config), "remote.$remotename.fetch", fetchspec(); + open H, "> .git/HEAD" or die $!; + print H "ref: ".lref()."\n" or die $!; + close H or die $!; + runcmd @git, qw(remote add), 'origin', $giturl; + } if (check_for_git()) { progress "fetching existing git history"; git_fetch_us(); @@ -1048,6 +1396,10 @@ sub clone ($) { progress "starting new git history"; } fetch_from_archive() or no_such_package; + my $vcsgiturl = $dsc->{'Vcs-Git'}; + if (length $vcsgiturl) { + runcmd @git, qw(remote add vcs-git), $vcsgiturl; + } runcmd @git, qw(reset --hard), lrref(); printdone "ready for work in $dstdir"; } @@ -1080,6 +1432,12 @@ sub check_not_dirty () { } } +sub commit_admin ($) { + my ($m) = @_; + progress "$m"; + runcmd_ordryrun_local @git, qw(commit -m), $m; +} + sub commit_quilty_patch () { my $output = cmdoutput @git, qw(status --porcelain); my %adds; @@ -1089,24 +1447,34 @@ sub commit_quilty_patch () { $adds{$1}++; } } + delete $adds{'.pc'}; # if there wasn't one before, don't add it if (!%adds) { progress "nothing quilty to commit, ok."; return; } runcmd_ordryrun_local @git, qw(add), sort keys %adds; - my $m = "Commit Debian 3.0 (quilt) metadata"; - progress "$m"; - runcmd_ordryrun_local @git, qw(commit -m), $m; + commit_admin "Commit Debian 3.0 (quilt) metadata"; +} + +sub get_source_format () { + if (!open F, "debian/source/format") { + die $! unless $!==&ENOENT; + return ''; + } + $_ = ; + F->error and die $!; + chomp; + return $_; } sub madformat ($) { my ($format) = @_; return 0 unless $format eq '3.0 (quilt)'; - progress "Format \`$format', urgh"; if ($noquilt) { progress "Not doing any fixup of \`$format' due to --no-quilt-fixup"; return 0; } + progress "Format \`$format', checking/updating patch stack"; return 1; } @@ -1165,7 +1533,7 @@ type commit tag $tag tagger $authline -$package release $cversion for $clogsuite [dgit] +$package release $cversion for $clogsuite ($csuite) [dgit] END close TO or die $!; @@ -1204,6 +1572,8 @@ sub dopush () { printdebug "actually entering push\n"; prep_ud(); + access_giturl(); # check that success is vaguely likely + my $clogpfn = ".git/dgit/changelog.822.tmp"; runcmd shell_cmd "exec >$clogpfn", qw(dpkg-parsechangelog); @@ -1212,13 +1582,14 @@ sub dopush () { my ($clogp, $cversion, $tag, $dscfn) = push_parse_changelog("$clogpfn"); - stat "../$dscfn" or + my $dscpath = "$buildproductsdir/$dscfn"; + stat_exists $dscpath or fail "looked for .dsc $dscfn, but $!;". " maybe you forgot to build"; - responder_send_file('dsc', "../$dscfn"); + responder_send_file('dsc', $dscpath); - push_parse_dsc("../$dscfn", $dscfn, $cversion); + push_parse_dsc($dscpath, $dscfn, $cversion); my $format = getfield $dsc, 'Format'; printdebug "format $format\n"; @@ -1228,16 +1599,21 @@ sub dopush () { check_not_dirty(); changedir $ud; progress "checking that $dscfn corresponds to HEAD"; - runcmd qw(dpkg-source -x --), "../../../../$dscfn"; + runcmd qw(dpkg-source -x --), + $dscpath =~ m#^/# ? $dscpath : "../../../$dscpath"; my ($tree,$dir) = mktree_in_ud_from_only_subdir(); changedir '../../../..'; - my @diffcmd = (@git, qw(diff --exit-code), $tree); + my $diffopt = $debug>0 ? '--exit-code' : '--quiet'; + my @diffcmd = (@git, qw(diff), $diffopt, $tree); printcmd \*DEBUG,$debugprefix."+",@diffcmd; $!=0; $?=0; - if (system @diffcmd) { - if ($! && $?==256) { + my $r = system @diffcmd; + if ($r) { + if ($r==256) { fail "$dscfn specifies a different tree to your HEAD commit;". - " perhaps you forgot to build"; + " perhaps you forgot to build". + ($diffopt eq '--exit-code' ? "" : + " (run with -D to see full diff output)"); } else { failedcmd @diffcmd; } @@ -1250,23 +1626,26 @@ sub dopush () { # (uploadbranch()); my $head = rev_parse('HEAD'); if (!$changesfile) { - my $multi = "../${package}_".(stripepoch $cversion)."_multi.changes"; - if (stat "$multi") { + my $multi = "$buildproductsdir/". + "${package}_".(stripepoch $cversion)."_multi.changes"; + if (stat_exists "$multi") { $changesfile = $multi; } else { - $!==&ENOENT or die "$multi: $!"; my $pat = "${package}_".(stripepoch $cversion)."_*.changes"; - my @cs = glob "../$pat"; + my @cs = glob "$buildproductsdir/$pat"; fail "failed to find unique changes file". - " (looked for $pat in .., or $multi);". + " (looked for $pat in $buildproductsdir, or $multi);". " perhaps you need to use dgit -C" unless @cs==1; ($changesfile) = @cs; } + } else { + $changesfile = "$buildproductsdir/$changesfile"; } responder_send_file('changes',$changesfile); responder_send_command("param head $head"); + responder_send_command("param csuite $csuite"); my $tfn = sub { ".git/dgit/tag$_[0]"; }; my $tagobjfn; @@ -1277,7 +1656,7 @@ sub dopush () { } else { $tagobjfn = push_mktag($head,$clogp,$tag, - "../$dscfn", + $dscpath, $changesfile,$changesfile, $tfn); } @@ -1290,27 +1669,24 @@ sub dopush () { if (!check_for_git()) { create_remote_git_repo(); } - runcmd_ordryrun @git, qw(push),access_giturl(),"HEAD:".rrref(); + runcmd_ordryrun @git, qw(push),access_giturl(), + "HEAD:".rrref(), "refs/tags/$tag"; runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), 'HEAD'; - if (!$we_are_responder) { - if (act_local()) { - rename "../$dscfn.tmp","../$dscfn" or die "$dscfn $!"; - } else { - progress "[new .dsc left in $dscfn.tmp]"; - } - } - if ($we_are_responder) { my $dryrunsuffix = act_local() ? "" : ".tmp"; responder_receive_files('signed-dsc-changes', - "../$dscfn$dryrunsuffix", + "$dscpath$dryrunsuffix", "$changesfile$dryrunsuffix"); } else { + if (act_local()) { + rename "$dscpath.tmp",$dscpath or die "$dscfn $!"; + } else { + progress "[new .dsc left in $dscpath.tmp]"; + } sign_changes $changesfile; } - runcmd_ordryrun @git, qw(push),access_giturl(),"refs/tags/$tag"; my $host = access_cfg('upload-host','RETURN-UNDEF'); my @hostarg = defined($host) ? ($host,) : (); runcmd_ordryrun @dput, @hostarg, $changesfile; @@ -1336,7 +1712,26 @@ sub cmd_clone { badusage "incorrect arguments to dgit clone"; } $dstdir ||= "$package"; + + if (stat_exists $dstdir) { + fail "$dstdir already exists"; + } + + my $cwd_remove; + if ($rmonerror && !$dryrun_level) { + $cwd_remove= getcwd(); + unshift @end, sub { + return unless defined $cwd_remove; + if (!chdir "$cwd_remove") { + return if $!==&ENOENT; + die "chdir $cwd_remove: $!"; + } + rmtree($dstdir) or die "remove $dstdir: $!\n"; + }; + } + clone($dstdir); + $cwd_remove = undef; } sub branchsuite () { @@ -1412,7 +1807,7 @@ sub cmd_push { is_fast_fwd(lrref(), 'HEAD') or fail "dgit push: HEAD is not a descendant". " of the archive's version.\n". - "$us: To overwrite it, use git-merge -s ours ".lrref()."."; + "$us: To overwrite it, use git merge -s ours ".lrref()."."; } else { $new_package or fail "package appears to be new in this suite;". @@ -1423,12 +1818,15 @@ sub cmd_push { #---------- remote commands' implementation ---------- -sub cmd_remote_push_responder { +sub cmd_remote_push_build_host { my ($nrargs) = shift @ARGV; my (@rargs) = @ARGV[0..$nrargs-1]; @ARGV = @ARGV[$nrargs..$#ARGV]; die unless @rargs; - my ($dir) = @rargs; + my ($dir,$vsnwant) = @rargs; + # vsnwant is a comma-separated list; we report which we have + # chosen in our ready response (so other end can tell if they + # offered several) $debugprefix = ' '; $we_are_responder = 1; @@ -1439,19 +1837,30 @@ sub cmd_remote_push_responder { open STDOUT, ">&STDERR" or die $!; autoflush STDOUT 1; - responder_send_command("dgit-remote-push-ready"); + $vsnwant //= 1; + fail "build host has dgit rpush protocol version". + " $rpushprotovsn but invocation host has $vsnwant" + unless grep { $rpushprotovsn eq $_ } split /,/, $vsnwant; + + responder_send_command("dgit-remote-push-ready $rpushprotovsn"); changedir $dir; &cmd_push; } +sub cmd_remote_push_responder { cmd_remote_push_build_host(); } +# ... for compatibility with proto vsn.1 dgit (just so that user gets +# a good error message) + our $i_tmp; -our $i_child_pid; sub i_cleanup { - local ($@); - if ($i_child_pid) { - printdebug "(killing remote child $i_child_pid)\n"; + local ($@, $?); + my $report = i_child_report(); + if (defined $report) { + printdebug "($report)\n"; + } elsif ($i_child_pid) { + printdebug "(killing build host child $i_child_pid)\n"; kill 15, $i_child_pid; } if (defined $i_tmp && !defined $initiator_tempdir) { @@ -1478,11 +1887,11 @@ sub cmd_rpush { $dir = nextarg; } $dir =~ s{^-}{./-}; - my @rargs = ($dir); + my @rargs = ($dir,$rpushprotovsn); my @rdgit; push @rdgit, @dgit; push @rdgit, @ropts; - push @rdgit, qw(remote-push-responder), (scalar @rargs), @rargs; + push @rdgit, qw(remote-push-build-host), (scalar @rargs), @rargs; push @rdgit, @ARGV; my @cmd = (@ssh, $host, shellquote @rdgit); printcmd \*DEBUG,$debugprefix."+",@cmd; @@ -1504,16 +1913,6 @@ sub cmd_rpush { }; i_method "i_resp", $icmd, $iargs; } - - my $pid = $i_child_pid; - $i_child_pid = undef; # prevents killing some other process with same pid - printdebug "waiting for remote child $pid..."; - my $got = waitpid $pid, 0; - die $! unless $got == $pid; - die "remote child failed $?" if $?; - - i_cleanup(); - exit 0; } sub i_resp_progress ($) { @@ -1523,7 +1922,15 @@ sub i_resp_progress ($) { } sub i_resp_complete { + my $pid = $i_child_pid; + $i_child_pid = undef; # prevents killing some other process with same pid + printdebug "waiting for build host child $pid...\n"; + my $got = waitpid $pid, 0; + die $! unless $got == $pid; + die "build host child failed $?" if $?; + i_cleanup(); + printdebug "all done\n"; exit 0; } @@ -1531,8 +1938,10 @@ sub i_resp_file ($) { my ($keyword) = @_; my $localname = i_method "i_localname", $keyword; my $localpath = "$i_tmp/$localname"; - stat $localpath and badproto \*RO, "file $keyword ($localpath) twice"; + stat_exists $localpath and + badproto \*RO, "file $keyword ($localpath) twice"; protocol_receive_file \*RO, $localpath; + i_method "i_file", $keyword; } our %i_param; @@ -1555,30 +1964,47 @@ sub i_resp_want ($) { print RI "files-end\n" or die $!; } -our ($i_clogp, $i_version, $i_tag, $i_dscfn); +our ($i_clogp, $i_version, $i_tag, $i_dscfn, $i_changesfn); -sub i_localname_parsed_changelog { return "remote-changelog.822"; } -sub i_localname_changes { return "remote.changes"; } -sub i_localname_dsc { +sub i_localname_parsed_changelog { + return "remote-changelog.822"; +} +sub i_file_parsed_changelog { ($i_clogp, $i_version, $i_tag, $i_dscfn) = push_parse_changelog "$i_tmp/remote-changelog.822"; die if $i_dscfn =~ m#/|^\W#; +} + +sub i_localname_dsc { + defined $i_dscfn or badproto \*RO, "dsc (before parsed-changelog)"; return $i_dscfn; } +sub i_file_dsc { } + +sub i_localname_changes { + defined $i_dscfn or badproto \*RO, "dsc (before parsed-changelog)"; + $i_changesfn = $i_dscfn; + $i_changesfn =~ s/\.dsc$/_dgit.changes/ or die; + return $i_changesfn; +} +sub i_file_changes { } sub i_want_signed_tag { printdebug Dumper(\%i_param, $i_dscfn); defined $i_param{'head'} && defined $i_dscfn && defined $i_clogp - or badproto \*RO, "sequencing error"; + && defined $i_param{'csuite'} + or badproto \*RO, "premature desire for signed-tag"; my $head = $i_param{'head'}; die if $head =~ m/[^0-9a-f]/ || $head !~ m/^../; + die unless $i_param{'csuite'} =~ m/^$suite_re$/; + $csuite = $&; push_parse_dsc $i_dscfn, 'remote dsc', $i_version; my $tagobjfn = push_mktag $head, $i_clogp, $i_tag, $i_dscfn, - 'remote.changes', 'remote changes', + $i_changesfn, 'remote changes', sub { "tag$_[0]"; }; return $tagobjfn; @@ -1586,8 +2012,8 @@ sub i_want_signed_tag { sub i_want_signed_dsc_changes { rename "$i_dscfn.tmp","$i_dscfn" or die "$i_dscfn $!"; - sign_changes 'remote.changes'; - return ($i_dscfn, 'remote.changes'); + sign_changes $i_changesfn; + return ($i_dscfn, $i_changesfn); } #---------- building etc. ---------- @@ -1599,19 +2025,124 @@ our $dscfn; our $fakeeditorenv = 'DGIT_FAKE_EDITOR_QUILT'; sub build_maybe_quilt_fixup () { - if (!open F, "debian/source/format") { - die $! unless $!==&ENOENT; - return; - } - $_ = ; - F->error and die $!; - chomp; - return unless madformat($_); + my $format=get_source_format; + return unless madformat $format; # sigh + + # Our objective is: + # - honour any existing .pc in case it has any strangeness + # - determine the git commit corresponding to the tip of + # the patch stack (if there is one) + # - if there is such a git commit, convert each subsequent + # git commit into a quilt patch with dpkg-source --commit + # - otherwise convert all the differences in the tree into + # a single git commit + # + # To do this we: + + # Our git tree doesn't necessarily contain .pc. (Some versions of + # dgit would include the .pc in the git tree.) If there isn't + # one, we need to generate one by unpacking the patches that we + # have. + # + # We first look for a .pc in the git tree. If there is one, we + # will use it. (This is not the normal case.) + # + # Otherwise need to regenerate .pc so that dpkg-source --commit + # can work. We do this as follows: + # 1. Collect all relevant .orig from parent directory + # 2. Generate a debian.tar.gz out of + # debian/{patches,rules,source/format} + # 3. Generate a fake .dsc containing just these fields: + # Format Source Version Files + # 4. Extract the fake .dsc + # Now the fake .dsc has a .pc directory. + # (In fact we do this in every case, because in future we will + # want to search for a good base commit for generating patches.) + # + # Then we can actually do the dpkg-source --commit + # 1. Make a new working tree with the same object + # store as our main tree and check out the main + # tree's HEAD. + # 2. Copy .pc from the fake's extraction, if necessary + # 3. Run dpkg-source --commit + # 4. If the result has changes to debian/, then + # - git-add them them + # - git-add .pc if we had a .pc in-tree + # - git-commit + # 5. If we had a .pc in-tree, delete it, and git-commit + # 6. Back in the main tree, fast forward to the new HEAD + my $clogp = parsechangelog(); - my $version = getfield $clogp, 'Version'; - my $author = getfield $clogp, 'Maintainer'; my $headref = rev_parse('HEAD'); + + prep_ud(); + changedir $ud; + + my $upstreamversion=$version; + $upstreamversion =~ s/-[^-]*$//; + + my $fakeversion="$upstreamversion-~~DGITFAKE"; + + my $fakedsc=new IO::File 'fake.dsc', '>' or die $!; + print $fakedsc <addfile($fh); + print $fakedsc " ".$md->hexdigest." $size $b\n" or die $!; + }; + + foreach my $f (<../../../../*>) { #/){ + my $b=$f; $b =~ s{.*/}{}; + next unless is_orig_file $b, srcfn $upstreamversion,''; + link $f, $b or die "$b $!"; + $dscaddfile->($b); + } + + my @files=qw(debian/source/format debian/rules); + if (stat_exists '../../../debian/patches') { + push @files, 'debian/patches'; + } + + my $debtar= srcfn $fakeversion,'.debian.tar.gz'; + runcmd qw(env GZIP=-1 tar -zcf), "./$debtar", qw(-C ../../..), @files; + + $dscaddfile->($debtar); + close $fakedsc or die $!; + + runcmd qw(sh -ec), 'exec dpkg-source --no-check -x fake.dsc >/dev/null'; + + my $fakexdir= $package.'-'.(stripepoch $upstreamversion); + rename $fakexdir, "fake" or die "$fakexdir $!"; + + mkdir "work" or die $!; + changedir "work"; + mktree_in_ud_here(); + runcmd @git, qw(reset --hard), $headref; + + my $mustdeletepc=0; + if (stat_exists ".pc") { + -d _ or die; + progress "Tree already contains .pc - will use it then delete it."; + $mustdeletepc=1; + } else { + rename '../fake/.pc','.pc' or die $!; + } + + my $author = getfield $clogp, 'Maintainer'; my $time = time; my $ncommits = 3; my $patchname = "auto-$version-$headref-$time"; @@ -1646,6 +2177,14 @@ END } commit_quilty_patch(); + + if ($mustdeletepc) { + runcmd @git, qw(rm -rq .pc); + commit_admin "Commit removal of .pc (quilt series tracking data)"; + } + + changedir '../../../..'; + runcmd @git, qw(pull --ff-only -q .git/dgit/unpack/work master); } sub quilt_fixup_editor () { @@ -1667,9 +2206,26 @@ sub quilt_fixup_editor () { exit 0; } +sub clean_tree () { + if ($cleanmode eq 'dpkg-source') { + runcmd_ordryrun_local @dpkgbuildpackage, qw(-T clean); + } elsif ($cleanmode eq 'git') { + runcmd_ordryrun_local @git, qw(clean -xdf); + } elsif ($cleanmode eq 'none') { + } else { + die "$cleanmode ?"; + } +} + +sub cmd_clean () { + badusage "clean takes no additional arguments" if @ARGV; + clean_tree(); +} + sub build_prep () { badusage "-p is not allowed when building" if defined $package; check_not_dirty(); + clean_tree(); my $clogp = parsechangelog(); $isuite = getfield $clogp, 'Distribution'; $package = getfield $clogp, 'Source'; @@ -1677,17 +2233,40 @@ sub build_prep () { build_maybe_quilt_fixup(); } +sub changesopts () { + my @opts =@changesopts[1..$#changesopts]; + if (!defined $changes_since_version) { + my @vsns = archive_query('archive_query'); + my @quirk = access_quirk(); + if ($quirk[0] eq 'backports') { + local $isuite = $quirk[2]; + local $csuite; + canonicalise_suite(); + push @vsns, archive_query('archive_query'); + } + if (@vsns) { + @vsns = map { $_->[0] } @vsns; + @vsns = sort { -version_compare($a, $b) } @vsns; + $changes_since_version = $vsns[0]; + progress "changelog will contain changes since $vsns[0]"; + } else { + $changes_since_version = '_'; + progress "package seems new, not specifying -v"; + } + } + if ($changes_since_version ne '_') { + unshift @opts, "-v$changes_since_version"; + } + return @opts; +} + sub cmd_build { - badusage "dgit build implies --clean=dpkg-source" - if $cleanmode ne 'dpkg-source'; build_prep(); runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc), changesopts(), @ARGV; printdone "build successful\n"; } sub cmd_git_build { - badusage "dgit git-build implies --clean=dpkg-source" - if $cleanmode ne 'dpkg-source'; build_prep(); my @cmd = (qw(git-buildpackage -us -uc --git-no-sign-tags), @@ -1709,13 +2288,7 @@ sub build_source { runcmd_ordryrun_local (@dpkgbuildpackage, qw(-us -uc -S)), changesopts(); } else { - if ($cleanmode eq 'git') { - runcmd_ordryrun_local @git, qw(clean -xdf); - } elsif ($cleanmode eq 'none') { - } else { - die "$cleanmode ?"; - } - my $pwd = cmdoutput qw(env - pwd); + my $pwd = must_getcwd(); my $leafdir = basename $pwd; changedir ".."; runcmd_ordryrun_local @dpkgsource, qw(-b --), $leafdir; @@ -1738,18 +2311,26 @@ sub cmd_sbuild { changedir ".."; my $pat = "${package}_".(stripepoch $version)."_*.changes"; if (act_local()) { - stat $dscfn or fail "$dscfn (in parent directory): $!"; - stat $sourcechanges or fail "$sourcechanges (in parent directory): $!"; + stat_exist $dscfn or fail "$dscfn (in parent directory): $!"; + stat_exists $sourcechanges + or fail "$sourcechanges (in parent directory): $!"; foreach my $cf (glob $pat) { next if $cf eq $sourcechanges; unlink $cf or fail "remove $cf: $!"; } } runcmd_ordryrun_local @sbuild, @ARGV, qw(-d), $isuite, $dscfn; - runcmd_ordryrun_local @mergechanges, glob $pat; + my @changesfiles = glob $pat; + @changesfiles = sort { + ($b =~ m/_source\.changes$/ <=> $a =~ m/_source\.changes$/) + or $a cmp $b + } @changesfiles; + fail "wrong number of different changes files (@changesfiles)" + unless @changesfiles; + runcmd_ordryrun_local @mergechanges, @changesfiles; my $multichanges = "${package}_".(stripepoch $version)."_multi.changes"; if (act_local()) { - stat $multichanges or fail "$multichanges: $!"; + stat_exists $multichanges or fail "$multichanges: $!"; } printdone "build successful, results in $multichanges\n" or die $!; } @@ -1758,6 +2339,7 @@ sub cmd_quilt_fixup { badusage "incorrect arguments to dgit quilt-fixup" if @ARGV; my $clogp = parsechangelog(); $version = getfield $clogp, 'Version'; + $package = getfield $clogp, 'Source'; build_maybe_quilt_fixup(); } @@ -1798,12 +2380,15 @@ sub parseopts () { } elsif (m/^--new$/) { push @ropts, $_; $new_package=1; - } elsif (m/^--(\w+)=(.*)/s && + } elsif (m/^--since-version=([^_]+|_)$/) { + push @ropts, $_; + $changes_since_version = $1; + } elsif (m/^--([-0-9a-z]+)=(.*)/s && ($om = $opts_opt_map{$1}) && length $om->[0]) { push @ropts, $_; $om->[0] = $2; - } elsif (m/^--(\w+):(.*)/s && + } elsif (m/^--([-0-9a-z]+):(.*)/s && !$opts_opt_cmdonly{$1} && ($om = $opts_opt_map{$1})) { push @ropts, $_; @@ -1819,6 +2404,9 @@ sub parseopts () { } elsif (m/^--distro=(.*)/s) { push @ropts, $_; $idistro = $1; + } elsif (m/^--build-products-dir=(.*)/s) { + push @ropts, $_; + $buildproductsdir = $1; } elsif (m/^--clean=(dpkg-source|git|none)$/s) { push @ropts, $_; $cleanmode = $1; @@ -1830,6 +2418,9 @@ sub parseopts () { } elsif (m/^--no-quilt-fixup$/s) { push @ropts, $_; $noquilt = 1; + } elsif (m/^--no-rm-on-error$/s) { + push @ropts, $_; + $rmonerror = 0; } else { badusage "unknown long option \`$_'"; } @@ -1851,7 +2442,10 @@ sub parseopts () { } elsif (s/^-N/-/) { push @ropts, $&; $new_package=1; - } elsif (m/^-[vm]/) { + } elsif (s/^-v([^_]+|_)$//s) { + push @ropts, $&; + $changes_since_version = $1; + } elsif (m/^-m/) { push @ropts, $&; push @changesopts, $_; $_ = ''; @@ -1864,6 +2458,9 @@ sub parseopts () { } elsif (s/^-C(.*)//s) { push @ropts, $&; $changesfile = $1; + if ($changesfile =~ s#^(.*)/##) { + $buildproductsdir = $1; + } } elsif (s/^-k(.*)//s) { $keyid=$1; } elsif (s/^-wn//s) { @@ -1887,8 +2484,6 @@ if ($ENV{$fakeeditorenv}) { quilt_fixup_editor(); } -delete $ENV{'DGET_UNPACK'}; - parseopts(); print STDERR "DRY RUN ONLY\n" if $dryrun_level > 1; print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n" @@ -1899,4 +2494,7 @@ if (!@ARGV) { } my $cmd = shift @ARGV; $cmd =~ y/-/_/; -{ no strict qw(refs); &{"cmd_$cmd"}(); } + +my $fn = ${*::}{"cmd_$cmd"}; +$fn or badusage "unknown operation $cmd"; +$fn->();