# dgit
# Integration between git and Debian-style archives
#
-# Copyright (C)2013-2015 Ian Jackson
+# Copyright (C)2013-2017 Ian Jackson
+# Copyright (C)2017 Sean Whitton
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
use strict;
-use Debian::Dgit;
+use Debian::Dgit qw(:DEFAULT :playground);
setup_sigwarn();
use IO::Handle;
use File::Temp qw(tempdir);
use File::Basename;
use Dpkg::Version;
+use Dpkg::Compression;
+use Dpkg::Compression::Process;
use POSIX;
use IPC::Open2;
use Digest::SHA;
use Digest::MD5;
-use List::Util qw(any);
use List::MoreUtils qw(pairwise);
+use Text::Glob qw(match_glob);
+use Fcntl qw(:DEFAULT :flock);
use Carp;
use Debian::Dgit;
our $our_version = 'UNRELEASED'; ###substituted###
+our $absurdity = undef; ###substituted###
our @rpushprotovsn_support = qw(4 3 2); # 4 is new tag format
our $protovsn;
-our $isuite = 'unstable';
+our $cmd;
+our $subcommand;
+our $isuite;
our $idistro;
our $package;
our @ropts;
our $overwrite_version; # undef: not specified; '': check changelog
our $quilt_mode;
our $quilt_modes_re = 'linear|smash|auto|nofix|nocheck|gbp|dpm|unapplied';
+our $dodep14tag;
+our $split_brain_save;
our $we_are_responder;
+our $we_are_initiator;
our $initiator_tempdir;
our $patches_applied_dirtily = 00;
our $tagformat_want;
our $tagformat;
our $tagformatfn;
+our $chase_dsc_distro=1;
+
+our %forceopts = map { $_=>0 }
+ qw(unrepresentable unsupported-source-format
+ dsc-changes-mismatch changes-origs-exactly
+ import-gitapply-absurd
+ import-gitapply-no-absurd
+ import-dsc-with-dgit-field);
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 $orig_f_comp_re = 'orig(?:-[-0-9a-z]+)?';
+our $orig_f_comp_re = qr{orig(?:-$extra_orig_namepart_re)?};
our $orig_f_sig_re = '\\.(?:asc|gpg|pgp)';
our $orig_f_tail_re = "$orig_f_comp_re\\.tar(?:\\.\\w+)?(?:$orig_f_sig_re)?";
our $git_authline_re = '^([^<>]+) \<(\S+)\> (\d+ [-+]\d+)$';
our $splitbraincache = 'dgit-intern/quilt-cache';
+our $rewritemap = 'dgit-rewrite/map';
+
+our @dpkg_source_ignores = qw(-i(?:^|/)\.git(?:/|$) -I.git);
our (@git) = qw(git);
our (@dget) = qw(dget);
-our (@curl) = qw(curl -f);
+our (@curl) = (qw(curl --proto-redir), '-all,http,https', qw(-L));
our (@dput) = qw(dput);
our (@debsign) = qw(debsign);
our (@gpg) = qw(gpg);
our (@sbuild) = qw(sbuild);
our (@ssh) = 'ssh';
our (@dgit) = qw(dgit);
-our (@dpkgbuildpackage) = qw(dpkg-buildpackage -i\.git/ -I.git);
-our (@dpkgsource) = qw(dpkg-source -i\.git/ -I.git);
+our (@aptget) = qw(apt-get);
+our (@aptcache) = qw(apt-cache);
+our (@dpkgbuildpackage) = (qw(dpkg-buildpackage), @dpkg_source_ignores);
+our (@dpkgsource) = (qw(dpkg-source), @dpkg_source_ignores);
our (@dpkggenchanges) = qw(dpkg-genchanges);
our (@mergechanges) = qw(mergechanges -f);
-our (@gbp) = qw(gbp);
+our (@gbp_build) = ('');
+our (@gbp_pq) = ('gbp pq');
our (@changesopts) = ('');
our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
'ssh' => \@ssh,
'dgit' => \@dgit,
'git' => \@git,
+ 'apt-get' => \@aptget,
+ 'apt-cache' => \@aptcache,
'dpkg-source' => \@dpkgsource,
'dpkg-buildpackage' => \@dpkgbuildpackage,
'dpkg-genchanges' => \@dpkggenchanges,
- 'gbp' => \@gbp,
+ 'gbp-build' => \@gbp_build,
+ 'gbp-pq' => \@gbp_pq,
'ch' => \@changesopts,
'mergechanges' => \@mergechanges);
scalar @{ $opts_opt_map{$_} }
} keys %opts_opt_map;
-sub finalise_opts_opts();
+sub parseopts_late_defaults();
+sub setup_gitattrs(;$);
+sub check_gitattrs($$);
+our $playground;
our $keyid;
autoflush STDOUT 1;
END {
local ($@, $?);
+ return unless forkcheck_mainprocess();
print STDERR "! $_\n" foreach $supplementary_message =~ m/^.+$/mg;
}
our $csuite;
our $instead_distro;
+if (!defined $absurdity) {
+ $absurdity = $0;
+ $absurdity =~ s{/[^/]+$}{/absurd} or die;
+}
+
sub debiantag ($$) {
my ($v,$distro) = @_;
return $tagformatfn->($v, $distro);
sub debiantag_maintview ($$) {
my ($v,$distro) = @_;
- $v =~ y/~:/_%/;
- return "$distro/$v";
+ return "$distro/".dep14_version_mangle $v;
}
sub madformat ($) { $_[0] eq '3.0 (quilt)' }
sub lrref () { return "refs/remotes/$remotename/".server_branch($csuite); }
sub rrref () { return server_ref($csuite); }
-sub lrfetchrefs () { return "refs/dgit-fetch/$csuite"; }
-sub lrfetchref () { return lrfetchrefs.'/'.server_branch($csuite); }
-
-# We fetch some parts of lrfetchrefs/*. Ideally we delete these
-# locally fetched refs because they have unhelpful names and clutter
-# up gitk etc. So we track whether we have "used up" head ref (ie,
-# whether we have made another local ref which refers to this object).
-#
-# (If we deleted them unconditionally, then we might end up
-# re-fetching the same git objects each time dgit fetch was run.)
-#
-# So, leach use of lrfetchrefs needs to be accompanied by arrangements
-# in git_fetch_us to fetch the refs in question, and possibly a call
-# to lrfetchref_used.
-
-our (%lrfetchrefs_f, %lrfetchrefs_d);
-# $lrfetchrefs_X{lrfetchrefs."/heads/whatever"} = $objid
-
-sub lrfetchref_used ($) {
- my ($fullrefname) = @_;
- my $objid = $lrfetchrefs_f{$fullrefname};
- $lrfetchrefs_d{$fullrefname} = $objid if defined $objid;
-}
-
sub stripepoch ($) {
my ($vsn) = @_;
$vsn =~ s/^\d+\://;
return "${package}_".(stripepoch $vsn)."_".($arch//'*').".changes";
}
+sub upstreamversion ($) {
+ my ($vsn) = @_;
+ $vsn =~ s/-[^-]+$//;
+ return $vsn;
+}
+
our $us = 'dgit';
initdebug('');
our @end;
END {
local ($?);
+ return unless forkcheck_mainprocess();
foreach my $f (@end) {
eval { $f->(); };
print STDERR "$us: cleanup: $@" if length $@;
sub badcfg { print STDERR "$us: invalid configuration: @_\n"; exit 12; }
+sub forceable_fail ($$) {
+ my ($forceoptsl, $msg) = @_;
+ fail $msg unless grep { $forceopts{$_} } @$forceoptsl;
+ print STDERR "warning: overriding problem due to --force:\n". $msg;
+}
+
+sub forceing ($) {
+ my ($forceoptsl) = @_;
+ my @got = grep { $forceopts{$_} } @$forceoptsl;
+ return 0 unless @got;
+ print STDERR
+ "warning: skipping checks or functionality due to --force-$got[0]\n";
+}
+
sub no_such_package () {
print STDERR "$us: package $package does not exist in suite $isuite\n";
exit 4;
}
-sub changedir ($) {
- my ($newdir) = @_;
- printdebug "CD $newdir\n";
- chdir $newdir or confess "chdir: $newdir: $!";
-}
-
sub deliberately ($) {
my ($enquiry) = @_;
return !!grep { $_ eq "--deliberately-$enquiry" } @deliberatelies;
$quilt_mode =~ m/gbp|dpm|unapplied/;
}
+sub opts_opt_multi_cmd {
+ my @cmd;
+ push @cmd, split /\s+/, shift @_;
+ push @cmd, @_;
+ @cmd;
+}
+
+sub gbp_pq {
+ return opts_opt_multi_cmd @gbp_pq;
+}
+
+sub dgit_privdir () {
+ our $dgit_privdir_made //= ensure_a_playground 'dgit';
+}
+
#---------- remote protocol support, common ----------
# remote push initiator/responder protocol:
# > param tagformat old|new
# > param maint-view MAINT-VIEW-HEAD
#
+# > param buildinfo-filename P_V_X.buildinfo # zero or more times
+# > file buildinfo # for buildinfos to sign
+#
# > previously REFNAME=OBJNAME # if --deliberately-not-fast-forward
# # goes into tag, for replay prevention
#
# [etc]
# < data-block NBYTES [transfer of signed changes]
# [etc]
+# < data-block NBYTES [transfer of each signed buildinfo
+# [etc] same number and order as "file buildinfo"]
+# ...
# < files-end
#
# > complete
our ($dscdata,$dscurl,$dsc,$dsc_checked,$skew_warning_vsn);
-sub runcmd {
- debugcmd "+",@_;
- $!=0; $?=-1;
- failedcmd @_ if system @_;
-}
-
sub act_local () { return $dryrun_level <= 1; }
sub act_scary () { return !$dryrun_level; }
sub printdone {
if (!$dryrun_level) {
- progress "dgit ok: @_";
+ progress "$us ok: @_";
} else {
progress "would be ok: @_ (but dry run only)";
}
dgit [dgit-opts] build [dpkg-buildpackage-opts]
dgit [dgit-opts] sbuild [sbuild-opts]
dgit [dgit-opts] push [dgit-opts] [suite]
+ dgit [dgit-opts] push-source [dgit-opts] [suite]
dgit [dgit-opts] rpush build-host:build-dir ...
important dgit options:
-k<keyid> sign tag and package with <keyid> instead of default
return scalar shift @ARGV;
}
+sub pre_help () {
+ not_necessarily_a_tree();
+}
sub cmd_help () {
print $helpmsg or die $!;
exit 0;
our $td = $ENV{DGIT_TEST_DUMMY_DIR} || "DGIT_TEST_DUMMY_DIR-unset";
our %defcfg = ('dgit.default.distro' => 'debian',
+ 'dgit.default.default-suite' => 'unstable',
+ 'dgit.default.old-dsc-distro' => 'debian',
+ 'dgit-suite.*-security.distro' => 'debian-security',
'dgit.default.username' => '',
'dgit.default.archive-query-default-component' => 'main',
'dgit.default.ssh' => 'ssh',
'dgit.default.archive-query' => 'madison:',
'dgit.default.sshpsql-dbname' => 'service=projectb',
- 'dgit.default.dgit-tag-format' => 'old,new,maint',
+ 'dgit.default.aptget-components' => 'main',
+ 'dgit.default.dgit-tag-format' => 'new,old,maint',
+ 'dgit.dsc-url-proto-ok.http' => 'true',
+ 'dgit.dsc-url-proto-ok.https' => 'true',
+ 'dgit.dsc-url-proto-ok.git' => 'true',
+ 'dgit.default.dsc-url-proto-ok' => 'false',
# old means "repo server accepts pushes with old dgit tags"
# new means "repo server accepts pushes with new dgit tags"
# maint means "repo server accepts split brain pushes"
'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' => 'new',
'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.git-url-suffix' => '',
'dgit-distro.debian.upload-host' => 'ftp-master', # for dput
'dgit-distro.debian.mirror' => 'http://ftp.debian.org/debian/',
+ 'dgit-distro.debian-security.archive-query' => 'aptget:',
+ 'dgit-distro.debian-security.mirror' => 'http://security.debian.org/debian-security/',
+ 'dgit-distro.debian-security.aptget-suite-map' => 's#-security$#/updates#',
+ 'dgit-distro.debian-security.aptget-suite-rmap' => 's#$#-security#',
+ 'dgit-distro.debian-security.nominal-distro' => 'debian',
'dgit-distro.debian.backports-quirk' => '(squeeze)-backports*',
'dgit-distro.debian-backports.mirror' => 'http://backports.debian.org/debian-backports/',
'dgit-distro.ubuntu.git-check' => 'false',
'dgit-distro.test-dummy.git-url' => "$td/git",
'dgit-distro.test-dummy.git-host' => "git",
'dgit-distro.test-dummy.git-path' => "$td/git",
- 'dgit-distro.test-dummy.archive-query' => "ftpmasterapi:",
+ 'dgit-distro.test-dummy.archive-query' => "dummycatapi:",
'dgit-distro.test-dummy.archive-query-url' => "file://$td/aq/",
'dgit-distro.test-dummy.mirror' => "file://$td/mirror/",
'dgit-distro.test-dummy.upload-host' => 'test-dummy',
);
-our %gitcfg;
+our %gitcfgs;
+our @gitcfgsources = qw(cmdline local global system);
+our $invoked_in_git_tree = 1;
sub git_slurp_config () {
- local ($debuglevel) = $debuglevel-2;
- local $/="\0";
-
- my @cmd = (@git, qw(config -z --get-regexp .*));
- debugcmd "|",@cmd;
+ # This algoritm is a bit subtle, but this is needed so that for
+ # options which we want to be single-valued, we allow the
+ # different config sources to override properly. See #835858.
+ foreach my $src (@gitcfgsources) {
+ next if $src eq 'cmdline';
+ # we do this ourselves since git doesn't handle it
- open GITS, "-|", @cmd or die $!;
- while (<GITS>) {
- chomp or die;
- printdebug "=> ", (messagequote $_), "\n";
- m/\n/ or die "$_ ?";
- push @{ $gitcfg{$`} }, $'; #';
+ $gitcfgs{$src} = git_slurp_config_src $src;
}
- $!=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];
+ foreach my $src (@gitcfgsources) {
+ my $l = $gitcfgs{$src}{$c};
+ confess "internal error ($l $c)" if $l && !ref $l;
+ printdebug"C $c ".(defined $l ?
+ join " ", map { messagequote "'$_'" } @$l :
+ "undef")."\n"
+ if $debuglevel >= 4;
+ $l or next;
+ @$l==1 or badcfg "multiple values for $c".
+ " (in $src git config)" if @$l > 1;
+ return $l->[0];
+ }
+ return undef;
}
sub cfg {
foreach my $c (@_) {
return undef if $c =~ /RETURN-UNDEF/;
+ printdebug "C? $c\n" if $debuglevel >= 5;
my $v = git_get_config($c);
return $v if defined $v;
my $dv = $defcfg{$c};
- return $dv if defined $dv;
+ if (defined $dv) {
+ printdebug "CD $c $dv\n" if $debuglevel >= 4;
+ return $dv;
+ }
}
badcfg "need value for one of: @_\n".
"$us: distro or suite appears not to be (properly) supported";
}
-sub access_basedistro () {
+sub not_necessarily_a_tree () {
+ # needs to be called from pre_*
+ @gitcfgsources = grep { $_ ne 'local' } @gitcfgsources;
+ $invoked_in_git_tree = 0;
+}
+
+sub access_basedistro__noalias () {
if (defined $idistro) {
return $idistro;
} else {
- return cfg("dgit-suite.$isuite.distro",
- "dgit.default.distro");
+ my $def = cfg("dgit-suite.$isuite.distro", 'RETURN-UNDEF');
+ return $def if defined $def;
+ foreach my $src (@gitcfgsources, 'internal') {
+ my $kl = $src eq 'internal' ? \%defcfg : $gitcfgs{$src};
+ next unless $kl;
+ foreach my $k (keys %$kl) {
+ next unless $k =~ m#^dgit-suite\.(.*)\.distro$#;
+ my $dpat = $1;
+ next unless match_glob $dpat, $isuite;
+ return $kl->{$k};
+ }
+ }
+ return cfg("dgit.default.distro");
}
}
+sub access_basedistro () {
+ my $noalias = access_basedistro__noalias();
+ my $canon = cfg("dgit-distro.$noalias.alias-canon",'RETURN-UNDEF');
+ return $canon // $noalias;
+}
+
+sub access_nomdistro () {
+ my $base = access_basedistro();
+ my $r = cfg("dgit-distro.$base.nominal-distro",'RETURN-UNDEF') // $base;
+ $r =~ m/^$distro_re$/ or badcfg
+ "bad syntax for (nominal) distro \`$r' (does not match /^$distro_re$/)";
+ return $r;
+}
+
sub access_quirk () {
# returns (quirk name, distro to use instead or undef, quirk-specific info)
my $basedistro = access_basedistro();
Push failed, before we got started.
You can retry the push, after fixing the problem, if you like.
END
- finalise_opts_opts();
+ parseopts_late_defaults();
}
sub notpushing () {
- finalise_opts_opts();
+ parseopts_late_defaults();
}
sub supplementary_message ($) {
unshift @l, $instead_distro;
@l = grep { defined } @l;
+ push @l, access_nomdistro();
+
if (access_forpush()) {
@l = map { ("$_/push", $_) } @l;
}
}
sub parsecontrol {
- my ($file, $desc) = @_;
+ my ($file, $desc, $allowsigned) = @_;
my $fh = new IO::Handle;
open $fh, '<', $file or die "$file: $!";
- my $c = parsecontrolfh($fh,$desc);
+ my $c = parsecontrolfh($fh,$desc,$allowsigned);
$fh->error and die $!;
close $fh;
return $c;
our %commit_getclogp_memo;
my $memo = $commit_getclogp_memo{$objid};
return $memo if $memo;
- mkpath '.git/dgit';
- my $mclog = ".git/dgit/clog-$objid";
+
+ my $mclog = dgit_privdir()."clog";
runcmd shell_cmd "exec >$mclog", @git, qw(cat-file blob),
"$objid:debian/changelog";
$commit_getclogp_memo{$objid} = parsechangelog("-l$mclog");
}
-sub must_getcwd () {
- my $d = getcwd();
- defined $d or fail "getcwd failed: $!";
- return $d;
+sub parse_dscdata () {
+ my $dscfh = new IO::File \$dscdata, '<' or die $!;
+ printdebug Dumper($dscdata) if $debuglevel>1;
+ $dsc = parsecontrolfh($dscfh,$dscurl,1);
+ printdebug Dumper($dsc) if $debuglevel>1;
}
our %rmad;
-sub archive_query ($) {
- my ($method) = @_;
+sub archive_query ($;@) {
+ my ($method) = shift @_;
+ fail "this operation does not support multiple comma-separated suites"
+ if $isuite =~ m/,/;
my $query = access_cfg('archive-query','RETURN-UNDEF');
$query =~ s/^(\w+):// or badcfg "invalid archive-query method \`$query'";
my $proto = $1;
my $data = $'; #';
- { no strict qw(refs); &{"${method}_${proto}"}($proto,$data); }
+ { no strict qw(refs); &{"${method}_${proto}"}($proto,$data,@_); }
+}
+
+sub archive_query_prepend_mirror {
+ my $m = access_cfg('mirror');
+ return map { [ $_->[0], $m.$_->[1], @$_[2..$#$_] ] } @_;
}
sub pool_dsc_subpath ($$) {
return "/pool/$component/$prefix/$package/".dscfn($vsn);
}
+sub cfg_apply_map ($$$) {
+ my ($varref, $what, $mapspec) = @_;
+ return unless $mapspec;
+
+ printdebug "config $what EVAL{ $mapspec; }\n";
+ $_ = $$varref;
+ eval "package Dgit::Config; $mapspec;";
+ die $@ if $@;
+ $$varref = $_;
+}
+
#---------- `ftpmasterapi' archive query method (nascent) ----------
sub archive_api_query_cmd ($) {
my ($subpath) = @_;
- my @cmd = qw(curl -sS);
+ my @cmd = (@curl, qw(-sS));
my $url = access_cfg('archive-query-url');
if ($url =~ m#^https://([-.0-9a-z]+)/#) {
my $host = $1;
return @cmd;
}
-sub api_query ($$) {
+sub api_query ($$;$) {
use JSON;
- my ($data, $subpath) = @_;
+ my ($data, $subpath, $ok404) = @_;
badcfg "ftpmasterapi archive query method takes no data part"
if length $data;
my @cmd = archive_api_query_cmd($subpath);
+ my $url = $cmd[$#cmd];
+ push @cmd, qw(-w %{http_code});
my $json = cmdoutput @cmd;
+ unless ($json =~ s/\d+\d+\d$//) {
+ failedcmd_report_cmd undef, @cmd;
+ fail "curl failed to print 3-digit HTTP code";
+ }
+ my $code = $&;
+ return undef if $code eq '404' && $ok404;
+ fail "fetch of $url gave HTTP code $code"
+ unless $url =~ m#^file://# or $code =~ m/^2/;
return decode_json($json);
}
-sub canonicalise_suite_ftpmasterapi () {
+sub canonicalise_suite_ftpmasterapi {
my ($proto,$data) = @_;
my $suites = api_query($data, 'suites');
my @matched;
return $cn;
}
-sub archive_query_ftpmasterapi () {
+sub archive_query_ftpmasterapi {
my ($proto,$data) = @_;
my $info = api_query($data, "dsc_in_suite/$isuite/$package");
my @rows;
if length $@;
}
@rows = sort { -version_compare($a->[0],$b->[0]) } @rows;
- return @rows;
+ return archive_query_prepend_mirror @rows;
+}
+
+sub file_in_archive_ftpmasterapi {
+ my ($proto,$data,$filename) = @_;
+ my $pat = $filename;
+ $pat =~ s/_/\\_/g;
+ $pat = "%/$pat";
+ $pat =~ s#[^-+_.0-9a-z/]# sprintf '%%%02x', ord $& #ge;
+ my $info = api_query($data, "file_in_archive/$pat", 1);
+}
+
+#---------- `aptget' archive query method ----------
+
+our $aptget_base;
+our $aptget_releasefile;
+our $aptget_configpath;
+
+sub aptget_aptget () { return @aptget, qw(-c), $aptget_configpath; }
+sub aptget_aptcache () { return @aptcache, qw(-c), $aptget_configpath; }
+
+sub aptget_cache_clean {
+ runcmd_ordryrun_local qw(sh -ec),
+ 'cd "$1"; find -atime +30 -type f -print0 | xargs -0r rm --',
+ 'x', $aptget_base;
+}
+
+sub aptget_lock_acquire () {
+ my $lockfile = "$aptget_base/lock";
+ open APTGET_LOCK, '>', $lockfile or die "open $lockfile: $!";
+ flock APTGET_LOCK, LOCK_EX or die "lock $lockfile: $!";
+}
+
+sub aptget_prep ($) {
+ my ($data) = @_;
+ return if defined $aptget_base;
+
+ badcfg "aptget archive query method takes no data part"
+ if length $data;
+
+ my $cache = $ENV{XDG_CACHE_DIR} // "$ENV{HOME}/.cache";
+
+ ensuredir $cache;
+ ensuredir "$cache/dgit";
+ my $cachekey =
+ access_cfg('aptget-cachekey','RETURN-UNDEF')
+ // access_nomdistro();
+
+ $aptget_base = "$cache/dgit/aptget";
+ ensuredir $aptget_base;
+
+ my $quoted_base = $aptget_base;
+ die "$quoted_base contains bad chars, cannot continue"
+ if $quoted_base =~ m/["\\]/; # apt.conf(5) says no escaping :-/
+
+ ensuredir $aptget_base;
+
+ aptget_lock_acquire();
+
+ aptget_cache_clean();
+
+ $aptget_configpath = "$aptget_base/apt.conf#$cachekey";
+ my $sourceslist = "source.list#$cachekey";
+
+ my $aptsuites = $isuite;
+ cfg_apply_map(\$aptsuites, 'suite map',
+ access_cfg('aptget-suite-map', 'RETURN-UNDEF'));
+
+ open SRCS, ">", "$aptget_base/$sourceslist" or die $!;
+ printf SRCS "deb-src %s %s %s\n",
+ access_cfg('mirror'),
+ $aptsuites,
+ access_cfg('aptget-components')
+ or die $!;
+
+ ensuredir "$aptget_base/cache";
+ ensuredir "$aptget_base/lists";
+
+ open CONF, ">", $aptget_configpath or die $!;
+ print CONF <<END;
+Debug::NoLocking "true";
+APT::Get::List-Cleanup "false";
+#clear APT::Update::Post-Invoke-Success;
+Dir::Etc::SourceList "$quoted_base/$sourceslist";
+Dir::State::Lists "$quoted_base/lists";
+Dir::Etc::preferences "$quoted_base/preferences";
+Dir::Cache::srcpkgcache "$quoted_base/cache/srcs#$cachekey";
+Dir::Cache::pkgcache "$quoted_base/cache/pkgs#$cachekey";
+END
+
+ foreach my $key (qw(
+ Dir::Cache
+ Dir::State
+ Dir::Cache::Archives
+ Dir::Etc::SourceParts
+ Dir::Etc::preferencesparts
+ )) {
+ ensuredir "$aptget_base/$key";
+ print CONF "$key \"$quoted_base/$key\";\n" or die $!;
+ };
+
+ my $oldatime = (time // die $!) - 1;
+ foreach my $oldlist (<$aptget_base/lists/*Release>) {
+ next unless stat_exists $oldlist;
+ my ($mtime) = (stat _)[9];
+ utime $oldatime, $mtime, $oldlist or die "$oldlist $!";
+ }
+
+ runcmd_ordryrun_local aptget_aptget(), qw(update);
+
+ my @releasefiles;
+ foreach my $oldlist (<$aptget_base/lists/*Release>) {
+ next unless stat_exists $oldlist;
+ my ($atime) = (stat _)[8];
+ next if $atime == $oldatime;
+ push @releasefiles, $oldlist;
+ }
+ my @inreleasefiles = grep { m#/InRelease$# } @releasefiles;
+ @releasefiles = @inreleasefiles if @inreleasefiles;
+ die "apt updated wrong number of Release files (@releasefiles), erk"
+ unless @releasefiles == 1;
+
+ ($aptget_releasefile) = @releasefiles;
+}
+
+sub canonicalise_suite_aptget {
+ my ($proto,$data) = @_;
+ aptget_prep($data);
+
+ my $release = parsecontrol $aptget_releasefile, "Release file", 1;
+
+ foreach my $name (qw(Codename Suite)) {
+ my $val = $release->{$name};
+ if (defined $val) {
+ printdebug "release file $name: $val\n";
+ $val =~ m/^$suite_re$/o or fail
+ "Release file ($aptget_releasefile) specifies intolerable $name";
+ cfg_apply_map(\$val, 'suite rmap',
+ access_cfg('aptget-suite-rmap', 'RETURN-UNDEF'));
+ return $val
+ }
+ }
+ return $isuite;
+}
+
+sub archive_query_aptget {
+ my ($proto,$data) = @_;
+ aptget_prep($data);
+
+ ensuredir "$aptget_base/source";
+ foreach my $old (<$aptget_base/source/*.dsc>) {
+ unlink $old or die "$old: $!";
+ }
+
+ my $showsrc = cmdoutput aptget_aptcache(), qw(showsrc), $package;
+ return () unless $showsrc =~ m/^package:\s*\Q$package\E\s*$/mi;
+ # avoids apt-get source failing with ambiguous error code
+
+ runcmd_ordryrun_local
+ shell_cmd 'cd "$1"/source; shift', $aptget_base,
+ aptget_aptget(), qw(--download-only --only-source source), $package;
+
+ my @dscs = <$aptget_base/source/*.dsc>;
+ fail "apt-get source did not produce a .dsc" unless @dscs;
+ fail "apt-get source produced several .dscs (@dscs)" unless @dscs==1;
+
+ my $pre_dsc = parsecontrol $dscs[0], $dscs[0], 1;
+
+ use URI::Escape;
+ my $uri = "file://". uri_escape $dscs[0];
+ $uri =~ s{\%2f}{/}gi;
+ return [ (getfield $pre_dsc, 'Version'), $uri ];
+}
+
+sub file_in_archive_aptget () { return undef; }
+
+#---------- `dummyapicat' archive query method ----------
+
+sub archive_query_dummycatapi { archive_query_ftpmasterapi @_; }
+sub canonicalise_suite_dummycatapi { canonicalise_suite_ftpmasterapi @_; }
+
+sub file_in_archive_dummycatapi ($$$) {
+ my ($proto,$data,$filename) = @_;
+ my $mirror = access_cfg('mirror');
+ $mirror =~ s#^file://#/# or die "$mirror ?";
+ my @out;
+ my @cmd = (qw(sh -ec), '
+ cd "$1"
+ find -name "$2" -print0 |
+ xargs -0r sha256sum
+ ', qw(x), $mirror, $filename);
+ debugcmd "-|", @cmd;
+ open FIA, "-|", @cmd or die $!;
+ while (<FIA>) {
+ chomp or die;
+ printdebug "| $_\n";
+ m/^(\w+) (\S+)$/ or die "$_ ?";
+ push @out, { sha256sum => $1, filename => $2 };
+ }
+ close FIA or die failedcmd @cmd;
+ return \@out;
}
#---------- `madison' archive query method ----------
sub archive_query_madison {
- return map { [ @$_[0..1] ] } madison_get_parse(@_);
+ return archive_query_prepend_mirror
+ map { [ @$_[0..1] ] } madison_get_parse(@_);
}
sub madison_get_parse {
return $r[0][2];
}
+sub file_in_archive_madison { return undef; }
+
#---------- `sshpsql' archive query method ----------
sub sshpsql ($$$) {
my ($vsn,$component,$filename,$sha256sum) = @$_;
[ $vsn, "/pool/$component/$filename",$digester,$sha256sum ];
} @rows;
- return @rows;
+ return archive_query_prepend_mirror @rows;
}
sub canonicalise_suite_sshpsql ($$) {
return $rows[0];
}
+sub file_in_archive_sshpsql ($$$) { return undef; }
+
#---------- `dummycat' archive query method ----------
sub canonicalise_suite_dummycat ($$) {
}
C->error and die "$dpath: $!";
close C;
- return sort { -version_compare($a->[0],$b->[0]); } @rows;
+ return archive_query_prepend_mirror
+ sort { -version_compare($a->[0],$b->[0]); } @rows;
}
+sub file_in_archive_dummycat () { return undef; }
+
#---------- tag format handling ----------
sub access_cfg_tagformats () {
split /\,/, access_cfg('dgit-tag-format');
}
+sub access_cfg_tagformats_can_splitbrain () {
+ my %y = map { $_ => 1 } access_cfg_tagformats;
+ foreach my $needtf (qw(new maint)) {
+ next if $y{$needtf};
+ return 0;
+ }
+ return 1;
+}
+
sub need_tagformat ($$) {
my ($fmt, $why) = @_;
fail "need to use tag format $fmt ($why) but also need".
$csuite = archive_query('canonicalise_suite');
if ($isuite ne $csuite) {
progress "canonical suite name for $isuite is $csuite";
+ } else {
+ progress "canonical suite name is $csuite";
}
}
canonicalise_suite();
my @vsns = archive_query('archive_query');
foreach my $vinfo (@vsns) {
- my ($vsn,$subpath,$digester,$digest) = @$vinfo;
- $dscurl = access_cfg('mirror').$subpath;
+ my ($vsn,$vsn_dscurl,$digester,$digest) = @$vinfo;
+ $dscurl = $vsn_dscurl;
$dscdata = url_get($dscurl);
if (!$dscdata) {
$skew_warning_vsn = $vsn if !defined $skew_warning_vsn;
fail "$dscurl has hash $got but".
" archive told us to expect $digest";
}
- my $dscfh = new IO::File \$dscdata, '<' or die $!;
- printdebug Dumper($dscdata) if $debuglevel>1;
- $dsc = parsecontrolfh($dscfh,$dscurl,1);
- printdebug Dumper($dsc) if $debuglevel>1;
+ parse_dscdata();
my $fmt = getfield $dsc, 'Format';
- fail "unsupported source format $fmt, sorry" unless $format_ok{$fmt};
+ $format_ok{$fmt} or forceable_fail [qw(unsupported-source-format)],
+ "unsupported source format $fmt, sorry";
+
$dsc_checked = !!$digester;
printdebug "get_archive_dsc: Version ".(getfield $dsc, 'Version')."\n";
return;
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 @cmd = (@curl, qw(-sS -I), $url);
my $result = cmdoutput @cmd;
$result =~ s/^\S+ 200 .*\n\r?\n//;
# curl -sS -I with https_proxy prints
}
our ($dsc_hash,$lastpush_mergeinput);
+our ($dsc_distro, $dsc_hint_tag, $dsc_hint_url);
-our $ud = '.git/dgit/unpack';
-sub prep_ud (;$) {
- my ($d) = @_;
- $d //= $ud;
- rmtree($d);
- mkpath '.git/dgit';
- mkdir $d or die $!;
+sub prep_ud () {
+ dgit_privdir(); # ensures that $dgit_privdir_made is based on $maindir
+ $playground = fresh_playground 'dgit/unpack';
}
sub mktree_in_ud_here () {
- runcmd qw(git init -q);
- runcmd qw(git config gc.auto 0);
- rmtree('.git/objects');
- symlink '../../../../objects','.git/objects' or die $!;
+ playtree_setup $gitcfgs{local};
}
sub git_write_tree () {
return $tree;
}
-sub remove_stray_gits () {
+sub git_add_write_tree () {
+ runcmd @git, qw(add -Af .);
+ return git_write_tree();
+}
+
+sub remove_stray_gits ($) {
+ my ($what) = @_;
my @gitscmd = qw(find -name .git -prune -print0);
debugcmd "|",@gitscmd;
open GITS, "-|", @gitscmd or die $!;
local $/="\0";
while (<GITS>) {
chomp or die;
- print STDERR "$us: warning: removing from source package: ",
+ print STDERR "$us: warning: removing from $what: ",
(messagequote $_), "\n";
rmtree $_;
}
$!=0; $?=0; close GITS or failedcmd @gitscmd;
}
-sub mktree_in_ud_from_only_subdir (;$) {
- my ($raw) = @_;
-
+sub mktree_in_ud_from_only_subdir ($;$) {
+ my ($what,$raw) = @_;
# changes into the subdir
+
my (@dirs) = <*/.>;
die "expected one subdir but found @dirs ?" unless @dirs==1;
$dirs[0] =~ m#^([^/]+)/\.$# or die;
my $dir = $1;
changedir $dir;
- remove_stray_gits();
+ remove_stray_gits($what);
mktree_in_ud_here();
if (!$raw) {
my ($format, $fopts) = get_source_format();
}
}
- runcmd @git, qw(add -Af);
- my $tree=git_write_tree();
+ my $tree=git_add_write_tree();
return ($tree,$dir);
}
+our @files_csum_info_fields =
+ (['Checksums-Sha256','Digest::SHA', 'new(256)', 'sha256sum'],
+ ['Checksums-Sha1', 'Digest::SHA', 'new(1)', 'sha1sum'],
+ ['Files', 'Digest::MD5', 'new()', 'md5sum']);
+
sub dsc_files_info () {
- foreach my $csumi (['Checksums-Sha256','Digest::SHA', 'new(256)'],
- ['Checksums-Sha1', 'Digest::SHA', 'new(1)'],
- ['Files', 'Digest::MD5', 'new()']) {
+ foreach my $csumi (@files_csum_info_fields) {
my ($fname, $module, $method) = @$csumi;
my $field = $dsc->{$fname};
next unless defined $field;
map { $_->{Filename} } dsc_files_info();
}
+sub files_compare_inputs (@) {
+ my $inputs = \@_;
+ my %record;
+ my %fchecked;
+
+ my $showinputs = sub {
+ return join "; ", map { $_->get_option('name') } @$inputs;
+ };
+
+ foreach my $in (@$inputs) {
+ my $expected_files;
+ my $in_name = $in->get_option('name');
+
+ printdebug "files_compare_inputs $in_name\n";
+
+ foreach my $csumi (@files_csum_info_fields) {
+ my ($fname) = @$csumi;
+ printdebug "files_compare_inputs $in_name $fname\n";
+
+ my $field = $in->{$fname};
+ next unless defined $field;
+
+ my @files;
+ foreach (split /\n/, $field) {
+ next unless m/\S/;
+
+ my ($info, $f) = m/^(\w+ \d+) (?:\S+ \S+ )?(\S+)$/ or
+ fail "could not parse $in_name $fname line \`$_'";
+
+ printdebug "files_compare_inputs $in_name $fname $f\n";
+
+ push @files, $f;
+
+ my $re = \ $record{$f}{$fname};
+ if (defined $$re) {
+ $fchecked{$f}{$in_name} = 1;
+ $$re eq $info or
+ fail "hash or size of $f varies in $fname fields".
+ " (between: ".$showinputs->().")";
+ } else {
+ $$re = $info;
+ }
+ }
+ @files = sort @files;
+ $expected_files //= \@files;
+ "@$expected_files" eq "@files" or
+ fail "file list in $in_name varies between hash fields!";
+ }
+ $expected_files or
+ fail "$in_name has no files list field(s)";
+ }
+ printdebug "files_compare_inputs ".Dumper(\%fchecked, \%record)
+ if $debuglevel>=2;
+
+ grep { keys %$_ == @$inputs-1 } values %fchecked
+ or fail "no file appears in all file lists".
+ " (looked in: ".$showinputs->().")";
+}
+
sub is_orig_file_in_dsc ($$) {
my ($f, $dsc_files_info) = @_;
return 0 if @$dsc_files_info <= 1;
return 1;
}
+# This function determines whether a .changes file is source-only from
+# the point of view of dak. Thus, it permits *_source.buildinfo
+# files.
+#
+# It does not, however, permit any other buildinfo files. After a
+# source-only upload, the buildds will try to upload files like
+# foo_1.2.3_amd64.buildinfo. If the package maintainer included files
+# named like this in their (otherwise) source-only upload, the uploads
+# of the buildd can be rejected by dak. Fixing the resultant
+# situation can require manual intervention. So we block such
+# .buildinfo files when the user tells us to perform a source-only
+# upload (such as when using the push-source subcommand with the -C
+# option, which calls this function).
+#
+# Note, though, that when dgit is told to prepare a source-only
+# upload, such as when subcommands like build-source and push-source
+# without -C are used, dgit has a more restrictive notion of
+# source-only .changes than dak: such uploads will never include
+# *_source.buildinfo files. This is because there is no use for such
+# files when using a tool like dgit to produce the source package, as
+# dgit ensures the source is identical to git HEAD.
+sub test_source_only_changes ($) {
+ my ($changes) = @_;
+ foreach my $l (split /\n/, getfield $changes, 'Files') {
+ $l =~ m/\S+$/ or next;
+ # \.tar\.[a-z0-9]+ covers orig.tar and the tarballs in native packages
+ unless ($& =~ m/(?:\.dsc|\.diff\.gz|\.tar\.[a-z0-9]+|_source\.buildinfo)$/) {
+ print "purportedly source-only changes polluted by $&\n";
+ return 0;
+ }
+ }
+ return 1;
+}
+
+sub changes_update_origs_from_dsc ($$$$) {
+ my ($dsc, $changes, $upstreamvsn, $changesfile) = @_;
+ my %changes_f;
+ printdebug "checking origs needed ($upstreamvsn)...\n";
+ $_ = getfield $changes, 'Files';
+ m/^\w+ \d+ (\S+ \S+) \S+$/m or
+ fail "cannot find section/priority from .changes Files field";
+ my $placementinfo = $1;
+ my %changed;
+ printdebug "checking origs needed placement '$placementinfo'...\n";
+ foreach my $l (split /\n/, getfield $dsc, 'Files') {
+ $l =~ m/\S+$/ or next;
+ my $file = $&;
+ printdebug "origs $file | $l\n";
+ next unless is_orig_file_of_vsn $file, $upstreamvsn;
+ printdebug "origs $file is_orig\n";
+ my $have = archive_query('file_in_archive', $file);
+ if (!defined $have) {
+ print STDERR <<END;
+archive does not support .orig check; hope you used --ch:--sa/-sd if needed
+END
+ return;
+ }
+ my $found_same = 0;
+ my @found_differ;
+ printdebug "origs $file \$#\$have=$#$have\n";
+ foreach my $h (@$have) {
+ my $same = 0;
+ my @differ;
+ foreach my $csumi (@files_csum_info_fields) {
+ my ($fname, $module, $method, $archivefield) = @$csumi;
+ next unless defined $h->{$archivefield};
+ $_ = $dsc->{$fname};
+ next unless defined;
+ m/^(\w+) .* \Q$file\E$/m or
+ fail ".dsc $fname missing entry for $file";
+ if ($h->{$archivefield} eq $1) {
+ $same++;
+ } else {
+ push @differ,
+ "$archivefield: $h->{$archivefield} (archive) != $1 (local .dsc)";
+ }
+ }
+ die "$file ".Dumper($h)." ?!" if $same && @differ;
+ $found_same++
+ if $same;
+ push @found_differ, "archive $h->{filename}: ".join "; ", @differ
+ if @differ;
+ }
+ printdebug "origs $file f.same=$found_same".
+ " #f._differ=$#found_differ\n";
+ if (@found_differ && !$found_same) {
+ fail join "\n",
+ "archive contains $file with different checksum",
+ @found_differ;
+ }
+ # Now we edit the changes file to add or remove it
+ foreach my $csumi (@files_csum_info_fields) {
+ my ($fname, $module, $method, $archivefield) = @$csumi;
+ next unless defined $changes->{$fname};
+ if ($found_same) {
+ # in archive, delete from .changes if it's there
+ $changed{$file} = "removed" if
+ $changes->{$fname} =~ s/^.* \Q$file\E$(?:)\n//m;
+ } elsif ($changes->{$fname} =~ m/^.* \Q$file\E$(?:)\n/m) {
+ # not in archive, but it's here in the .changes
+ } else {
+ my $dsc_data = getfield $dsc, $fname;
+ $dsc_data =~ m/^(.* \Q$file\E$)\n/m or die "$dsc_data $file ?";
+ my $extra = $1;
+ $extra =~ s/ \d+ /$&$placementinfo /
+ or die "$fname $extra >$dsc_data< ?"
+ if $fname eq 'Files';
+ $changes->{$fname} .= "\n". $extra;
+ $changed{$file} = "added";
+ }
+ }
+ }
+ if (%changed) {
+ foreach my $file (keys %changed) {
+ progress sprintf
+ "edited .changes for archive .orig contents: %s %s",
+ $changed{$file}, $file;
+ }
+ my $chtmp = "$changesfile.tmp";
+ $changes->save($chtmp);
+ if (act_local()) {
+ rename $chtmp,$changesfile or die "$changesfile $!";
+ } else {
+ progress "[new .changes left in $changesfile]";
+ }
+ } else {
+ progress "$changesfile already has appropriate .orig(s) (if any)";
+ }
+}
+
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;
+ if ($author =~ m/^[^"\@]+\,/) {
+ # single entry Maintainer field with unquoted comma
+ $author = ($& =~ y/,//rd).$'; # strip the comma
+ }
+ # git wants a single author; any remaining commas in $author
+ # are by now preceded by @ (or "). It seems safer to punt on
+ # "..." for now rather than attempting to dequote or something.
+ $author =~ s#,.*##ms unless $author =~ m/"/;
my $date = cmdoutput qw(date), '+%s %z', qw(-d), getfield($clogp,'Date');
my $authline = "$author $date";
$authline =~ m/$git_authline_re/o or
vendor_patches_distro(Dpkg::Vendor::get_current_vendor(),
"Dpkg::Vendor \`current vendor'");
vendor_patches_distro(access_basedistro(),
- "distro being accessed");
+ "(base) distro being accessed");
+ vendor_patches_distro(access_nomdistro(),
+ "(nominal) distro being accessed");
}
sub generate_commits_from_dsc () {
# See big comment in fetch_from_archive, below.
+ # See also README.dsc-import.
prep_ud();
- changedir $ud;
+ changedir $playground;
my @dfi = dsc_files_info();
foreach my $fi (@dfi) {
my $f = $fi->{Filename};
die "$f ?" if $f =~ m#/|^\.|\.dsc$|\.tmp$#;
+ my $upper_f = "$maindir/../$f";
+
+ printdebug "considering reusing $f: ";
+
+ if (link_ltarget "$upper_f,fetch", $f) {
+ printdebug "linked (using ...,fetch).\n";
+ } elsif ((printdebug "($!) "),
+ $! != ENOENT) {
+ fail "accessing ../$f,fetch: $!";
+ } elsif (link_ltarget $upper_f, $f) {
+ printdebug "linked.\n";
+ } elsif ((printdebug "($!) "),
+ $! != ENOENT) {
+ fail "accessing ../$f: $!";
+ } else {
+ printdebug "absent.\n";
+ }
- link_ltarget "../../../$f", $f
- or $!==&ENOENT
- or die "$f $!";
-
- complete_file_from_dsc('.', $fi)
+ my $refetched;
+ complete_file_from_dsc('.', $fi, \$refetched)
or next;
- if (is_orig_file_in_dsc($f, \@dfi)) {
- link $f, "../../../../$f"
- or $!==&EEXIST
- or die "$f $!";
+ printdebug "considering saving $f: ";
+
+ if (link $f, $upper_f) {
+ printdebug "linked.\n";
+ } elsif ((printdebug "($!) "),
+ $! != EEXIST) {
+ fail "saving ../$f: $!";
+ } elsif (!$refetched) {
+ printdebug "no need.\n";
+ } elsif (link $f, "$upper_f,fetch") {
+ printdebug "linked (using ...,fetch).\n";
+ } elsif ((printdebug "($!) "),
+ $! != EEXIST) {
+ fail "saving ../$f,fetch: $!";
+ } else {
+ printdebug "cannot.\n";
}
}
# from the debian/changelog, so we record the tree objects now and
# make them into commits later.
my @tartrees;
- my $upstreamv = $dsc->{version};
- $upstreamv =~ s/-[^-]+$//;
+ my $upstreamv = upstreamversion $dsc->{version};
my $orig_f_base = srcfn $upstreamv, '';
foreach my $fi (@dfi) {
if defined $compr_ext && !defined $cname;
my $compr_proc =
new Dpkg::Compression::Process compression => $cname;
- my @compr_cmd = $compr_proc->get_uncompress_cmdline();
+ @compr_cmd = $compr_proc->get_uncompress_cmdline();
my $compr_fh = new IO::Handle;
my $compr_pid = open $compr_fh, "-|" // die $!;
if (!$compr_pid) {
$input = $compr_fh;
}
- rmtree "../unpack-tar";
- mkdir "../unpack-tar" or die $!;
+ rmtree "_unpack-tar";
+ mkdir "_unpack-tar" or die $!;
my @tarcmd = qw(tar -x -f -
--no-same-owner --no-same-permissions
--no-acls --no-xattrs --no-selinux);
my $tar_pid = fork // die $!;
if (!$tar_pid) {
- chdir "../unpack-tar" or die $!;
+ chdir "_unpack-tar" or die $!;
open STDIN, "<&", $input or die $!;
exec @tarcmd;
die "dgit (child): exec $tarcmd[0]: $!";
!$? or failedcmd @tarcmd;
close $input or
- (@compr_cmd ? failedcmd @compr_cmd
+ (@compr_cmd ? ($?==SIGPIPE || failedcmd @compr_cmd)
: die $!);
# finally, we have the results in "tarball", but maybe
# with the wrong permissions
- runcmd qw(chmod -R +rwX ../unpack-tar);
- changedir "../unpack-tar";
- my ($tree) = mktree_in_ud_from_only_subdir(1);
- changedir "../../unpack";
- rmtree "../unpack-tar";
+ runcmd qw(chmod -R +rwX _unpack-tar);
+ changedir "_unpack-tar";
+ remove_stray_gits($f);
+ mktree_in_ud_here();
+
+ my ($tree) = git_add_write_tree();
+ my $tentries = cmdoutput @git, qw(ls-tree -z), $tree;
+ if ($tentries =~ m/^\d+ tree (\w+)\t[^\000]+\000$/s) {
+ $tree = $1;
+ printdebug "one subtree $1\n";
+ } else {
+ printdebug "multiple subtrees\n";
+ }
+ changedir "..";
+ rmtree "_unpack-tar";
my $ent = [ $f, $tree ];
push @tartrees, {
push @cmd, qw(-x --), $dscfn;
runcmd @cmd;
- my ($tree,$dir) = mktree_in_ud_from_only_subdir();
+ my ($tree,$dir) = mktree_in_ud_from_only_subdir("source package");
if (madformat $dsc->{format}) {
check_for_vendor_patches();
}
my @pcmd = qw(dpkg-source --before-build .);
runcmd shell_cmd 'exec >/dev/null', @pcmd;
rmtree '.pc';
- runcmd @git, qw(add -Af);
- $dappliedtree = git_write_tree();
+ $dappliedtree = git_add_write_tree();
}
my @clogcmd = qw(dpkg-parsechangelog --format rfc822 --all);
printdebug "import clog $r1clogp->{version} becomes r1\n";
}
die $! if CLOGS->error;
- close CLOGS or $?==(SIGPIPE<<8) or failedcmd @clogcmd;
+ close CLOGS or $?==SIGPIPE or failedcmd @clogcmd;
$clogp or fail "package changelog has no entries!";
my $authline = clogp_authline $clogp;
my $changes = getfield $clogp, 'Changes';
+ $changes =~ s/^\n//; # Changes: \n
my $cversion = getfield $clogp, 'Version';
if (@tartrees) {
[dgit dummy commit]
END
- runcmd @git, qw(checkout -b dapplied), $dappliedcommit;
+ runcmd @git, qw(checkout -q -b dapplied), $dappliedcommit;
+
+ runcmd @git, qw(checkout -q -b unpa), $rawimport_hash;
+
+ # We need the answers to be reproducible
+ my @authline = clogp_authline($clogp);
+ local $ENV{GIT_COMMITTER_NAME} = $authline[0];
+ local $ENV{GIT_COMMITTER_EMAIL} = $authline[1];
+ local $ENV{GIT_COMMITTER_DATE} = $authline[2];
+ local $ENV{GIT_AUTHOR_NAME} = $authline[0];
+ local $ENV{GIT_AUTHOR_EMAIL} = $authline[1];
+ local $ENV{GIT_AUTHOR_DATE} = $authline[2];
+
+ my $path = $ENV{PATH} or die;
+
+ # we use ../../gbp-pq-output, which (given that we are in
+ # $playground/PLAYTREE, and $playground is .git/dgit/unpack,
+ # is .git/dgit.
+
+ foreach my $use_absurd (qw(0 1)) {
+ runcmd @git, qw(checkout -q unpa);
+ runcmd @git, qw(update-ref -d refs/heads/patch-queue/unpa);
+ local $ENV{PATH} = $path;
+ if ($use_absurd) {
+ chomp $@;
+ progress "warning: $@";
+ $path = "$absurdity:$path";
+ progress "$us: trying slow absurd-git-apply...";
+ rename "../../gbp-pq-output","../../gbp-pq-output.0"
+ or $!==ENOENT
+ or die $!;
+ }
+ eval {
+ die "forbid absurd git-apply\n" if $use_absurd
+ && forceing [qw(import-gitapply-no-absurd)];
+ die "only absurd git-apply!\n" if !$use_absurd
+ && forceing [qw(import-gitapply-absurd)];
+
+ local $ENV{DGIT_ABSURD_DEBUG} = $debuglevel if $use_absurd;
+ local $ENV{PATH} = $path if $use_absurd;
+
+ my @showcmd = (gbp_pq, qw(import));
+ my @realcmd = shell_cmd
+ 'exec >/dev/null 2>>../../gbp-pq-output', @showcmd;
+ debugcmd "+",@realcmd;
+ if (system @realcmd) {
+ die +(shellquote @showcmd).
+ " failed: ".
+ failedcmd_waitstatus()."\n";
+ }
- runcmd @git, qw(checkout -b unpa), $rawimport_hash;
- runcmd shell_cmd 'exec >/dev/null', @gbp, qw(pq import);
- my $gapplied = git_rev_parse('HEAD');
- my $gappliedtree = cmdoutput @git, qw(rev-parse HEAD:);
- $gappliedtree eq $dappliedtree or
- fail <<END;
+ my $gapplied = git_rev_parse('HEAD');
+ my $gappliedtree = cmdoutput @git, qw(rev-parse HEAD:);
+ $gappliedtree eq $dappliedtree or
+ fail <<END;
gbp-pq import and dpkg-source disagree!
gbp-pq import gave commit $gapplied
gbp-pq import gave tree $gappliedtree
dpkg-source --before-build gave tree $dappliedtree
END
- $rawimport_hash = $gapplied;
+ $rawimport_hash = $gapplied;
+ };
+ last unless $@;
+ }
+ if ($@) {
+ { local $@; eval { runcmd qw(cat ../../gbp-pq-output); }; }
+ die $@;
+ }
}
progress "synthesised git commit from .dsc $cversion";
@output = $lastpush_mergeinput;
}
}
- changedir '../../../..';
- rmtree($ud);
+ changedir $maindir;
+ rmtree $playground;
return @output;
}
-sub complete_file_from_dsc ($$) {
- our ($dstdir, $fi) = @_;
- # Ensures that we have, in $dir, the file $fi, with the correct
+sub complete_file_from_dsc ($$;$) {
+ our ($dstdir, $fi, $refetched) = @_;
+ # Ensures that we have, in $dstdir, the file $fi, with the correct
# contents. (Downloading it from alongside $dscurl if necessary.)
+ # If $refetched is defined, can overwrite "$dstdir/$fi->{Filename}"
+ # and will set $$refetched=1 if it did so (or tried to).
my $f = $fi->{Filename};
my $tf = "$dstdir/$f";
my $downloaded = 0;
+ my $got;
+ my $checkhash = sub {
+ open F, "<", "$tf" or die "$tf: $!";
+ $fi->{Digester}->reset();
+ $fi->{Digester}->addfile(*F);
+ F->error and die $!;
+ $got = $fi->{Digester}->hexdigest();
+ return $got eq $fi->{Hash};
+ };
+
if (stat_exists $tf) {
- progress "using existing $f";
+ if ($checkhash->()) {
+ progress "using existing $f";
+ return 1;
+ }
+ if (!$refetched) {
+ fail "file $f has hash $got but .dsc".
+ " demands hash $fi->{Hash} ".
+ "(perhaps you should delete this file?)";
+ }
+ progress "need to fetch correct version of $f";
+ unlink $tf or die "$tf $!";
+ $$refetched = 1;
} else {
- my $furl = $dscurl;
- $furl =~ s{/[^/]+$}{};
- $furl .= "/$f";
- die "$f ?" unless $f =~ m/^\Q${package}\E_/;
- die "$f ?" if $f =~ m#/#;
- runcmd_ordryrun_local @curl,qw(-o),$tf,'--',"$furl";
- return 0 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
+ printdebug "$tf does not exist, need to fetch\n";
+ }
+
+ my $furl = $dscurl;
+ $furl =~ s{/[^/]+$}{};
+ $furl .= "/$f";
+ die "$f ?" unless $f =~ m/^\Q${package}\E_/;
+ die "$f ?" if $f =~ m#/#;
+ runcmd_ordryrun_local @curl,qw(-f -o),$tf,'--',"$furl";
+ return 0 if !act_local();
+
+ $checkhash->() 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?)");
+ "(got wrong file from archive!)";
return 1;
}
}
}
-sub git_fetch_us () {
- # Want to fetch only what we are going to use, unless
- # deliberately-not-ff, in which case we must fetch everything.
+#---------- git fetch ----------
- my @specs = deliberately_not_fast_forward ? qw(tags/*) :
- map { "tags/$_" }
- (quiltmode_splitbrain
- ? (map { $_->('*',access_basedistro) }
- \&debiantag_new, \&debiantag_maintview)
- : debiantags('*',access_basedistro));
- push @specs, server_branch($csuite);
- push @specs, qw(heads/*) if deliberately_not_fast_forward;
+sub lrfetchrefs () { return "refs/dgit-fetch/".access_basedistro(); }
+sub lrfetchref () { return lrfetchrefs.'/'.server_branch($csuite); }
+
+# We fetch some parts of lrfetchrefs/*. Ideally we delete these
+# locally fetched refs because they have unhelpful names and clutter
+# up gitk etc. So we track whether we have "used up" head ref (ie,
+# whether we have made another local ref which refers to this object).
+#
+# (If we deleted them unconditionally, then we might end up
+# re-fetching the same git objects each time dgit fetch was run.)
+#
+# So, each use of lrfetchrefs needs to be accompanied by arrangements
+# in git_fetch_us to fetch the refs in question, and possibly a call
+# to lrfetchref_used.
+
+our (%lrfetchrefs_f, %lrfetchrefs_d);
+# $lrfetchrefs_X{lrfetchrefs."/heads/whatever"} = $objid
+
+sub lrfetchref_used ($) {
+ my ($fullrefname) = @_;
+ my $objid = $lrfetchrefs_f{$fullrefname};
+ $lrfetchrefs_d{$fullrefname} = $objid if defined $objid;
+}
+
+sub git_lrfetch_sane {
+ my ($url, $supplementary, @specs) = @_;
+ # Make a 'refs/'.lrfetchrefs.'/*' be just like on server,
+ # at least as regards @specs. Also leave the results in
+ # %lrfetchrefs_f, and arrange for lrfetchref_used to be
+ # able to clean these up.
+ #
+ # With $supplementary==1, @specs must not contain wildcards
+ # and we add to our previous fetches (non-atomically).
# This is rather miserable:
- # When git-fetch --prune is passed a fetchspec ending with a *,
+ # When git fetch --prune is passed a fetchspec ending with a *,
# it does a plausible thing. If there is no * then:
# - it matches subpaths too, even if the supplied refspec
# starts refs, and behaves completely madly if the source
# We want to fetch a fixed ref, and we don't know in advance
# if it exists, so this is not suitable.
#
- # Our workaround is to use git-ls-remote. git-ls-remote has its
+ # Our workaround is to use git ls-remote. git ls-remote has its
# own qairks. Notably, it has the absurd multi-tail-matching
- # behaviour: git-ls-remote R refs/foo can report refs/foo AND
+ # behaviour: git ls-remote R refs/foo can report refs/foo AND
# refs/refs/foo etc.
#
# Also, we want an idempotent snapshot, but we have to make two
- # calls to the remote: one to git-ls-remote and to git-fetch. The
- # solution is use git-ls-remote to obtain a target state, and
- # git-fetch to try to generate it. If we don't manage to generate
+ # calls to the remote: one to git ls-remote and to git fetch. The
+ # solution is use git ls-remote to obtain a target state, and
+ # git fetch to try to generate it. If we don't manage to generate
# the target state, we try again.
+ printdebug "git_lrfetch_sane suppl=$supplementary specs @specs\n";
+
my $specre = join '|', map {
my $x = $_;
$x =~ s/\W/\\$&/g;
- $x =~ s/\\\*$/.*/;
+ my $wildcard = $x =~ s/\\\*$/.*/;
+ die if $wildcard && $supplementary;
"(?:refs/$x)";
} @specs;
- printdebug "git_fetch_us specre=$specre\n";
+ printdebug "git_lrfetch_sane specre=$specre\n";
my $wanted_rref = sub {
local ($_) = @_;
- return m/^(?:$specre)$/o;
+ return m/^(?:$specre)$/;
};
my $fetch_iteration = 0;
FETCH_ITERATION:
for (;;) {
+ printdebug "git_lrfetch_sane iteration $fetch_iteration\n";
if (++$fetch_iteration > 10) {
fail "too many iterations trying to get sane fetch!";
}
my @look = map { "refs/$_" } @specs;
- my @lcmd = (@git, qw(ls-remote -q --refs), access_giturl(), @look);
+ my @lcmd = (@git, qw(ls-remote -q --refs), $url, @look);
debugcmd "|",@lcmd;
my %wantr;
my ($objid,$rrefname) = ($1,$2);
if (!$wanted_rref->($rrefname)) {
print STDERR <<END;
-warning: git-ls-remote @look reported $rrefname; this is silly, ignoring it.
+warning: git ls-remote @look reported $rrefname; this is silly, ignoring it.
END
next;
}
# OK, now %want is exactly what we want for refs in @specs
my @fspecs = map {
- return () if !m/\*$/ && !exists $wantr{"refs/$_"};
+ !m/\*$/ && !exists $wantr{"refs/$_"} ? () :
"+refs/$_:".lrfetchrefs."/$_";
} @specs;
- my @fcmd = (@git, qw(fetch -p -n -q), access_giturl(), @fspecs);
- runcmd_ordryrun_local @git, qw(fetch -p -n -q), access_giturl(),
- @fspecs;
+ printdebug "git_lrfetch_sane fspecs @fspecs\n";
+
+ my @fcmd = (@git, qw(fetch -p -n -q), $url, @fspecs);
+ runcmd_ordryrun_local @fcmd if @fspecs;
- %lrfetchrefs_f = ();
+ if (!$supplementary) {
+ %lrfetchrefs_f = ();
+ }
my %objgot;
git_for_each_ref(lrfetchrefs, sub {
$objgot{$objid} = 1;
});
- foreach my $lrefname (sort keys %lrfetchrefs_f) {
+ if ($supplementary) {
+ last;
+ }
+
+ foreach my $lrefname (sort keys %lrfetchrefs_f) {
my $rrefname = 'refs'.substr($lrefname, length lrfetchrefs);
if (!exists $wantr{$rrefname}) {
if ($wanted_rref->($rrefname)) {
printdebug <<END;
-git-fetch @fspecs created $lrefname which git-ls-remote @look didn't list.
+git-fetch @fspecs created $lrefname which git ls-remote @look didn't list.
END
} else {
print STDERR <<END
-warning: git-fetch @fspecs created $lrefname; this is silly, deleting it.
+warning: git fetch @fspecs created $lrefname; this is silly, deleting it.
END
}
runcmd_ordryrun_local @git, qw(update-ref -d), $lrefname;
next if $got eq $want;
if (!defined $objgot{$want}) {
print STDERR <<END;
-warning: git-ls-remote suggests we want $lrefname
+warning: git ls-remote suggests we want $lrefname
warning: and it should refer to $want
-warning: but git-fetch didn't fetch that object to any relevant ref.
+warning: but git fetch didn't fetch that object to any relevant ref.
warning: This may be due to a race with someone updating the server.
warning: Will try again...
END
next FETCH_ITERATION;
}
printdebug <<END;
-git-fetch @fspecs made $lrefname=$got but want git-ls-remote @look says $want
+git-fetch @fspecs made $lrefname=$got but want git ls-remote @look says $want
END
runcmd_ordryrun_local @git, qw(update-ref -m),
- "dgit fetch git-fetch fixup", $lrefname, $want;
+ "dgit fetch git fetch fixup", $lrefname, $want;
$lrfetchrefs_f{$lrefname} = $want;
}
last;
}
- printdebug "git_fetch_us: git-fetch --no-insane emulation complete\n",
+
+ if (defined $csuite) {
+ printdebug "git_lrfetch_sane: tidying any old suite lrfetchrefs\n";
+ git_for_each_ref("refs/dgit-fetch/$csuite", sub {
+ my ($objid,$objtype,$lrefname,$reftail) = @_;
+ next if $lrfetchrefs_f{$lrefname}; # $csuite eq $distro ?
+ runcmd_ordryrun_local @git, qw(update-ref -d), $lrefname;
+ });
+ }
+
+ printdebug "git_lrfetch_sane: git fetch --no-insane emulation complete\n",
Dumper(\%lrfetchrefs_f);
+}
+
+sub git_fetch_us () {
+ # Want to fetch only what we are going to use, unless
+ # deliberately-not-ff, in which case we must fetch everything.
+
+ my @specs = deliberately_not_fast_forward ? qw(tags/*) :
+ map { "tags/$_" }
+ (quiltmode_splitbrain
+ ? (map { $_->('*',access_nomdistro) }
+ \&debiantag_new, \&debiantag_maintview)
+ : debiantags('*',access_nomdistro));
+ push @specs, server_branch($csuite);
+ push @specs, $rewritemap;
+ push @specs, qw(heads/*) if deliberately_not_fast_forward;
+
+ my $url = access_giturl();
+ git_lrfetch_sane $url, 0, @specs;
my %here;
- my @tagpats = debiantags('*',access_basedistro);
+ my @tagpats = debiantags('*',access_nomdistro);
git_for_each_ref([map { "refs/tags/$_" } @tagpats], sub {
my ($objid,$objtype,$fullrefname,$reftail) = @_;
} elsif ($here{$lref} eq $objid) {
lrfetchref_used $fullrefname;
} else {
- print STDERR \
- "Not updateting $lref from $here{$lref} to $objid.\n";
+ print STDERR
+ "Not updating $lref from $here{$lref} to $objid.\n";
}
});
}
+#---------- dsc and archive handling ----------
+
sub mergeinfo_getclogp ($) {
# Ensures thit $mi->{Clogp} exists and returns it
my ($mi) = @_;
return getfield( (mergeinfo_getclogp $_[0]), 'Version' );
}
+sub fetch_from_archive_record_1 ($) {
+ my ($hash) = @_;
+ runcmd @git, qw(update-ref -m), "dgit fetch $csuite",
+ 'DGIT_ARCHIVE', $hash;
+ cmdoutput @git, qw(log -n2), $hash;
+ # ... gives git a chance to complain if our commit is malformed
+}
+
+sub fetch_from_archive_record_2 ($) {
+ my ($hash) = @_;
+ my @upd_cmd = (@git, qw(update-ref -m), 'dgit fetch', lrref(), $hash);
+ if (act_local()) {
+ cmdoutput @upd_cmd;
+ } else {
+ dryrun_report @upd_cmd;
+ }
+}
+
+sub parse_dsc_field_def_dsc_distro () {
+ $dsc_distro //= cfg qw(dgit.default.old-dsc-distro
+ dgit.default.distro);
+}
+
+sub parse_dsc_field ($$) {
+ my ($dsc, $what) = @_;
+ my $f;
+ foreach my $field (@ourdscfield) {
+ $f = $dsc->{$field};
+ last if defined $f;
+ }
+
+ if (!defined $f) {
+ progress "$what: NO git hash";
+ parse_dsc_field_def_dsc_distro();
+ } elsif (($dsc_hash, $dsc_distro, $dsc_hint_tag, $dsc_hint_url)
+ = $f =~ m/^(\w+)\s+($distro_re)\s+($versiontag_re)\s+(\S+)(?:\s|$)/) {
+ progress "$what: specified git info ($dsc_distro)";
+ $dsc_hint_tag = [ $dsc_hint_tag ];
+ } elsif ($f =~ m/^\w+\s*$/) {
+ $dsc_hash = $&;
+ parse_dsc_field_def_dsc_distro();
+ $dsc_hint_tag = [ debiantags +(getfield $dsc, 'Version'),
+ $dsc_distro ];
+ progress "$what: specified git hash";
+ } else {
+ fail "$what: invalid Dgit info";
+ }
+}
+
+sub resolve_dsc_field_commit ($$) {
+ my ($already_distro, $already_mapref) = @_;
+
+ return unless defined $dsc_hash;
+
+ my $mapref =
+ defined $already_mapref &&
+ ($already_distro eq $dsc_distro || !$chase_dsc_distro)
+ ? $already_mapref : undef;
+
+ my $do_fetch;
+ $do_fetch = sub {
+ my ($what, @fetch) = @_;
+
+ local $idistro = $dsc_distro;
+ my $lrf = lrfetchrefs;
+
+ if (!$chase_dsc_distro) {
+ progress
+ "not chasing .dsc distro $dsc_distro: not fetching $what";
+ return 0;
+ }
+
+ progress
+ ".dsc names distro $dsc_distro: fetching $what";
+
+ my $url = access_giturl();
+ if (!defined $url) {
+ defined $dsc_hint_url or fail <<END;
+.dsc Dgit metadata is in context of distro $dsc_distro
+for which we have no configured url and .dsc provides no hint
+END
+ my $proto =
+ $dsc_hint_url =~ m#^([-+0-9a-zA-Z]+):# ? $1 :
+ $dsc_hint_url =~ m#^/# ? 'file' : 'bad-syntax';
+ parse_cfg_bool "dsc-url-proto-ok", 'false',
+ cfg("dgit.dsc-url-proto-ok.$proto",
+ "dgit.default.dsc-url-proto-ok")
+ or fail <<END;
+.dsc Dgit metadata is in context of distro $dsc_distro
+for which we have no configured url;
+.dsc provides hinted url with protocol $proto which is unsafe.
+(can be overridden by config - consult documentation)
+END
+ $url = $dsc_hint_url;
+ }
+
+ git_lrfetch_sane $url, 1, @fetch;
+
+ return $lrf;
+ };
+
+ my $rewrite_enable = do {
+ local $idistro = $dsc_distro;
+ access_cfg('rewrite-map-enable', 'RETURN-UNDEF');
+ };
+
+ if (parse_cfg_bool 'rewrite-map-enable', 'true', $rewrite_enable) {
+ if (!defined $mapref) {
+ my $lrf = $do_fetch->("rewrite map", $rewritemap) or return;
+ $mapref = $lrf.'/'.$rewritemap;
+ }
+ my $rewritemapdata = git_cat_file $mapref.':map';
+ if (defined $rewritemapdata
+ && $rewritemapdata =~ m/^$dsc_hash(?:[ \t](\w+))/m) {
+ progress
+ "server's git history rewrite map contains a relevant entry!";
+
+ $dsc_hash = $1;
+ if (defined $dsc_hash) {
+ progress "using rewritten git hash in place of .dsc value";
+ } else {
+ progress "server data says .dsc hash is to be disregarded";
+ }
+ }
+ }
+
+ if (!defined git_cat_file $dsc_hash) {
+ my @tags = map { "tags/".$_ } @$dsc_hint_tag;
+ my $lrf = $do_fetch->("additional commits", @tags) &&
+ defined git_cat_file $dsc_hash
+ or fail <<END;
+.dsc Dgit metadata requires commit $dsc_hash
+but we could not obtain that object anywhere.
+END
+ foreach my $t (@tags) {
+ my $fullrefname = $lrf.'/'.$t;
+# print STDERR "CHK $t $fullrefname ".Dumper(\%lrfetchrefs_f);
+ next unless $lrfetchrefs_f{$fullrefname};
+ next unless is_fast_fwd "$fullrefname~0", $dsc_hash;
+ lrfetchref_used $fullrefname;
+ }
+ }
+}
+
sub fetch_from_archive () {
+ ensure_setup_existing_tree();
+
# Ensures that lrref() is what is actually in the archive, one way
# or another, according to us - ie this client's
# appropritaely-updated archive view. Also returns the commit id.
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";
- }
+ parse_dsc_field($dsc, 'last upload to archive');
+ resolve_dsc_field_commit access_basedistro,
+ lrfetchrefs."/".$rewritemap
} else {
progress "no version available from the archive";
}
};
if (defined $dsc_hash) {
- fail "missing remote git history even though dsc has hash -".
- " could not find ref ".rref()." at ".access_giturl()
- unless $lastpush_hash;
ensure_we_have_orig();
- if ($dsc_hash eq $lastpush_hash) {
+ if (!$lastpush_hash || $dsc_hash eq $lastpush_hash) {
@mergeinputs = $dsc_mergeinput
} elsif (is_fast_fwd($dsc_hash,$lastpush_hash)) {
print STDERR <<END or die $!;
my $author = clogp_authline $useclogp;
my $cversion = getfield $useclogp, 'Version';
- my $mcf = ".git/dgit/mergecommit";
+ my $mcf = dgit_privdir()."/mergecommit";
open MC, ">", $mcf or die "$mcf $!";
print MC <<END or die $!;
tree $tree
} else {
$hash = $mergeinputs[0]{Commit};
}
- progress "fetch hash=$hash\n";
+ printdebug "fetch hash=$hash\n";
my $chkff = sub {
my ($lasth, $what) = @_;
die "$lasth $hash $what ?" unless is_fast_fwd($lasth, $hash);
};
- $chkff->($lastpush_hash, 'dgit repo server tip (last push)');
+ $chkff->($lastpush_hash, 'dgit repo server tip (last push)')
+ if $lastpush_hash;
$chkff->($lastfetch_hash, 'local tracking tip (last fetch)');
- runcmd @git, qw(update-ref -m), "dgit fetch $csuite",
- 'DGIT_ARCHIVE', $hash;
- cmdoutput @git, qw(log -n2), $hash;
- # ... gives git a chance to complain if our commit is malformed
+ fetch_from_archive_record_1($hash);
if (defined $skew_warning_vsn) {
- mkpath '.git/dgit';
printdebug "SKEW CHECK WANT $skew_warning_vsn\n";
my $gotclogp = commit_getclogp($hash);
my $got_vsn = getfield $gotclogp, 'Version';
}
if ($lastfetch_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;
- }
+ fetch_from_archive_record_2($hash);
}
lrfetchref_used lrfetchref();
+ check_gitattrs($hash, "fetched source tree");
+
unshift @end, $del_lrfetchrefs;
return $hash;
}
my $driver = 'dpkg-mergechangelogs';
my $cb = "merge.$driver";
- my $attrs = '.git/info/attributes';
- ensuredir '.git/info';
+ confess unless defined $maindir;
+ my $attrs = "$maindir_gitcommon/info/attributes";
+ ensuredir "$maindir_gitcommon/info";
open NATTRS, ">", "$attrs.new" or die "$attrs.new $!";
if (!open ATTRS, "<", $attrs) {
$setup->('name', 'DEBFULLNAME');
}
+sub ensure_setup_existing_tree () {
+ my $k = "remote.$remotename.skipdefaultupdate";
+ my $c = git_get_config $k;
+ return if defined $c;
+ set_local_git_config $k, 'true';
+}
+
+sub open_main_gitattrs () {
+ confess 'internal error no maindir' unless defined $maindir;
+ my $gai = new IO::File "$maindir_gitcommon/info/attributes"
+ or $!==ENOENT
+ or die "open $maindir_gitcommon/info/attributes: $!";
+ return $gai;
+}
+
+sub is_gitattrs_setup () {
+ my $gai = open_main_gitattrs();
+ return 0 unless $gai;
+ while (<$gai>) {
+ return 1 if m{^\[attr\]dgit-defuse-attrs\s};
+ }
+ $gai->error and die $!;
+ return 0;
+}
+
+sub setup_gitattrs (;$) {
+ my ($always) = @_;
+ return unless $always || access_cfg_bool(1, 'setup-gitattributes');
+
+ if (is_gitattrs_setup()) {
+ progress <<END;
+[attr]dgit-defuse-attrs already found in .git/info/attributes
+ not doing further gitattributes setup
+END
+ return;
+ }
+ my $af = "$maindir_gitcommon/info/attributes";
+ ensuredir "$maindir_gitcommon/info";
+ open GAO, "> $af.new" or die $!;
+ print GAO <<END or die $!;
+* dgit-defuse-attrs
+[attr]dgit-defuse-attrs $negate_harmful_gitattrs
+# ^ see GITATTRIBUTES in dgit(7) and dgit setup-new-tree in dgit(1)
+END
+ my $gai = open_main_gitattrs();
+ if ($gai) {
+ while (<$gai>) {
+ chomp;
+ print GAO $_, "\n" or die $!;
+ }
+ $gai->error and die $!;
+ }
+ close GAO or die $!;
+ rename "$af.new", "$af" or die "install $af: $!";
+}
+
sub setup_new_tree () {
setup_mergechangelogs();
setup_useremail();
+ setup_gitattrs();
+}
+
+sub check_gitattrs ($$) {
+ my ($treeish, $what) = @_;
+
+ return if is_gitattrs_setup;
+
+ local $/="\0";
+ my @cmd = (@git, qw(ls-tree -lrz --), "${treeish}:");
+ debugcmd "|",@cmd;
+ my $gafl = new IO::File;
+ open $gafl, "-|", @cmd or die $!;
+ while (<$gafl>) {
+ chomp or die;
+ s/^\d+\s+\w+\s+\w+\s+(\d+)\t// or die;
+ next if $1 == 0;
+ next unless m{(?:^|/)\.gitattributes$};
+
+ # oh dear, found one
+ print STDERR <<END;
+dgit: warning: $what contains .gitattributes
+dgit: .gitattributes have not been defused. Recommended: dgit setup-new-tree.
+END
+ close $gafl;
+ return;
+ }
+ # tree contains no .gitattributes files
+ $?=0; $!=0; close $gafl or failedcmd @cmd;
+}
+
+
+sub multisuite_suite_child ($$$) {
+ my ($tsuite, $merginputs, $fn) = @_;
+ # in child, sets things up, calls $fn->(), and returns undef
+ # in parent, returns canonical suite name for $tsuite
+ my $canonsuitefh = IO::File::new_tmpfile;
+ my $pid = fork // die $!;
+ if (!$pid) {
+ forkcheck_setup();
+ $isuite = $tsuite;
+ $us .= " [$isuite]";
+ $debugprefix .= " ";
+ progress "fetching $tsuite...";
+ canonicalise_suite();
+ print $canonsuitefh $csuite, "\n" or die $!;
+ close $canonsuitefh or die $!;
+ $fn->();
+ return undef;
+ }
+ waitpid $pid,0 == $pid or die $!;
+ fail "failed to obtain $tsuite: ".waitstatusmsg() if $? && $?!=256*4;
+ seek $canonsuitefh,0,0 or die $!;
+ local $csuite = <$canonsuitefh>;
+ die $! unless defined $csuite && chomp $csuite;
+ if ($? == 256*4) {
+ printdebug "multisuite $tsuite missing\n";
+ return $csuite;
+ }
+ printdebug "multisuite $tsuite ok (canon=$csuite)\n";
+ push @$merginputs, {
+ Ref => lrref,
+ Info => $csuite,
+ };
+ return $csuite;
+}
+
+sub fork_for_multisuite ($) {
+ my ($before_fetch_merge) = @_;
+ # if nothing unusual, just returns ''
+ #
+ # if multisuite:
+ # returns 0 to caller in child, to do first of the specified suites
+ # in child, $csuite is not yet set
+ #
+ # returns 1 to caller in parent, to finish up anything needed after
+ # in parent, $csuite is set to canonicalised portmanteau
+
+ my $org_isuite = $isuite;
+ my @suites = split /\,/, $isuite;
+ return '' unless @suites > 1;
+ printdebug "fork_for_multisuite: @suites\n";
+
+ my @mergeinputs;
+
+ my $cbasesuite = multisuite_suite_child($suites[0], \@mergeinputs,
+ sub { });
+ return 0 unless defined $cbasesuite;
+
+ fail "package $package missing in (base suite) $cbasesuite"
+ unless @mergeinputs;
+
+ my @csuites = ($cbasesuite);
+
+ $before_fetch_merge->();
+
+ foreach my $tsuite (@suites[1..$#suites]) {
+ $tsuite =~ s/^-/$cbasesuite-/;
+ my $csubsuite = multisuite_suite_child($tsuite, \@mergeinputs,
+ sub {
+ @end = ();
+ fetch();
+ exit 0;
+ });
+ # xxx collecte the ref here
+
+ $csubsuite =~ s/^\Q$cbasesuite\E-/-/;
+ push @csuites, $csubsuite;
+ }
+
+ foreach my $mi (@mergeinputs) {
+ my $ref = git_get_ref $mi->{Ref};
+ die "$mi->{Ref} ?" unless length $ref;
+ $mi->{Commit} = $ref;
+ }
+
+ $csuite = join ",", @csuites;
+
+ my $previous = git_get_ref lrref;
+ if ($previous) {
+ unshift @mergeinputs, {
+ Commit => $previous,
+ Info => "local combined tracking branch",
+ Warning =>
+ "archive seems to have rewound: local tracking branch is ahead!",
+ };
+ }
+
+ foreach my $ix (0..$#mergeinputs) {
+ $mergeinputs[$ix]{Index} = $ix;
+ }
+
+ @mergeinputs = sort {
+ -version_compare(mergeinfo_version $a,
+ mergeinfo_version $b) # highest version first
+ or
+ $a->{Index} <=> $b->{Index}; # earliest in spec first
+ } @mergeinputs;
+
+ my @needed;
+
+ NEEDED:
+ foreach my $mi (@mergeinputs) {
+ printdebug "multisuite merge check $mi->{Info}\n";
+ foreach my $previous (@needed) {
+ next unless is_fast_fwd $mi->{Commit}, $previous->{Commit};
+ printdebug "multisuite merge un-needed $previous->{Info}\n";
+ next NEEDED;
+ }
+ push @needed, $mi;
+ printdebug "multisuite merge this-needed\n";
+ $mi->{Character} = '+';
+ }
+
+ $needed[0]{Character} = '*';
+
+ my $output = $needed[0]{Commit};
+
+ if (@needed > 1) {
+ printdebug "multisuite merge nontrivial\n";
+ my $tree = cmdoutput qw(git rev-parse), $needed[0]{Commit}.':';
+
+ my $commit = "tree $tree\n";
+ my $msg = "Combine archive branches $csuite [dgit]\n\n".
+ "Input branches:\n";
+
+ foreach my $mi (sort { $a->{Index} <=> $b->{Index} } @mergeinputs) {
+ printdebug "multisuite merge include $mi->{Info}\n";
+ $mi->{Character} //= ' ';
+ $commit .= "parent $mi->{Commit}\n";
+ $msg .= sprintf " %s %-25s %s\n",
+ $mi->{Character},
+ (mergeinfo_version $mi),
+ $mi->{Info};
+ }
+ my $authline = clogp_authline mergeinfo_getclogp $needed[0];
+ $msg .= "\nKey\n".
+ " * marks the highest version branch, which choose to use\n".
+ " + marks each branch which was not already an ancestor\n\n".
+ "[dgit multi-suite $csuite]\n";
+ $commit .=
+ "author $authline\n".
+ "committer $authline\n\n";
+ $output = make_commit_text $commit.$msg;
+ printdebug "multisuite merge generated $output\n";
+ }
+
+ fetch_from_archive_record_1($output);
+ fetch_from_archive_record_2($output);
+
+ progress "calculated combined tracking suite $csuite";
+
+ return 1;
+}
+
+sub clone_set_head () {
+ open H, "> .git/HEAD" or die $!;
+ print H "ref: ".lref()."\n" or die $!;
+ close H or die $!;
+}
+sub clone_finish ($) {
+ my ($dstdir) = @_;
+ runcmd @git, qw(reset --hard), lrref();
+ runcmd qw(bash -ec), <<'END';
+ set -o pipefail
+ git ls-tree -r --name-only -z HEAD | \
+ xargs -0r touch -h -r . --
+END
+ printdone "ready for work in $dstdir";
}
sub clone ($) {
+ # in multisuite, returns twice!
+ # once in parent after first suite fetched,
+ # and then again in child after everything is finished
my ($dstdir) = @_;
- canonicalise_suite();
badusage "dry run makes no sense with clone" unless act_local();
+
+ my $multi_fetched = fork_for_multisuite(sub {
+ printdebug "multi clone before fetch merge\n";
+ changedir $dstdir;
+ record_maindir();
+ });
+ if ($multi_fetched) {
+ printdebug "multi clone after fetch merge\n";
+ clone_set_head();
+ clone_finish($dstdir);
+ return;
+ }
+ printdebug "clone main body\n";
+
+ canonicalise_suite();
my $hasgit = check_for_git();
mkdir $dstdir or fail "create \`$dstdir': $!";
changedir $dstdir;
runcmd @git, qw(init -q);
+ record_maindir();
+ setup_new_tree();
+ clone_set_head();
my $giturl = access_giturl(1);
if (defined $giturl) {
- 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) {
$vcsgiturl =~ s/\s+-b\s+\S+//g;
runcmd @git, qw(remote add vcs-git), $vcsgiturl;
}
- setup_new_tree();
- runcmd @git, qw(reset --hard), lrref();
- printdone "ready for work in $dstdir";
+ clone_finish($dstdir);
}
sub fetch () {
+ canonicalise_suite();
if (check_for_git()) {
git_fetch_us();
}
}
sub pull () {
- fetch();
+ my $multi_fetched = fork_for_multisuite(sub { });
+ fetch() unless $multi_fetched; # parent
+ return if $multi_fetched eq '0'; # child
runcmd_ordryrun_local @git, qw(merge -m),"Merge from $csuite [dgit]",
lrref();
printdone "fetched to ".lrref()." and merged into HEAD";
return if $ignoredirty;
- my @cmd = (@git, qw(diff --quiet HEAD));
- debugcmd "+",@cmd;
- $!=0; $?=-1; system @cmd;
- return if !$?;
- if ($?==256) {
- fail "working tree is dirty (does not match HEAD)";
- } else {
- failedcmd @cmd;
- }
+ git_check_unmodified();
}
sub commit_admin ($) {
}
my @adds = map { s/[][*?\\]/\\$&/g; $_; } sort keys %adds;
runcmd_ordryrun_local @git, qw(add -f), @adds;
- commit_admin "Commit Debian 3.0 (quilt) metadata";
+ commit_admin <<END
+Commit Debian 3.0 (quilt) metadata
+
+[dgit ($our_version) quilt-fixup]
+END
}
sub get_source_format () {
return 1;
}
+sub maybe_split_brain_save ($$$) {
+ my ($headref, $dgitview, $msg) = @_;
+ # => message fragment "$saved" describing disposition of $dgitview
+ return "commit id $dgitview" unless defined $split_brain_save;
+ my @cmd = (shell_cmd 'cd "$1"; shift', $maindir,
+ @git, qw(update-ref -m),
+ "dgit --dgit-view-save $msg HEAD=$headref",
+ $split_brain_save, $dgitview);
+ runcmd @cmd;
+ return "and left in $split_brain_save";
+}
+
# An "infopair" is a tuple [ $thing, $what ]
# (often $thing is a commit hash; $what is a description)
} else {
my $v = $i_arch_v->[0];
progress "Checking package changelog for archive version $v ...";
+ my $cd;
eval {
my @xa = ("-f$v", "-t$v");
my $vclogp = parsechangelog @xa;
- my $cv = [ (getfield $vclogp, 'Version'),
- "Version field from dpkg-parsechangelog @xa" ];
+ my $gf = sub {
+ my ($fn) = @_;
+ [ (getfield $vclogp, $fn),
+ "$fn field from dpkg-parsechangelog @xa" ];
+ };
+ my $cv = $gf->('Version');
infopair_cond_equal($i_arch_v, $cv);
+ $cd = $gf->('Distribution');
};
if ($@) {
$@ =~ s/^dgit: //gm;
fail "$@".
"Perhaps debian/changelog does not mention $v ?";
}
+ fail <<END if $cd->[0] =~ m/UNRELEASED/;
+$cd->[1] is $cd->[0]
+Your tree seems to based on earlier (not uploaded) $v.
+END
}
}
: !length $overwrite_version ? " --overwrite"
: " --overwrite=".$overwrite_version;
- mkpath '.git/dgit';
- my $pmf = ".git/dgit/pseudomerge";
+ my $pmf = dgit_privdir()."/pseudomerge";
open MC, ">", $pmf or die "$pmf $!";
print MC <<END or die $!;
tree $tree
parent $dgitview
parent $archive_hash
author $authline
-commiter $authline
+committer $authline
$msg_msg
# this: $dgitview'
#
+ return $dgitview unless defined $archive_hash;
+ return $dgitview if deliberately_not_fast_forward();
+
printdebug "splitbrain_pseudomerge...\n";
my $i_arch_v = pseudomerge_version_check($clogp, $archive_hash);
- return $dgitview unless defined $archive_hash;
-
if (!defined $overwrite_version) {
progress "Checking that HEAD inciudes all changes in archive...";
}
return $dgitview if is_fast_fwd $archive_hash, $dgitview;
- my $t_dep14 = debiantag_maintview $i_arch_v->[0], access_basedistro;
- my $i_dep14 = infopair_lrf_tag_lookup($t_dep14, "maintainer view tag");
- my $t_dgit = debiantag_new $i_arch_v->[0], access_basedistro;
- my $i_dgit = infopair_lrf_tag_lookup($t_dgit, "dgit view tag");
- my $i_archive = [ $archive_hash, "current archive contents" ];
-
- printdebug "splitbrain_pseudomerge i_archive @$i_archive\n";
-
- infopair_cond_equal($i_dgit, $i_archive);
- infopair_cond_ff($i_dep14, $i_dgit);
- $overwrite_version // infopair_cond_ff($i_dep14, [ $maintview, 'HEAD' ]);
+ if (defined $overwrite_version) {
+ } elsif (!eval {
+ my $t_dep14 = debiantag_maintview $i_arch_v->[0], access_nomdistro;
+ my $i_dep14 = infopair_lrf_tag_lookup($t_dep14, "maintainer view tag");
+ my $t_dgit = debiantag_new $i_arch_v->[0], access_nomdistro;
+ my $i_dgit = infopair_lrf_tag_lookup($t_dgit, "dgit view tag");
+ my $i_archive = [ $archive_hash, "current archive contents" ];
+
+ printdebug "splitbrain_pseudomerge i_archive @$i_archive\n";
+
+ infopair_cond_equal($i_dgit, $i_archive);
+ infopair_cond_ff($i_dep14, $i_dgit);
+ infopair_cond_ff($i_dep14, [ $maintview, 'HEAD' ]);
+ 1;
+ }) {
+ print STDERR <<END;
+$us: check failed (maybe --overwrite is needed, consult documentation)
+END
+ die "$@";
+ }
my $r = pseudomerge_make_commit
$clogp, $dgitview, $archive_hash, $i_arch_v,
"dgit --quilt=$quilt_mode",
(defined $overwrite_version ? <<END_OVERWR : <<END_MAKEFF);
-Declare fast forward from $overwrite_version
+Declare fast forward from $i_arch_v->[0]
END_OVERWR
Make fast forward from $i_arch_v->[0]
END_MAKEFF
+ maybe_split_brain_save $maintview, $r, "pseudomerge";
+
progress "Made pseudo-merge of $i_arch_v->[0] into dgit view.";
return $r;
}
my $i_arch_v = pseudomerge_version_check($clogp, $archive_hash);
- my @tagformats = access_cfg_tagformats();
- my @t_overwr =
- map { $_->($i_arch_v->[0], access_basedistro) }
- (grep { m/^(?:old|hist)$/ } @tagformats)
- ? \&debiantags : \&debiantag_new;
- my $i_overwr = infopair_lrf_tag_lookup \@t_overwr, "previous version tag";
- my $i_archive = [ $archive_hash, "current archive contents" ];
-
- infopair_cond_equal($i_overwr, $i_archive);
-
return $head if is_fast_fwd $archive_hash, $head;
my $m = "Declare fast forward from $i_arch_v->[0]";
my $clogp = Dpkg::Control::Hash->new();
$clogp->load($clogpfn) or die;
- $package = getfield $clogp, 'Source';
+ my $clogpackage = getfield $clogp, 'Source';
+ $package //= $clogpackage;
+ fail "-p specified $package but changelog specified $clogpackage"
+ unless $package eq $clogpackage;
my $cversion = getfield $clogp, 'Version';
- my $tag = debiantag($cversion, access_basedistro);
- runcmd @git, qw(check-ref-format), $tag;
+
+ if (!$we_are_initiator) {
+ # rpush initiator can't do this because it doesn't have $isuite yet
+ my $tag = debiantag($cversion, access_nomdistro);
+ runcmd @git, qw(check-ref-format), $tag;
+ }
my $dscfn = dscfn($cversion);
TfSuffix => '-maintview',
View => 'maint',
};
- }
+ } elsif ($dodep14tag eq 'no' ? 0
+ : $dodep14tag eq 'want' ? access_cfg_tagformats_can_splitbrain
+ : $dodep14tag eq 'always'
+ ? (access_cfg_tagformats_can_splitbrain or fail <<END)
+--dep14tag-always (or equivalent in config) means server must support
+ both "new" and "maint" tag formats, but config says it doesn't.
+END
+ : die "$dodep14tag ?") {
+ push @tagwants, {
+ TagFn => \&debiantag_maintview,
+ Objid => $dgithead,
+ TfSuffix => '-dgit',
+ View => 'dgit',
+ };
+ };
foreach my $tw (@tagwants) {
- $tw->{Tag} = $tw->{TagFn}($cversion, access_basedistro);
+ $tw->{Tag} = $tw->{TagFn}($cversion, access_nomdistro);
$tw->{Tfn} = sub { $tfbase.$tw->{TfSuffix}.$_[0]; };
}
printdebug 'push_tagwants: ', Dumper(\@_, \@tagwants);
die unless $tagwants->[0]{View} eq 'dgit';
- $dsc->{$ourdscfield[0]} = $tagwants->[0]{Objid};
+ my $declaredistro = access_nomdistro();
+ my $reader_giturl = do { local $access_forpush=0; access_giturl(); };
+ $dsc->{$ourdscfield[0]} = join " ",
+ $tagwants->[0]{Objid}, $declaredistro, $tagwants->[0]{Tag},
+ $reader_giturl;
$dsc->save("$dscfn.tmp") or die $!;
my $changes = parsecontrol($changesfile,$changesfilewhat);
# to control the "tagger" (b) we can do remote signing
my $authline = clogp_authline $clogp;
my $delibs = join(" ", "",@deliberatelies);
- my $declaredistro = access_basedistro();
my $mktag = sub {
my ($tw) = @_;
prep_ud();
access_giturl(); # check that success is vaguely likely
+ rpush_handle_protovsn_bothends() if $we_are_initiator;
select_tagformat();
- my $clogpfn = ".git/dgit/changelog.822.tmp";
+ my $clogpfn = dgit_privdir()."/changelog.822.tmp";
runcmd shell_cmd "exec >$clogpfn", qw(dpkg-parsechangelog);
responder_send_file('parsed-changelog', $clogpfn);
my $dscpath = "$buildproductsdir/$dscfn";
stat_exists $dscpath or
- fail "looked for .dsc $dscfn, but $!;".
+ fail "looked for .dsc $dscpath, but $!;".
" maybe you forgot to build";
responder_send_file('dsc', $dscpath);
my $dgithead = $actualhead;
my $maintviewhead = undef;
+ my $upstreamversion = upstreamversion $clogp->{Version};
+
if (madformat_wantfixup($format)) {
# user might have not used dgit build, so maybe do this now:
if (quiltmode_splitbrain()) {
- my $upstreamversion = $clogp->{Version};
- $upstreamversion =~ s/-[^-]*$//;
- changedir $ud;
+ changedir $playground;
quilt_make_fake_dsc($upstreamversion);
- my ($dgitview, $cachekey) =
+ my $cachekey;
+ ($dgithead, $cachekey) =
quilt_check_splitbrain_cache($actualhead, $upstreamversion);
- $dgitview or fail
+ $dgithead or fail
"--quilt=$quilt_mode but no cached dgit view:
- perhaps tree changed since dgit build[-source] ?";
+ perhaps HEAD changed since dgit build[-source] ?";
$split_brain = 1;
$dgithead = splitbrain_pseudomerge($clogp,
- $actualhead, $dgitview,
+ $actualhead, $dgithead,
$archive_hash);
$maintviewhead = $actualhead;
- changedir '../../../..';
+ changedir $maindir;
prep_ud(); # so _only_subdir() works, below
} else {
commit_quilty_patch();
}
}
- changedir $ud;
+ changedir $playground;
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();
+ $dscpath =~ m#^/# ? $dscpath : "$maindir/$dscpath";
+ my ($tree,$dir) = mktree_in_ud_from_only_subdir("source package");
check_for_vendor_patches() if madformat($dsc->{format});
- changedir '../../../..';
- my $diffopt = $debuglevel>0 ? '--exit-code' : '--quiet';
- my @diffcmd = (@git, qw(diff), $diffopt, $tree, $dgithead);
+ changedir $maindir;
+ my @diffcmd = (@git, qw(diff --quiet), $tree, $dgithead);
debugcmd "+",@diffcmd;
$!=0; $?=-1;
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)");
+ my $referent = $split_brain ? $dgithead : 'HEAD';
+ my $diffs = cmdoutput @git, qw(diff --stat), $tree, $dgithead;
+
+ my @mode_changes;
+ my $raw = cmdoutput @git,
+ qw(diff --no-renames -z -r --raw), $tree, $dgithead;
+ my $changed;
+ foreach (split /\0/, $raw) {
+ if (defined $changed) {
+ push @mode_changes, "$changed: $_\n" if $changed;
+ $changed = undef;
+ next;
+ } elsif (m/^:0+ 0+ /) {
+ $changed = '';
+ } elsif (m/^:(?:10*)?(\d+) (?:10*)?(\d+) /) {
+ $changed = "Mode change from $1 to $2"
+ } else {
+ die "$_ ?";
+ }
+ }
+ if (@mode_changes) {
+ fail <<END.(join '', @mode_changes).<<END;
+HEAD specifies a different tree to $dscfn:
+$diffs
+END
+There is a problem with your source tree (see dgit(7) for some hints).
+To see a full diff, run git diff $tree $referent
+END
+ }
+
+ fail <<END;
+HEAD specifies a different tree to $dscfn:
+$diffs
+Perhaps you forgot to build. Or perhaps there is a problem with your
+ source tree (see dgit(7) for some hints). To see a full diff, run
+ git diff $tree $referent
+END
} else {
failedcmd @diffcmd;
}
$changesfile = "$buildproductsdir/$changesfile";
}
+ # Check that changes and .dsc agree enough
+ $changesfile =~ m{[^/]*$};
+ my $changes = parsecontrol($changesfile,$&);
+ files_compare_inputs($dsc, $changes)
+ unless forceing [qw(dsc-changes-mismatch)];
+
+ # Perhaps adjust .dsc to contain right set of origs
+ changes_update_origs_from_dsc($dsc, $changes, $upstreamversion,
+ $changesfile)
+ unless forceing [qw(changes-origs-exactly)];
+
# Checks complete, we're going to try and go ahead:
responder_send_file('changes',$changesfile);
responder_send_command("param head $dgithead");
responder_send_command("param csuite $csuite");
+ responder_send_command("param isuite $isuite");
responder_send_command("param tagformat $tagformat");
if (defined $maintviewhead) {
die unless ($protovsn//4) >= 4;
responder_send_command("param maint-view $maintviewhead");
}
+ # Perhaps send buildinfo(s) for signing
+ my $changes_files = getfield $changes, 'Files';
+ my @buildinfos = ($changes_files =~ m/ .* (\S+\.buildinfo)$/mg);
+ foreach my $bi (@buildinfos) {
+ responder_send_command("param buildinfo-filename $bi");
+ responder_send_file('buildinfo', "$buildproductsdir/$bi");
+ }
+
if (deliberately_not_fast_forward) {
git_for_each_ref(lrfetchrefs, sub {
my ($objid,$objtype,$lrfetchrefname,$reftail) = @_;
}
my @tagwants = push_tagwants($cversion, $dgithead, $maintviewhead,
- ".git/dgit/tag");
+ dgit_privdir()."/tag");
my @tagobjfns;
supplementary_message(<<'END');
push @pushrefs, $forceflag."refs/tags/$tw->{Tag}";
}
- runcmd_ordryrun @git, qw(push),access_giturl(), @pushrefs;
+ runcmd_ordryrun @git,
+ qw(-c push.followTags=false 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.
-If you want to try again, you must use a new version number.
+Push failed, while obtaining signatures on the .changes and .dsc.
+If it was just that the signature failed, you may try again by using
+debsign by hand to sign the changes
+ $changesfile
+and then dput to complete the upload.
+If you need to change the package, you must use a new version number.
END
if ($we_are_responder) {
my $dryrunsuffix = act_local() ? "" : ".tmp";
+ my @rfiles = ($dscpath, $changesfile);
+ push @rfiles, map { "$buildproductsdir/$_" } @buildinfos;
responder_receive_files('signed-dsc-changes',
- "$dscpath$dryrunsuffix",
- "$changesfile$dryrunsuffix");
+ map { "$_$dryrunsuffix" } @rfiles);
} else {
if (act_local()) {
rename "$dscpath.tmp",$dscpath or die "$dscfn $!";
responder_send_command("complete");
}
+sub pre_clone () {
+ not_necessarily_a_tree();
+}
sub cmd_clone {
parseopts();
- notpushing();
my $dstdir;
badusage "-p is not allowed with clone; specify as argument instead"
if defined $package;
} else {
badusage "incorrect arguments to dgit clone";
}
- $dstdir ||= "$package";
+ notpushing();
+ $dstdir ||= "$package";
if (stat_exists $dstdir) {
fail "$dstdir already exists";
}
return if $!==&ENOENT;
die "chdir $cwd_remove: $!";
}
+ printdebug "clone rmonerror removing $dstdir\n";
if (stat $dstdir) {
rmtree($dstdir) or die "remove $dstdir: $!\n";
- } elsif (!grep { $! == $_ }
+ } elsif (grep { $! == $_ }
(ENOENT, ENOTDIR, EACCES, EPERM, ELOOP)) {
} else {
print STDERR "check whether to remove $dstdir: $!\n";
}
sub branchsuite () {
- my $branch = cmdoutput_errok @git, qw(symbolic-ref HEAD);
- if ($branch =~ m#$lbranch_re#o) {
+ my $branch = git_get_symref();
+ if (defined $branch && $branch =~ m#$lbranch_re#o) {
return $1;
} else {
return undef;
}
sub fetchpullargs () {
- notpushing();
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
+ $isuite = branchsuite();
if (!$isuite) {
my $clogp = parsechangelog();
- $isuite = getfield $clogp, 'Distribution';
+ my $clogsuite = getfield $clogp, 'Distribution';
+ $isuite= $clogsuite if $clogsuite ne 'UNRELEASED';
}
- canonicalise_suite();
- progress "fetching from suite $csuite";
} elsif (@ARGV==1) {
($isuite) = @ARGV;
- canonicalise_suite();
} else {
badusage "incorrect arguments to dgit fetch or dgit pull";
}
+ notpushing();
}
sub cmd_fetch {
parseopts();
fetchpullargs();
+ my $multi_fetched = fork_for_multisuite(sub { });
+ exit 0 if $multi_fetched;
fetch();
}
sub cmd_pull {
parseopts();
fetchpullargs();
+ if (quiltmode_splitbrain()) {
+ my ($format, $fopts) = get_source_format();
+ madformat($format) and fail <<END
+dgit pull not yet supported in split view mode (--quilt=$quilt_mode)
+END
+ }
pull();
}
-sub cmd_push {
+sub prep_push () {
parseopts();
+ build_or_push_prep_early();
pushing();
- 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";
+ badusage "incorrect arguments to dgit $subcommand";
}
- $isuite = getfield $clogp, 'Distribution';
if ($new_package) {
local ($package) = $existing_package; # this is a hack
canonicalise_suite();
if (defined $specsuite &&
$specsuite ne $isuite &&
$specsuite ne $csuite) {
- fail "dgit push: changelog specifies $isuite ($csuite)".
+ fail "dgit $subcommand: changelog specifies $isuite ($csuite)".
" but command line specifies $specsuite";
}
+}
+
+sub cmd_push {
+ prep_push();
+ dopush();
+}
+
+sub cmd_push_source {
+ prep_push();
+ if ($changesfile) {
+ my $changes = parsecontrol("$buildproductsdir/$changesfile",
+ "source changes file");
+ unless (test_source_only_changes($changes)) {
+ fail "user-specified changes file is not source-only";
+ }
+ } else {
+ # Building a source package is very fast, so just do it
+ build_source_for_push();
+ }
dopush();
}
#---------- remote commands' implementation ----------
-sub cmd_remote_push_build_host {
+sub pre_remote_push_build_host {
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 $!;
" but invocation host has $vsnwant"
unless defined $protovsn;
- responder_send_command("dgit-remote-push-ready $protovsn");
- rpush_handle_protovsn_bothends();
changedir $dir;
+}
+sub cmd_remote_push_build_host {
+ responder_send_command("dgit-remote-push-ready $protovsn");
&cmd_push;
}
+sub pre_remote_push_responder { pre_remote_push_build_host(); }
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)
}
}
-END { i_cleanup(); }
+END {
+ return unless forkcheck_mainprocess();
+ i_cleanup();
+}
sub i_method {
my ($base,$selector,@args) = @_;
{ no strict qw(refs); &{"${base}_${selector}"}(@args); }
}
+sub pre_rpush () {
+ not_necessarily_a_tree();
+}
sub cmd_rpush {
- pushing();
my $host = nextarg;
my $dir;
if ($host =~ m/^((?:[^][]|\[[^][]*\])*)\:/) {
my @cmd = (@ssh, $host, shellquote @rdgit);
debugcmd "+",@cmd;
+ $we_are_initiator=1;
+
if (defined $initiator_tempdir) {
rmtree $initiator_tempdir;
mkdir $initiator_tempdir, 0700 or die "$initiator_tempdir: $!";
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+)(?: (.*))?$/;
sub i_resp_want ($) {
my ($keyword) = @_;
die "$keyword ?" if $i_wanted{$keyword}++;
+
+ defined $i_param{'csuite'} or badproto \*RO, "premature desire, no csuite";
+ $isuite = $i_param{'isuite'} // $i_param{'csuite'};
+ die unless $isuite =~ m/^$suite_re$/;
+
+ pushing();
+ rpush_handle_protovsn_bothends();
+
+ fail "rpush negotiated protocol version $protovsn".
+ " which does not support quilt mode $quilt_mode"
+ if quiltmode_splitbrain;
+
my @localpaths = i_method "i_want", $keyword;
printdebug "[[ $keyword @localpaths\n";
foreach my $localpath (@localpaths) {
print RI "files-end\n" or die $!;
}
-our ($i_clogp, $i_version, $i_dscfn, $i_changesfn);
+our ($i_clogp, $i_version, $i_dscfn, $i_changesfn, @i_buildinfos);
sub i_localname_parsed_changelog {
return "remote-changelog.822";
}
sub i_file_dsc { }
+sub i_localname_buildinfo ($) {
+ my $bi = $i_param{'buildinfo-filename'};
+ defined $bi or badproto \*RO, "buildinfo before filename";
+ defined $i_changesfn or badproto \*RO, "buildinfo before changes";
+ $bi =~ m{^\Q$package\E_[!-.0-~]*\.buildinfo$}s
+ or badproto \*RO, "improper buildinfo filename";
+ return $&;
+}
+sub i_file_buildinfo {
+ my $bi = $i_param{'buildinfo-filename'};
+ my $bd = parsecontrol "$i_tmp/$bi", $bi;
+ my $ch = parsecontrol "$i_tmp/$i_changesfn", 'changes';
+ if (!forceing [qw(buildinfo-changes-mismatch)]) {
+ files_compare_inputs($bd, $ch);
+ (getfield $bd, $_) eq (getfield $ch, $_) or
+ fail "buildinfo mismatch $_"
+ foreach qw(Source Version);
+ !defined $bd->{$_} or
+ fail "buildinfo contains $_"
+ foreach qw(Changes Changed-by Distribution);
+ }
+ push @i_buildinfos, $bi;
+ delete $i_param{'buildinfo-filename'};
+}
+
sub i_localname_changes {
defined $i_dscfn or badproto \*RO, "dsc (before parsed-changelog)";
$i_changesfn = $i_dscfn;
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);
+ return ($i_dscfn, $i_changesfn, @i_buildinfos);
}
#---------- building etc. ----------
my ($patchname,$author,$msg, $xinfo) = @_;
$xinfo //= '';
- mkpath '.git/dgit';
+ mkpath '.git/dgit'; # we are in playtree
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;
+ $msg =~ s/\n+/\n\n/;
print O <<END or die $!;
-Description: $msg
-Author: $author
-$xinfo
+From: $author
+${xinfo}Subject: $msg
---
END
local $ENV{'EDITOR'} = cmdoutput qw(realpath --), $0;
local $ENV{'VISUAL'} = $ENV{'EDITOR'};
local $ENV{$fakeeditorenv} = cmdoutput qw(realpath --), $descfn;
- runcmd @dpkgsource, qw(--commit .), $patchname;
+ runcmd @dpkgsource, qw(--commit --include-removal .), $patchname;
}
}
-sub quiltify_trees_differ ($$;$$) {
- my ($x,$y,$finegrained,$ignorenamesr) = @_;
+sub quiltify_trees_differ ($$;$$$) {
+ my ($x,$y,$finegrained,$ignorenamesr,$unrepres) = @_;
# returns true iff the two tree objects differ other than in debian/
# with $finegrained,
# returns bitmask 01 - differ in upstream files except .gitignore
# 02 - differ in .gitignore
# if $ignorenamesr is defined, $ingorenamesr->{$fn}
# is set for each modified .gitignore filename $fn
+ # if $unrepres is defined, array ref to which is appeneded
+ # a list of unrepresentable changes (removals of upstream files
+ # (as messages)
local $/=undef;
- my @cmd = (@git, qw(diff-tree --name-only -z));
- push @cmd, qw(-r) if $finegrained;
+ my @cmd = (@git, qw(diff-tree -z --no-renames));
+ push @cmd, qw(--name-only) unless $unrepres;
+ push @cmd, qw(-r) if $finegrained || $unrepres;
push @cmd, $x, $y;
my $diffs= cmdoutput @cmd;
my $r = 0;
+ my @lmodes;
foreach my $f (split /\0/, $diffs) {
+ if ($unrepres && !@lmodes) {
+ @lmodes = $f =~ m/^\:(\w+) (\w+) \w+ \w+ / or die "$_ ?";
+ next;
+ }
+ my ($oldmode,$newmode) = @lmodes;
+ @lmodes = ();
+
next if $f =~ m#^debian(?:/.*)?$#s;
+
+ if ($unrepres) {
+ eval {
+ die "not a plain file or symlink\n"
+ unless $newmode =~ m/^(?:10|12)\d{4}$/ ||
+ $oldmode =~ m/^(?:10|12)\d{4}$/;
+ if ($oldmode =~ m/[^0]/ &&
+ $newmode =~ m/[^0]/) {
+ # both old and new files exist
+ die "mode or type changed\n" if $oldmode ne $newmode;
+ die "modified symlink\n" unless $newmode =~ m/^10/;
+ } elsif ($oldmode =~ m/[^0]/) {
+ # deletion
+ die "deletion of symlink\n"
+ unless $oldmode =~ m/^10/;
+ } else {
+ # creation
+ die "creation with non-default mode\n"
+ unless $newmode =~ m/^100644$/ or
+ $newmode =~ m/^120000$/;
+ }
+ };
+ if ($@) {
+ local $/="\n"; chomp $@;
+ push @$unrepres, [ $f, "$@ ($oldmode->$newmode)" ];
+ }
+ }
+
my $isignore = $f =~ m#^(?:.*/)?.gitignore$#s;
$r |= $isignore ? 02 : 01;
$ignorenamesr->{$f}=1 if $ignorenamesr && $isignore;
local $ENV{GIT_COMMITTER_NAME} = $authline[0];
local $ENV{GIT_COMMITTER_EMAIL} = $authline[1];
local $ENV{GIT_COMMITTER_DATE} = $authline[2];
-
+ local $ENV{GIT_AUTHOR_NAME} = $authline[0];
+ local $ENV{GIT_AUTHOR_EMAIL} = $authline[1];
+ local $ENV{GIT_AUTHOR_DATE} = $authline[2];
+
if ($quilt_mode =~ m/gbp|unapplied/ &&
- ($diffbits->{H2O} & 01)) {
+ ($diffbits->{O2H} & 01)) {
my $msg =
"--quilt=$quilt_mode specified, implying patches-unapplied git tree\n".
" but git tree differs from orig in upstream files.";
($diffbits->{O2A} & 01)) { # some patches
quiltify_splitbrain_needed();
progress "dgit view: creating patches-applied version using gbp pq";
- runcmd shell_cmd 'exec >/dev/null', @gbp, qw(pq import);
+ runcmd shell_cmd 'exec >/dev/null', gbp_pq, qw(import);
# gbp pq import creates a fresh branch; push back to dgit-view
runcmd @git, qw(update-ref refs/heads/dgit-view HEAD);
runcmd @git, qw(checkout -q dgit-view);
.gitignores: but, such patches exist in debian/patches.
END
}
- if (($diffbits->{H2O} & 02) && # user has modified .gitignore
+ if (($diffbits->{O2H} & 02) && # user has modified .gitignore
!($diffbits->{O2A} & 02)) { # patches do not change .gitignore
quiltify_splitbrain_needed();
progress "dgit view: creating patch to represent .gitignore changes";
.gitignore file(s). This patch is autogenerated, to provide these
updates to users of the official Debian archive view of the package.
-[dgit version $our_version]
+[dgit ($our_version) update-gitignore]
---
END
close GIPATCH or die "$gipatch: $!";
print SERIES "auto-gitignore\n" or die $!;
close SERIES or die $!;
runcmd @git, qw(add -- debian/patches/series), $gipatch;
- commit_admin "Commit patch to update .gitignore";
+ commit_admin <<END
+Commit patch to update .gitignore
+
+[dgit ($our_version) update-gitignore-quilt-fixup]
+END
}
- my $dgitview = git_rev_parse 'refs/heads/dgit-view';
+ my $dgitview = git_rev_parse 'HEAD';
- changedir '../../../..';
- ensuredir ".git/logs/refs/dgit-intern";
- my $makelogfh = new IO::File ".git/logs/refs/$splitbraincache", '>>'
+ changedir $maindir;
+ # When we no longer need to support squeeze, use --create-reflog
+ # instead of this:
+ ensuredir "$maindir_gitcommon/logs/refs/dgit-intern";
+ my $makelogfh = new IO::File "$maindir_gitcommon/logs/refs/$splitbraincache", '>>'
or die $!;
+
+ my $oldcache = git_get_ref "refs/$splitbraincache";
+ if ($oldcache eq $dgitview) {
+ my $tree = cmdoutput qw(git rev-parse), "$dgitview:";
+ # git update-ref doesn't always update, in this case. *sigh*
+ my $dummy = make_commit_text <<END;
+tree $tree
+parent $dgitview
+author Dgit <dgit\@example.com> 1000000000 +0000
+committer Dgit <dgit\@example.com> 1000000000 +0000
+
+Dummy commit - do not use
+END
+ runcmd @git, qw(update-ref -m), "dgit $our_version - dummy",
+ "refs/$splitbraincache", $dummy;
+ }
runcmd @git, qw(update-ref -m), $cachekey, "refs/$splitbraincache",
$dgitview;
- progress "dgit view: created (commit id $dgitview)";
+ changedir "$playground/work";
- changedir '.git/dgit/unpack/work';
+ my $saved = maybe_split_brain_save $headref, $dgitview, "converted";
+ progress "dgit view: created ($saved)";
}
sub quiltify ($$$$) {
$commitdata =~ m/^author (.*) \d+ [-+0-9]+$/m or die "$cc ?";
my $author = $1;
+ my $commitdate = cmdoutput
+ @git, qw(log -n1 --pretty=format:%aD), $cc;
+
$msg =~ s/^(.*)\n*/$1\n/ or die "$cc $msg ?";
+ my $strip_nls = sub { $msg =~ s/\n+$//; $msg .= "\n"; };
+ $strip_nls->();
+
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 $patchname;
+ my $patchdir;
+
+ my $gbp_check_suitable = sub {
+ $_ = shift;
+ my ($what) = @_;
+
+ eval {
+ die "contains unexpected slashes\n" if m{//} || m{/$};
+ die "contains leading punctuation\n" if m{^\W} || m{/\W};
+ die "contains bad character(s)\n" if m{[^-a-z0-9_.+=~/]}i;
+ die "is series file\n" if m{$series_filename_re}o;
+ die "too long" if length > 200;
+ };
+ return $_ unless $@;
+ print STDERR "quiltifying commit $cc:".
+ " ignoring/dropping Gbp-Pq $what: $@";
+ return undef;
+ };
+
+ if ($msg =~ s/^ (?: gbp(?:-pq)? : \s* name \s+ |
+ gbp-pq-name: \s* )
+ (\S+) \s* \n //ixm) {
+ $patchname = $gbp_check_suitable->($1, 'Name');
+ }
+ if ($msg =~ s/^ (?: gbp(?:-pq)? : \s* topic \s+ |
+ gbp-pq-topic: \s* )
+ (\S+) \s* \n //ixm) {
+ $patchdir = $gbp_check_suitable->($1, 'Topic');
+ }
+
+ $strip_nls->();
+
+ if (!defined $patchname) {
+ $patchname = $title;
+ $patchname =~ s/[.:]$//;
+ use Text::Iconv;
+ eval {
+ my $converter = new Text::Iconv qw(UTF-8 ASCII//TRANSLIT);
+ my $translitname = $converter->convert($patchname);
+ die unless defined $translitname;
+ $patchname = $translitname;
+ };
+ print STDERR
+ "dgit: patch title transliteration error: $@"
+ if $@;
+ $patchname =~ y/ A-Z/-a-z/;
+ $patchname =~ y/-a-z0-9_.+=~//cd;
+ $patchname =~ s/^\W/x-$&/;
+ $patchname = substr($patchname,0,40);
+ $patchname .= ".patch";
+ }
+ if (!defined $patchdir) {
+ $patchdir = '';
+ }
+ if (length $patchdir) {
+ $patchname = "$patchdir/$patchname";
+ }
+ if ($patchname =~ m{^(.*)/}) {
+ mkpath "debian/patches/$1";
+ }
+
my $index;
for ($index='';
stat "debian/patches/$patchname$index";
runcmd @git, qw(checkout -q), $target, qw(debian/changelog);
quiltify_dpkg_commit "$patchname$index", $author, $msg,
+ "Date: $commitdate\n".
"X-Dgit-Generated: $clogp->{Version} $cc\n";
runcmd @git, qw(checkout -q), $cc, qw(debian/changelog);
check_for_vendor_patches();
if (quiltmode_splitbrain) {
- foreach my $needtf (qw(new maint)) {
- next if grep { $_ eq $needtf } access_cfg_tagformats;
- fail <<END
+ fail <<END unless access_cfg_tagformats_can_splitbrain;
quilt mode $quilt_mode requires split view so server needs to support
both "new" and "maint" tag formats, but config says it doesn't.
END
- }
}
my $clogp = parsechangelog();
my $headref = git_rev_parse('HEAD');
prep_ud();
- changedir $ud;
+ changedir $playground;
- my $upstreamversion=$version;
- $upstreamversion =~ s/-[^-]*$//;
+ my $upstreamversion = upstreamversion $version;
if ($fopts->{'single-debian-patch'}) {
quilt_fixup_singlepatch($clogp, $headref, $upstreamversion);
die 'bug' if $split_brain && !$need_split_build_invocation;
- changedir '../../../..';
+ changedir $maindir;
runcmd_ordryrun_local
- @git, qw(pull --ff-only -q .git/dgit/unpack/work master);
+ @git, qw(pull --ff-only -q), "$playground/work", qw(master);
}
sub quilt_fixup_mkwork ($) {
my ($upstreamversion, $fn) = @_;
# calls $fn->($leafname);
- foreach my $f (<../../../../*>) { #/){
+ foreach my $f (<$maindir/../*>) { #/){
my $b=$f; $b =~ s{.*/}{};
{
local ($debuglevel) = $debuglevel-1;
sub quilt_fixup_delete_pc () {
runcmd @git, qw(rm -rqf .pc);
- commit_admin "Commit removal of .pc (quilt series tracking data)";
+ commit_admin <<END
+Commit removal of .pc (quilt series tracking data)
+
+[dgit ($our_version) upgrade quilt-remove-pc]
+END
}
sub quilt_fixup_singlepatch ($$$) {
debian/control debian/changelog);
foreach my $maybe (qw(debian/patches debian/source/options
debian/tests/control)) {
- next unless stat_exists "../../../$maybe";
+ next unless stat_exists "$maindir/$maybe";
push @files, $maybe;
}
my $debtar= srcfn $fakeversion,'.debian.tar.gz';
- runcmd qw(env GZIP=-1n tar -zcf), "./$debtar", qw(-C ../../..), @files;
+ runcmd qw(env GZIP=-1n tar -zcf), "./$debtar", qw(-C), $maindir, @files;
$dscaddfile->($debtar);
close $fakedsc or die $!;
sub quilt_check_splitbrain_cache ($$) {
my ($headref, $upstreamversion) = @_;
# Called only if we are in (potentially) split brain mode.
- # Called in $ud.
+ # Called in playground.
# Computes the cache key and looks in the cache.
# Returns ($dgit_view_commitid, $cachekey) or (undef, $cachekey)
my $srcshash = Digest::SHA->new(256);
my %sfs = ( %INC, '$0(dgit)' => $0 );
foreach my $sfk (sort keys %sfs) {
- next unless m/^\$0\b/ || m{^Debian/Dgit\b};
+ next unless $sfk =~ m/^\$0\b/ || $sfk =~ m{^Debian/Dgit\b};
$srcshash->add($sfk," ");
$srcshash->add(hashfile($sfs{$sfk}));
$srcshash->add("\n");
push @cachekey, $srcshash->hexdigest();
$splitbrain_cachekey = "@cachekey";
- my @cmd = (@git, qw(reflog), '--pretty=format:%H %gs',
+ my @cmd = (@git, qw(log -g), '--pretty=format:%H %gs',
$splitbraincache);
printdebug "splitbrain cachekey $splitbrain_cachekey\n";
debugcmd "|(probably)",@cmd;
my $child = open GC, "-|"; defined $child or die $!;
if (!$child) {
- chdir '../../..' or die $!;
- if (!stat ".git/logs/refs/$splitbraincache") {
+ chdir $maindir or die $!;
+ if (!stat "$maindir_gitcommon/logs/refs/$splitbraincache") {
$! == ENOENT or die $!;
printdebug ">(no reflog)\n";
exit 0;
my $cachehit = $1;
quilt_fixup_mkwork($headref);
+ my $saved = maybe_split_brain_save $headref, $cachehit, "cache-hit";
if ($cachehit ne $headref) {
- progress "dgit view: found cached (commit id $cachehit)";
+ progress "dgit view: found cached ($saved)";
runcmd @git, qw(checkout -q -b dgit-view), $cachehit;
$split_brain = 1;
return ($cachehit, $splitbrain_cachekey);
# 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
+ # - 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
# Another situation we may have to cope with is gbp-style
# We would want to detect these, so we know to escape into
# quilt_fixup_gbp. However, this is in general not possible.
# Consider a package with a one patch which the dgit user reverts
- # (with git-revert or the moral equivalent).
+ # (with git revert or the moral equivalent).
#
# That is indistinguishable in contents from a patches-unapplied
# tree. And looking at the history to distinguish them is not
changedir 'fake';
- remove_stray_gits();
+ remove_stray_gits("source package");
mktree_in_ud_here();
rmtree '.pc';
- runcmd @git, qw(add -Af .);
- my $unapplied=git_write_tree();
+ rmtree 'debian'; # git checkout commitish paths does not delete!
+ runcmd @git, qw(checkout -f), $headref, qw(-- debian);
+ my $unapplied=git_add_write_tree();
printdebug "fake orig tree object $unapplied\n";
ensuredir '.pc';
- runcmd qw(sh -ec),
- 'exec dpkg-source --before-build . >/dev/null';
+ my @bbcmd = (qw(sh -ec), 'exec dpkg-source --before-build . >/dev/null');
+ $!=0; $?=-1;
+ if (system @bbcmd) {
+ failedcmd @bbcmd if $? < 0;
+ fail <<END;
+failed to apply your git tree's patch stack (from debian/patches/) to
+ the corresponding upstream tarball(s). Your source tree and .orig
+ are probably too inconsistent. dgit can only fix up certain kinds of
+ anomaly (depending on the quilt mode). See --quilt= in dgit(1).
+END
+ }
changedir '..';
changedir '../fake';
rmtree '.pc';
- runcmd @git, qw(add -Af .);
- my $oldtiptree=git_write_tree();
+ my $oldtiptree=git_add_write_tree();
printdebug "fake o+d/p tree object $unapplied\n";
changedir '../work';
# be. This is mostly for error reporting.
my %editedignores;
+ my @unrepres;
my $diffbits = {
# H = user's HEAD
# O = orig, without patches applied
# A = "applied", ie orig with H's debian/patches applied
- H2O => quiltify_trees_differ($headref, $unapplied, 1,\%editedignores),
+ O2H => quiltify_trees_differ($unapplied,$headref, 1,
+ \%editedignores, \@unrepres),
H2A => quiltify_trees_differ($headref, $oldtiptree,1),
O2A => quiltify_trees_differ($unapplied,$oldtiptree,1),
};
my @dl;
foreach my $b (qw(01 02)) {
- foreach my $v (qw(H2O O2A H2A)) {
+ foreach my $v (qw(O2H O2A H2A)) {
push @dl, ($diffbits->{$v} & $b) ? '##' : '==';
}
}
printdebug "differences \@dl @dl.\n";
progress sprintf
+"$us: base trees orig=%.20s o+d/p=%.20s",
+ $unapplied, $oldtiptree;
+ progress sprintf
"$us: quilt differences: src: %s orig %s gitignores: %s orig %s\n".
"$us: quilt differences: HEAD %s o+d/p HEAD %s o+d/p",
$dl[0], $dl[1], $dl[3], $dl[4],
$dl[2], $dl[5];
+ if (@unrepres) {
+ print STDERR "dgit: cannot represent change: $_->[1]: $_->[0]\n"
+ foreach @unrepres;
+ forceable_fail [qw(unrepresentable)], <<END;
+HEAD has changes to .orig[s] which are not representable by `3.0 (quilt)'
+END
+ }
+
my @failsuggestion;
- if (!($diffbits->{H2O} & $diffbits->{O2A})) {
+ if (!($diffbits->{O2H} & $diffbits->{O2A})) {
push @failsuggestion, "This might be a patches-unapplied branch.";
} elsif (!($diffbits->{H2A} & $diffbits->{O2A})) {
push @failsuggestion, "This might be a patches-applied branch.";
}
push @failsuggestion, "Maybe you need to specify one of".
- " --quilt=gbp --quilt=dpm --quilt=unapplied ?";
+ " --[quilt=]gbp --[quilt=]dpm --quilt=unapplied ?";
if (quiltmode_splitbrain()) {
quiltify_splitbrain($clogp, $unapplied, $headref,
maybe_unapply_patches_again();
}
-sub build_prep () {
- notpushing();
- badusage "-p is not allowed when building" if defined $package;
- check_not_dirty();
- clean_tree();
+sub build_or_push_prep_early () {
+ our $build_or_push_prep_early_done //= 0;
+ return if $build_or_push_prep_early_done++;
+ badusage "-p is not allowed with dgit $subcommand" if defined $package;
my $clogp = parsechangelog();
$isuite = getfield $clogp, 'Distribution';
$package = getfield $clogp, 'Source';
$version = getfield $clogp, 'Version';
+}
+
+sub build_prep_early () {
+ build_or_push_prep_early();
+ notpushing();
+ check_not_dirty();
+}
+
+sub build_prep () {
+ build_prep_early();
+ clean_tree();
build_maybe_quilt_fixup();
if ($rmchanges) {
my $pat = changespat $version;
return $r;
}
+sub in_parent (&) {
+ my ($fn) = @_;
+ my $wasdir = must_getcwd();
+ changedir "..";
+ $fn->();
+ changedir $wasdir;
+}
+
+sub postbuild_mergechanges ($) { # must run with CWD=.. (eg in in_parent)
+ my ($msg_if_onlyone) = @_;
+ # If there is only one .changes file, fail with $msg_if_onlyone,
+ # or if that is undef, be a no-op.
+ # Returns the changes file to report to the user.
+ my $pat = changespat $version;
+ my @changesfiles = glob $pat;
+ @changesfiles = sort {
+ ($b =~ m/_source\.changes$/ <=> $a =~ m/_source\.changes$/)
+ or $a cmp $b
+ } @changesfiles;
+ my $result;
+ if (@changesfiles==1) {
+ fail <<END.$msg_if_onlyone if defined $msg_if_onlyone;
+only one changes file from build (@changesfiles)
+END
+ $result = $changesfiles[0];
+ } elsif (@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 = 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}: $!";
+ }
+ }
+ $result = $multichanges;
+ } else {
+ fail "wrong number of different changes files (@changesfiles)";
+ }
+ printdone "build successful, results in $result\n" or die $!;
+}
+
+sub midbuild_checkchanges () {
+ my $pat = changespat $version;
+ return if $rmchanges;
+ my @unwanted = map { s#^\.\./##; $_; } glob "../$pat";
+ @unwanted = grep { $_ ne changespat $version,'source' } @unwanted;
+ fail <<END
+changes files other than source matching $pat already present; building would result in ambiguity about the intended results.
+Suggest you delete @unwanted.
+END
+ if @unwanted;
+}
+
+sub midbuild_checkchanges_vanilla ($) {
+ my ($wantsrc) = @_;
+ midbuild_checkchanges() if $wantsrc == 1;
+}
+
+sub postbuild_mergechanges_vanilla ($) {
+ my ($wantsrc) = @_;
+ if ($wantsrc == 1) {
+ in_parent {
+ postbuild_mergechanges(undef);
+ };
+ } else {
+ printdone "build successful\n";
+ }
+}
+
sub cmd_build {
+ build_prep_early();
my @dbp = (@dpkgbuildpackage, qw(-us -uc), changesopts_initial(), @ARGV);
my $wantsrc = massage_dbp_args \@dbp;
if ($wantsrc > 0) {
build_source();
+ midbuild_checkchanges_vanilla $wantsrc;
} else {
build_prep();
}
runcmd_ordryrun_local @dbp;
}
maybe_unapply_patches_again();
- printdone "build successful\n";
+ postbuild_mergechanges_vanilla $wantsrc;
+}
+
+sub pre_gbp_build {
+ $quilt_mode //= 'gbp';
}
sub cmd_gbp_build {
+ build_prep_early();
+
+ # gbp can make .origs out of thin air. In my tests it does this
+ # even for a 1.0 format package, with no origs present. So I
+ # guess it keys off just the version number. We don't know
+ # exactly what .origs ought to exist, but let's assume that we
+ # should run gbp if: the version has an upstream part and the main
+ # orig is absent.
+ my $upstreamversion = upstreamversion $version;
+ my $origfnpat = srcfn $upstreamversion, '.orig.tar.*';
+ my $gbp_make_orig = $version =~ m/-/ && !(() = glob "../$origfnpat");
+
+ if ($gbp_make_orig) {
+ clean_tree();
+ $cleanmode = 'none'; # don't do it again
+ $need_split_build_invocation = 1;
+ }
+
my @dbp = @dpkgbuildpackage;
my $wantsrc = massage_dbp_args \@dbp, \@ARGV;
- my @cmd;
- if (length executable_on_path('git-buildpackage')) {
- @cmd = qw(git-buildpackage);
- } else {
- @cmd = qw(gbp buildpackage);
+ if (!length $gbp_build[0]) {
+ if (length executable_on_path('git-buildpackage')) {
+ $gbp_build[0] = qw(git-buildpackage);
+ } else {
+ $gbp_build[0] = 'gbp buildpackage';
+ }
+ }
+ my @cmd = opts_opt_multi_cmd @gbp_build;
+
+ push @cmd, (qw(-us -uc --git-no-sign-tags),
+ "--git-builder=".(shellquote @dbp));
+
+ if ($gbp_make_orig) {
+ my $priv = dgit_privdir();
+ my $ok = "$priv/origs-gen-ok";
+ unlink $ok or $!==&ENOENT or die $!;
+ my @origs_cmd = @cmd;
+ push @origs_cmd, qw(--git-cleaner=true);
+ push @origs_cmd, "--git-prebuild=".
+ "touch ".(shellquote $ok)." ".(shellquote "$priv/no-such-dir/ok");
+ push @origs_cmd, @ARGV;
+ if (act_local()) {
+ debugcmd @origs_cmd;
+ system @origs_cmd;
+ do { local $!; stat_exists $ok; }
+ or failedcmd @origs_cmd;
+ } else {
+ dryrun_report @origs_cmd;
+ }
}
- push @cmd, (qw(-us -uc --git-no-sign-tags), "--git-builder=@dbp");
if ($wantsrc > 0) {
build_source();
+ midbuild_checkchanges_vanilla $wantsrc;
} else {
if (!$clean_using_builder) {
push @cmd, '--git-cleaner=true';
}
maybe_unapply_patches_again();
if ($wantsrc < 2) {
- 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";
+ postbuild_mergechanges_vanilla $wantsrc;
}
sub cmd_git_build { cmd_gbp_build(); } # compatibility with <= 1.0
+sub build_source_for_push {
+ build_source();
+ maybe_unapply_patches_again();
+ $changesfile = $sourcechanges;
+}
+
sub build_source {
- my $our_cleanmode = $cleanmode;
- if ($need_split_build_invocation) {
- # Pretend that clean is being done some other way. This
- # forces us not to try to use dpkg-buildpackage to clean and
- # build source all in one go; and instead we run dpkg-source
- # (and build_prep() will do the clean since $clean_using_builder
- # is false).
- $our_cleanmode = 'ELSEWHERE';
- }
- if ($our_cleanmode =~ m/^dpkg-source/) {
- # dpkg-source invocation (below) will clean, so build_prep shouldn't
- $clean_using_builder = 1;
- }
+ build_prep_early();
build_prep();
$sourcechanges = changespat $version,'source';
if (act_local()) {
or fail "remove $sourcechanges: $!";
}
$dscfn = dscfn($version);
- if ($our_cleanmode eq 'dpkg-source') {
- maybe_apply_patches_dirtily();
- runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc -S),
- changesopts();
- } elsif ($our_cleanmode eq 'dpkg-source-d') {
- maybe_apply_patches_dirtily();
- runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc -S -d),
- changesopts();
+ my @cmd = (@dpkgsource, qw(-b --));
+ if ($split_brain) {
+ changedir $playground;
+ runcmd_ordryrun_local @cmd, "work";
+ my @udfiles = <${package}_*>;
+ changedir $maindir;
+ foreach my $f (@udfiles) {
+ printdebug "source copy, found $f\n";
+ next unless
+ $f eq $dscfn or
+ ($f =~ m/\.debian\.tar(?:\.\w+)$/ &&
+ $f eq srcfn($version, $&));
+ printdebug "source copy, found $f - renaming\n";
+ rename "$playground/$f", "../$f" or $!==ENOENT
+ or fail "put in place new source file ($f): $!";
+ }
} else {
- my @cmd = (@dpkgsource, qw(-b --));
- if ($split_brain) {
- changedir $ud;
- runcmd_ordryrun_local @cmd, "work";
- my @udfiles = <${package}_*>;
- changedir "../../..";
- foreach my $f (@udfiles) {
- printdebug "source copy, found $f\n";
- next unless
- $f eq $dscfn or
- ($f =~ m/\.debian\.tar(?:\.\w+)$/ &&
- $f eq srcfn($version, $&));
- printdebug "source copy, found $f - renaming\n";
- rename "$ud/$f", "../$f" or $!==ENOENT
- or fail "put in place new source file ($f): $!";
- }
- } else {
- my $pwd = must_getcwd();
- my $leafdir = basename $pwd;
- changedir "..";
- runcmd_ordryrun_local @cmd, $leafdir;
- changedir $pwd;
- }
- runcmd_ordryrun_local qw(sh -ec),
- 'exec >$1; shift; exec "$@"','x',
- "../$sourcechanges",
- @dpkggenchanges, qw(-S), changesopts();
+ my $pwd = must_getcwd();
+ my $leafdir = basename $pwd;
+ changedir "..";
+ runcmd_ordryrun_local @cmd, $leafdir;
+ changedir $pwd;
}
+ runcmd_ordryrun_local qw(sh -ec),
+ 'exec >$1; shift; exec "$@"','x',
+ "../$sourcechanges",
+ @dpkggenchanges, qw(-S), changesopts();
}
sub cmd_build_source {
+ build_prep_early();
badusage "build-source takes no additional arguments" if @ARGV;
build_source();
maybe_unapply_patches_again();
sub cmd_sbuild {
build_source();
- my $pat = changespat $version;
- if (!$rmchanges) {
- my @unwanted = map { s#^\.\./##; $_; } glob "../$pat";
- @unwanted = grep { $_ ne changespat $version,'source' } @unwanted;
- fail "changes files other than source matching $pat".
- " already present (@unwanted);".
- " building would result in ambiguity about the intended results"
- if @unwanted;
- }
- my $wasdir = must_getcwd();
- changedir "..";
- if (act_local()) {
- stat_exists $dscfn or fail "$dscfn (in parent directory): $!";
- stat_exists $sourcechanges
- or fail "$sourcechanges (in parent directory): $!";
- }
- 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==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 = 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}: $!";
+ midbuild_checkchanges();
+ in_parent {
+ if (act_local()) {
+ stat_exists $dscfn or fail "$dscfn (in parent directory): $!";
+ stat_exists $sourcechanges
+ or fail "$sourcechanges (in parent directory): $!";
}
- }
- changedir $wasdir;
+ runcmd_ordryrun_local @sbuild, qw(-d), $isuite, @ARGV, $dscfn;
+ };
maybe_unapply_patches_again();
- printdone "build successful, results in $multichanges\n" or die $!;
+ in_parent {
+ postbuild_mergechanges(<<END);
+perhaps you need to pass -A ? (sbuild's default is to build only
+arch-specific binaries; dgit 1.4 used to override that.)
+END
+ };
}
sub cmd_quilt_fixup {
badusage "incorrect arguments to dgit quilt-fixup" if @ARGV;
- my $clogp = parsechangelog();
- $version = getfield $clogp, 'Version';
- $package = getfield $clogp, 'Source';
- check_not_dirty();
+ build_prep_early();
clean_tree();
build_maybe_quilt_fixup();
}
+sub import_dsc_result {
+ my ($dstref, $newhash, $what_log, $what_msg) = @_;
+ my @cmd = (@git, qw(update-ref -m), $what_log, $dstref, $newhash);
+ runcmd @cmd;
+ check_gitattrs($newhash, "source tree");
+
+ progress "dgit: import-dsc: $what_msg";
+}
+
+sub cmd_import_dsc {
+ my $needsig = 0;
+
+ while (@ARGV) {
+ last unless $ARGV[0] =~ m/^-/;
+ $_ = shift @ARGV;
+ last if m/^--?$/;
+ if (m/^--require-valid-signature$/) {
+ $needsig = 1;
+ } else {
+ badusage "unknown dgit import-dsc sub-option \`$_'";
+ }
+ }
+
+ badusage "usage: dgit import-dsc .../PATH/TO/.DSC BRANCH" unless @ARGV==2;
+ my ($dscfn, $dstbranch) = @ARGV;
+
+ badusage "dry run makes no sense with import-dsc" unless act_local();
+
+ my $force = $dstbranch =~ s/^\+// ? +1 :
+ $dstbranch =~ s/^\.\.// ? -1 :
+ 0;
+ my $info = $force ? " $&" : '';
+ $info = "$dscfn$info";
+
+ my $specbranch = $dstbranch;
+ $dstbranch = "refs/heads/$dstbranch" unless $dstbranch =~ m#^refs/#;
+ $dstbranch = cmdoutput @git, qw(check-ref-format --normalize), $dstbranch;
+
+ my @symcmd = (@git, qw(symbolic-ref -q HEAD));
+ my $chead = cmdoutput_errok @symcmd;
+ defined $chead or $?==256 or failedcmd @symcmd;
+
+ fail "$dstbranch is checked out - will not update it"
+ if defined $chead and $chead eq $dstbranch;
+
+ my $oldhash = git_get_ref $dstbranch;
+
+ open D, "<", $dscfn or fail "open import .dsc ($dscfn): $!";
+ $dscdata = do { local $/ = undef; <D>; };
+ D->error and fail "read $dscfn: $!";
+ close C;
+
+ # we don't normally need this so import it here
+ use Dpkg::Source::Package;
+ my $dp = new Dpkg::Source::Package filename => $dscfn,
+ require_valid_signature => $needsig;
+ {
+ local $SIG{__WARN__} = sub {
+ print STDERR $_[0];
+ return unless $needsig;
+ fail "import-dsc signature check failed";
+ };
+ if (!$dp->is_signed()) {
+ warn "$us: warning: importing unsigned .dsc\n";
+ } else {
+ my $r = $dp->check_signature();
+ die "->check_signature => $r" if $needsig && $r;
+ }
+ }
+
+ parse_dscdata();
+
+ $package = getfield $dsc, 'Source';
+
+ parse_dsc_field($dsc, "Dgit metadata in .dsc")
+ unless forceing [qw(import-dsc-with-dgit-field)];
+ parse_dsc_field_def_dsc_distro();
+
+ $isuite = 'DGIT-IMPORT-DSC';
+ $idistro //= $dsc_distro;
+
+ notpushing();
+
+ if (defined $dsc_hash) {
+ progress "dgit: import-dsc of .dsc with Dgit field, using git hash";
+ resolve_dsc_field_commit undef, undef;
+ }
+ if (defined $dsc_hash) {
+ my @cmd = (qw(sh -ec),
+ "echo $dsc_hash | git cat-file --batch-check");
+ my $objgot = cmdoutput @cmd;
+ if ($objgot =~ m#^\w+ missing\b#) {
+ fail <<END
+.dsc contains Dgit field referring to object $dsc_hash
+Your git tree does not have that object. Try `git fetch' from a
+plausible server (browse.dgit.d.o? alioth?), and try the import-dsc again.
+END
+ }
+ if ($oldhash && !is_fast_fwd $oldhash, $dsc_hash) {
+ if ($force > 0) {
+ progress "Not fast forward, forced update.";
+ } else {
+ fail "Not fast forward to $dsc_hash";
+ }
+ }
+ import_dsc_result $dstbranch, $dsc_hash,
+ "dgit import-dsc (Dgit): $info",
+ "updated git ref $dstbranch";
+ return 0;
+ }
+
+ fail <<END
+Branch $dstbranch already exists
+Specify ..$specbranch for a pseudo-merge, binding in existing history
+Specify +$specbranch to overwrite, discarding existing history
+END
+ if $oldhash && !$force;
+
+ my @dfi = dsc_files_info();
+ foreach my $fi (@dfi) {
+ my $f = $fi->{Filename};
+ my $here = "../$f";
+ if (lstat $here) {
+ next if stat $here;
+ fail "lstat $here works but stat gives $! !";
+ }
+ fail "stat $here: $!" unless $! == ENOENT;
+ my $there = $dscfn;
+ if ($dscfn =~ m#^(?:\./+)?\.\./+#) {
+ $there = $';
+ } elsif ($dscfn =~ m#^/#) {
+ $there = $dscfn;
+ } else {
+ fail "cannot import $dscfn which seems to be inside working tree!";
+ }
+ $there =~ s#/+[^/]+$## or
+ fail "import $dscfn requires ../$f, but it does not exist";
+ $there .= "/$f";
+ my $test = $there =~ m{^/} ? $there : "../$there";
+ stat $test or fail "import $dscfn requires $test, but: $!";
+ symlink $there, $here or fail "symlink $there to $here: $!";
+ progress "made symlink $here -> $there";
+# print STDERR Dumper($fi);
+ }
+ my @mergeinputs = generate_commits_from_dsc();
+ die unless @mergeinputs == 1;
+
+ my $newhash = $mergeinputs[0]{Commit};
+
+ if ($oldhash) {
+ if ($force > 0) {
+ progress "Import, forced update - synthetic orphan git history.";
+ } elsif ($force < 0) {
+ progress "Import, merging.";
+ my $tree = cmdoutput @git, qw(rev-parse), "$newhash:";
+ my $version = getfield $dsc, 'Version';
+ my $clogp = commit_getclogp $newhash;
+ my $authline = clogp_authline $clogp;
+ $newhash = make_commit_text <<END;
+tree $tree
+parent $newhash
+parent $oldhash
+author $authline
+committer $authline
+
+Merge $package ($version) import into $dstbranch
+END
+ } else {
+ die; # caught earlier
+ }
+ }
+
+ import_dsc_result $dstbranch, $newhash,
+ "dgit import-dsc: $info",
+ "results are in in git ref $dstbranch";
+}
+
+sub pre_archive_api_query () {
+ not_necessarily_a_tree();
+}
sub cmd_archive_api_query {
badusage "need only 1 subpath argument" unless @ARGV==1;
my ($subpath) = @ARGV;
+ local $isuite = 'DGIT-API-QUERY-CMD';
my @cmd = archive_api_query_cmd($subpath);
+ push @cmd, qw(-f);
debugcmd ">",@cmd;
exec @cmd or fail "exec curl: $!\n";
}
+sub repos_server_url () {
+ $package = '_dgit-repos-server';
+ local $access_forpush = 1;
+ local $isuite = 'DGIT-REPOS-SERVER';
+ my $url = access_giturl();
+}
+
+sub pre_clone_dgit_repos_server () {
+ not_necessarily_a_tree();
+}
sub cmd_clone_dgit_repos_server {
badusage "need destination argument" unless @ARGV==1;
my ($destdir) = @ARGV;
- $package = '_dgit-repos-server';
- my @cmd = (@git, qw(clone), access_giturl(), $destdir);
+ my $url = repos_server_url();
+ my @cmd = (@git, qw(clone), $url, $destdir);
debugcmd ">",@cmd;
exec @cmd or fail "exec git clone: $!\n";
}
+sub pre_print_dgit_repos_server_source_url () {
+ not_necessarily_a_tree();
+}
+sub cmd_print_dgit_repos_server_source_url {
+ badusage "no arguments allowed to dgit print-dgit-repos-server-source-url"
+ if @ARGV;
+ my $url = repos_server_url();
+ print $url, "\n" or die $!;
+}
+
+sub pre_print_dpkg_source_ignores {
+ not_necessarily_a_tree();
+}
+sub cmd_print_dpkg_source_ignores {
+ badusage "no arguments allowed to dgit print-dpkg-source-ignores"
+ if @ARGV;
+ print "@dpkg_source_ignores\n" or die $!;
+}
+
sub cmd_setup_mergechangelogs {
badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
+ local $isuite = 'DGIT-SETUP-TREE';
setup_mergechangelogs(1);
}
sub cmd_setup_useremail {
- badusage "no arguments allowed to dgit setup-mergechangelogs" if @ARGV;
+ badusage "no arguments allowed to dgit setup-useremail" if @ARGV;
+ local $isuite = 'DGIT-SETUP-TREE';
setup_useremail(1);
}
+sub cmd_setup_gitattributes {
+ badusage "no arguments allowed to dgit setup-useremail" if @ARGV;
+ local $isuite = 'DGIT-SETUP-TREE';
+ setup_gitattrs(1);
+}
+
sub cmd_setup_new_tree {
badusage "no arguments allowed to dgit setup-tree" if @ARGV;
+ local $isuite = 'DGIT-SETUP-TREE';
setup_new_tree();
}
}
our (%valopts_long, %valopts_short);
+our (%funcopts_long);
our @rvalopts;
+our (@modeopt_cfgs);
sub defvalopt ($$$$) {
my ($long,$short,$val_re,$how) = @_;
defvalopt '--existing-package','', '.*', \$existing_package;
defvalopt '--build-products-dir','','.*', \$buildproductsdir;
defvalopt '--clean', '', $cleanmode_re, \$cleanmode;
+defvalopt '--package', '-p', $package_re, \$package;
defvalopt '--quilt', '', $quilt_modes_re, \$quilt_mode;
-defvalopt '', '-c', '.*=.*', sub { push @git, '-c', @_; };
-
defvalopt '', '-C', '.+', sub {
($changesfile) = (@_);
if ($changesfile =~ s#^(.*)/##) {
" absolute, not relative, directory."
};
+sub defoptmodes ($@) {
+ my ($varref, $cfgkey, $default, %optmap) = @_;
+ my %permit;
+ while (my ($opt,$val) = each %optmap) {
+ $funcopts_long{$opt} = sub { $$varref = $val; };
+ $permit{$val} = $val;
+ }
+ push @modeopt_cfgs, {
+ Var => $varref,
+ Key => $cfgkey,
+ Default => $default,
+ Vals => \%permit
+ };
+}
+
+defoptmodes \$dodep14tag, qw( dep14tag want
+ --dep14tag want
+ --no-dep14tag no
+ --always-dep14tag always );
+
sub parseopts () {
my $om;
($om = $opts_opt_map{$1})) {
push @ropts, $_;
push @$om, $2;
+ } elsif (m/^--(gbp|dpm)$/s) {
+ push @ropts, "--quilt=$1";
+ $quilt_mode = $1;
} elsif (m/^--ignore-dirty$/s) {
push @ropts, $_;
$ignoredirty = 1;
} elsif (m/^--no-rm-on-error$/s) {
push @ropts, $_;
$rmonerror = 0;
+ } elsif (m/^--no-chase-dsc-distro$/s) {
+ push @ropts, $_;
+ $chase_dsc_distro = 0;
} elsif (m/^--overwrite$/s) {
push @ropts, $_;
$overwrite_version = '';
} elsif (m/^--overwrite=(.+)$/s) {
push @ropts, $_;
$overwrite_version = $1;
+ } elsif (m/^--delayed=(\d+)$/s) {
+ push @ropts, $_;
+ push @dput, $_;
+ } elsif (m/^--dgit-view-save=(.+)$/s) {
+ push @ropts, $_;
+ $split_brain_save = $1;
+ $split_brain_save =~ s#^(?!refs/)#refs/heads/#;
} elsif (m/^--(no-)?rm-old-changes$/s) {
push @ropts, $_;
$rmchanges = !$1;
} elsif (m/^--deliberately-($deliberately_re)$/s) {
push @ropts, $_;
push @deliberatelies, $&;
+ } elsif (m/^--force-(.*)/ && defined $forceopts{$1}) {
+ push @ropts, $&;
+ $forceopts{$1} = 1;
+ $_='';
+ } elsif (m/^--force-/) {
+ print STDERR
+ "$us: warning: ignoring unknown force option $_\n";
+ $_='';
} elsif (m/^--dgit-tag-format=(old|new)$/s) {
# undocumented, for testing
push @ropts, $_;
# undocumented, for testing
push @ropts, $_;
$need_split_build_invocation = 1;
+ } elsif (m/^--config-lookup-explode=(.+)$/s) {
+ # undocumented, for testing
+ push @ropts, $_;
+ $gitcfgs{cmdline}{$1} = 'CONFIG-LOOKUP-EXPLODE';
+ # ^ it's supposed to be an array ref
} elsif (m/^(--[-0-9a-z]+)(=|$)/ && ($oi = $valopts_long{$1})) {
$val = $2 ? $' : undef; #';
$valopt->($oi->{Long});
+ } elsif ($funcopts_long{$_}) {
+ push @ropts, $_;
+ $funcopts_long{$_}();
} else {
badusage "unknown long option \`$_'";
}
} elsif (s/^-wc$//s) {
push @ropts, $&;
$cleanmode = 'check';
+ } elsif (s/^-c([^=]*)\=(.*)$//s) {
+ push @git, '-c', $&;
+ $gitcfgs{cmdline}{$1} = [ $2 ];
+ } elsif (s/^-c([^=]+)$//s) {
+ push @git, '-c', $&;
+ $gitcfgs{cmdline}{$1} = [ 'true' ];
} elsif (m/^-[a-zA-Z]/ && ($oi = $valopts_short{$&})) {
$val = $'; #';
$val = undef unless length $val;
}
}
-sub finalise_opts_opts () {
+sub check_env_sanity () {
+ my $blocked = new POSIX::SigSet;
+ sigprocmask SIG_UNBLOCK, $blocked, $blocked or die $!;
+
+ eval {
+ foreach my $name (qw(PIPE CHLD)) {
+ my $signame = "SIG$name";
+ my $signum = eval "POSIX::$signame" // die;
+ ($SIG{$name} // 'DEFAULT') eq 'DEFAULT' or
+ die "$signame is set to something other than SIG_DFL\n";
+ $blocked->ismember($signum) and
+ die "$signame is blocked\n";
+ }
+ };
+ return unless $@;
+ chomp $@;
+ fail <<END;
+On entry to dgit, $@
+This is a bug produced by something in in your execution environment.
+Giving up.
+END
+}
+
+
+sub parseopts_late_defaults () {
+ $isuite //= cfg("dgit-distro.$idistro.default-suite", 'RETURN-UNDEF')
+ if defined $idistro;
+ $isuite //= cfg('dgit.default.default-suite');
+
foreach my $k (keys %opts_opt_map) {
my $om = $opts_opt_map{$k};
}
foreach my $c (access_cfg_cfgs("opts-$k")) {
- my $vl = $gitcfg{$c};
- printdebug "CL $c ",
- ($vl ? join " ", map { shellquote } @$vl : ""),
+ my @vl =
+ map { $_ ? @$_ : () }
+ map { $gitcfgs{$_}{$c} }
+ reverse @gitcfgsources;
+ printdebug "CL $c ", (join " ", map { shellquote } @vl),
"\n" if $debuglevel >= 4;
- next unless $vl;
+ 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,
+ @vl,
@$om[$insertpos..$#$om] );
}
}
+
+ 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';
+ $quilt_mode =~ m/^($quilt_modes_re)$/
+ or badcfg "unknown quilt-mode \`$quilt_mode'";
+ $quilt_mode = $1;
+ }
+
+ foreach my $moc (@modeopt_cfgs) {
+ local $access_forpush;
+ my $vr = $moc->{Var};
+ next if defined $$vr;
+ $$vr = access_cfg($moc->{Key}, 'RETURN-UNDEF') // $moc->{Default};
+ my $v = $moc->{Vals}{$$vr};
+ badcfg "unknown $moc->{Key} setting \`$$vr'" unless defined $v;
+ $$vr = $v;
+ }
+
+ $need_split_build_invocation ||= quiltmode_splitbrain();
+
+ 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;
+ }
}
if ($ENV{$fakeeditorenv}) {
}
parseopts();
-git_slurp_config();
+check_env_sanity();
print STDERR "DRY RUN ONLY\n" if $dryrun_level > 1;
print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n"
print STDERR $helpmsg or die $!;
exit 8;
}
-my $cmd = shift @ARGV;
+$cmd = $subcommand = 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';
- $quilt_mode =~ m/^($quilt_modes_re)$/
- or badcfg "unknown quilt-mode \`$quilt_mode'";
- $quilt_mode = $1;
-}
+my $pre_fn = ${*::}{"pre_$cmd"};
+$pre_fn->() if $pre_fn;
-$need_split_build_invocation ||= quiltmode_splitbrain();
-
-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;
-}
+record_maindir if $invoked_in_git_tree;
+git_slurp_config();
my $fn = ${*::}{"cmd_$cmd"};
$fn or badusage "unknown operation $cmd";