X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?p=dgit.git;a=blobdiff_plain;f=dgit;h=38c1497f808cd9ec990a02a4b230ccad74c643df;hp=76a073fce893ac3d3283483d51a7801a98083991;hb=7121a993cd2206d3f5066e97ebbc397fe079c18f;hpb=4c06b83c1207a4a5d92575ef5117be6f2fab6104 diff --git a/dgit b/dgit index 76a073fc..38c1497f 100755 --- a/dgit +++ b/dgit @@ -18,7 +18,9 @@ # along with this program. If not, see . use strict; -$SIG{__WARN__} = sub { die $_[0]; }; + +use Debian::Dgit; +setup_sigwarn(); use IO::Handle; use Data::Dumper; @@ -37,7 +39,8 @@ use Debian::Dgit; our $our_version = 'UNRELEASED'; ###substituted### -our $rpushprotovsn = 2; +our @rpushprotovsn_support = qw(3 2); +our $protovsn; our $isuite = 'unstable'; our $idistro; @@ -54,8 +57,9 @@ our $rmonerror = 1; our @deliberatelies; our %previously; our $existing_package = 'dpkg'; -our $cleanmode = 'dpkg-source'; +our $cleanmode; our $changes_since_version; +our $rmchanges; our $quilt_mode; our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck'; our $we_are_responder; @@ -64,6 +68,7 @@ our $initiator_tempdir; our %format_ok = map { $_=>1 } ("1.0","3.0 (native)","3.0 (quilt)"); our $suite_re = '[-+.0-9a-z]+'; +our $cleanmode_re = 'dpkg-source(?:-d)?|git|git-ff|check|none'; our (@git) = qw(git); our (@dget) = qw(dget); @@ -71,7 +76,7 @@ our (@curl) = qw(curl -f); our (@dput) = qw(dput); our (@debsign) = qw(debsign); our (@gpg) = qw(gpg); -our (@sbuild) = qw(sbuild -A); +our (@sbuild) = qw(sbuild); our (@ssh) = 'ssh'; our (@dgit) = qw(dgit); our (@dpkgbuildpackage) = qw(dpkg-buildpackage -i\.git/ -I.git); @@ -88,18 +93,32 @@ our %opts_opt_map = ('dget' => \@dget, # accept for compatibility '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; @@ -129,6 +148,11 @@ sub dscfn ($) { return srcfn($vsn,".dsc"); } +sub changespat ($;$) { + my ($vsn, $arch) = @_; + return "${package}_".(stripepoch $vsn)."_".($arch//'*').".changes"; +} + our $us = 'dgit'; initdebug(''); @@ -137,7 +161,7 @@ END { local ($?); foreach my $f (@end) { eval { $f->(); }; - warn "$us: cleanup: $@" if length $@; + print STDERR "$us: cleanup: $@" if length $@; } }; @@ -173,7 +197,9 @@ sub deliberately_not_fast_forward () { #---------- 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 ... ... +# where is ,... ... +# < dgit-remote-push-ready # # > file parsed-changelog # [indicates that output of dpkg-parsechangelog follows] @@ -268,7 +294,7 @@ sub protocol_send_file ($$) { 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"; @@ -404,7 +430,8 @@ our $helpmsg = < 'debian', 'dgit.default.archive-query' => 'madison:', 'dgit.default.sshpsql-dbname' => 'service=projectb', 'dgit-distro.debian.archive-query' => 'ftpmasterapi:', - 'dgit-distro.debian.git-host' => 'dgit-git.debian.net', - 'dgit-distro.debian.git-user-force' => 'dgit', - 'dgit-distro.debian.git-proto' => 'git+ssh://', - 'dgit-distro.debian.git-path' => '/dgit/debian/repos', - 'dgit-distro.debian.git-create' => 'true', - 'dgit-distro.debian.git-check' => 'ssh-cmd', + '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' => '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/push.git-create' => 'true', + 'dgit-distro.debian/push.git-check' => 'ssh-cmd', 'dgit-distro.debian.archive-query-url', 'https://api.ftp-master.debian.org/', # 'dgit-distro.debian.archive-query-tls-key', # '/etc/ssl/certs/%HOST%.pem:/etc/dgit/%HOST%.pem', @@ -460,13 +491,8 @@ our %defcfg = ('dgit.default.distro' => 'debian', # '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/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.git-url' => 'https://git.dgit.debian.org', + 'dgit-distro.debian.git-url-suffix' => '', '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*', @@ -486,28 +512,36 @@ our %defcfg = ('dgit.default.distro' => 'debian', '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 () { + 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 { @@ -549,10 +583,66 @@ sub access_quirk () { 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 () { @@ -570,13 +660,13 @@ 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 @@ -603,10 +693,21 @@ sub access_cfg (@) { } 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/) { @@ -646,15 +747,19 @@ sub access_gituserhost () { sub access_giturl (;$) { my ($optional) = @_; my $url = access_cfg('git-url','RETURN-UNDEF'); - if (!defined $url) { + my $suffix; + if (!length $url) { my $proto = access_cfg('git-proto', 'RETURN-UNDEF'); return undef unless defined $proto; $url = $proto. access_gituserhost(). access_cfg('git-path'); + } else { + $suffix = access_cfg('git-url-suffix','RETURN-UNDEF'); } - return "$url/$package.git"; + $suffix //= '.git'; + return "$url/$package$suffix"; } sub parsecontrolfh ($$;$) { @@ -893,7 +998,7 @@ sub sshpsql ($$$) { open P, "-|", @cmd or die $!; while (

) { chomp or die; - printdebug("$debugprefix>|$_|\n"); + printdebug(">|$_|\n"); push @rows, $_; } $!=0; $?=0; close P or failedcmd @cmd; @@ -1054,6 +1159,27 @@ sub check_for_git () { } failedcmd @cmd unless $r =~ m/^[01]$/; return $r+0; + } elsif ($how eq 'url') { + my $prefix = access_cfg('git-check-url','git-url'); + my $suffix = access_cfg('git-check-suffix','git-suffix', + 'RETURN-UNDEF') // '.git'; + 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); + my $code = $1; + if ($code eq '404') { + return 0; + } elsif ($code eq '200') { + return 1; + } else { + die; + } } elsif ($how eq 'true') { return 1; } elsif ($how eq 'false') { @@ -1100,6 +1226,22 @@ sub git_write_tree () { 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 () { + 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) = <*/.>; @@ -1107,9 +1249,10 @@ 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_exists '.git'; + + remove_stray_gits(); mktree_in_ud_here(); - my $format=get_source_format(); + my ($format, $fopts) = get_source_format(); if (madformat($format)) { rmtree '.pc'; } @@ -1245,11 +1388,12 @@ sub generate_commit_from_dsc () { my $f = $fi->{Filename}; die "$f ?" if $f =~ m#/|^\.|\.dsc$|\.tmp$#; - link "../../../$f", $f + link_ltarget "../../../$f", $f or $!==&ENOENT or die "$f $!"; - complete_file_from_dsc('.', $fi); + complete_file_from_dsc('.', $fi) + or next; if (is_orig_file($f)) { link $f, "../../../../$f" @@ -1344,10 +1488,10 @@ sub complete_file_from_dsc ($$) { my $furl = $dscurl; $furl =~ s{/[^/]+$}{}; $furl .= "/$f"; - die "$f ?" unless $f =~ m/^${package}_/; + die "$f ?" unless $f =~ m/^\Q${package}\E_/; die "$f ?" if $f =~ m#/#; runcmd_ordryrun_local @curl,qw(-o),$tf,'--',"$furl"; - next if !act_local(); + return 0 if !act_local(); $downloaded = 1; } @@ -1361,13 +1505,16 @@ sub complete_file_from_dsc ($$) { " demands hash $fi->{Hash} ". ($downloaded ? "(got wrong file from archive!)" : "(perhaps you should delete this file?)"); + + return 1; } 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); + complete_file_from_dsc('..', $fi) + or next; } } @@ -1510,7 +1657,10 @@ sub set_local_git_config ($$) { 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'; @@ -1537,12 +1687,32 @@ sub setup_mergechangelogs () { 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(); badusage "dry run makes no sense with clone" unless act_local(); my $hasgit = check_for_git(); - mkdir $dstdir or die "$dstdir $!"; + mkdir $dstdir or fail "create \`$dstdir': $!"; changedir $dstdir; runcmd @git, qw(init -q); my $giturl = access_giturl(1); @@ -1566,7 +1736,7 @@ sub clone ($) { $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"; } @@ -1587,7 +1757,14 @@ sub pull () { } sub check_not_dirty () { + foreach my $f (qw(local-options local-patch-header)) { + if (stat_exists "debian/source/$f") { + fail "git tree contains debian/source/$f"; + } + } + return if $ignoredirty; + my @cmd = (@git, qw(diff --quiet HEAD)); debugcmd "+",@cmd; $!=0; $?=0; system @cmd; @@ -1619,11 +1796,32 @@ sub commit_quilty_patch () { progress "nothing quilty to commit, ok."; return; } - runcmd_ordryrun_local @git, qw(add), sort keys %adds; + my @adds = map { s/[][*?\\]/\\$&/g; $_; } sort keys %adds; + runcmd_ordryrun_local @git, qw(add -f), @adds; commit_admin "Commit Debian 3.0 (quilt) metadata"; } sub get_source_format () { + my %options; + if (open F, "debian/source/options") { + while () { + next if m/^\s*\#/; + next unless m/\S/; + s/\s+$//; # ignore missing final newline + if (m/\s*\#\s*/) { + my ($k, $v) = ($`, $'); #'); + $v =~ s/^"(.*)"$/$1/; + $options{$k} = $v; + } else { + $options{$_} = 1; + } + } + F->error and die $!; + close F; + } else { + die $! unless $!==&ENOENT; + } + if (!open F, "debian/source/format") { die $! unless $!==&ENOENT; return ''; @@ -1631,7 +1829,7 @@ sub get_source_format () { $_ = ; F->error and die $!; chomp; - return $_; + return ($_, \%options); } sub madformat ($) { @@ -1718,6 +1916,9 @@ END 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; @@ -1747,6 +1948,10 @@ sub sign_changes ($) { 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 @@ -1796,20 +2001,13 @@ sub dopush ($) { 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/". - "${package}_".(stripepoch $cversion)."_multi.changes"; + my $multi = "$buildproductsdir/".changespat $cversion,'multi'; if (stat_exists "$multi") { $changesfile = $multi; } else { - my $pat = "${package}_".(stripepoch $cversion)."_*.changes"; + my $pat = changespat $cversion; my @cs = glob "$buildproductsdir/$pat"; fail "failed to find unique changes file". " (looked for $pat in $buildproductsdir, or $multi);". @@ -1837,6 +2035,11 @@ sub dopush ($) { 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); @@ -1847,11 +2050,19 @@ sub dopush ($) { $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(); } @@ -1859,6 +2070,10 @@ sub dopush ($) { $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', @@ -1873,16 +2088,25 @@ sub dopush ($) { sign_changes $changesfile; } + supplementary_message(<&STDOUT" or die $!; @@ -2038,11 +2274,16 @@ sub cmd_remote_push_build_host { 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; @@ -2088,7 +2329,8 @@ sub cmd_rpush { $dir = nextarg; } $dir =~ s{^-}{./-}; - my @rargs = ($dir,$rpushprotovsn); + my @rargs = ($dir); + push @rargs, join ",", @rpushprotovsn_support; my @rdgit; push @rdgit, @dgit; push @rdgit, @ropts; @@ -2106,7 +2348,9 @@ sub cmd_rpush { } $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+)(?: (.*))?$/; @@ -2122,6 +2366,11 @@ sub i_resp_progress ($) { 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 @@ -2258,7 +2507,7 @@ END 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; + runcmd @dpkgsource, qw(--commit .), $patchname; } } @@ -2312,9 +2561,10 @@ sub quiltify ($$) { # should be contained within debian/patches. changedir '../fake'; + remove_stray_gits(); mktree_in_ud_here(); rmtree '.pc'; - runcmd @git, 'add', '.'; + runcmd @git, qw(add -Af .); my $oldtiptree=git_write_tree(); changedir '../work'; @@ -2415,7 +2665,7 @@ sub quiltify ($$) { my $abbrev = sub { my $x = $_[0]{Commit}; $x =~ s/(.*?[0-9a-z]{8})[0-9a-z]*$/$1/; - return $; + return $x; }; my $reportnot = sub { my ($notp) = @_; @@ -2502,12 +2752,95 @@ sub quiltify ($$) { } sub build_maybe_quilt_fixup () { - my $format=get_source_format; + my ($format,$fopts) = get_source_format; return unless madformat $format; # sigh check_for_vendor_patches(); + my $clogp = parsechangelog(); + my $headref = git_rev_parse('HEAD'); + + prep_ud(); + changedir $ud; + + my $upstreamversion=$version; + $upstreamversion =~ s/-[^-]*$//; + + if ($fopts->{'single-debian-patch'}) { + quilt_fixup_singlepatch($clogp, $headref, $upstreamversion); + } else { + quilt_fixup_multipatch($clogp, $headref, $upstreamversion); + } + + changedir '../../../..'; + runcmd_ordryrun_local + @git, qw(pull --ff-only -q .git/dgit/unpack/work master); +} + +sub quilt_fixup_mkwork ($) { + my ($headref) = @_; + + mkdir "work" or die $!; + changedir "work"; + mktree_in_ud_here(); + runcmd @git, qw(reset --hard), $headref; +} + +sub quilt_fixup_linkorigs ($$) { + my ($upstreamversion, $fn) = @_; + # calls $fn->($leafname); + + foreach my $f (<../../../../*>) { #/){ + my $b=$f; $b =~ s{.*/}{}; + { + local ($debuglevel) = $debuglevel-1; + printdebug "QF linkorigs $b, $f ?\n"; + } + next unless is_orig_file $b, srcfn $upstreamversion,''; + printdebug "QF linkorigs $b, $f Y\n"; + link_ltarget $f, $b or die "$b $!"; + $fn->($b); + } +} + +sub quilt_fixup_delete_pc () { + runcmd @git, qw(rm -rqf .pc); + commit_admin "Commit removal of .pc (quilt series tracking data)"; +} + +sub quilt_fixup_singlepatch ($$$) { + my ($clogp, $headref, $upstreamversion) = @_; + + progress "starting quiltify (single-debian-patch)"; + + # dpkg-source --commit generates new patches even if + # single-debian-patch is in debian/source/options. In order to + # get it to generate debian/patches/debian-changes, it is + # necessary to build the source package. + + quilt_fixup_linkorigs($upstreamversion, sub { }); + quilt_fixup_mkwork($headref); + + rmtree("debian/patches"); + + runcmd @dpkgsource, qw(-b .); + chdir ".."; + runcmd @dpkgsource, qw(-x), (srcfn $version, ".dsc"); + rename srcfn("$upstreamversion", "/debian/patches"), + "work/debian/patches"; + + chdir "work"; + commit_quilty_patch(); + + +} + +sub quilt_fixup_multipatch ($$$) { + my ($clogp, $headref, $upstreamversion) = @_; + + progress "starting quiltify (multiple patches, $quilt_mode mode)"; + # Our objective is: # - honour any existing .pc in case it has any strangeness # - determine the git commit corresponding to the tip of @@ -2531,7 +2864,7 @@ sub build_maybe_quilt_fixup () { # 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} + # debian/{patches,rules,source/format,source/options} # 3. Generate a fake .dsc containing just these fields: # Format Source Version Files # 4. Extract the fake .dsc @@ -2552,15 +2885,6 @@ sub build_maybe_quilt_fixup () { # 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 = git_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 $!; @@ -2584,16 +2908,12 @@ END 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); - } + quilt_fixup_linkorigs($upstreamversion, $dscaddfile); my @files=qw(debian/source/format debian/rules); - if (stat_exists '../../../debian/patches') { - push @files, 'debian/patches'; + foreach my $maybe (qw(debian/patches debian/source/options)) { + next unless stat_exists "../../../$maybe"; + push @files, $maybe; } my $debtar= srcfn $fakeversion,'.debian.tar.gz'; @@ -2607,10 +2927,7 @@ END 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; + quilt_fixup_mkwork($headref); my $mustdeletepc=0; if (stat_exists ".pc") { @@ -2632,12 +2949,8 @@ END commit_quilty_patch(); if ($mustdeletepc) { - runcmd @git, qw(rm -rqf .pc); - commit_admin "Commit removal of .pc (quilt series tracking data)"; + quilt_fixup_delete_pc(); } - - changedir '../../../..'; - runcmd @git, qw(pull --ff-only -q .git/dgit/unpack/work master); } sub quilt_fixup_editor () { @@ -2661,7 +2974,10 @@ sub quilt_fixup_editor () { #----- other building ----- +our $suppress_clean; + sub clean_tree () { + return if $suppress_clean; if ($cleanmode eq 'dpkg-source') { runcmd_ordryrun_local @dpkgbuildpackage, qw(-T clean); } elsif ($cleanmode eq 'dpkg-source-d') { @@ -2684,10 +3000,12 @@ sub clean_tree () { 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(); @@ -2696,10 +3014,23 @@ sub build_prep () { $package = getfield $clogp, 'Source'; $version = getfield $clogp, 'Version'; build_maybe_quilt_fixup(); + if ($rmchanges) { + my $pat = changespat $version; + foreach my $f (glob "$buildproductsdir/$pat") { + if (act_local()) { + unlink $f or fail "remove old changes file $f: $!"; + } else { + progress "would remove $f"; + } + } + } } -sub changesopts () { +sub changesopts_initial () { my @opts =@changesopts[1..$#changesopts]; +} + +sub changesopts_version () { if (!defined $changes_since_version) { my @vsns = archive_query('archive_query'); my @quirk = access_quirk(); @@ -2720,40 +3051,60 @@ sub changesopts () { } } if ($changes_since_version ne '_') { - unshift @opts, "-v$changes_since_version"; + return ("-v$changes_since_version"); + } else { + return (); } - return @opts; } -sub massage_dbp_args ($) { - my ($cmd) = @_; - return unless $cleanmode =~ m/git|none/; +sub changesopts () { + return (changesopts_initial(), changesopts_version()); +} + +sub massage_dbp_args ($;$) { + my ($cmd,$xargs) = @_; + if ($cleanmode eq 'dpkg-source') { + $suppress_clean = 1; + return; + } debugcmd '#massaging#', @$cmd if $debuglevel>1; my @newcmd = shift @$cmd; # -nc has the side effect of specifying -b if nothing else specified push @newcmd, '-nc'; # and some combinations of -S, -b, et al, are errors, rather than # later simply overriding earlier - push @newcmd, '-F' unless grep { m/^-[bBASF]$/ } @$cmd; + push @newcmd, '-F' unless grep { m/^-[bBASFgG]$/ } (@$cmd, @$xargs); push @newcmd, @$cmd; @$cmd = @newcmd; } sub cmd_build { - build_prep(); - my @dbp = (@dpkgbuildpackage, qw(-us -uc), changesopts(), @ARGV); + my @dbp = (@dpkgbuildpackage, qw(-us -uc), changesopts_initial(), @ARGV); massage_dbp_args \@dbp; + build_prep(); + push @dbp, changesopts_version(); runcmd_ordryrun_local @dbp; printdone "build successful\n"; } -sub cmd_git_build { - build_prep(); +sub cmd_gbp_build { my @dbp = @dpkgbuildpackage; - massage_dbp_args \@dbp; - my @cmd = - (qw(git-buildpackage -us -uc --git-no-sign-tags), - "--git-builder=@dbp"); + massage_dbp_args \@dbp, \@ARGV; + + my @cmd; + if (length executable_on_path('git-buildpackage')) { + @cmd = qw(git-buildpackage); + } else { + @cmd = qw(gbp buildpackage); + } + push @cmd, (qw(-us -uc --git-no-sign-tags), "--git-builder=@dbp"); + + if ($cleanmode eq 'dpkg-source') { + $suppress_clean = 1; + } else { + push @cmd, '--git-cleaner=true'; + } + build_prep(); unless (grep { m/^--git-debian-branch|^--git-ignore-branch/ } @ARGV) { canonicalise_suite(); push @cmd, "--git-debian-branch=".lbranch(); @@ -2762,17 +3113,22 @@ sub cmd_git_build { runcmd_ordryrun_local @cmd, @ARGV; printdone "build successful\n"; } +sub cmd_git_build { cmd_gbp_build(); } # compatibility with <= 1.0 sub build_source { + if ($cleanmode =~ m/^dpkg-source/) { + # dpkg-source will clean, so we shouldn't + $suppress_clean = 1; + } build_prep(); - $sourcechanges = "${package}_".(stripepoch $version)."_source.changes"; + $sourcechanges = changespat $version,'source'; $dscfn = dscfn($version); if ($cleanmode eq 'dpkg-source') { - runcmd_ordryrun_local (@dpkgbuildpackage, qw(-us -uc -S)), - changesopts(); + runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc -S), + changesopts(); } elsif ($cleanmode eq 'dpkg-source-d') { - runcmd_ordryrun_local (@dpkgbuildpackage, qw(-us -uc -S -d)), - changesopts(); + runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc -S -d), + changesopts(); } else { my $pwd = must_getcwd(); my $leafdir = basename $pwd; @@ -2795,7 +3151,7 @@ sub cmd_build_source { sub cmd_sbuild { build_source(); changedir ".."; - my $pat = "${package}_".(stripepoch $version)."_*.changes"; + my $pat = changespat $version; if (act_local()) { stat_exists $dscfn or fail "$dscfn (in parent directory): $!"; stat_exists $sourcechanges @@ -2805,18 +3161,27 @@ sub cmd_sbuild { unlink $cf or fail "remove $cf: $!"; } } - runcmd_ordryrun_local @sbuild, @ARGV, qw(-d), $isuite, $dscfn; + runcmd_ordryrun_local @sbuild, qw(-d), $isuite, @ARGV, $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; + unless @changesfiles==2; + my $binchanges = parsecontrol($changesfiles[1], "binary changes file"); + foreach my $l (split /\n/, getfield $binchanges, 'Files') { + fail "$l found in binaries changes file $binchanges" + if $l =~ m/\.dsc$/; + } runcmd_ordryrun_local @mergechanges, @changesfiles; - my $multichanges = "${package}_".(stripepoch $version)."_multi.changes"; + my $multichanges = changespat $version,'multi'; if (act_local()) { stat_exists $multichanges or fail "$multichanges: $!"; + foreach my $cf (glob $pat) { + next if $cf eq $multichanges; + rename "$cf", "$cf.inmulti" or fail "$cf\{,.inmulti}: $!"; + } } printdone "build successful, results in $multichanges\n" or die $!; } @@ -2826,6 +3191,8 @@ sub cmd_quilt_fixup { my $clogp = parsechangelog(); $version = getfield $clogp, 'Version'; $package = getfield $clogp, 'Source'; + check_not_dirty(); + clean_tree(); build_maybe_quilt_fixup(); } @@ -2848,7 +3215,17 @@ sub cmd_clone_dgit_repos_server { 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 ---------- @@ -2858,6 +3235,44 @@ sub cmd_version { exit 0; } +our (%valopts_long, %valopts_short); +our @rvalopts; + +sub defvalopt ($$$$) { + my ($long,$short,$val_re,$how) = @_; + my $oi = { Long => $long, Short => $short, Re => $val_re, How => $how }; + $valopts_long{$long} = $oi; + $valopts_short{$short} = $oi; + # $how subref should: + # do whatever assignemnt or thing it likes with $_[0] + # if the option should not be passed on to remote, @rvalopts=() + # or $how can be a scalar ref, meaning simply assign the value +} + +defvalopt '--since-version', '-v', '[^_]+|_', \$changes_since_version; +defvalopt '--distro', '-d', '.+', \$idistro; +defvalopt '', '-k', '.+', \$keyid; +defvalopt '--existing-package','', '.*', \$existing_package; +defvalopt '--build-products-dir','','.*', \$buildproductsdir; +defvalopt '--clean', '', $cleanmode_re, \$cleanmode; +defvalopt '--quilt', '', $quilt_modes_re, \$quilt_mode; + +defvalopt '', '-c', '.*=.*', sub { push @git, '-c', @_; }; + +defvalopt '', '-C', '.+', sub { + ($changesfile) = (@_); + if ($changesfile =~ s#^(.*)/##) { + $buildproductsdir = $1; + } +}; + +defvalopt '--initiator-tempdir','','.*', sub { + ($initiator_tempdir) = (@_); + $initiator_tempdir =~ m#^/# or + badusage "--initiator-tempdir must be used specify an". + " absolute, not relative, directory." +}; + sub parseopts () { my $om; @@ -2867,6 +3282,27 @@ sub parseopts () { @ssh = ($ENV{'GIT_SSH'}); } + my $oi; + my $val; + my $valopt = sub { + my ($what) = @_; + @rvalopts = ($_); + if (!defined $val) { + badusage "$what needs a value" unless @ARGV; + $val = shift @ARGV; + push @rvalopts, $val; + } + badusage "bad value \`$val' for $what" unless + $val =~ m/^$oi->{Re}$(?!\n)/s; + my $how = $oi->{How}; + if (ref($how) eq 'SCALAR') { + $$how = $val; + } else { + $how->($val); + } + push @ropts, @rvalopts; + }; + while (@ARGV) { last unless $ARGV[0] =~ m/^-/; $_ = shift @ARGV; @@ -2888,10 +3324,7 @@ sub parseopts () { } elsif (m/^--new$/) { push @ropts, $_; $new_package=1; - } 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, $_; @@ -2901,30 +3334,6 @@ sub parseopts () { ($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(?:-d)?|git|git-ff|check|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; @@ -2934,9 +3343,15 @@ sub parseopts () { } elsif (m/^--no-rm-on-error$/s) { push @ropts, $_; $rmonerror = 0; + } elsif (m/^--(no-)?rm-old-changes$/s) { + push @ropts, $_; + $rmchanges = !$1; } elsif (m/^--deliberately-($deliberately_re)$/s) { push @ropts, $_; push @deliberatelies, $&; + } elsif (m/^(--[-0-9a-z]+)(=|$)/ && ($oi = $valopts_long{$1})) { + $val = $2 ? $' : undef; #'; + $valopt->($oi->{Long}); } else { badusage "unknown long option \`$_'"; } @@ -2957,30 +3372,10 @@ sub parseopts () { } 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'; @@ -2999,6 +3394,11 @@ sub parseopts () { } elsif (s/^-wc$//s) { push @ropts, $&; $cleanmode = 'check'; + } elsif (m/^-[a-zA-Z]/ && ($oi = $valopts_short{$&})) { + $val = $'; #'; + $val = undef unless length $val; + $valopt->($oi->{Short}); + $_ = ''; } else { badusage "unknown short option \`$_'"; } @@ -3007,11 +3407,41 @@ sub parseopts () { } } +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; @@ -3022,7 +3452,13 @@ if (!@ARGV) { my $cmd = shift @ARGV; $cmd =~ y/-/_/; +if (!defined $rmchanges) { + local $access_forpush; + $rmchanges = access_cfg_bool(0, 'rm-old-changes'); +} + if (!defined $quilt_mode) { + local $access_forpush; $quilt_mode = cfg('dgit.force.quilt-mode', 'RETURN-UNDEF') // access_cfg('quilt-mode', 'RETURN-UNDEF') // 'linear'; @@ -3031,6 +3467,15 @@ if (!defined $quilt_mode) { $quilt_mode = $1; } +if (!defined $cleanmode) { + local $access_forpush; + $cleanmode = access_cfg('clean-mode', 'RETURN-UNDEF'); + $cleanmode //= 'dpkg-source'; + + badcfg "unknown clean-mode \`$cleanmode'" unless + $cleanmode =~ m/^($cleanmode_re)$(?!\n)/s; +} + my $fn = ${*::}{"cmd_$cmd"}; $fn or badusage "unknown operation $cmd"; $fn->();