# along with this program. If not, see <http://www.gnu.org/licenses/>.
use strict;
-$SIG{__WARN__} = sub { die $_[0]; };
+
+use Debian::Dgit;
+setup_sigwarn();
use IO::Handle;
use Data::Dumper;
our $our_version = 'UNRELEASED'; ###substituted###
-our $rpushprotovsn = 2;
+our @rpushprotovsn_support = qw(3 2);
+our $protovsn;
our $isuite = 'unstable';
our $idistro;
'sbuild' => \@sbuild,
'ssh' => \@ssh,
'dgit' => \@dgit,
+ 'git' => \@git,
'dpkg-source' => \@dpkgsource,
'dpkg-buildpackage' => \@dpkgbuildpackage,
'dpkg-genchanges' => \@dpkggenchanges,
'ch' => \@changesopts,
'mergechanges' => \@mergechanges);
-our %opts_opt_cmdonly = ('gpg' => 1);
+our %opts_opt_cmdonly = ('gpg' => 1, 'git' => 1);
+our %opts_cfg_insertpos = map {
+ $_,
+ scalar @{ $opts_opt_map{$_} }
+} keys %opts_opt_map;
+
+sub finalise_opts_opts();
our $keyid;
autoflush STDOUT 1;
+our $supplementary_message = '';
+
+END {
+ local ($@, $?);
+ print STDERR "! $_\n" foreach $supplementary_message =~ m/^.+$/mg;
+}
+
our $remotename = 'dgit';
our @ourdscfield = qw(Dgit Vcs-Dgit-Master);
our $csuite;
#---------- remote protocol support, common ----------
# remote push initiator/responder protocol:
-# < dgit-remote-push-ready [optional extra info ignored by old initiators]
+# $ dgit remote-push-build-host <n-rargs> <rargs>... <push-args>...
+# where <rargs> is <push-host-dir> <supported-proto-vsn>,... ...
+# < dgit-remote-push-ready <actual-proto-vsn>
#
# > file parsed-changelog
# [indicates that output of dpkg-parsechangelog follows]
sub protocol_read_bytes ($$) {
my ($fh, $nbytes) = @_;
- $nbytes =~ m/^[1-9]\d{0,5}$/ or badproto \*RO, "bad byte count";
+ $nbytes =~ m/^[1-9]\d{0,5}$|^0$/ or badproto \*RO, "bad byte count";
my $d;
my $got = read $fh, $d, $nbytes;
$got==$nbytes or badproto_badread $fh, "data block";
'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/push.git-url' => '',
- 'dgit-distro.debian/push.git-host' => 'dgit-git.debian.net',
+ 'dgit-distro.debian/push.git-host' => 'push.dgit.debian.org',
'dgit-distro.debian/push.git-user-force' => 'dgit',
'dgit-distro.debian/push.git-proto' => 'git+ssh://',
'dgit-distro.debian/push.git-path' => '/dgit/debian/repos',
# 'dgit-distro.debian.archive-query-tls-curl-args',
# '--ca-path=/etc/ssl/ca-debian',
# ^ this is a workaround but works (only) on DSA-administered machines
- 'dgit-distro.debian.diverts.alioth' => '/alioth',
'dgit-distro.debian.git-url' => 'https://git.dgit.debian.org',
'dgit-distro.debian.git-url-suffix' => '',
- 'dgit-distro.debian/push.diverts.alioth' => '/alioth',
- 'dgit-distro.debian/alioth.git-host' => 'git.debian.org',
- 'dgit-distro.debian/alioth.git-user-force' => '',
- 'dgit-distro.debian/alioth.git-proto' => 'git+ssh://',
- 'dgit-distro.debian/alioth.git-path' => '/git/dgit-repos/repos',
- 'dgit-distro.debian/alioth.git-create' => 'ssh-cmd',
'dgit-distro.debian.upload-host' => 'ftp-master', # for dput
'dgit-distro.debian.mirror' => 'http://ftp.debian.org/debian/',
'dgit-distro.debian.backports-quirk' => '(squeeze)-backports*',
'dgit-distro.test-dummy.upload-host' => 'test-dummy',
);
-sub git_get_config ($) {
- my ($c) = @_;
+our %gitcfg;
- our %git_get_config_memo;
- if (exists $git_get_config_memo{$c}) {
- return $git_get_config_memo{$c};
- }
+sub git_slurp_config () {
+ local ($debuglevel) = $debuglevel-2;
+ local $/="\0";
- my $v;
- my @cmd = (@git, qw(config --), $c);
- {
- local ($debuglevel) = $debuglevel-2;
- $v = cmdoutput_errok @cmd;
- };
- if ($?==0) {
- } elsif ($?==256) {
- $v = undef;
- } else {
- failedcmd @cmd;
+ my @cmd = (@git, qw(config -z --get-regexp .*));
+ debugcmd "|",@cmd;
+
+ open GITS, "-|", @cmd or failedcmd @cmd;
+ while (<GITS>) {
+ chomp or die;
+ printdebug "=> ", (messagequote $_), "\n";
+ m/\n/ or die "$_ ?";
+ push @{ $gitcfg{$`} }, $'; #';
}
- $git_get_config_memo{$c} = $v;
- return $v;
+ $!=0; $?=0;
+ close GITS
+ or ($!==0 && $?==256)
+ or failedcmd @cmd;
+}
+
+sub git_get_config ($) {
+ my ($c) = @_;
+ my $l = $gitcfg{$c};
+ printdebug"C $c ".(defined $l ? messagequote "'$l'" : "undef")."\n"
+ if $debuglevel >= 4;
+ $l or return undef;
+ @$l==1 or badcfg "multiple values for $c" if @$l > 1;
+ return $l->[0];
}
sub cfg {
return ('none',undef);
}
-our $access_pushing = 0;
+our $access_forpush;
+
+sub parse_cfg_bool ($$$) {
+ my ($what,$def,$v) = @_;
+ $v //= $def;
+ return
+ $v =~ m/^[ty1]/ ? 1 :
+ $v =~ m/^[fn0]/ ? 0 :
+ badcfg "$what needs t (true, y, 1) or f (false, n, 0) not \`$v'";
+}
+
+sub access_forpush_config () {
+ my $d = access_basedistro();
+
+ return 1 if
+ $new_package &&
+ parse_cfg_bool('new-private-pushers', 0,
+ cfg("dgit-distro.$d.new-private-pushers",
+ 'RETURN-UNDEF'));
+
+ my $v = cfg("dgit-distro.$d.readonly", 'RETURN-UNDEF');
+ $v //= 'a';
+ return
+ $v =~ m/^[ty1]/ ? 0 : # force readonly, forpush = 0
+ $v =~ m/^[fn0]/ ? 1 : # force nonreadonly, forpush = 1
+ $v =~ m/^[a]/ ? '' : # auto, forpush = ''
+ badcfg "readonly needs t (true, y, 1) or f (false, n, 0) or a (auto)";
+}
+
+sub access_forpush () {
+ $access_forpush //= access_forpush_config();
+ return $access_forpush;
+}
sub pushing () {
- $access_pushing = 1;
+ die "$access_forpush ?" if ($access_forpush // 1) ne 1;
+ badcfg "pushing but distro is configured readonly"
+ if access_forpush_config() eq '0';
+ $access_forpush = 1;
+ $supplementary_message = <<'END' unless $we_are_responder;
+Push failed, before we got started.
+You can retry the push, after fixing the problem, if you like.
+END
+ finalise_opts_opts();
+}
+
+sub notpushing () {
+ finalise_opts_opts();
+}
+
+sub supplementary_message ($) {
+ my ($msg) = @_;
+ if (!$we_are_responder) {
+ $supplementary_message = $msg;
+ return;
+ } elsif ($protovsn >= 3) {
+ responder_send_command "supplementary-message ".length($msg)
+ or die $!;
+ print PO $msg or die $!;
+ }
}
sub access_distros () {
unshift @l, $instead_distro;
@l = grep { defined } @l;
- if ($access_pushing) {
+ if (access_forpush()) {
@l = map { ("$_/push", $_) } @l;
}
@l;
}
-sub access_cfg (@) {
+sub access_cfg_cfgs (@) {
my (@keys) = @_;
my @cfgs;
# The nesting of these loops determines the search order. We put
}
push @cfgs, map { "dgit.default.$_" } @realkeys;
push @cfgs, @rundef;
+ return @cfgs;
+}
+
+sub access_cfg (@) {
+ my (@keys) = @_;
+ my (@cfgs) = access_cfg_cfgs(@keys);
my $value = cfg(@cfgs);
return $value;
}
+sub access_cfg_bool ($$) {
+ my ($def, @keys) = @_;
+ parse_cfg_bool($keys[0], $def, access_cfg(@keys, 'RETURN-UNDEF'));
+}
+
sub string_to_ssh ($) {
my ($spec) = @_;
if ($spec =~ m/\s/) {
my ($optional) = @_;
my $url = access_cfg('git-url','RETURN-UNDEF');
my $suffix;
- if (!defined $url) {
+ if (!length $url) {
my $proto = access_cfg('git-proto', 'RETURN-UNDEF');
return undef unless defined $proto;
$url =
open P, "-|", @cmd or die $!;
while (<P>) {
chomp or die;
- printdebug("$debugprefix>|$_|\n");
+ printdebug(">|$_|\n");
push @rows, $_;
}
$!=0; $?=0; close P or failedcmd @cmd;
my $url = "$prefix/$package$suffix";
my @cmd = (qw(curl -sS -I), $url);
my $result = cmdoutput @cmd;
+ $result =~ s/^\S+ 200 .*\n\r?\n//;
+ # curl -sS -I with https_proxy prints
+ # HTTP/1.0 200 Connection established
$result =~ m/^\S+ (404|200) /s or
fail "unexpected results from git check query - ".
Dumper($prefix, $result);
return $tree;
}
+sub remove_stray_gits () {
+ my @gitscmd = qw(find -name .git -prune -print0);
+ debugcmd "|",@gitscmd;
+ open GITS, "-|", @gitscmd or failedcmd @gitscmd;
+ {
+ local $/="\0";
+ while (<GITS>) {
+ chomp or die;
+ print STDERR "$us: warning: removing from source package: ",
+ (messagequote $_), "\n";
+ rmtree $_;
+ }
+ }
+ $!=0; $?=0; close GITS or failedcmd @gitscmd;
+}
+
sub mktree_in_ud_from_only_subdir () {
# changes into the subdir
my (@dirs) = <*/.>;
$dirs[0] =~ m#^([^/]+)/\.$# or die;
my $dir = $1;
changedir $dir;
- fail "source package contains .git directory" if stat_exists '.git';
+
+ remove_stray_gits();
mktree_in_ud_here();
my $format=get_source_format();
if (madformat($format)) {
runcmd @git, qw(config), $k, $v;
}
-sub setup_mergechangelogs () {
+sub setup_mergechangelogs (;$) {
+ my ($always) = @_;
+ return unless $always || access_cfg_bool(1, 'setup-mergechangelogs');
+
my $driver = 'dpkg-mergechangelogs';
my $cb = "merge.$driver";
my $attrs = '.git/info/attributes';
rename "$attrs.new", "$attrs" or die "$attrs: $!";
}
+sub setup_useremail (;$) {
+ my ($always) = @_;
+ return unless $always || access_cfg_bool(1, 'setup-useremail');
+
+ my $setup = sub {
+ my ($k, $envvar) = @_;
+ my $v = access_cfg("user-$k", 'RETURN-UNDEF') // $ENV{$envvar};
+ return unless defined $v;
+ set_local_git_config "user.$k", $v;
+ };
+
+ $setup->('email', 'DEBEMAIL');
+ $setup->('name', 'DEBFULLNAME');
+}
+
+sub setup_new_tree () {
+ setup_mergechangelogs();
+ setup_useremail();
+}
+
sub clone ($) {
my ($dstdir) = @_;
canonicalise_suite();
$vcsgiturl =~ s/\s+-b\s+\S+//g;
runcmd @git, qw(remote add vcs-git), $vcsgiturl;
}
- setup_mergechangelogs();
+ setup_new_tree();
runcmd @git, qw(reset --hard), lrref();
printdone "ready for work in $dstdir";
}
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;
sub dopush ($) {
my ($forceflag) = @_;
printdebug "actually entering push\n";
+ supplementary_message(<<'END');
+Push failed, while preparing your push.
+You can retry the push, after fixing the problem, if you like.
+END
prep_ud();
access_giturl(); # check that success is vaguely likely
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 = git_rev_parse('HEAD');
if (!$changesfile) {
my $multi = "$buildproductsdir/".
my $tfn = sub { ".git/dgit/tag$_[0]"; };
my $tagobjfn;
+ supplementary_message(<<'END');
+Push failed, while signing the tag.
+You can retry the push, after fixing the problem, if you like.
+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);
$changesfile,$changesfile,
$tfn);
}
+ 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;
+ supplementary_message(<<'END');
+Push failed, while updating the remote git repository - see messages above.
+If you want to try again, you should use a new version number.
+END
if (!check_for_git()) {
create_remote_git_repo();
}
$forceflag."HEAD:".rrref(), $forceflag."refs/tags/$tag";
runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), 'HEAD';
+ supplementary_message(<<'END');
+Push failed, after updating the remote git repository.
+If you want to try again, you must use a new version number.
+END
if ($we_are_responder) {
my $dryrunsuffix = act_local() ? "" : ".tmp";
responder_receive_files('signed-dsc-changes',
sign_changes $changesfile;
}
+ supplementary_message(<<'END');
+Push failed, while uploading package(s) to the archive server.
+You can retry the upload of exactly these same files with dput of:
+ $changesfile
+If that .changes file is broken, you will need to use a new version
+number for your next attempt at the upload.
+END
my $host = access_cfg('upload-host','RETURN-UNDEF');
my @hostarg = defined($host) ? ($host,) : ();
runcmd_ordryrun @dput, @hostarg, $changesfile;
printdone "pushed and uploaded $cversion";
+ supplementary_message('');
responder_send_command("complete");
}
sub cmd_clone {
parseopts();
+ notpushing();
my $dstdir;
badusage "-p is not allowed with clone; specify as argument instead"
if defined $package;
}
sub fetchpullargs () {
+ notpushing();
if (!defined $package) {
my $sourcep = parsecontrol('debian/control','debian/control');
$package = getfield $sourcep, 'Source';
}
sub cmd_push {
- pushing();
parseopts();
+ pushing();
badusage "-p is not allowed with dgit push" if defined $package;
check_not_dirty();
my $clogp = parsechangelog();
fail "dgit push: changelog specifies $isuite ($csuite)".
" but command line specifies $specsuite";
}
+ supplementary_message(<<'END');
+Push failed, while checking state of the archive.
+You can retry the push, after fixing the problem, if you like.
+END
if (check_for_git()) {
git_fetch_us();
}
#---------- remote commands' implementation ----------
sub cmd_remote_push_build_host {
- pushing();
my ($nrargs) = shift @ARGV;
my (@rargs) = @ARGV[0..$nrargs-1];
@ARGV = @ARGV[$nrargs..$#ARGV];
$we_are_responder = 1;
$us .= " (build host)";
+ pushing();
+
open PI, "<&STDIN" or die $!;
open STDIN, "/dev/null" or die $!;
open PO, ">&STDOUT" 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;
+ ($protovsn) = grep {
+ $vsnwant =~ m{^(?:.*,)?$_(?:,.*)?$}
+ } @rpushprotovsn_support;
+
+ fail "build host has dgit rpush protocol versions ".
+ (join ",", @rpushprotovsn_support).
+ " but invocation host has $vsnwant"
+ unless defined $protovsn;
- responder_send_command("dgit-remote-push-ready $rpushprotovsn");
+ responder_send_command("dgit-remote-push-ready $protovsn");
changedir $dir;
&cmd_push;
$dir = nextarg;
}
$dir =~ s{^-}{./-};
- my @rargs = ($dir,$rpushprotovsn);
+ my @rargs = ($dir);
+ push @rargs, join ",", @rpushprotovsn_support;
my @rdgit;
push @rdgit, @dgit;
push @rdgit, @ropts;
}
$i_child_pid = open2(\*RO, \*RI, @cmd);
changedir $i_tmp;
- initiator_expect { m/^dgit-remote-push-ready/ };
+ ($protovsn) = initiator_expect { m/^dgit-remote-push-ready (\S+)/ };
+ die "$protovsn ?" unless grep { $_ eq $protovsn } @rpushprotovsn_support;
+ $supplementary_message = '' unless $protovsn >= 3;
for (;;) {
my ($icmd,$iargs) = initiator_expect {
m/^(\S+)(?: (.*))?$/;
progress $msg;
}
+sub i_resp_supplementary_message ($) {
+ my ($rhs) = @_;
+ $supplementary_message = protocol_read_bytes \*RO, $rhs;
+}
+
sub i_resp_complete {
my $pid = $i_child_pid;
$i_child_pid = undef; # prevents killing some other process with same pid
# should be contained within debian/patches.
changedir '../fake';
+ remove_stray_gits();
mktree_in_ud_here();
rmtree '.pc';
runcmd @git, 'add', '.';
sub cmd_clean () {
badusage "clean takes no additional arguments" if @ARGV;
+ notpushing();
clean_tree();
}
sub build_prep () {
+ notpushing();
badusage "-p is not allowed when building" if defined $package;
check_not_dirty();
clean_tree();
sub cmd_setup_mergechangelogs {
badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
- setup_mergechangelogs();
+ setup_mergechangelogs(1);
+}
+
+sub cmd_setup_useremail {
+ badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
+ setup_useremail(1);
+}
+
+sub cmd_setup_new_tree {
+ badusage "no arguments allowed to dgit setup-tree" if @ARGV;
+ setup_new_tree();
}
#---------- argument parsing and main program ----------
} elsif (m/^--since-version=([^_]+|_)$/) {
push @ropts, $_;
$changes_since_version = $1;
- } elsif (m/^--([-0-9a-z]+)=(.*)/s &&
+ } elsif (m/^--([-0-9a-z]+)=(.+)/s &&
($om = $opts_opt_map{$1}) &&
length $om->[0]) {
push @ropts, $_;
}
}
+sub finalise_opts_opts () {
+ foreach my $k (keys %opts_opt_map) {
+ my $om = $opts_opt_map{$k};
+
+ my $v = access_cfg("cmd-$k", 'RETURN-UNDEF');
+ if (defined $v) {
+ badcfg "cannot set command for $k"
+ unless length $om->[0];
+ $om->[0] = $v;
+ }
+
+ foreach my $c (access_cfg_cfgs("opts-$k")) {
+ my $vl = $gitcfg{$c};
+ printdebug "CL $c ",
+ ($vl ? join " ", map { shellquote } @$vl : ""),
+ "\n" if $debuglevel >= 4;
+ next unless $vl;
+ badcfg "cannot configure options for $k"
+ if $opts_opt_cmdonly{$k};
+ my $insertpos = $opts_cfg_insertpos{$k};
+ @$om = ( @$om[0..$insertpos-1],
+ @$vl,
+ @$om[$insertpos..$#$om] );
+ }
+ }
+}
+
if ($ENV{$fakeeditorenv}) {
+ git_slurp_config();
quilt_fixup_editor();
}
parseopts();
+git_slurp_config();
+
print STDERR "DRY RUN ONLY\n" if $dryrun_level > 1;
print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n"
if $dryrun_level == 1;
$cmd =~ y/-/_/;
if (!defined $quilt_mode) {
+ local $access_forpush;
$quilt_mode = cfg('dgit.force.quilt-mode', 'RETURN-UNDEF')
// access_cfg('quilt-mode', 'RETURN-UNDEF')
// 'linear';