use IPC::Open2;
use Digest::SHA;
use Digest::MD5;
+use List::Util qw(any);
+use List::MoreUtils qw(pairwise);
use Debian::Dgit;
our $our_version = 'UNRELEASED'; ###substituted###
-our @rpushprotovsn_support = qw(3 2);
+our @rpushprotovsn_support = qw(4 3 2); # 4 is new tag format
our $protovsn;
our $isuite = 'unstable';
our $we_are_responder;
our $initiator_tempdir;
our $patches_applied_dirtily = 00;
+our $tagformat_want;
+our $tagformat;
+our $tagformatfn;
our %format_ok = map { $_=>1 } ("1.0","3.0 (native)","3.0 (quilt)");
our $csuite;
our $instead_distro;
+sub debiantag ($$) {
+ my ($v,$distro) = @_;
+ return $tagformatfn->($v, $distro);
+}
+
+sub debiantag_maintview ($$) {
+ my ($v,$distro) = @_;
+ $v =~ y/~:/_%/;
+ return "$distro/$v";
+}
+
sub lbranch () { return "$branchprefix/$csuite"; }
my $lbranch_re = '^refs/heads/'.$branchprefix.'/([^/.]+)$';
sub lref () { return "refs/heads/".lbranch(); }
# where <rargs> is <push-host-dir> <supported-proto-vsn>,... ...
# < dgit-remote-push-ready <actual-proto-vsn>
#
+# occasionally:
+#
+# > progress NBYTES
+# [NBYTES message]
+#
+# > supplementary-message NBYTES # $protovsn >= 3
+# [NBYTES message]
+#
+# main sequence:
+#
# > file parsed-changelog
# [indicates that output of dpkg-parsechangelog follows]
# > data-block NBYTES
# > file changes
# [etc]
#
-# > param head HEAD
+# > param head DGIT-VIEW-HEAD
+# > param csuite SUITE
+# > param tagformat old|new
+# > param maint-view MAINT-VIEW-HEAD
+#
+# > previously REFNAME=OBJNAME # if --deliberately-not-fast-forward
+# # goes into tag, for replay prevention
#
# > want signed-tag
# [indicates that signed tag is wanted]
sub runcmd {
debugcmd "+",@_;
- $!=0; $?=0;
+ $!=0; $?=-1;
failedcmd @_ if system @_;
}
'dgit.default.ssh' => 'ssh',
'dgit.default.archive-query' => 'madison:',
'dgit.default.sshpsql-dbname' => 'service=projectb',
+ 'dgit.default.dgit-tag-format' => 'old,new,maint',
'dgit-distro.debian.archive-query' => 'ftpmasterapi:',
'dgit-distro.debian.git-check' => 'url',
'dgit-distro.debian.git-check-suffix' => '/info/refs',
'dgit-distro.debian.new-private-pushers' => 't',
+ 'dgit-distro.debian.dgit-tag-format' => 'old',
'dgit-distro.debian/push.git-url' => '',
'dgit-distro.debian/push.git-host' => 'push.dgit.debian.org',
'dgit-distro.debian/push.git-user-force' => 'dgit',
my @cmd = (@git, qw(config -z --get-regexp .*));
debugcmd "|",@cmd;
- open GITS, "-|", @cmd or failedcmd @cmd;
+ open GITS, "-|", @cmd or die $!;
while (<GITS>) {
chomp or die;
printdebug "=> ", (messagequote $_), "\n";
return sort { -version_compare($a->[0],$b->[0]); } @rows;
}
+#---------- tag format handling ----------
+
+sub access_cfg_tagformats () {
+ split /\,/, access_cfg('dgit-tag-format');
+}
+
+sub need_tagformat ($$) {
+ my ($fmt, $why) = @_;
+ fail "need to use tag format $fmt ($why) but also need".
+ " to use tag format $tagformat_want->[0] ($tagformat_want->[1])".
+ " - no way to proceed"
+ if $tagformat_want && $tagformat_want->[0] ne $fmt;
+ $tagformat_want = [$fmt, $why, $tagformat_want->[2] // 0];
+}
+
+sub select_tagformat () {
+ # sets $tagformatfn
+ return if $tagformatfn && !$tagformat_want;
+ die 'bug' if $tagformatfn && $tagformat_want;
+ # ... $tagformat_want assigned after previous select_tagformat
+
+ my (@supported) = grep { $_ ne 'maint' } access_cfg_tagformats();
+ printdebug "select_tagformat supported @supported\n";
+
+ $tagformat_want //= [ $supported[0], "distro access configuration", 0 ];
+ printdebug "select_tagformat specified @$tagformat_want\n";
+
+ my ($fmt,$why,$override) = @$tagformat_want;
+
+ fail "target distro supports tag formats @supported".
+ " but have to use $fmt ($why)"
+ unless $override
+ or grep { $_ eq $fmt } @supported;
+
+ $tagformat_want = undef;
+ $tagformat = $fmt;
+ $tagformatfn = ${*::}{"debiantag_$fmt"};
+
+ fail "trying to use unknown tag format \`$fmt' ($why) !"
+ unless $tagformatfn;
+}
+
#---------- archive query entrypoints and rest of program ----------
sub canonicalise_suite () {
" 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+)$/) {
+ if (defined $r and $r =~ m/^divert (\w+)$/) {
my $divert=$1;
my ($usedistro,) = access_distros();
# NB that if we are pushing, $usedistro will be $distro/push
progress "diverting to $divert (using config for $instead_distro)";
return check_for_git();
}
- failedcmd @cmd unless $r =~ m/^[01]$/;
+ failedcmd @cmd unless defined $r and $r =~ m/^[01]$/;
return $r+0;
} elsif ($how eq 'url') {
my $prefix = access_cfg('git-check-url','git-url');
sub remove_stray_gits () {
my @gitscmd = qw(find -name .git -prune -print0);
debugcmd "|",@gitscmd;
- open GITS, "-|", @gitscmd or failedcmd @gitscmd;
+ open GITS, "-|", @gitscmd or die $!;
{
local $/="\0";
while (<GITS>) {
sub mktree_in_ud_from_only_subdir () {
# changes into the subdir
my (@dirs) = <*/.>;
- die unless @dirs==1;
+ die "@dirs ?" unless @dirs==1;
$dirs[0] =~ m#^([^/]+)/\.$# or die;
my $dir = $1;
changedir $dir;
runcmd_ordryrun_local @git, qw(fetch -p -n -q), access_giturl(), @specs;
my %here;
- my $tagpat = debiantag('*',access_basedistro);
+ my @tagpats = debiantags('*',access_basedistro);
- git_for_each_ref("refs/tags/".$tagpat, sub {
+ git_for_each_ref([map { "refs/tags/$_" } @tagpats], sub {
my ($objid,$objtype,$fullrefname,$reftail) = @_;
printdebug "currently $fullrefname=$objid\n";
$here{$fullrefname} = $objid;
});
- git_for_each_ref(lrfetchrefs."/tags/".$tagpat, sub {
+ git_for_each_ref([map { lrfetchrefs."/tags/".$_ } @tagpats], sub {
my ($objid,$objtype,$fullrefname,$reftail) = @_;
my $lref = "refs".substr($fullrefname, length lrfetchrefs);
printdebug "offered $lref=$objid\n";
my @cmd = (@git, qw(diff --quiet HEAD));
debugcmd "+",@cmd;
- $!=0; $?=0; system @cmd;
- return if !$! && !$?;
- if (!$! && $?==256) {
+ $!=0; $?=-1; system @cmd;
+ return if !$?;
+ if ($?==256) {
fail "working tree is dirty (does not match HEAD)";
} else {
failedcmd @cmd;
my $dscfn = dscfn($cversion);
- return ($clogp, $cversion, $tag, $dscfn);
+ return ($clogp, $cversion, $dscfn);
}
sub push_parse_dsc ($$$) {
" but debian/changelog is for $package $cversion";
}
-sub push_mktag ($$$$$$$) {
- my ($head,$clogp,$tag,
- $dscfn,
+sub push_tagwants ($$$$) {
+ my ($cversion, $dgithead, $maintviewhead, $tfbase) = @_;
+ my @tagwants;
+ push @tagwants, {
+ TagFn => \&debiantag,
+ Objid => $dgithead,
+ TfSuffix => '',
+ View => 'dgit',
+ };
+ if (defined $maintviewhead) {
+ push @tagwants, {
+ TagFn => \&debiantag_maintview,
+ Objid => $maintviewhead,
+ TfSuffix => '-maintview',
+ View => 'maint',
+ };
+ }
+ foreach my $tw (@tagwants) {
+ $tw->{Tag} = $tw->{TagFn}($cversion, access_basedistro);
+ $tw->{Tfn} = sub { $tfbase.$tw->{TfSuffix}.$_[0]; };
+ }
+ return @tagwants;
+}
+
+sub push_mktags ($$ $$ $) {
+ my ($clogp,$dscfn,
$changesfile,$changesfilewhat,
- $tfn) = @_;
+ $tagwants) = @_;
- $dsc->{$ourdscfield[0]} = $head;
+ die unless $tagwants->[0]{View} eq 'dgit';
+
+ $dsc->{$ourdscfield[0]} = $tagwants->[0]{Objid};
$dsc->save("$dscfn.tmp") or die $!;
my $changes = parsecontrol($changesfile,$changesfilewhat);
my $authline = clogp_authline $clogp;
my $delibs = join(" ", "",@deliberatelies);
my $declaredistro = access_basedistro();
- open TO, '>', $tfn->('.tmp') or die $!;
- print TO <<END or die $!;
+
+ my $mktag = sub {
+ my ($tw) = @_;
+ my $tfn = $tw->{Tfn};
+ my $head = $tw->{Objid};
+ my $tag = $tw->{Tag};
+
+ open TO, '>', $tfn->('.tmp') or die $!;
+ print TO <<END or die $!;
object $head
type commit
tag $tag
tagger $authline
+END
+ if ($tw->{View} eq 'dgit') {
+ print TO <<END or die $!;
$package release $cversion for $clogsuite ($csuite) [dgit]
[dgit distro=$declaredistro$delibs]
END
- foreach my $ref (sort keys %previously) {
- print TO <<END or die $!;
+ foreach my $ref (sort keys %previously) {
+ print TO <<END or die $!;
[dgit previously:$ref=$previously{$ref}]
END
- }
+ }
+ } elsif ($tw->{View} eq 'maint') {
+ print TO <<END or die $!;
+$package release $cversion for $clogsuite ($csuite)
+(maintainer view tag generated by dgit --quilt=$quilt_mode)
+END
+ } else {
+ die Dumper($tw)."?";
+ }
- close TO or die $!;
+ close TO or die $!;
- my $tagobjfn = $tfn->('.tmp');
- if ($sign) {
- if (!defined $keyid) {
- $keyid = access_cfg('keyid','RETURN-UNDEF');
- }
- if (!defined $keyid) {
- $keyid = getfield $clogp, 'Maintainer';
- }
- 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');
+ my $tagobjfn = $tfn->('.tmp');
+ if ($sign) {
+ if (!defined $keyid) {
+ $keyid = access_cfg('keyid','RETURN-UNDEF');
+ }
+ if (!defined $keyid) {
+ $keyid = getfield $clogp, 'Maintainer';
+ }
+ 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;
+ };
- return ($tagobjfn);
+ my @r = map { $mktag->($_); } @$tagwants;
+ return @r;
}
sub sign_changes ($) {
Push failed, while preparing your push.
You can retry the push, after fixing the problem, if you like.
END
+
+ need_tagformat 'new', "quilt mode $quilt_mode"
+ if quiltmode_splitbrain;
+
prep_ud();
access_giturl(); # check that success is vaguely likely
+ select_tagformat();
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) =
+ my ($clogp, $cversion, $dscfn) =
push_parse_changelog("$clogpfn");
my $dscpath = "$buildproductsdir/$dscfn";
my $format = getfield $dsc, 'Format';
printdebug "format $format\n";
+ my $actualhead = git_rev_parse('HEAD');
+ my $dgithead = $actualhead;
+ my $maintviewhead = undef;
+
if (madformat($format)) {
# user might have not used dgit build, so maybe do this now:
- commit_quilty_patch();
+ if (quiltmode_splitbrain()) {
+ my $upstreamversion = $clogp->{Version};
+ $upstreamversion =~ s/-[^-]*$//;
+ changedir $ud;
+ quilt_make_fake_dsc($upstreamversion);
+ my ($dgitview, $cachekey) =
+ quilt_check_splitbrain_cache($actualhead, $upstreamversion);
+ $dgitview or fail
+ "--quilt=$quilt_mode but no cached dgit view:
+ perhaps tree changed since dgit build[-source] ?";
+ $split_brain = 1;
+ $dgithead = $dgitview;
+ $maintviewhead = $actualhead;
+ changedir '../../../..';
+ prep_ud(); # so _only_subdir() works, below
+ } else {
+ commit_quilty_patch();
+ }
}
- die 'xxx fast forward (should not depend on quilt mode, but will always be needed if we did $split_brain)' if $split_brain;
-
check_not_dirty();
changedir $ud;
progress "checking that $dscfn corresponds to HEAD";
check_for_vendor_patches() if madformat($dsc->{format});
changedir '../../../..';
my $diffopt = $debuglevel>0 ? '--exit-code' : '--quiet';
- my @diffcmd = (@git, qw(diff), $diffopt, $tree);
+ my @diffcmd = (@git, qw(diff), $diffopt, $tree, $dgithead);
debugcmd "+",@diffcmd;
- $!=0; $?=0;
+ $!=0; $?=-1;
my $r = system @diffcmd;
if ($r) {
if ($r==256) {
failedcmd @diffcmd;
}
}
- my $head = git_rev_parse('HEAD');
if (!$changesfile) {
my $pat = changespat $cversion;
my @cs = glob "$buildproductsdir/$pat";
}
responder_send_file('changes',$changesfile);
- responder_send_command("param head $head");
+ responder_send_command("param head $dgithead");
responder_send_command("param csuite $csuite");
+ responder_send_command("param tagformat $tagformat");
+ if (quiltmode_splitbrain) {
+ die unless ($protovsn//4) >= 4;
+ responder_send_command("param maint-view $maintviewhead");
+ }
if (deliberately_not_fast_forward) {
git_for_each_ref(lrfetchrefs, sub {
});
}
- my $tfn = sub { ".git/dgit/tag$_[0]"; };
- my $tagobjfn;
+ my @tagwants = push_tagwants($cversion, $dgithead, $maintviewhead,
+ ".git/dgit/tag");
+ my @tagobjfns;
supplementary_message(<<'END');
Push failed, while signing the tag.
END
# If we manage to sign but fail to record it anywhere, it's fine.
if ($we_are_responder) {
- $tagobjfn = $tfn->('.signed.tmp');
- responder_receive_files('signed-tag', $tagobjfn);
+ @tagobjfns = map { $_->{Tfn}('.signed-tmp') } @tagwants;
+ responder_receive_files('signed-tag', @tagobjfns);
} else {
- $tagobjfn =
- push_mktag($head,$clogp,$tag,
- $dscpath,
- $changesfile,$changesfile,
- $tfn);
+ @tagobjfns = push_mktags($clogp,$dscpath,
+ $changesfile,$changesfile,
+ \@tagwants);
}
supplementary_message(<<'END');
Push failed, *after* signing the tag.
If you want to try again, you should use a new version number.
END
- 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;
+ pairwise { $a->{TagObjFn} = $b } @tagwants, @tagobjfns;
+
+ foreach my $tw (@tagwants) {
+ my $tag = $tw->{Tag};
+ my $tagobjfn = $tw->{TagObjFn};
+ 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;
+ }
supplementary_message(<<'END');
Push failed, while updating the remote git repository - see messages above.
if (!check_for_git()) {
create_remote_git_repo();
}
- runcmd_ordryrun @git, qw(push),access_giturl(),
- $forceflag."HEAD:".rrref(), $forceflag."refs/tags/$tag";
- runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), 'HEAD';
+
+ my @pushrefs = $forceflag."HEAD:".rrref();
+ foreach my $tw (@tagwants) {
+ my $view = $tw->{View};
+ next unless $view eq 'dgit'
+ or any { $_ eq $view } access_cfg_tagformats();
+ push @pushrefs, $forceflag."refs/tags/$tw->{Tag}";
+ }
+
+ runcmd_ordryrun @git, qw(push),access_giturl(), @pushrefs;
+ runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), $dgithead;
supplementary_message(<<'END');
Push failed, after updating the remote git repository.
unless defined $protovsn;
responder_send_command("dgit-remote-push-ready $protovsn");
-
+ rpush_handle_protovsn_bothends();
changedir $dir;
&cmd_push;
}
# ... for compatibility with proto vsn.1 dgit (just so that user gets
# a good error message)
+sub rpush_handle_protovsn_bothends () {
+ if ($protovsn < 4) {
+ need_tagformat 'old', "rpush negotiated protocol $protovsn";
+ }
+ select_tagformat();
+}
+
our $i_tmp;
sub i_cleanup {
($protovsn) = initiator_expect { m/^dgit-remote-push-ready (\S+)/ };
die "$protovsn ?" unless grep { $_ eq $protovsn } @rpushprotovsn_support;
$supplementary_message = '' unless $protovsn >= 3;
+
+ fail "rpush negotiated protocol version $protovsn".
+ " which does not support quilt mode $quilt_mode"
+ if quiltmode_splitbrain;
+
+ rpush_handle_protovsn_bothends();
for (;;) {
my ($icmd,$iargs) = initiator_expect {
m/^(\S+)(?: (.*))?$/;
print RI "files-end\n" or die $!;
}
-our ($i_clogp, $i_version, $i_tag, $i_dscfn, $i_changesfn);
+our ($i_clogp, $i_version, $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) =
+ ($i_clogp, $i_version, $i_dscfn) =
push_parse_changelog "$i_tmp/remote-changelog.822";
die if $i_dscfn =~ m#/|^\W#;
}
my $head = $i_param{'head'};
die if $head =~ m/[^0-9a-f]/ || $head !~ m/^../;
+ my $maintview = $i_param{'maint-view'};
+ die if defined $maintview && $maintview =~ m/[^0-9a-f]/;
+
+ select_tagformat();
+ if ($protovsn >= 4) {
+ my $p = $i_param{'tagformat'} // '<undef>';
+ $p eq $tagformat
+ or badproto \*RO, "tag format mismatch: $p vs. $tagformat";
+ }
+
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]"; };
+ my @tagwants = push_tagwants $i_version, $head, $maintview, "tag";
- return $tagobjfn;
+ return
+ push_mktags $i_clogp, $i_dscfn,
+ $i_changesfn, 'remote changes',
+ \@tagwants;
}
sub i_want_signed_dsc_changes {
die "$quilt_mode ?";
}
- my $time = time;
+ my $time = $ENV{'GIT_COMMITTER_DATE'} || time;
+ $time =~ s/\s.*//; # trim timezone from GIT_COMMITTER_DATE
my $ncommits = 3;
my $msg = cmdoutput @git, qw(log), "-n$ncommits";
my $splitbrain_cachekey;
progress
- "dgit: split brain (separate dgit view) may needed (--quilt=$quilt_mode).";
+ "dgit: split brain (separate dgit view) may be needed (--quilt=$quilt_mode).";
# we look in the reflog of dgit-intern/quilt-cache
# we look for an entry whose message is the key for the cache lookup
my @cachekey = (qw(dgit), $our_version);
} elsif (m/^--deliberately-($deliberately_re)$/s) {
push @ropts, $_;
push @deliberatelies, $&;
+ } elsif (m/^--dgit-tag-format=(old|new)$/s) {
+ # undocumented, for testing
+ push @ropts, $_;
+ $tagformat_want = [ $1, 'command line', 1 ];
+ # 1 menas overrides distro configuration
} elsif (m/^--always-split-source-build$/s) {
# undocumented, for testing
push @ropts, $_;