+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 (<C>) {
+ 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) {
+ progress "canonical suite name for $isuite is $csuite";
+ }
+}
+
+sub get_archive_dsc () {
+ canonicalise_suite();
+ my @vsns = archive_query('archive_query');
+ foreach my $vinfo (@vsns) {
+ 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,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'";
+ }
+}
+
+sub create_remote_git_repo () {
+ my $how = access_cfg('git-create');
+ 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'";
+ }
+}
+
+our ($dsc_hash,$lastpush_hash);
+
+our $ud = '.git/dgit/unpack';
+
+sub prep_ud () {
+ rmtree($ud);
+ mkpath '.git/dgit';
+ mkdir $ud or die $!;
+}
+
+sub mktree_in_ud_here () {
+ runcmd qw(git init -q);
+ rmtree('.git/objects');
+ 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) = <*/.>;
+ die unless @dirs==1;
+ $dirs[0] =~ m#^([^/]+)/\.$# or die;
+ my $dir = $1;
+ changedir $dir;
+ 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=git_write_tree();
+ return ($tree,$dir);
+}
+
+sub dsc_files_info () {
+ foreach my $csumi (['Checksums-Sha256','Digest::SHA', 'new(256)'],
+ ['Checksums-Sha1', 'Digest::SHA', 'new(1)'],
+ ['Files', 'Digest::MD5', 'new()']) {
+ my ($fname, $module, $method) = @$csumi;
+ my $field = $dsc->{$fname};
+ next unless defined $field;
+ eval "use $module; 1;" or die $@;
+ my @out;
+ foreach (split /\n/, $field) {
+ next unless m/\S/;
+ m/^(\w+) (\d+) (\S+)$/ or
+ fail "could not parse .dsc $fname line \`$_'";
+ my $digester = eval "$module"."->$method;" or die $@;
+ push @out, {
+ Hash => $1,
+ Bytes => $2,
+ Filename => $3,
+ Digester => $digester,
+ };
+ }
+ return @out;
+ }
+ fail "missing any supported Checksums-* or Files field in ".
+ $dsc->get_option('name');
+}
+
+sub dsc_files () {
+ map { $_->{Filename} } dsc_files_info();
+}
+
+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 ($) {
+ my ($file) = @_;
+ return cmdoutput @git, qw(hash-object -w -t commit), $file;
+}
+
+sub clogp_authline ($) {
+ my ($clogp) = @_;
+ my $author = getfield $clogp, 'Maintainer';
+ $author =~ s#,.*##ms;
+ my $date = cmdoutput qw(date), '+%s %z', qw(-d), getfield($clogp,'Date');
+ my $authline = "$author $date";
+ $authline =~ m/^[^<>]+ \<\S+\> \d+ [-+]\d+$/ or
+ fail "unexpected commit author line format \`$authline'".
+ " (was generated from changelog Maintainer field)";
+ return $authline;
+}
+
+sub generate_commit_from_dsc () {
+ prep_ud();
+ changedir $ud;
+
+ foreach my $fi (dsc_files_info()) {
+ my $f = $fi->{Filename};
+ die "$f ?" if $f =~ m#/|^\.|\.dsc$|\.tmp$#;
+
+ 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 $!";
+ }
+ }
+
+ 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");
+ my $authline = clogp_authline $clogp;
+ my $changes = getfield $clogp, 'Changes';
+ open C, ">../commit.tmp" or die $!;
+ print C <<END or die $!;
+tree $tree
+author $authline
+committer $authline
+
+$changes
+
+# imported from the archive
+END
+ close C or die $!;
+ my $outputhash = make_commit qw(../commit.tmp);
+ my $cversion = getfield $clogp, 'Version';
+ progress "synthesised git commit from .dsc $cversion";
+ if ($lastpush_hash) {
+ runcmd @git, qw(reset --hard), $lastpush_hash;
+ runcmd qw(sh -ec), 'dpkg-parsechangelog >>../changelogold.tmp';
+ my $oldclogp = parsecontrol('../changelogold.tmp','previous changelog');
+ my $oversion = getfield $oldclogp, 'Version';
+ my $vcmp =
+ version_compare($oversion, $cversion);
+ if ($vcmp < 0) {
+ # git upload/ is earlier vsn than archive, use archive
+ open C, ">../commit2.tmp" or die $!;
+ print C <<END or die $!;
+tree $tree
+parent $lastpush_hash
+parent $outputhash
+author $authline
+committer $authline
+
+Record $package ($cversion) in archive suite $csuite
+END
+ $outputhash = make_commit qw(../commit2.tmp);
+ } elsif ($vcmp > 0) {
+ print STDERR <<END or die $!;
+
+Version actually in archive: $cversion (older)
+Last allegedly pushed/uploaded: $oversion (newer or same)
+$later_warning_msg
+END
+ $outputhash = $lastpush_hash;
+ } else {
+ $outputhash = $lastpush_hash;
+ }
+ }
+ changedir '../../../..';
+ runcmd @git, qw(update-ref -m),"dgit fetch import $cversion",
+ 'DGIT_ARCHIVE', $outputhash;
+ cmdoutput @git, qw(log -n2), $outputhash;
+ # ... gives git a chance to complain if our commit is malformed
+ rmtree($ud);
+ 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);
+ complete_file_from_dsc('..', $fi);
+ }
+}
+
+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();
+}
+
+sub fetch_from_archive () {
+ # ensures that lrref() is what is actually in the archive,
+ # one way or another
+ get_archive_dsc();
+
+ if ($dsc) {
+ foreach my $field (@ourdscfield) {
+ $dsc_hash = $dsc->{$field};
+ last if defined $dsc_hash;
+ }
+ if (defined $dsc_hash) {
+ $dsc_hash =~ m/\w+/ or fail "invalid hash in .dsc \`$dsc_hash'";
+ $dsc_hash = $&;
+ progress "last upload to archive specified git hash";
+ } else {
+ progress "last upload to archive has NO git hash";
+ }
+ } else {
+ progress "no version available from the archive";
+ }
+
+ $lastpush_hash = git_get_ref(lrref());
+ printdebug "previous reference hash=$lastpush_hash\n";
+ my $hash;
+ if (defined $dsc_hash) {
+ 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();
+ if ($dsc_hash eq $lastpush_hash) {
+ } elsif (is_fast_fwd($dsc_hash,$lastpush_hash)) {
+ print STDERR <<END or die $!;
+
+Git commit in archive is behind the last version allegedly pushed/uploaded.
+Commit referred to by archive: $dsc_hash
+Last allegedly pushed/uploaded: $lastpush_hash
+$later_warning_msg
+END
+ $hash = $lastpush_hash;
+ } else {
+ fail "archive's .dsc refers to ".$dsc_hash.
+ " but this is an ancestor of ".$lastpush_hash;
+ }
+ } elsif ($dsc) {
+ $hash = generate_commit_from_dsc();
+ } elsif ($lastpush_hash) {
+ # only in git, not in the archive yet
+ $hash = $lastpush_hash;
+ print STDERR <<END or die $!;
+
+Package not found in the archive, but has allegedly been pushed using dgit.
+$later_warning_msg
+END
+ } else {
+ printdebug "nothing found!\n";
+ if (defined $skew_warning_vsn) {
+ print STDERR <<END or die $!;
+
+Warning: relevant archive skew detected.
+Archive allegedly contains $skew_warning_vsn
+But we were not able to obtain any version from the archive or git.
+
+END
+ }
+ return 0;
+ }
+ printdebug "current hash=$hash\n";
+ if ($lastpush_hash) {
+ fail "not fast forward on last upload branch!".
+ " (archive's version left in DGIT_ARCHIVE)"
+ unless is_fast_fwd($lastpush_hash, $hash);
+ }
+ if (defined $skew_warning_vsn) {
+ mkpath '.git/dgit';
+ printdebug "SKEW CHECK WANT $skew_warning_vsn\n";
+ my $clogf = ".git/dgit/changelog.tmp";
+ runcmd shell_cmd "exec >$clogf",
+ @git, qw(cat-file blob), "$hash:debian/changelog";
+ my $gotclogp = parsechangelog("-l$clogf");
+ my $got_vsn = getfield $gotclogp, 'Version';
+ printdebug "SKEW CHECK GOT $got_vsn\n";
+ if (version_compare($got_vsn, $skew_warning_vsn) < 0) {
+ print STDERR <<END or die $!;
+
+Warning: archive skew detected. Using the available version:
+Archive allegedly contains $skew_warning_vsn
+We were able to obtain only $got_vsn
+
+END
+ }
+ }
+ if ($lastpush_hash ne $hash) {
+ my @upd_cmd = (@git, qw(update-ref -m), 'dgit fetch', lrref(), $hash);
+ if (act_local()) {
+ cmdoutput @upd_cmd;
+ } else {
+ dryrun_report @upd_cmd;
+ }
+ }
+ return 1;
+}
+
+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);
+ 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 ($hasgit) {
+ progress "fetching existing git history";
+ git_fetch_us();
+ runcmd_ordryrun_local @git, qw(fetch origin);
+ } else {
+ progress "starting new git history";
+ }
+ fetch_from_archive() or no_such_package;
+ my $vcsgiturl = $dsc->{'Vcs-Git'};
+ $vcsgiturl =~ s/\s+-b\s+\S+//g;
+ if (length $vcsgiturl) {
+ runcmd @git, qw(remote add vcs-git), $vcsgiturl;
+ }
+ runcmd @git, qw(reset --hard), lrref();
+ printdone "ready for work in $dstdir";
+}
+
+sub fetch () {
+ if (check_for_git()) {
+ git_fetch_us();
+ }
+ fetch_from_archive() or no_such_package();
+ printdone "fetched into ".lrref();
+}
+
+sub pull () {
+ fetch();
+ runcmd_ordryrun_local @git, qw(merge -m),"Merge from $csuite [dgit]",
+ lrref();
+ printdone "fetched to ".lrref()." and merged into HEAD";
+}
+
+sub check_not_dirty () {
+ return if $ignoredirty;
+ my @cmd = (@git, qw(diff --quiet HEAD));
+ printcmd(\*DEBUG,$debugprefix."+",@cmd) if $debug>0;
+ $!=0; $?=0; system @cmd;
+ return if !$! && !$?;
+ if (!$! && $?==256) {
+ fail "working tree is dirty (does not match HEAD)";
+ } else {
+ failedcmd @cmd;
+ }
+}
+
+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;
+ foreach my $l (split /\n/, $output) {
+ next unless $l =~ m/\S/;
+ if ($l =~ m{^(?:\?\?| M) (.pc|debian/patches)}) {
+ $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;
+ commit_admin "Commit Debian 3.0 (quilt) metadata";
+}
+
+sub get_source_format () {
+ if (!open F, "debian/source/format") {
+ die $! unless $!==&ENOENT;
+ return '';
+ }
+ $_ = <F>;
+ F->error and die $!;
+ chomp;
+ return $_;
+}
+
+sub madformat ($) {
+ my ($format) = @_;
+ return 0 unless $format eq '3.0 (quilt)';
+ if ($quilt_mode eq 'nocheck') {
+ progress "Not doing any fixup of \`$format' due to --no-quilt-fixup";
+ return 0;
+ }
+ progress "Format \`$format', checking/updating patch stack";
+ return 1;
+}
+
+sub push_parse_changelog ($) {
+ my ($clogpfn) = @_;
+
+ my $clogp = Dpkg::Control::Hash->new();
+ $clogp->load($clogpfn) or die;
+
+ $package = getfield $clogp, 'Source';
+ my $cversion = getfield $clogp, 'Version';
+ my $tag = debiantag($cversion);
+ runcmd @git, qw(check-ref-format), $tag;
+
+ my $dscfn = dscfn($cversion);
+
+ return ($clogp, $cversion, $tag, $dscfn);
+}
+
+sub push_parse_dsc ($$$) {
+ my ($dscfn,$dscfnwhat, $cversion) = @_;
+ $dsc = parsecontrol($dscfn,$dscfnwhat);
+ my $dversion = getfield $dsc, 'Version';
+ my $dscpackage = getfield $dsc, 'Source';
+ ($dscpackage eq $package && $dversion eq $cversion) or
+ fail "$dscfn is for $dscpackage $dversion".
+ " but debian/changelog is for $package $cversion";
+}
+
+sub push_mktag ($$$$$$$) {
+ my ($head,$clogp,$tag,
+ $dscfn,
+ $changesfile,$changesfilewhat,
+ $tfn) = @_;
+
+ $dsc->{$ourdscfield[0]} = $head;
+ $dsc->save("$dscfn.tmp") or die $!;
+
+ my $changes = parsecontrol($changesfile,$changesfilewhat);
+ foreach my $field (qw(Source Distribution Version)) {
+ $changes->{$field} eq $clogp->{$field} or
+ fail "changes field $field \`$changes->{$field}'".
+ " does not match changelog \`$clogp->{$field}'";
+ }
+
+ my $cversion = getfield $clogp, 'Version';
+ my $clogsuite = getfield $clogp, 'Distribution';
+
+ # 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);
+ open TO, '>', $tfn->('.tmp') or die $!;
+ print TO <<END or die $!;
+object $head
+type commit
+tag $tag
+tagger $authline
+
+$package release $cversion for $clogsuite ($csuite) [dgit]
+[dgit distro=$distro$delibs]
+END
+ close TO or die $!;
+
+ my $tagobjfn = $tfn->('.tmp');
+ if ($sign) {
+ if (!defined $keyid) {
+ $keyid = access_cfg('keyid','RETURN-UNDEF');
+ }
+ unlink $tfn->('.tmp.asc') or $!==&ENOENT or die $!;
+ my @sign_cmd = (@gpg, qw(--detach-sign --armor));
+ push @sign_cmd, qw(-u),$keyid if defined $keyid;
+ push @sign_cmd, $tfn->('.tmp');
+ runcmd_ordryrun @sign_cmd;
+ if (act_scary()) {
+ $tagobjfn = $tfn->('.signed.tmp');
+ runcmd shell_cmd "exec >$tagobjfn", qw(cat --),
+ $tfn->('.tmp'), $tfn->('.tmp.asc');
+ }
+ }
+
+ return ($tagobjfn);
+}
+
+sub sign_changes ($) {
+ my ($changesfile) = @_;
+ if ($sign) {
+ my @debsign_cmd = @debsign;
+ push @debsign_cmd, "-k$keyid" if defined $keyid;
+ push @debsign_cmd, "-p$gpg[0]" if $gpg[0] ne 'gpg';
+ push @debsign_cmd, $changesfile;
+ runcmd_ordryrun @debsign_cmd;
+ }
+}
+
+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);
+
+ responder_send_file('parsed-changelog', $clogpfn);
+
+ my ($clogp, $cversion, $tag, $dscfn) =
+ push_parse_changelog("$clogpfn");
+
+ my $dscpath = "$buildproductsdir/$dscfn";
+ stat_exists $dscpath or
+ fail "looked for .dsc $dscfn, but $!;".
+ " maybe you forgot to build";
+
+ responder_send_file('dsc', $dscpath);
+
+ push_parse_dsc($dscpath, $dscfn, $cversion);
+
+ my $format = getfield $dsc, 'Format';
+ printdebug "format $format\n";
+ if (madformat($format)) {
+ commit_quilty_patch();
+ }
+ check_not_dirty();
+ changedir $ud;
+ progress "checking that $dscfn corresponds to HEAD";
+ runcmd qw(dpkg-source -x --),
+ $dscpath =~ m#^/# ? $dscpath : "../../../$dscpath";
+ my ($tree,$dir) = mktree_in_ud_from_only_subdir();
+ changedir '../../../..';
+ my $diffopt = $debug>0 ? '--exit-code' : '--quiet';
+ my @diffcmd = (@git, qw(diff), $diffopt, $tree);
+ printcmd \*DEBUG,$debugprefix."+",@diffcmd;
+ $!=0; $?=0;
+ my $r = system @diffcmd;
+ if ($r) {
+ if ($r==256) {
+ fail "$dscfn specifies a different tree to your HEAD commit;".
+ " perhaps you forgot to build".
+ ($diffopt eq '--exit-code' ? "" :
+ " (run with -D to see full diff output)");
+ } else {
+ failedcmd @diffcmd;
+ }
+ }
+#fetch from alioth
+#do fast forward check and maybe fake merge
+# if (!is_fast_fwd(mainbranch
+# runcmd @git, qw(fetch -p ), "$alioth_git/$package.git",
+# map { lref($_).":".rref($_) }
+# (uploadbranch());
+ my $head = rev_parse('HEAD');
+ if (!$changesfile) {
+ my $multi = "$buildproductsdir/".
+ "${package}_".(stripepoch $cversion)."_multi.changes";
+ if (stat_exists "$multi") {
+ $changesfile = $multi;
+ } else {
+ my $pat = "${package}_".(stripepoch $cversion)."_*.changes";
+ my @cs = glob "$buildproductsdir/$pat";
+ fail "failed to find unique changes file".
+ " (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;
+
+ if ($we_are_responder) {
+ $tagobjfn = $tfn->('.signed.tmp');
+ responder_receive_files('signed-tag', $tagobjfn);
+ } else {
+ $tagobjfn =
+ push_mktag($head,$clogp,$tag,
+ $dscpath,
+ $changesfile,$changesfile,
+ $tfn);
+ }
+
+ my $tag_obj_hash = cmdoutput @git, qw(hash-object -w -t tag), $tagobjfn;
+ runcmd_ordryrun @git, qw(verify-tag), $tag_obj_hash;
+ runcmd_ordryrun_local @git, qw(update-ref), "refs/tags/$tag", $tag_obj_hash;
+ runcmd_ordryrun @git, qw(tag -v --), $tag;
+
+ if (!check_for_git()) {
+ create_remote_git_repo();
+ }
+ 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) {
+ my $dryrunsuffix = act_local() ? "" : ".tmp";
+ responder_receive_files('signed-dsc-changes',
+ "$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;
+ }
+
+ my $host = access_cfg('upload-host','RETURN-UNDEF');
+ my @hostarg = defined($host) ? ($host,) : ();
+ runcmd_ordryrun @dput, @hostarg, $changesfile;
+ printdone "pushed and uploaded $cversion";
+
+ responder_send_command("complete");
+}
+
+sub cmd_clone {
+ parseopts();
+ my $dstdir;
+ badusage "-p is not allowed with clone; specify as argument instead"
+ if defined $package;
+ if (@ARGV==1) {
+ ($package) = @ARGV;
+ } elsif (@ARGV==2 && $ARGV[1] =~ m#^\w#) {
+ ($package,$isuite) = @ARGV;
+ } elsif (@ARGV==2 && $ARGV[1] =~ m#^[./]#) {
+ ($package,$dstdir) = @ARGV;
+ } elsif (@ARGV==3) {
+ ($package,$isuite,$dstdir) = @ARGV;
+ } else {
+ 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 () {
+ my $branch = cmdoutput_errok @git, qw(symbolic-ref HEAD);
+ if ($branch =~ m#$lbranch_re#o) {
+ return $1;
+ } else {
+ return undef;
+ }
+}
+
+sub fetchpullargs () {
+ if (!defined $package) {
+ my $sourcep = parsecontrol('debian/control','debian/control');
+ $package = getfield $sourcep, 'Source';
+ }
+ if (@ARGV==0) {
+# $isuite = branchsuite(); # this doesn't work because dak hates canons
+ if (!$isuite) {
+ my $clogp = parsechangelog();
+ $isuite = getfield $clogp, 'Distribution';
+ }
+ canonicalise_suite();
+ progress "fetching from suite $csuite";
+ } elsif (@ARGV==1) {
+ ($isuite) = @ARGV;
+ canonicalise_suite();
+ } else {
+ badusage "incorrect arguments to dgit fetch or dgit pull";
+ }
+}
+
+sub cmd_fetch {
+ parseopts();
+ fetchpullargs();
+ fetch();
+}
+
+sub cmd_pull {
+ parseopts();
+ fetchpullargs();
+ pull();
+}
+
+sub cmd_push {
+ parseopts();
+ badusage "-p is not allowed with dgit push" if defined $package;
+ check_not_dirty();
+ my $clogp = parsechangelog();
+ $package = getfield $clogp, 'Source';
+ my $specsuite;
+ if (@ARGV==0) {
+ } elsif (@ARGV==1) {
+ ($specsuite) = (@ARGV);
+ } else {
+ badusage "incorrect arguments to dgit push";
+ }
+ $isuite = getfield $clogp, 'Distribution';
+ if ($new_package) {
+ local ($package) = $existing_package; # this is a hack
+ canonicalise_suite();
+ }
+ if (defined $specsuite && $specsuite ne $isuite) {
+ canonicalise_suite();
+ $csuite eq $specsuite or
+ fail "dgit push: changelog specifies $isuite ($csuite)".
+ " but command line specifies $specsuite";
+ }
+ if (check_for_git()) {
+ git_fetch_us();
+ }
+ if (fetch_from_archive()) {
+ 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().".";
+ } else {
+ $new_package or
+ fail "package appears to be new in this suite;".
+ " if this is intentional, use --new";
+ }
+ dopush();
+}
+
+#---------- remote commands' implementation ----------
+
+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,$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;
+
+ open PI, "<&STDIN" or die $!;
+ open STDIN, "/dev/null" or die $!;
+ open PO, ">&STDOUT" or die $!;
+ autoflush PO 1;
+ open STDOUT, ">&STDERR" or die $!;
+ autoflush STDOUT 1;
+
+ $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;
+
+sub i_cleanup {
+ 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) {
+ changedir "/";
+ eval { rmtree $i_tmp; };
+ }
+}
+
+END { i_cleanup(); }
+
+sub i_method {
+ my ($base,$selector,@args) = @_;
+ $selector =~ s/\-/_/g;
+ { no strict qw(refs); &{"${base}_${selector}"}(@args); }
+}
+
+sub cmd_rpush {
+ my $host = nextarg;
+ my $dir;
+ if ($host =~ m/^((?:[^][]|\[[^][]*\])*)\:/) {
+ $host = $1;
+ $dir = $'; #';
+ } else {
+ $dir = nextarg;
+ }
+ $dir =~ s{^-}{./-};
+ my @rargs = ($dir,$rpushprotovsn);
+ my @rdgit;
+ push @rdgit, @dgit;
+ push @rdgit, @ropts;
+ push @rdgit, qw(remote-push-build-host), (scalar @rargs), @rargs;
+ push @rdgit, @ARGV;
+ my @cmd = (@ssh, $host, shellquote @rdgit);
+ printcmd \*DEBUG,$debugprefix."+",@cmd;
+
+ if (defined $initiator_tempdir) {
+ rmtree $initiator_tempdir;
+ mkdir $initiator_tempdir, 0700 or die "$initiator_tempdir: $!";
+ $i_tmp = $initiator_tempdir;
+ } else {
+ $i_tmp = tempdir();
+ }
+ $i_child_pid = open2(\*RO, \*RI, @cmd);
+ changedir $i_tmp;
+ initiator_expect { m/^dgit-remote-push-ready/ };
+ for (;;) {
+ my ($icmd,$iargs) = initiator_expect {
+ m/^(\S+)(?: (.*))?$/;
+ ($1,$2);
+ };
+ i_method "i_resp", $icmd, $iargs;
+ }
+}
+
+sub i_resp_progress ($) {
+ my ($rhs) = @_;
+ my $msg = protocol_read_bytes \*RO, $rhs;
+ progress $msg;
+}
+
+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;
+}
+
+sub i_resp_file ($) {
+ my ($keyword) = @_;
+ my $localname = i_method "i_localname", $keyword;
+ my $localpath = "$i_tmp/$localname";
+ stat_exists $localpath and
+ badproto \*RO, "file $keyword ($localpath) twice";
+ protocol_receive_file \*RO, $localpath;
+ i_method "i_file", $keyword;
+}
+
+our %i_param;
+
+sub i_resp_param ($) {
+ $_[0] =~ m/^(\S+) (.*)$/ or badproto \*RO, "bad param spec";
+ $i_param{$1} = $2;
+}
+
+our %i_wanted;
+
+sub i_resp_want ($) {
+ my ($keyword) = @_;
+ die "$keyword ?" if $i_wanted{$keyword}++;
+ my @localpaths = i_method "i_want", $keyword;
+ printdebug "[[ $keyword @localpaths\n";
+ foreach my $localpath (@localpaths) {
+ protocol_send_file \*RI, $localpath;
+ }
+ print RI "files-end\n" or die $!;
+}
+
+our ($i_clogp, $i_version, $i_tag, $i_dscfn, $i_changesfn);
+
+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
+ && 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,
+ $i_changesfn, 'remote changes',
+ sub { "tag$_[0]"; };
+
+ return $tagobjfn;
+}
+
+sub i_want_signed_dsc_changes {
+ rename "$i_dscfn.tmp","$i_dscfn" or die "$i_dscfn $!";
+ sign_changes $i_changesfn;
+ return ($i_dscfn, $i_changesfn);
+}
+
+#---------- building etc. ----------
+
+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 <<END or die $!;
+Description: $msg
+Author: $author
+$xinfo
+---
+
+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;
+ }
+}
+
+sub quiltify_trees_differ ($$) {
+ my ($x,$y) = @_;
+ # returns 1 iff the two tree objects differ other than in debian/
+ local $/=undef;
+ my @cmd = (@git, qw(diff-tree --name-only -z), $x, $y);
+ my $diffs= cmdoutput @cmd;
+ foreach my $f (split /\0/, $diffs) {
+ next if $f eq 'debian';
+ return 1;
+ }
+ return 0;
+}
+
+sub quiltify_tree_sentinelfiles ($) {
+ # lists the `sentinel' files present in the tree
+ my ($x) = @_;
+ my $r = cmdoutput @git, qw(ls-tree --name-only), $x,
+ qw(-- debian/rules debian/control);
+ $r =~ s/\n/,/g;
+ return $r;
+}
+
+sub quiltify ($$) {
+ my ($clogp,$target) = @_;
+
+ # Quilt patchification algorithm
+ #
+ # We search backwards through the history of the main tree's HEAD
+ # (T) looking for a start commit S whose tree object is identical
+ # to to the patch tip tree (ie the tree corresponding to the
+ # current dpkg-committed patch series). For these purposes
+ # `identical' disregards anything in debian/ - this wrinkle is
+ # necessary because dpkg-source treates debian/ specially.
+ #
+ # We can only traverse edges where at most one of the ancestors'
+ # trees differs (in changes outside in debian/). And we cannot
+ # handle edges which change .pc/ or debian/patches. To avoid
+ # going down a rathole we avoid traversing edges which introduce
+ # debian/rules or debian/control. And we set a limit on the
+ # number of edges we are willing to look at.
+ #
+ # If we succeed, we walk forwards again. For each traversed edge
+ # PC (with P parent, C child) (starting with P=S and ending with
+ # C=T) to we do this:
+ # - git checkout C
+ # - dpkg-source --commit with a patch name and message derived from C
+ # After traversing PT, we git commit the changes which
+ # should be contained within debian/patches.
+
+ changedir '../fake';
+ mktree_in_ud_here();
+ rmtree '.pc';
+ runcmd @git, 'add', '.';
+ my $oldtiptree=git_write_tree();
+ changedir '../work';
+
+ # The search for the path S..T is breadth-first. We maintain a
+ # todo list containing search nodes. A search node identifies a
+ # commit, and looks something like this:
+ # $p = {
+ # Commit => $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;
+ # 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 $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 <<END or die $!;
+Format: 3.0 (quilt)
+Source: $package
+Version: $fakeversion
+Files:
+END
+
+ my $dscaddfile=sub {
+ my ($b) = @_;
+
+ my $md = new Digest::MD5;
+
+ my $fh = new IO::File $b, '<' or die "$b $!";
+ stat $fh or die $!;
+ my $size = -s _;
+
+ $md->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 $!;
+ }
+
+ quiltify($clogp,$headref);
+
+ if (!open P, '>>', ".pc/applied-patches") {
+ $!==&ENOENT or die $!;
+ } else {
+ close P;
+ }
+
+ 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 () {
+ my $descfn = $ENV{$fakeeditorenv};
+ my $editing = $ARGV[$#ARGV];
+ open I1, '<', $descfn or die "$descfn: $!";
+ open I2, '<', $editing or die "$editing: $!";
+ unlink $editing or die "$editing: $!";
+ open O, '>', $editing or die "$editing: $!";
+ while (<I1>) { print O or die $!; } I1->error and die $!;
+ my $copying = 0;
+ while (<I2>) {
+ $copying ||= m/^\-\-\- /;
+ next unless $copying;
+ print O or die $!;
+ }
+ I2->error and die $!;
+ close O or die $1;
+ exit 0;
+}
+
+#----- other building -----
+
+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';
+ $version = getfield $clogp, 'Version';
+ 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<version>";
+ }
+ }
+ if ($changes_since_version ne '_') {
+ unshift @opts, "-v$changes_since_version";
+ }
+ return @opts;
+}
+
+sub cmd_build {
+ build_prep();
+ runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc), changesopts(), @ARGV;
+ printdone "build successful\n";
+}
+
+sub cmd_git_build {
+ build_prep();
+ my @cmd =
+ (qw(git-buildpackage -us -uc --git-no-sign-tags),
+ "--git-builder=@dpkgbuildpackage");
+ unless (grep { m/^--git-debian-branch|^--git-ignore-branch/ } @ARGV) {
+ canonicalise_suite();
+ push @cmd, "--git-debian-branch=".lbranch();
+ }
+ push @cmd, changesopts();
+ runcmd_ordryrun_local @cmd, @ARGV;
+ printdone "build successful\n";
+}
+
+sub build_source {
+ build_prep();
+ $sourcechanges = "${package}_".(stripepoch $version)."_source.changes";
+ $dscfn = dscfn($version);
+ if ($cleanmode eq 'dpkg-source') {
+ runcmd_ordryrun_local (@dpkgbuildpackage, qw(-us -uc -S)),
+ changesopts();
+ } else {
+ my $pwd = must_getcwd();
+ my $leafdir = basename $pwd;
+ changedir "..";
+ runcmd_ordryrun_local @dpkgsource, qw(-b --), $leafdir;
+ changedir $pwd;
+ runcmd_ordryrun_local qw(sh -ec),
+ 'exec >$1; shift; exec "$@"','x',
+ "../$sourcechanges",
+ @dpkggenchanges, qw(-S), changesopts();
+ }
+}
+
+sub cmd_build_source {
+ badusage "build-source takes no additional arguments" if @ARGV;
+ build_source();
+ printdone "source built, results in $dscfn and $sourcechanges";
+}
+
+sub cmd_sbuild {
+ build_source();
+ changedir "..";
+ my $pat = "${package}_".(stripepoch $version)."_*.changes";
+ if (act_local()) {
+ 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;
+ 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_exists $multichanges or fail "$multichanges: $!";
+ }
+ printdone "build successful, results in $multichanges\n" or die $!;
+}
+
+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();
+}
+
+sub cmd_archive_api_query {
+ badusage "need only 1 subpath argument" unless @ARGV==1;
+ my ($subpath) = @ARGV;
+ my @cmd = archive_api_query_cmd($subpath);
+ exec @cmd or fail "exec curl: $!\n";
+}
+
+#---------- argument parsing and main program ----------
+
+sub cmd_version {
+ print "dgit version $our_version\n" or die $!;
+ exit 0;
+}
+
+sub parseopts () {
+ my $om;
+
+ if (defined $ENV{'DGIT_SSH'}) {
+ @ssh = string_to_ssh $ENV{'DGIT_SSH'};
+ } elsif (defined $ENV{'GIT_SSH'}) {
+ @ssh = ($ENV{'GIT_SSH'});
+ }
+
+ while (@ARGV) {
+ last unless $ARGV[0] =~ m/^-/;
+ $_ = shift @ARGV;
+ last if m/^--?$/;
+ if (m/^--/) {
+ if (m/^--dry-run$/) {
+ push @ropts, $_;
+ $dryrun_level=2;
+ } elsif (m/^--damp-run$/) {
+ push @ropts, $_;
+ $dryrun_level=1;
+ } elsif (m/^--no-sign$/) {
+ push @ropts, $_;
+ $sign=0;
+ } elsif (m/^--help$/) {
+ cmd_help();
+ } elsif (m/^--version$/) {
+ cmd_version();
+ } elsif (m/^--new$/) {
+ push @ropts, $_;
+ $new_package=1;
+ } 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/^--([-0-9a-z]+):(.*)/s &&
+ !$opts_opt_cmdonly{$1} &&
+ ($om = $opts_opt_map{$1})) {
+ push @ropts, $_;
+ push @$om, $2;
+ } elsif (m/^--existing-package=(.*)/s) {
+ push @ropts, $_;
+ $existing_package = $1;
+ } elsif (m/^--initiator-tempdir=(.*)/s) {
+ $initiator_tempdir = $1;
+ $initiator_tempdir =~ m#^/# or
+ badusage "--initiator-tempdir must be used specify an".
+ " absolute, not relative, directory."
+ } 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;
+ } 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, $_;
+ $quilt_mode = 'nocheck';
+ } elsif (m/^--no-rm-on-error$/s) {
+ push @ropts, $_;
+ $rmonerror = 0;
+ } elsif (m/^--deliberately-($suite_re)$/s) {
+ push @ropts, $_;
+ push @deliberatelies, $&;
+ } else {
+ badusage "unknown long option \`$_'";
+ }
+ } else {
+ while (m/^-./s) {
+ if (s/^-n/-/) {
+ push @ropts, $&;
+ $dryrun_level=2;
+ } elsif (s/^-L/-/) {
+ push @ropts, $&;
+ $dryrun_level=1;
+ } elsif (s/^-h/-/) {
+ cmd_help();
+ } elsif (s/^-D/-/) {
+ push @ropts, $&;
+ open DEBUG, ">&STDERR" or die $!;
+ autoflush DEBUG 1;
+ $debug++;
+ } elsif (s/^-N/-/) {
+ push @ropts, $&;
+ $new_package=1;
+ } elsif (s/^-v([^_]+|_)$//s) {
+ push @ropts, $&;
+ $changes_since_version = $1;
+ } elsif (m/^-m/) {
+ push @ropts, $&;
+ push @changesopts, $_;
+ $_ = '';
+ } elsif (s/^-c(.*=.*)//s) {
+ push @ropts, $&;
+ push @git, '-c', $1;
+ } elsif (s/^-d(.+)//s) {
+ push @ropts, $&;
+ $idistro = $1;
+ } elsif (s/^-C(.+)//s) {
+ push @ropts, $&;
+ $changesfile = $1;
+ if ($changesfile =~ s#^(.*)/##) {
+ $buildproductsdir = $1;
+ }
+ } elsif (s/^-k(.+)//s) {
+ $keyid=$1;
+ } 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) {
+ push @ropts, $&;
+ $cleanmode = 'git';
+ } elsif (s/^-wd$//s) {
+ push @ropts, $&;
+ $cleanmode = 'dpkg-source';
+ } else {
+ badusage "unknown short option \`$_'";
+ }
+ }
+ }
+ }
+}
+
+if ($ENV{$fakeeditorenv}) {
+ quilt_fixup_editor();