summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore9
-rw-r--r--DEVELOPER-CERTIFICATE38
-rw-r--r--Debian/Dgit.pm408
-rw-r--r--Debian/Dgit/Infra.pm17
-rw-r--r--Debian/Dgit/Policy/Debian.pm42
-rw-r--r--Makefile77
-rw-r--r--NOTES17
-rw-r--r--README.dsc-import106
-rwxr-xr-xabsurd/git115
-rw-r--r--debian/changelog996
-rw-r--r--debian/control22
-rw-r--r--debian/copyright51
-rwxr-xr-xdebian/rules30
-rw-r--r--debian/tests/control36
-rw-r--r--debian/tests/control.in2
-rwxr-xr-xdgit5744
-rwxr-xr-xdgit-badcommit-fixup315
-rw-r--r--dgit-maint-gbp.7.pod126
-rw-r--r--dgit-maint-merge.7.pod439
-rw-r--r--dgit-maint-native.7.pod116
-rw-r--r--dgit-nmu-simple.7.pod139
-rwxr-xr-xdgit-repos-server627
-rw-r--r--dgit-sponsorship.7.pod313
-rw-r--r--dgit-user.7.pod432
-rw-r--r--dgit.11233
-rw-r--r--dgit.7425
-rwxr-xr-xinfra/cgit-regen-config26
-rwxr-xr-xinfra/dgit-mirror-rsync171
-rwxr-xr-xinfra/dgit-repos-admin-debian220
-rwxr-xr-xinfra/dgit-repos-policy-debian553
-rwxr-xr-xinfra/dgit-repos-policy-trusting58
-rwxr-xr-xinfra/dgit-repos-server1178
-rwxr-xr-xinfra/dgit-ssh-dispatch181
-rwxr-xr-xinfra/drs-cron-wrap14
-rwxr-xr-xinfra/get-dm-txt21
-rwxr-xr-xinfra/get-suites26
-rwxr-xr-xinfra/ssh-wrap10
-rwxr-xr-xlocal-pod-man13
-rw-r--r--tests/Makefile14
-rwxr-xr-xtests/adhoc51
-rwxr-xr-xtests/drs-git-ext6
-rwxr-xr-xtests/dsd-ssh18
-rwxr-xr-xtests/enumerate-tests109
-rw-r--r--tests/lib943
-rw-r--r--tests/lib-build-modes235
-rw-r--r--tests/lib-core38
-rw-r--r--tests/lib-import-chk97
-rw-r--r--tests/lib-mirror42
-rw-r--r--tests/lib-orig-include-exclude67
-rw-r--r--tests/lib-reprepro97
-rw-r--r--tests/lib-restricts22
-rw-r--r--tests/pkg-srcs/example_1.0-1+absurd.debian.tar.xzbin0 -> 1416 bytes
-rw-r--r--tests/pkg-srcs/example_1.0-1+absurd.dsc22
-rw-r--r--tests/pkg-srcs/example_1.0-1.100.debian.tar.xzbin0 -> 2108 bytes
-rw-r--r--tests/pkg-srcs/example_1.0-1.100.dsc22
-rw-r--r--tests/pkg-srcs/example_1.0-1.debian.tar.xzbin0 -> 1304 bytes
-rw-r--r--tests/pkg-srcs/example_1.0-1.dsc22
-rw-r--r--tests/pkg-srcs/example_1.0.orig-docs.tar.gzbin0 -> 236 bytes
-rw-r--r--tests/pkg-srcs/example_1.0.orig.tar.gzbin0 -> 373 bytes
-rw-r--r--tests/pkg-srcs/sunxi-tools_1.2-2.~~dgittest.debian.tar.gzbin0 -> 5182 bytes
-rw-r--r--tests/pkg-srcs/sunxi-tools_1.2-2.~~dgittest.dsc22
-rw-r--r--tests/pkg-srcs/sunxi-tools_1.2.orig.tar.gzbin0 -> 35076 bytes
-rwxr-xr-xtests/run-all28
-rwxr-xr-xtests/setup/examplegit53
-rwxr-xr-xtests/setup/gnupg30
-rwxr-xr-xtests/tartree-edit145
-rwxr-xr-xtests/tests/absurd-gitapply17
-rwxr-xr-xtests/tests/badcommit-rewrite47
-rwxr-xr-xtests/tests/build-modes35
-rwxr-xr-xtests/tests/build-modes-asplit5
-rwxr-xr-xtests/tests/build-modes-gbp39
-rwxr-xr-xtests/tests/build-modes-gbp-asplit5
-rwxr-xr-xtests/tests/build-modes-sbuild18
-rwxr-xr-xtests/tests/clone-clogsigpipe10
-rwxr-xr-xtests/tests/clone-gitnosuite11
-rwxr-xr-xtests/tests/clone-nogit2
-rwxr-xr-xtests/tests/clone-reprepro33
-rwxr-xr-xtests/tests/debpolicy-dbretry67
-rwxr-xr-xtests/tests/debpolicy-newreject121
-rwxr-xr-xtests/tests/debpolicy-quilt-gbp4
-rwxr-xr-xtests/tests/defdistro-dsd-clone-drs14
-rwxr-xr-xtests/tests/defdistro-mirror5
-rwxr-xr-xtests/tests/defdistro-rpush4
-rwxr-xr-xtests/tests/defdistro-setup19
-rwxr-xr-xtests/tests/distropatches-reject81
-rwxr-xr-xtests/tests/downstream-gitless111
-rwxr-xr-xtests/tests/drs-clone-nogit2
-rwxr-xr-xtests/tests/drs-push-masterupdate50
-rwxr-xr-xtests/tests/drs-push-rejects79
-rwxr-xr-xtests/tests/dsd-clone-drs17
-rwxr-xr-xtests/tests/dsd-clone-nogit4
-rwxr-xr-xtests/tests/dsd-divert7
-rwxr-xr-xtests/tests/fetch-localgitonly6
-rwxr-xr-xtests/tests/fetch-somegit-notlast2
-rwxr-xr-xtests/tests/gbp-orig77
-rwxr-xr-xtests/tests/gitattributes256
-rwxr-xr-xtests/tests/gitconfig38
-rwxr-xr-xtests/tests/import-dsc100
-rwxr-xr-xtests/tests/import-maintmangle41
-rwxr-xr-xtests/tests/import-native69
-rwxr-xr-xtests/tests/import-nonnative17
-rwxr-xr-xtests/tests/import-tarbomb49
-rwxr-xr-xtests/tests/inarchivecopy79
-rwxr-xr-xtests/tests/mirror80
-rwxr-xr-xtests/tests/mirror-debnewgit26
-rwxr-xr-xtests/tests/mirror-private30
-rwxr-xr-xtests/tests/mismatches-contents24
-rwxr-xr-xtests/tests/mismatches-dscchanges34
-rwxr-xr-xtests/tests/multisuite57
-rwxr-xr-xtests/tests/newtag-clone-nogit4
-rwxr-xr-xtests/tests/oldnewtagalt25
-rwxr-xr-xtests/tests/oldtag-clone-nogit4
-rwxr-xr-xtests/tests/orig-include-exclude21
-rwxr-xr-xtests/tests/orig-include-exclude-chkquery33
-rwxr-xr-xtests/tests/overwrite-chkclog50
-rwxr-xr-xtests/tests/overwrite-junk22
-rwxr-xr-xtests/tests/overwrite-splitbrains27
-rwxr-xr-xtests/tests/overwrite-version20
-rwxr-xr-xtests/tests/protocol-compat83
-rwxr-xr-xtests/tests/push-buildproductsdir2
-rwxr-xr-xtests/tests/push-newpackage16
-rwxr-xr-xtests/tests/push-nextdgit2
-rwxr-xr-xtests/tests/quilt67
-rwxr-xr-xtests/tests/quilt-gbp61
-rwxr-xr-xtests/tests/quilt-gbp-build-modes13
-rwxr-xr-xtests/tests/quilt-gbp-build-modes-sbuild12
-rwxr-xr-xtests/tests/quilt-singlepatch39
-rwxr-xr-xtests/tests/quilt-splitbrains140
-rwxr-xr-xtests/tests/quilt-useremail27
-rwxr-xr-xtests/tests/rpush2
-rwxr-xr-xtests/tests/spelling16
-rwxr-xr-xtests/tests/tag-updates37
-rwxr-xr-xtests/tests/test-list-uptodate11
-rwxr-xr-xtests/tests/trustingpolicy-replay85
-rwxr-xr-xtests/tests/unrepresentable52
-rwxr-xr-xtests/tests/version-opt32
-rw-r--r--tests/tstunt/Dpkg/Changelog/Parse.pm71
-rwxr-xr-xtests/tstunt/debuild4
-rwxr-xr-xtests/tstunt/dpkg-parsechangelog78
-rwxr-xr-xtests/tstunt/gpg6
-rwxr-xr-xtests/tstunt/gpg-agent6
-rwxr-xr-xtests/tstunt/lintian3
-rwxr-xr-xtests/using-intree17
-rw-r--r--tests/worktrees/example_1.0.tarbin0 -> 122880 bytes
-rw-r--r--tests/worktrees/pari-extra_3-1.tarbin92160 -> 81920 bytes
-rw-r--r--tests/worktrees/pari-extra_drs.tarbin153600 -> 153600 bytes
-rw-r--r--tests/worktrees/ruby-rails-3.2_test.tarbin133120 -> 215040 bytes
147 files changed, 17468 insertions, 1841 deletions
diff --git a/.gitignore b/.gitignore
index 737875f..13e2c4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,15 @@
*~
tests/tmp
debian/dgit
+debian/dgit-infrastructure
debian/files
debian/*.substvars
debian/*.log
+debian/debhelper-build-stamp
+dgit-user.7
+dgit-nmu-simple.7
+dgit-maint-native.7
+dgit-maint-merge.7
+dgit-maint-gbp.7
+dgit-sponsorship.7
+substituted
diff --git a/DEVELOPER-CERTIFICATE b/DEVELOPER-CERTIFICATE
new file mode 100644
index 0000000..912d22e
--- /dev/null
+++ b/DEVELOPER-CERTIFICATE
@@ -0,0 +1,38 @@
+Developer Certificate of Origin
+Version 1.1
+
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+1 Letterman Drive
+Suite D4700
+San Francisco, CA, 94129
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+ have the right to submit it under the open source license
+ indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+ of my knowledge, is covered under an appropriate open source
+ license and I have the right under that license to submit that
+ work with modifications, whether created in whole or in part
+ by me, under the same open source license (unless I am
+ permitted to submit under a different license), as indicated
+ in the file; or
+
+(c) The contribution was provided directly to me by some other
+ person who certified (a), (b) or (c) and I have not modified
+ it.
+
+(d) I understand and agree that this project and the contribution
+ are public and that a record of the contribution (including all
+ personal information I submit with it, including my sign-off) is
+ maintained indefinitely and may be redistributed consistent with
+ this project or the open source license(s) involved.
+
diff --git a/Debian/Dgit.pm b/Debian/Dgit.pm
new file mode 100644
index 0000000..ba1c288
--- /dev/null
+++ b/Debian/Dgit.pm
@@ -0,0 +1,408 @@
+# -*- perl -*-
+# dgit
+# Debian::Dgit: functions common to dgit and its helpers and servers
+#
+# Copyright (C) 2015-2016 Ian Jackson
+#
+# 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
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package Debian::Dgit;
+
+use strict;
+use warnings;
+
+use Carp;
+use POSIX;
+use IO::Handle;
+use Config;
+use Digest::SHA;
+use Data::Dumper;
+use IPC::Open2;
+
+BEGIN {
+ use Exporter ();
+ our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
+
+ $VERSION = 1.00;
+ @ISA = qw(Exporter);
+ @EXPORT = qw(setup_sigwarn forkcheck_setup forkcheck_mainprocess
+ dep14_version_mangle
+ debiantags debiantag_old debiantag_new
+ server_branch server_ref
+ stat_exists link_ltarget
+ hashfile
+ fail ensuredir executable_on_path
+ waitstatusmsg failedcmd_waitstatus
+ failedcmd_report_cmd failedcmd
+ cmdoutput cmdoutput_errok
+ git_rev_parse git_cat_file
+ git_get_ref git_for_each_ref
+ git_for_each_tag_referring is_fast_fwd
+ $package_re $component_re $deliberately_re
+ $distro_re $versiontag_re
+ $branchprefix
+ initdebug enabledebug enabledebuglevel
+ printdebug debugcmd
+ $debugprefix *debuglevel *DEBUG
+ shellquote printcmd messagequote);
+ # implicitly uses $main::us
+ %EXPORT_TAGS = ( policyflags => [qw(NOFFCHECK FRESHREPO NOCOMMITCHECK)] );
+ @EXPORT_OK = @{ $EXPORT_TAGS{policyflags} };
+}
+
+our @EXPORT_OK;
+
+our $package_re = '[0-9a-z][-+.0-9a-z]*';
+our $component_re = '[0-9a-zA-Z][-+.0-9a-zA-Z]*';
+our $deliberately_re = "(?:TEST-)?$package_re";
+our $distro_re = $component_re;
+our $versiontag_re = qr{[-+.\%_0-9a-zA-Z/]+};
+our $branchprefix = 'dgit';
+
+# policy hook exit status bits
+# see dgit-repos-server head comment for documentation
+# 1 is reserved in case something fails with `exit 1' and to spot
+# dynamic loader, runtime, etc., failures, which report 127 or 255
+sub NOFFCHECK () { return 0x2; }
+sub FRESHREPO () { return 0x4; }
+sub NOCOMMITCHECK () { return 0x8; }
+
+our $debugprefix;
+our $debuglevel = 0;
+
+our $forkcheck_mainprocess;
+
+sub forkcheck_setup () {
+ $forkcheck_mainprocess = $$;
+}
+
+sub forkcheck_mainprocess () {
+ # You must have called forkcheck_setup or setup_sigwarn already
+ getppid != $forkcheck_mainprocess;
+}
+
+sub setup_sigwarn () {
+ forkcheck_setup();
+ $SIG{__WARN__} = sub {
+ die $_[0] if forkcheck_mainprocess;
+ };
+}
+
+sub initdebug ($) {
+ ($debugprefix) = @_;
+ open DEBUG, ">/dev/null" or die $!;
+}
+
+sub enabledebug () {
+ open DEBUG, ">&STDERR" or die $!;
+ DEBUG->autoflush(1);
+ $debuglevel ||= 1;
+}
+
+sub enabledebuglevel ($) {
+ my ($newlevel) = @_; # may be undef (eg from env var)
+ die if $debuglevel;
+ $newlevel //= 0;
+ $newlevel += 0;
+ return unless $newlevel;
+ $debuglevel = $newlevel;
+ enabledebug();
+}
+
+sub printdebug {
+ print DEBUG $debugprefix, @_ or die $! if $debuglevel>0;
+}
+
+sub messagequote ($) {
+ local ($_) = @_;
+ s{\\}{\\\\}g;
+ s{\n}{\\n}g;
+ s{\x08}{\\b}g;
+ s{\t}{\\t}g;
+ s{[\000-\037\177]}{ sprintf "\\x%02x", ord $& }ge;
+ $_;
+}
+
+sub shellquote {
+ my @out;
+ local $_;
+ defined or confess 'internal error' foreach @_;
+ foreach my $a (@_) {
+ $_ = $a;
+ if (!length || m{[^-=_./:0-9a-z]}i) {
+ s{['\\]}{'\\$&'}g;
+ push @out, "'$_'";
+ } else {
+ push @out, $_;
+ }
+ }
+ return join ' ', @out;
+}
+
+sub printcmd {
+ my $fh = shift @_;
+ my $intro = shift @_;
+ print $fh $intro," " or die $!;
+ print $fh shellquote @_ or die $!;
+ print $fh "\n" or die $!;
+}
+
+sub debugcmd {
+ my $extraprefix = shift @_;
+ printcmd(\*DEBUG,$debugprefix.$extraprefix,@_) if $debuglevel>0;
+}
+
+sub dep14_version_mangle ($) {
+ my ($v) = @_;
+ # DEP-14 patch proposed 2016-11-09 "Version Mangling"
+ $v =~ y/~:/_%/;
+ $v =~ s/\.(?=\.|$|lock$)/.#/g;
+ return $v;
+}
+
+sub debiantag_old ($$) {
+ my ($v,$distro) = @_;
+ return "$distro/". dep14_version_mangle $v;
+}
+
+sub debiantag_new ($$) {
+ my ($v,$distro) = @_;
+ return "archive/$distro/".dep14_version_mangle $v;
+}
+
+sub debiantags ($$) {
+ my ($version,$distro) = @_;
+ map { $_->($version, $distro) } (\&debiantag_new, \&debiantag_old);
+}
+
+sub server_branch ($) { return "$branchprefix/$_[0]"; }
+sub server_ref ($) { return "refs/".server_branch($_[0]); }
+
+sub stat_exists ($) {
+ my ($f) = @_;
+ return 1 if stat $f;
+ return 0 if $!==&ENOENT;
+ die "stat $f: $!";
+}
+
+sub _us () {
+ $::us // ($0 =~ m#[^/]*$#, $&);
+}
+
+sub fail {
+ my $s = "@_\n";
+ $s =~ s/\n\n$/\n/;
+ my $prefix = _us().": ";
+ $s =~ s/^/$prefix/gm;
+ die $s;
+}
+
+sub ensuredir ($) {
+ my ($dir) = @_; # does not create parents
+ return if mkdir $dir;
+ return if $! == EEXIST;
+ die "mkdir $dir: $!";
+}
+
+sub executable_on_path ($) {
+ my ($program) = @_;
+ return 1 if $program =~ m{/};
+ my @path = split /:/, ($ENV{PATH} // "/usr/local/bin:/bin:/usr/bin");
+ foreach my $pe (@path) {
+ my $here = "$pe/$program";
+ return $here if stat_exists $here && -x _;
+ }
+ return undef;
+}
+
+our @signames = split / /, $Config{sig_name};
+
+sub waitstatusmsg () {
+ if (!$?) {
+ return "terminated, reporting successful completion";
+ } elsif (!($? & 255)) {
+ return "failed with error exit status ".WEXITSTATUS($?);
+ } elsif (WIFSIGNALED($?)) {
+ my $signum=WTERMSIG($?);
+ return "died due to fatal signal ".
+ ($signames[$signum] // "number $signum").
+ ($? & 128 ? " (core dumped)" : ""); # POSIX(3pm) has no WCOREDUMP
+ } else {
+ return "failed with unknown wait status ".$?;
+ }
+}
+
+sub failedcmd_report_cmd {
+ my $intro = shift @_;
+ $intro //= "failed command";
+ { local ($!); printcmd \*STDERR, _us().": $intro:", @_ or die $!; };
+}
+
+sub failedcmd_waitstatus {
+ if ($? < 0) {
+ return "failed to fork/exec: $!";
+ } elsif ($?) {
+ return "subprocess ".waitstatusmsg();
+ } else {
+ return "subprocess produced invalid output";
+ }
+}
+
+sub failedcmd {
+ # Expects $!,$? as set by close - see below.
+ # To use with system(), set $?=-1 first.
+ #
+ # Actual behaviour of perl operations:
+ # success $!==0 $?==0 close of piped open
+ # program failed $!==0 $? >0 close of piped open
+ # syscall failure $! >0 $?=-1 close of piped open
+ # failure $! >0 unchanged close of something else
+ # success trashed $?==0 system
+ # program failed trashed $? >0 system
+ # syscall failure $! >0 unchanged system
+ failedcmd_report_cmd undef, @_;
+ fail failedcmd_waitstatus();
+}
+
+sub cmdoutput_errok {
+ confess Dumper(\@_)." ?" if grep { !defined } @_;
+ debugcmd "|",@_;
+ open P, "-|", @_ or die "$_[0] $!";
+ my $d;
+ $!=0; $?=0;
+ { local $/ = undef; $d = <P>; }
+ die $! if P->error;
+ if (!close P) { printdebug "=>!$?\n"; return undef; }
+ chomp $d;
+ if ($debuglevel > 0) {
+ $d =~ m/^.*/;
+ my $dd = $&;
+ my $more = (length $' ? '...' : ''); #');
+ $dd =~ s{[^\n -~]|\\}{ sprintf "\\x%02x", ord $& }ge;
+ printdebug "=> \`$dd'",$more,"\n";
+ }
+ return $d;
+}
+
+sub cmdoutput {
+ my $d = cmdoutput_errok @_;
+ defined $d or failedcmd @_;
+ return $d;
+}
+
+sub link_ltarget ($$) {
+ my ($old,$new) = @_;
+ lstat $old or return undef;
+ if (-l _) {
+ $old = cmdoutput qw(realpath --), $old;
+ }
+ my $r = link $old, $new;
+ $r = symlink $old, $new if !$r && $!==EXDEV;
+ $r or die "(sym)link $old $new: $!";
+}
+
+sub hashfile ($) {
+ my ($fn) = @_;
+ my $h = Digest::SHA->new(256);
+ $h->addfile($fn);
+ return $h->hexdigest();
+}
+
+sub git_rev_parse ($) {
+ return cmdoutput qw(git rev-parse), "$_[0]~0";
+}
+
+sub git_cat_file ($) {
+ my ($objname) = @_;
+ # => ($type, $data) or ('missing', undef)
+ # in scalar context, just the data
+ our ($gcf_pid, $gcf_i, $gcf_o);
+ if (!$gcf_pid) {
+ my @cmd = qw(git cat-file --batch);
+ debugcmd "GCF|", @cmd;
+ $gcf_pid = open2 $gcf_o, $gcf_i, @cmd or die $!;
+ }
+ printdebug "GCF>| ", $objname, "\n";
+ print $gcf_i $objname, "\n" or die $!;
+ my $x = <$gcf_o>;
+ printdebug "GCF<| ", $x;
+ if ($x =~ m/ (missing)$/) { return ($1, undef); }
+ my ($type, $size) = $x =~ m/^.* (\w+) (\d+)\n/ or die "$objname ?";
+ my $data;
+ (read $gcf_o, $data, $size) == $size or die "$objname $!";
+ $x = <$gcf_o>;
+ $x eq "\n" or die "$objname ($_) $!";
+ return ($type, $data);
+}
+
+sub git_for_each_ref ($$;$) {
+ my ($pattern,$func,$gitdir) = @_;
+ # calls $func->($objid,$objtype,$fullrefname,$reftail);
+ # $reftail is RHS of ref after refs/[^/]+/
+ # breaks if $pattern matches any ref `refs/blah' where blah has no `/'
+ # $pattern may be an array ref to mean multiple patterns
+ $pattern = [ $pattern ] unless ref $pattern;
+ my @cmd = (qw(git for-each-ref), @$pattern);
+ if (defined $gitdir) {
+ @cmd = ('sh','-ec','cd "$1"; shift; exec "$@"','x', $gitdir, @cmd);
+ }
+ open GFER, "-|", @cmd or die $!;
+ debugcmd "|", @cmd;
+ while (<GFER>) {
+ chomp or die "$_ ?";
+ printdebug "|> ", $_, "\n";
+ m#^(\w+)\s+(\w+)\s+(refs/[^/]+/(\S+))$# or die "$_ ?";
+ $func->($1,$2,$3,$4);
+ }
+ $!=0; $?=0; close GFER or die "$pattern $? $!";
+}
+
+sub git_get_ref ($) {
+ # => '' if no such ref
+ my ($refname) = @_;
+ local $_ = $refname;
+ s{^refs/}{[r]efs/} or die "$refname $_ ?";
+ return cmdoutput qw(git for-each-ref --format=%(objectname)), $_;
+}
+
+sub git_for_each_tag_referring ($$) {
+ my ($objreferring, $func) = @_;
+ # calls $func->($tagobjid,$refobjid,$fullrefname,$tagname);
+ printdebug "git_for_each_tag_referring ",
+ ($objreferring // 'UNDEF'),"\n";
+ git_for_each_ref('refs/tags', sub {
+ my ($tagobjid,$objtype,$fullrefname,$tagname) = @_;
+ return unless $objtype eq 'tag';
+ my $refobjid = git_rev_parse $tagobjid;
+ return unless
+ !defined $objreferring # caller wants them all
+ or $tagobjid eq $objreferring
+ or $refobjid eq $objreferring;
+ $func->($tagobjid,$refobjid,$fullrefname,$tagname);
+ });
+}
+
+sub is_fast_fwd ($$) {
+ my ($ancestor,$child) = @_;
+ my @cmd = (qw(git merge-base), $ancestor, $child);
+ my $mb = cmdoutput_errok @cmd;
+ if (defined $mb) {
+ return git_rev_parse($mb) eq git_rev_parse($ancestor);
+ } else {
+ $?==256 or failedcmd @cmd;
+ return 0;
+ }
+}
+
+1;
diff --git a/Debian/Dgit/Infra.pm b/Debian/Dgit/Infra.pm
new file mode 100644
index 0000000..eff460b
--- /dev/null
+++ b/Debian/Dgit/Infra.pm
@@ -0,0 +1,17 @@
+# -*- perl -*-
+
+package Debian::Dgit::Infra;
+
+use strict;
+use warnings;
+
+# Scripts and programs which are going to `use Debian::Dgit' but which
+# live in dgit-infrastructure (ie are installed with install-infra)
+# should `use Debian::Dgit::Infra' first. All this module does is
+# adjust @INC so that the script gets the version of the script from
+# the dgit-infrastructure package (which is installed in a different
+# location and may be a different version).
+
+# unshift @INC, q{/usr/share/dgit/infra/perl5}; ###substituted###
+
+1;
diff --git a/Debian/Dgit/Policy/Debian.pm b/Debian/Dgit/Policy/Debian.pm
new file mode 100644
index 0000000..12f1ee1
--- /dev/null
+++ b/Debian/Dgit/Policy/Debian.pm
@@ -0,0 +1,42 @@
+# -*- perl -*-
+
+package Debian::Dgit::Policy::Debian;
+
+use strict;
+use warnings;
+
+use POSIX;
+
+BEGIN {
+ use Exporter ();
+ our ($VERSION, @ISA, @EXPORT, @EXPORT_OK, %EXPORT_TAGS);
+
+ $VERSION = 1.00;
+ @ISA = qw(Exporter);
+ @EXPORT = qw(poldb_path poldb_setup $poldbh);
+ %EXPORT_TAGS = ( );
+ @EXPORT_OK = qw();
+}
+
+our @EXPORT_OK;
+
+our $poldbh;
+
+sub poldb_path ($) {
+ my ($repos) = @_;
+ return "$repos/policy.sqlite3";
+}
+
+sub poldb_setup ($;$) {
+ my ($policydb, $hook) = @_;
+
+ $poldbh ||= DBI->connect("dbi:SQLite:$policydb",'','', {
+ RaiseError=>1, PrintError=>1, AutoCommit=>0
+ });
+
+ $hook->() if $hook;
+
+ $poldbh->do("PRAGMA foreign_keys = ON");
+}
+
+1;
diff --git a/Makefile b/Makefile
index f456d93..d2a91d8 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
# dgit
# Integration between git and Debian-style archives
#
-# Copyright (C)2013 Ian Jackson
+# Copyright (C)2013-2016 Ian Jackson
#
# 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
@@ -25,20 +25,85 @@ prefix?=/usr/local
bindir=$(prefix)/bin
mandir=$(prefix)/share/man
+perldir=$(prefix)/share/perl5
man1dir=$(mandir)/man1
+man7dir=$(mandir)/man7
+infraexamplesdir=$(prefix)/share/doc/dgit-infrastructure/examples
+txtdocdir=$(prefix)/share/doc/dgit
+absurddir=$(prefix)/share/dgit/absurd
-PROGRAMS=dgit dgit-repos-server
+PROGRAMS=dgit dgit-badcommit-fixup
MAN1PAGES=dgit.1
-all:
+MAN7PAGES=dgit.7 \
+ dgit-user.7 dgit-nmu-simple.7 \
+ dgit-maint-native.7 \
+ dgit-maint-merge.7 dgit-maint-gbp.7 \
+ dgit-sponsorship.7
-install: installdirs
- $(INSTALL_PROGRAM) $(PROGRAMS) $(DESTDIR)$(bindir)
+TXTDOCS=README.dsc-import
+PERLMODULES=Debian/Dgit.pm
+ABSURDITIES=git
+
+INFRA_PROGRAMS=dgit-repos-server dgit-ssh-dispatch \
+ dgit-repos-policy-debian dgit-repos-admin-debian \
+ dgit-repos-policy-trusting dgit-mirror-rsync
+INFRA_EXAMPLES=get-dm-txt ssh-wrap drs-cron-wrap get-suites
+INFRA_PERLMODULES= \
+ Debian/Dgit.pm \
+ Debian/Dgit/Infra.pm \
+ Debian/Dgit/Policy/Debian.pm
+
+all: $(MAN7PAGES) $(addprefix substituted/,$(PROGRAMS))
+
+substituted/%: %
+ mkdir -p substituted
+ perl -pe 's{\bundef\b}{'\''$(absurddir)'\''} if m/###substituted###/' \
+ <$< >$@
+
+install: installdirs all
+ $(INSTALL_PROGRAM) $(addprefix substituted/,$(PROGRAMS)) \
+ $(DESTDIR)$(bindir)
+ $(INSTALL_PROGRAM) $(addprefix absurd/,$(ABSURDITIES)) \
+ $(DESTDIR)$(absurddir)
$(INSTALL_DATA) $(MAN1PAGES) $(DESTDIR)$(man1dir)
+ $(INSTALL_DATA) $(MAN7PAGES) $(DESTDIR)$(man7dir)
+ $(INSTALL_DATA) $(TXTDOCS) $(DESTDIR)$(txtdocdir)
+ set -e; for m in $(PERLMODULES); do \
+ $(INSTALL_DATA) $$m $(DESTDIR)$(perldir)/$${m%/*}; \
+ done
installdirs:
- $(INSTALL_DIR) $(DESTDIR)$(bindir) $(DESTDIR)$(man1dir)
+ $(INSTALL_DIR) $(DESTDIR)$(bindir) \
+ $(DESTDIR)$(man1dir) $(DESTDIR)$(man7dir) \
+ $(DESTDIR)$(txtdocdir) $(DESTDIR)$(absurddir) \
+ $(addprefix $(DESTDIR)$(perldir)/, $(dir $(PERLMODULES)))
+
+install-infra: installdirs-infra
+ $(INSTALL_PROGRAM) $(addprefix infra/, $(INFRA_PROGRAMS)) \
+ $(DESTDIR)$(bindir)
+ $(INSTALL_PROGRAM) $(addprefix infra/, $(INFRA_EXAMPLES)) \
+ $(DESTDIR)$(infraexamplesdir)
+ set -e; for m in $(INFRA_PERLMODULES); do \
+ $(INSTALL_DATA) $$m $(DESTDIR)$(perldir)/$${m%/*}; \
+ done
+
+installdirs-infra:
+ $(INSTALL_DIR) $(DESTDIR)$(bindir) $(DESTDIR)$(infraexamplesdir) \
+ $(addprefix $(DESTDIR)$(perldir)/, $(dir $(INFRA_PERLMODULES)))
check installcheck:
clean distclean mostlyclean maintainer-clean:
+ rm -rf tests/tmp substituted
+ set -e; for m in $(MAN7PAGES); do \
+ test -e $$m.pod && rm -f $$m; \
+ done
+
+%.7: %.7.pod
+ pod2man --section=7 --date="Debian Project" --center="dgit" \
+ --name=$(subst .7,,$@) \
+ $^ $@
+
+%.view: %
+ man -l $*
diff --git a/NOTES b/NOTES
deleted file mode 100644
index 05fe7b8..0000000
--- a/NOTES
+++ /dev/null
@@ -1,17 +0,0 @@
-remote is dgit
- can do fetch thing?
- push is deliberately broken?
-
-remote refs
- refs/dgit/<suite>
-local refs
- refs/heads/<suite>
- refs/remotes/dgit/<suite>
-
-Vcs-Dgit-Master: <commit>
- specifies commit hash corresponding to the thing uploaded
- optional commit hash corresponding to pristine tar??
-
-Investigate git-dpm
-14:03 <col> https://wiki.debian.org/PackagingWithGit/GitDpm although I think
- its manual page is more useful really.
diff --git a/README.dsc-import b/README.dsc-import
new file mode 100644
index 0000000..1ec53b0
--- /dev/null
+++ b/README.dsc-import
@@ -0,0 +1,106 @@
+We would like to: represent the input tarballs as a commit each (which
+all get merged together as if by git merge -s subtree), and for quilt
+packages, each patch as a commit. But w want to avoid (as much as
+possible) reimplementing the package extraction algorithm in
+dpkg-source.
+
+dpkg-source does not currently provide interfaces that look like they
+are intended for what dgit wants to do. And dgit wants to work with
+old versions of dpkg, so I have implemented the following algorithm
+rather than wait for such interfaces added (even supposing that a sane
+interface could be designed, which is doubtful):
+
+* dgit will untar each input tarball.
+
+ This will be done by scanning the .dsc for things whose names look
+ like (compressed) tarballs, and using the interfaces provided by
+ Dpkg::Compression to get at the tarball.
+
+ Each input tarball unpack will be done separately, and will be
+ followed by git add and git write-tree, to obtain a git tree object
+ corresponding to the tarball contents.
+
+ That tree object will be made into a commit object with no parents.
+ (The package changelog will be searched for the earliest version
+ with the right upstream version component, and the information found
+ there used for the commit object's metadata.)
+
+* For `3.0 (quilt), dgit will run
+ dpkg-source -x --skip-patches
+
+ git plumbing will be used to make the result into a tree and a
+ commit. The commit will have as parents all the tarballs previously
+ mentioned. The main orig tarball will be the leftmost parent and
+ the debian tarball the rightmost parent. The metadata will come
+ from the .dsc and/or the final changelog entry.
+
+ dgit will then dpkg-source --before-build and record the resulting
+ tree, too.
+
+ Then, dgit will switch back to the patches-unapplied version and use
+ `gbp pq import' (in the private working area) to turn the
+ patches-unapplied tree into a patches-applied one.
+
+ Finally dgit will check that the gbp pq generated patches-applied
+ version has the same git tree object as the one generated by
+ dpkg-source --before-build.
+
+* For source formats other than `3.0 (quilt)', dgit will do simply
+ dpkg-source -x.
+
+ Again, it will make that into a tree and a commit.
+
+* For source formats with only single file entry in the .dsc, the
+ (one) tarball is not imported separately (since its tree object
+ would be the same as the extracted object), and the commit of the
+ dpkg-source -x output has no parents.
+
+* As currently, there will be a final no-change-to-the-tree
+ pseudomerge commit which stitches the package into the relevant dgit
+ suite branch. (By `pseudomerge' we mean something that looks as if
+ it was made with git merge -s ours.)
+
+* As currently, dgit will take steps so that none of the git trees
+ discussed above contain a .pc directory.
+
+
+This has the following properties:
+
+* Each input tarball is represented by a different commit; in usual
+ cases these commits will be the same for every upload of the same
+ upstream version.
+
+* For `3.0 (quilt)' each patch's changes to the upstream files appears
+ as a single git commit (as is the effect of the debian tarball);
+ also, there is a commit object whose tree is just the debian/
+ directory, which might well be the same as certain debian-only git
+ workflow trees.
+
+* For `1.0' non-native, the effect of the diff is represented as a
+ commit. So eg `git blame' will show synthetic commits corresponding
+ to the correct parts of the input source package.
+
+* It is possible to `git cherry-pick' etc. commits representing `3.0
+ (quilt)' patches. It is even possible fish out the patch stack as
+ git branch and rebase it elsewhere etc., since the patch stack is
+ represented as a contiguous series of commits which make only the
+ relevant upstream changes.
+
+* Every orig tarball in the source package is decompressed twice, but
+ disk space for only one extra copy of its unpacked contents is
+ needed. (The converse would be possible in principle but would be
+ very hard to arrange with the current interfaces provided by the
+ various tools.)
+
+* No back doors into the innards of dpkg-source (nor changes to
+ dpkg-dev) are required.
+
+* dgit does grow a dependency on git-buildpackage.
+
+* Knowledge of the source format embedded in dgit is is restricted to
+ some relatively straightforward processing of filenames found in
+ .dsc files.
+
+* dgit now depends on dpkg-source -x --skip-patches followed by
+ dpkg-source --before-build being the same as dpkg-source -x
+ (for `3.0 (quilt)').
diff --git a/absurd/git b/absurd/git
new file mode 100755
index 0000000..0f562b5
--- /dev/null
+++ b/absurd/git
@@ -0,0 +1,115 @@
+#!/bin/sh
+set -e
+
+case "$DGIT_ABSURD_DEBUG" in
+''|0) exec 3>/dev/null ;;
+1) exec 3>>../../gbp-pq-output ;;
+*) exec 3>>../../gbp-pq-output 2>&3 ;;
+esac
+
+log () {
+ echo >&3 "DGIT ABSURD GIT APPLY (DEBUG) $*"
+ echo >&2 "DGIT ABSURD GIT APPLY (STDERR) $*"
+}
+
+fail () {
+ log "FAILED: $*"
+ exit 127
+}
+
+self=${0%/*}
+npath=${PATH#$self:}
+if test "x$PATH" = "x$npath"; then
+ fail "PATH FILTER FAIL ($0 $self $PATH)"
+fi
+
+bypass=true
+for arg in "$@"; do
+ case "$arg" in
+ apply) bypass=false; break ;;
+ -*) ;;
+ *) bypass=true; break ;;
+ esac
+done
+
+if $bypass; then
+ PATH=$npath
+ echo >&3 "DGIT ABSURD GIT APPLY - BYPASS: $*"
+ exec git "$@"
+fi
+
+log "NO BYPASS: $*"
+
+case "$DGIT_ABSURD_DEBUG" in
+''|0|1) ;;
+*) set -x ;;
+esac
+
+#exec >/dev/tty 2>&1
+
+index=0
+noo=0
+
+for arg in "$@"; do
+ case "$noo.$arg" in
+ 1.--index)
+ index=1
+ continue
+ ;;
+ 1.--whitespace=fix)
+ continue
+ ;;
+ ?.-*)
+ fail "UNKNOWN OPTION $arg ($*)"
+ ;;
+ 0.apply)
+ ;;
+ 1.*) patch="$arg"
+ ;;
+ *)
+ fail "BAD USAGE $arg ($noo $*)"
+ esac
+ noo=$(( $noo + 1 ))
+done
+
+if [ $noo != 2 ]; then
+ fail "NO PATCH ($*)"
+fi
+
+pwd=`pwd`
+patch=${patch#$pwd/debian/patches/}
+rm -f debian/patches/series
+
+# Work around #848611.
+# We need a stunt filename which the source package must not
+# contain. A trick is to use the commit hash of HEAD, whose
+# hash value cannot appear in any file in its own tree.
+omgwtf="dgit-omg-wtf-$(git rev-parse HEAD)"
+cat <<END >debian/patches/$omgwtf
+---
+--- a/$omgwtf 2016-10-31 23:28:47.314155919 +0000
++++ b/$omgwtf 2016-12-18 22:40:01.870058270 +0000
+@@ -0,0 +1 @@
++:
+END
+printf "%s\n" "$omgwtf" >debian/patches/series
+printf "%s\n" "$patch" >>debian/patches/series
+
+# Just in case some joker tries to patch .git/something
+mv .git ../.git
+set +e
+dpkg-source --before-build .
+rc=$?
+set -e
+rm -rf .git
+mv ../.git .
+test $rc = 0
+
+rm -f $omgwtf debian/patches/$omgwtf
+
+rm -rf .pc
+git checkout debian/patches/series
+git add -Af .
+
+log "APPLIED $patch"
+#printf 'APPLIED '; date --iso-8601=ns
diff --git a/debian/changelog b/debian/changelog
index 4014687..54cc53a 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,14 +1,998 @@
-dgit (0.22-experimental2) experimental; urgency=low
+dgit (4.0) experimental; urgency=low
- * Dummy upload in a talk.
+ * dgit: --deliberately-not-fast-forward works properly in
+ split view quilt modes (suppressing the pseudomerge).
- -- Ian Jackson <ijackson@chiark.greenend.org.uk> Sun, 31 Aug 2014 03:06:51 +0100
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Sun, 12 Feb 2017 22:22:31 +0000
-dgit (0.22-experimental1) experimental; urgency=low
+dgit (3.10) unstable; urgency=medium
- * Dummy test upload.
+ Bugfixes:
+ * dgit: Copy several user.* settings from main tree git local config
+ to dgit private workarea. Closes:#853085.
+ * dgit: Strip initial newline from Changes line from dpkg-parsechangelog
+ so as to avoid blank line in commit messages. Closes:#853093.
+ * dgit: Do not fail when run with detached HEAD. Closes:#853022.
+ * dgit: Be much better about commas in maintainer changelog names.
+ Closes:#852661.
+
+ Test suite:
+ * quilt-useremail: New test for user config copying (#853085).
+ * lib-import-chk: Test that commits have smae authorship as appears in
+ the changelog. (Or, at least, the same authorship set.)
+ * import-maintmangle: New test for changelog Maintainer mangling.
+
+ Documentation:
+ * Fix typos. Closes:#853125. [Nicholas D Steeves]
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Mon, 06 Feb 2017 17:49:39 +0000
+
+dgit (3.9) unstable; urgency=medium
+
+ Improvements:
+ * dgit --overwrite: Check that the overwritten version's changelog entry
+ is not UNRELEASED. This could easily happen if this release was being
+ made from a git branch which predates the previous package upload.
+
+ Documentation:
+ * dgit-maint-merge(7): Get git clone url right. Closes:#852609.
+ * dgit-maint-merge(7): Quote sample clone commands. Closes:#852615.
+
+ Test suite:
+ * overwrite-chkclog: test UNRELEASED handling.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Wed, 25 Jan 2017 16:21:53 +0000
+
+dgit (3.8) unstable; urgency=medium
+
+ Bugfixes:
+ * Make dgit-setup-* work in default distro.
+
+ Test suite:
+ * defdistro-setup: Test that setup-* functions distro selection works.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Mon, 23 Jan 2017 16:21:30 +0000
+
+dgit (3.7) unstable; urgency=medium
+
+ Bugfixes:
+ * Fix clone-dgit-repos-server and print-dgit-repos-server-print-url.
+ Closes:#851906.
+
+ Documentation:
+ * dgit-maint-merge(7): Explain when workflow is unsuitable
+ (Closes:#852090) and improve the patch-header (Closes:#851897.)
+
+ Internal changes:
+ * New %.view target: `make dgit-maint-merge.7.view' runs `man -l ...'
+
+ Test suite:
+ * defdistro-dsd-clone-drs: New test which would have detected
+ #851906 (and hopefully #850521).
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Sun, 22 Jan 2017 17:30:24 +0000
+
+dgit (3.6) unstable; urgency=medium
+
+ Bugfixes:
+ * Actually use the url from a Dgit .dsc field naming an unknown distro.
+ Closes:#851728.
+ * Add dummy implementation of file_in_archive_aptget copied from
+ file_in_archive_dummycat. Re:#851697. [ Peter Green ]
+
+ Minor improvements:
+ * Use `confess' to print a stack trace in a couple of internal error
+ rcases.
+
+ Infrastructure:
+ * Properly honour NOCOMMITCHECK policy hook exit status.
+ Closes:#851800.
+ * Do not reject commits with no author/committer name (but still insist
+ on email address and date). Peter Green reports that eg
+ 71e128629ec786f3 in upstream xen.git is such a commit (and is accepted
+ by github). Closes:#851716.
+
+ Test suite:
+ * downstream-gitless: Test import of .dsc from unknown distro.
+ * downstream-gitless: Test import of .dsc with unsafe url.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Thu, 19 Jan 2017 01:15:03 +0000
+
+dgit (3.5) unstable; urgency=medium
+
+ Bugfixes:
+ * gitattributes: Defuse gitattributes in private working area even if we
+ don't do it in the user's tree (because of user configuration).
+ * gitattributes: When cloning, do not print spurious warning about
+ actually-defused gitattributes. Closes:#851624.
+ * gitattributes: Improve comment left in .git/info/attributes.
+
+ Test suite:
+ * gitattributes: Many improvements to test case.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Tue, 17 Jan 2017 22:36:01 +0000
+
+dgit (3.4) unstable; urgency=low
+
+ Test suite:
+ * drs-push-rejects: Set origin's url to an ad-hoc expression
+ which produces the right ext:: rune, as dgit would.
+ Closes:#851580.
+ * Replace references to /home/ian in various worktrees with
+ references to /nonexistent, to catch inadvertant accesses.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Mon, 16 Jan 2017 17:27:35 +0000
+
+dgit (3.3) unstable; urgency=medium
+
+ Behavioural changes to work around gitattributes file transformations:
+ * Suppress file-transforming gitattributes in private work areas.
+ * Configure suppression in user's trees in dgit clone and setup-new-tree.
+ * Provide dgit setup-gitattributes to do this explicitly.
+ * Documentation.
+
+ Bugfixes:
+ * dgit: Remove a leftover debugging print.
+ * dgit: Set default dsc import distro when there is no Dgit field.
+ * dgit: Set default dsc import distro when suppressing Dgit field.
+ * dgit: Option parsing: Fix undefined $suite in some import-dsc.
+ Closes:#851213.
+
+ Packaging:
+ * Remove redundant use of List::Util qw(any). Closes:#851280.
+ * Remove redundant Recommends on libtext-iconv-perl.
+
+ Test suite:
+ * Move default dsc distro config setting to lib. We need this
+ for the .dscs we have in tests/pkg-srcs/.
+ * defdistro-import-dsc: Drop this test.
+ * protocol-compat: check that we use the right distro
+ information when importing.
+ * Internal change: fix handling of nonempty distro=
+ * gitattributes: New test for .gitattributes handling.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Mon, 16 Jan 2017 10:03:08 +0000
+
+dgit (3.2) unstable; urgency=medium
+
+ Bugfixes:
+ * dgit: Do not execute END blocks in children. So far symptoms of this
+ bug seem to be limited to duplicated error messages but I have not
+ done a thorough analysis. Closes:#850052.
+ * dgit-infrastructure: dgit-repos-policy-debian: Remirror a package when
+ it becomes public (ie, make the repo available much more promptly when
+ the package passes NEW). Closes:#849789.
+ * dgit: Fix a warning message about ref (mainly, tag) updates.
+
+ Documentation:
+ * dgit-maint-merge(7): Use git-deborig(1).
+ [Sean Whitton] Closes:#850953.
+ * dgit-user(7): Fix some typos.
+
+ Internals:
+ * Fix a typo in a comment.
+
+ Test suite:
+ * infra: mirroring and policy hooks: Improve some debugging output.
+ * infra: mirror-private: test that package becomes public. (#849789)
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Thu, 12 Jan 2017 02:11:34 +0000
+
+dgit (3.1) unstable; urgency=medium
+
+ Bugfixes:
+ * dgit import-dsc: Do not crash with undefined $isuite. Closes:#850781.
+ * dgit build: Do not sometimes crash with undefined $isuite.
+ * dgit: Do not nedlessly re-fetch the rewrite map.
+ * dgit: After downloading .debian.* files, save them in `..', too
+ (ie do this not just for .origs).
+ * dgit: When fetching, refetch files with hash mismatches (and save them
+ as `...,fetch'), so we can distinguish them from any built locally.
+ Closes:#850824.
+
+ Test suite:
+ * Add test for import-dsc with default distro. (Detects #850781.)
+
+ Administrivia:
+ * Fix a dgit 3.0 changelog bullet referring to refs/dgit-fetch/DISTRO.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Tue, 10 Jan 2017 17:50:27 +0000
+
+dgit (3.0) unstable; urgency=medium
+
+ Protocol change:
+ * Dgit: field now records the nominal distro name, and a hint
+ for a tag and url where the git objects (including any rewrite
+ map) can be fetched.
+ * Use this information, where provided. Closes:#850431.
+
+ Bugfixes:
+ * dgit config handling: Honour command-line and context-provided
+ suite and distro more reliably and consistently.
+ * Parsing of extended Dgit fields by import-dsc was broken;
+ and is now fixed even for more-extended ones.
+ * dgit clone-dgit-repos-server uses readonly access.
+ Closes:#850521.
+ * fetch and pull ignore the changelog suite when it is UNRELEASED.
+ Closes:#848646.
+ * dgit-badcommit-fixup: Do not investigate symrefs. Closes:#850547.
+
+ Minor new feature:
+ * distro alias facility in config space. (Primarily for testing.)
+ * Undocumented --config-lookup-explode= feature. (For testing.)
+ * Provide `dgit print-dgit-repos-server-source-url'. Re:#850521.
+ * Honour dgit-distro.*.default-suite and dgit.default.default-suite.
+
+ Other improvements:
+ * Improve debugging output a bit.
+ * Use refs/dgit-fetch/DISTRO rather than refs/dgit-fetch/SUITE,
+ which leads to less duplication and so less clutter.
+ * Enforce a reasonable syntax for nominal distro names.
+ * When generating orig+debian/patches view, copy debian/ from
+ HEAD. This makes less noise in diffs. Closes:#850095.
+
+ Docuentation [Sean Whitton and Ian Jackson]:
+ * dgit-sponsorship(7): Use --no-dep14tag. Closes:#849105.
+ * dgit-maint-merge(7): Use debian/source/patch-header. Closes:849120.
+ * dgit(7): Updated `trouble' section to suggest having dpkg-source
+ delete the autotools output (with a patch if necessary).
+ * dgit(1): Several minor updates and fixes. Closes:#850519.
+
+ Test suite:
+ * Internal improvements.
+ * badcommit-rewrite: Fix operation using installed version of fixup.
+ * Arrange to pass --debug-quick-random to gpg-agent.
+ * Strip block count out of find -ls output - it is unstable!
+ * gbp-orig: Add a missing -m, without which git would run an
+ editor if stdout was a tty (!)
+ * Add t-stunt-parsechangelog to a few tests which were missing it.
+ * Tests for the new protocol feature.
+ * Fail tests if we look up any configuration relating to Debian.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Mon, 09 Jan 2017 16:43:10 +0000
+
+dgit (2.16.2) unstable; urgency=low
+
+ dgit-badcommit-fixup:
+ * Fix crash when running for 2nd time in bare repo.
+ * In --check mode, exit with status 2 if things are not fine.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Sat, 07 Jan 2017 13:31:50 +0000
+
+dgit (2.16.1) UNRELEASED; urgency=low
+
+ * dgit-badcommit-fixup: New mode --check which is readonly.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Sat, 07 Jan 2017 13:04:49 +0000
+
+dgit (2.16) unstable; urgency=low
+
+ Dealing with fallout from #849041:
+ * Provide dgit-badcommit-fixup history-rewriting script.
+ * New rewrite map feature, which allows dgit git server to adjust
+ clients' interpretation of Dgit fields, so that history-rewriting is
+ effective. (Feature is only partially implemented right now -
+ enough to dig current Debian users out of the hole.) Re:#850431.
+
+ Test suite:
+ * New test case for history-rewriting.
+ * Change `local foo=$(bar)' idiom to `local foo; foo=$(bar)' since
+ the former does not trip set -e even if bar fails :-(.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Fri, 06 Jan 2017 20:46:30 +0000
+
+dgit (2.15) UNRELEASED; urgency=high
+
+ Infastructure:
+ * Prevent introduction of new commits which lack `committer'
+ information. Ie, prevent the reception of new commits afflicted by
+ #849041. Existing commits are tolerated.
+
+ Test suite:
+ * Be much stricter about messages from git-fsck.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Thu, 05 Jan 2017 18:20:23 +0000
+
+dgit (2.14) unstable; urgency=critical
+
+ CRITICAL BUGFIX:
+ * Do not generate bogus commits with --overwrite or import-dsc.
+ Closes:#849041.
+
+ Test suite:
+ * Run a lot of git-fsck.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Wed, 04 Jan 2017 22:52:55 +0000
+
+dgit (2.13) unstable; urgency=high
+
+ Changed behaviour:
+ * quilt fixup: Permit creation of patches which delete files, by psssing
+ --include-removal to dpkg-source, and tolerating it when we do our
+ quilt fixup analysis. dpkg-source has supported this since at least
+ stretch. Closes:#848901.
+
+ Error messages:
+ * Improve "cannot represent change" message: print the git old and new
+ modes too.
+
+ Bugfix:
+ * Import: Switch back to unpa branch on patch import iterations.
+ In particular, do not fail utterly if dpkg-source and gbp disagree.
+ Closes:#848843.
+
+ Documentation [Sean Whitton]:
+ * dgit-maint-gbp(7): Remove reference to closed bug. Closes:#848725.
+ * dgit-sponsorship(7): Update in light of fixed #844129. Closes:#848789.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Wed, 21 Dec 2016 01:32:41 +0000
+
+dgit (2.12) unstable; urgency=high
+
+ Changed behaviours:
+ * By default, generate a DEP-14 tag as well as a dgit archive/*
+ tag, even in non-split-view quilt modes. Closes:#844129.
+ * Version tags mangling: Protect dots, as per proposed update to DEP-14.
+
+ Documentation:
+ * dgit-maint-merge(7): Explain how to change to this workflow
+ from an existing git workflow. [Sean Whitton] Closes:#847807.
+ * dgit-maint-native(7): Clarify that we mean native source format.
+ [Phil Hands] Closes:#847987.
+
+ Error messages:
+ * Slightly better message when .dsc not found. Apropos of #844128.
+ * Give better advice if .dsc/.changes signing fails: if no changes
+ are needed to the package, user may indeed just debsign and dput.
+ Closes:#844131.
+ * Produce better error reporting when absurd git wrapper fails
+ on a patch during .dsc import. Apropos of #848391.
+
+ Bugfixes:
+ * If we cannot hardlink origs into our extraction area, use symlinks
+ instead. Closes:#844570.
+ * Suppress some leftover debugging output from import-dsc.
+ Closes:#847658.
+ * Do not fail when cloning a package containing dangling symlinks.
+ Closes:#848512.
+ * Do not fail to import a .dsc containing patches which patch files
+ multiple times, due to #848611. Closes:#848391.
+ * Do not fail to import a .dsc containing patches to .git/ (!)
+ * infra: dgit-repos-policy-debian which broke due to recent git setting
+ GIT_ALTERNATE_OBJECT_DIRECTORIES in the pre-receive-hook.
+ (fixes test suite regression in stretch).
+
+ Test suite:
+ * Provide and use stunt lintian and debuild, to avoid lintian
+ complaining about our stupid test packages.
+ (fixes test suite regression in stretch).
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Mon, 19 Dec 2016 17:35:18 +0000
+
+dgit (2.11) unstable; urgency=medium
+
+ Documentation:
+ * dgit-user(7): Better explanation of combined suites (comma syntax).
+ Thanks to Sean Whitton for review and suggestions.
+ * dgit(1), dgit(7): Better reference docs for combined suites.
+ * dgit(1): Improve formatting of rpush section.
+
+ Test suite:
+ * Replace make in Test-Depends with build-essential. Most of the tests
+ do in fact run dpkg-buildpackage which bombs out if build-essential is
+ missing.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Tue, 08 Nov 2016 22:41:29 +0000
+
+dgit (2.10) unstable; urgency=medium
+
+ New features:
+ * Support the Debian *-security suites.
+ * New comma-separated multiple-suite merging facility (readonly),
+ so that users can easily track "jessie, or jessie-security".
+ * dgit-user(7): Suggest `dgit clone P jessie,-security'.
+
+ Bugfixes:
+ * Cope when an orig tarball is a tarbomb. Ie, if it contains
+ other than one single directory toplevel. Closes:#843422.
+ * Actually honour the branch name, if we are on dgit branch, to specify
+ the suite, as documented in the manpage.
+ * When cloning a distro which has no git server, correctly leave
+ the user on the local dgit branch, not on `master'.
+ * Fix an unconditional print that was supposed to be a printdebug:
+ origs <blah>.orig.tar.gz f.same=1 #f._differ=-1
+ * Print a slightly better message if .git found in orig tarball(s).
+
+ Test suite:
+ * Test suite: Add fakeroot and make to Test-Depends. These aren't
+ necessarily pulled in by anything else. (dpkg-dev Recommends
+ build-essential. But we don't actually need build-essential.)
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Tue, 08 Nov 2016 01:08:51 +0000
+
+dgit (2.9) unstable; urgency=medium
+
+ New features:
+ * During push, automatically calculate which .origs are required,
+ so user never needs [--ch:]-sa or [--ch:]-sd. Closes:#829116.
+ * New import-dsc feature.
+ * New option --dgit-view-save= for split view quilt modes.
+ In particular, means that the output of a split view quilt-fixup
+ is left somewhere useful.
+ * dgit clone: Set timestamps in cloned tree to a single unified time.
+ This makes it less likely that the user will trip over any
+ timestamp-dependent FTBFS bugs (eg #842452).
+ * Support dgit --delayed= push (with a warning in the manpage
+ about possible skew).
+ * dgit gbp-build will arrange to let gbp buildpackage generate
+ .orig tarballs if it seems applicable. Closes:#841094.
+
+ Documentation improvements:
+ * dgit-*(7). Many new tutorial manpages, several written and many
+ improved by Sean Whitton.
+ * dgit(7): Substantial updates, including documenting split view.
+ * dgit(1): Better cross-references.
+ * dgit(1): Remove obsolete workflow information.
+ * dgit(1): Improved BUGS section.
+ * Fix changelog entry for SIGPIPE to correctly mention
+ Closes:#841090.
+
+ Bugfixes:
+ * Split brain mode: Fix --new. Closes:#842577.
+ * Properly look for .origs etc. in .., fetching them less often.
+ Closes:#842386.
+ * Reject `dgit pull' in split view quilt modes, to avoid
+ creating unfortunate wreckage on non-dgit-view branches.
+ Closes:#842608.
+ * Cope when cloning suite which doesn't receive uploads,
+ like testing. Closes:#842621.
+ * Properly fetch all archive dgit view tags, as we intended.
+ * Actually provide a -p (--package=) option (!)
+
+ Test suite fixes:
+ * Test suite: Explicitly configure user.name and user.email, so
+ that tests work when environment doesn't have defaults.
+ Closes:#842279 (I hope).
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Mon, 31 Oct 2016 12:47:18 +0000
+
+dgit (2.8) unstable; urgency=medium
+
+ * When in split build mode for `gbp-build' or `build', run
+ mergechanges as is required. Closes:#841990.
+ * Test suite: build-mode-*: Check that right .changes comes out
+ (detects #841990).
+ * Defend against debian/patches/series being an unusual object, in case
+ dpkg-source doesn't, in absurd git-apply fallback.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Tue, 25 Oct 2016 17:29:23 +0100
+
+dgit (2.7) unstable; urgency=medium
+
+ Absurd bugfix for serious bug:
+ * Work around `git-apply' problems (eg #841865, #829067) exposed by
+ `gbp pq import' (#841866) by sometimes falling back to an emulation of
+ git-apply in terms of dpkg-source --before-build. Closes:#841867.
+
+ Minor changes:
+ * dgit(1): Reorder the options, moving more important ones earlier.
+ * dgit(1): Some more info about --deliberately.
+ * Provide various --force-something options. Please don't use them.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Mon, 24 Oct 2016 02:37:28 +0100
+
+dgit (2.6) unstable; urgency=medium
+
+ Fixes to HTTP handling:
+ * Check for non-2xx HTTP status codes from ftpmaster api server.
+ * Always honour --curl= and --curl:.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Sun, 23 Oct 2016 14:57:22 +0100
+
+dgit (2.5) unstable; urgency=low
+
+ Substantive changes:
+ * Do not crash in split brain quilt modes when the two brains are
+ actually identical. (Eg --quilt=gbp with no patches.) Closes:#841770.
+ * Switch to new archive/ tag format by default, even in
+ non-split-brain mode.
+ * Provide --gbp and --dpm as aliases for --quilt=gbp and --quilt=dpm.
+
+ Documentation:
+ * dgit-maint-merge(7): New tutorial manpage from Sean Whitton.
+
+ Test suite:
+ * Introduce setup/gnupg, to help work around gnupg2 bug #841143
+ and improve performance by amortising gnupg migration cost.
+ * Various bugfixes.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Sun, 23 Oct 2016 13:20:23 +0100
+
+dgit (2.4) unstable; urgency=low
+
+ Bugfixes:
+ * split brain cache: Fix a wrong implicit reference to $_.
+ Closes:#841383.
+ * split brain cache: Make sure to write reflog entries for cache updates
+ even if the eventual tree (and therefore commit) is the same.
+ Otherwise, after updating dgit, the cache might have the right answer
+ but not be refreshed even by a build.
+ * dgit gbp-build: No longer invent a --git-debian-branch option.
+ Usually the user is a maintainer using split brain, and we should rely
+ on their own gbp configuration to specify the right check.
+ Closes:#841100.
+
+ Minor docs fix:
+ * dgit(1): Document which --ch: options are a good idea.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Thu, 20 Oct 2016 16:31:54 +0100
+
+dgit (2.3) unstable; urgency=low
+
+ * With --overwrite, do not check all sorts of tags (which may
+ not exist, or might contain wrong things). Closes:#841101.
+ * When generating pseudomerge in quilt split brain mode due to
+ --overwrite, actually include the version number in the commit
+ message.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Tue, 18 Oct 2016 01:58:05 +0100
+
+dgit (2.2) unstable; urgency=low
+
+ * Fix config relating to Debian to actually make split brain mode
+ work. Closes:#841085.
+ * Detect SIGPIPE (and SIGCHLD) being blocked or ignored.
+ Closes:#841090.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Mon, 17 Oct 2016 17:31:18 +0100
+
+dgit (2.1) unstable; urgency=low
+
+ * Do not crash due in clone to failure to handle dpkg-parsechangelog
+ SIGPIPE. Closes:#840989. Avoids:
+ dgit: failed command: dpkg-parsechangelog --format rfc822 --all
+ dgit: subprocess died due to fatal signal PIPE
+ * git- prefixes: Fix some occurrences of `git-foo' in infrastructure,
+ messages, and test suite. Filter out .../git-core from PATH in
+ test suite so that we catch future occurrences.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Sun, 16 Oct 2016 19:05:14 +0100
+
+dgit (2.0) unstable; urgency=low
+
+ Incompatible change:
+ * dgit sbuild: does not pass -A to sbuild. Consequently the default
+ build is now simply sbuild's default. With older sbuilds it was
+ possible to override dgit's -A by passing another option. But this
+ has been changed recently and now this default setting is very awkward
+ to change for the dgit user.
+ * dgit gbp-build: Make --quilt=gbp the default. (See below.)
+ * New tag format (for dgit view) archive/debian/VERSION.
+
+ Major new feature:
+ * --quilt=gbp, --quilt=dpm, --quilt=unpacked: Introduce facility for
+ split view (dgit/mainiainer view), to improve compatibility with some
+ workflow tools.
+
+ New checks and improved behaviours in dgit:
+ * When running dpkg-buildpackage, cope if user specified -g or -G.
+ * dgit sbuild: check that the set of .changes files found is as we
+ expect, before calling mergechanges. Re:#800060.
+ * dgit sbuild: Rename the used-up .changes files to `.inmulti' to
+ avoid accidental use of the wrong one (by software, or by users).
+ * dgit sbuild: Check that the binary .changes file doesn't contain a
+ .dsc.
+ * Introduce --rm-old-changes to delete previous builds' changes files.
+ * Remove any pre-existing _source.changes file before building source,
+ as a safety check.
+ * No longer tolerate a multitude of .changes files when doing push.
+ Instead, insist on a single one. Closes:#800110.
+ * dgit sbuild no longer deletes extranious .changes files; instead
+ we rely on --rm-old-changes, or failing that, fail early.
+ * When doing quilt linearisation, treat upstream .gitignores not
+ in the toplevel the same way we treat ones in the toplevel.
+ * When automatically generating quilt patch, honour GIT_COMMITTER_DATE
+ for filename creation (makes filename deterministic in test suite).
+ * New --overwrite option, replaces need to for user to use
+ git merge -s ours. Closes:#838718.
+ * When generating quilt patches from git commits, make patches that
+ look quite like git-format-patch output (rather than strange things
+ based on an obselete interpretation of DEP-3).
+ * When generating quilt patches from git commits, honour (and strip)
+ any Gbp-Pq headers (that we understand).
+ * Several dgit-generated commits now have slightly better annotations
+ from dgit about what it was doing.
+ * Before committing to push, check that .dsc and .changes correspond.
+ Closes:#800060.
+ * Better error message if non-split-brain patch stack no longer
+ applies (due to new upstream version, or user messing with it).
+ Closes:#833025.
+ * Better error message if HEAD contains changes unrepresentable
+ by `3.0 (quilt)'. Closes:#834618.
+ * Much better error message when HEAD and .dsc do not match.
+ Closes:#809516.
+
+ Infrastructure:
+ * dgit-repos-policy-debian: Better error handling.
+ * dgit-repos-policy-debian.: fix git-cat-file-handling with multiple
+ taints in db (!).
+ * dgit-infrastructure has, and uses, its own copies of the perl modules.
+ This avoids introducing a versioned dependency between dgit and
+ dgit-infrastructure (and also makes it easier to test cross-version
+ compatibility).
+
+ Documentation:
+ * Document the dgit-distro.DISTRO.quilt-mode config setting.
+ * Clarify the --clean= options' documentation. Closes:#800054.
+ * Discourage use of the --PROGRAM:OPTION escape hatch. (Apropos
+ of various bug reports including #800060 and #833025.)
+ * Document the expected form of HEAD for each --quilt= mode.
+
+ Bugfixes:
+ * When cleaning up after failed clone, stat the to-be-cleaned-up
+ directory before running rmtree on it. Closes:#796773.
+ * Do not call "warn" on failure of cleanup handler in END block
+ (since warn has been made fatal and aborts the cleanup chain).
+ * Print better error message (with `fail' rather than `die') if
+ `dgit clone' cannot create the destination directory.
+ * Properly substitute $changesfile in one of the `You can retry'
+ messages. Closes:#800078.
+ * Pass --ch:* and -v options to dpkg-buildpackage when building
+ source. Fixes bad Perl poetry syntax. Closes:#829121.
+ * When synthesing a commit from a .dsc from the archive, stop
+ internal git reset from printing a confusing message about HEAD.
+ * Turn off git gc in the private working areas.
+ * Do not fail to do some important quilt processing in some
+ --quilt modes.
+ * Fix two calls to chdir without proper error checking.
+ * Fix a couple of bugs in error reporting.
+ * Fix several bugs in .orig detection/recognition.
+ * Tidy up refs/dgit-fetch/ after dgit fetch (if successful).
+ * Fix handling of in-archive copies.
+ * Don't break if user has push.followTags=true. Closes:#827878.
+ * Arrange for the special dgit remote to be skipped by git fetch --all
+ etc. And no longer configure a fetch spec, since it won't work
+ anyway. Closes:#827892.
+ * Allow local git config options to override user-global ones,
+ as is proper. Closes:#835858.
+ * When generating patch filenames from titles, first transliterate
+ them (lossily) to ascii. Closes:#834807.
+
+ Test suite:
+ * When sbuild fails, do not crash due to sed not finding the log
+ file. Instead, simply tolerate the absence of the log file.
+ * Put --no-arch-all in build-modes-sbuild act, not only its real_act.
+ Cosmetic change only.
+ * Set GIT_COMMITTER_DATE and GIT_AUTHOR_DATE and increment them
+ explicitly in drs-push-rejects test. This avoids date dependencies
+ which can cause that test to fail on fast computers.
+ * Remove some spurios .debs from the example_1.0.tar.
+ * Increase sqlite_busy_timeout in debpolicy-dbretry, because old
+ zealot is very slow and we need to give the other processes time
+ to rollback and release the lock.
+ * Test quilt single-debian-patch.
+ * Provide `tartree-edit gitfetchinfo' etc. to help with comparing
+ different test case git working tree tarballs.
+ * Test dgit-repos-policy-debian with multiple (identical, as it happens)
+ existing taints.
+ * Provide better log output for certain failures.
+ * Many new tests (especially for new functionality).
+ * Add missing debhelper (>=8) to test suite's global Depends.
+ * tstunt arrangements: Fix mishandling of PERLLIB, etc.
+ * tstunt-parsechangelog: Produce Timestamp field (like official one
+ does, now).
+ * Do not fail when git requires --allow-unrelated-histories.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Sun, 16 Oct 2016 12:12:50 +0100
+
+dgit (1.4) unstable; urgency=high
+
+ Bugfixes:
+ * Unbreak --dry-run (`exiting subroutine via next', broken in
+ ac221d67, bug released in 0.22).
+ * When running git-add in commit-quilty-patch, properly escape
+ filenames (which git-add treats as glob patterns).
+ * When running git-add in commit-quilty-patch, use -f and sometimes -A,
+ so as to avoid being broken by any .gitignore, etc.
+ * When quilt linearisation fails, print the right information in
+ the error message. (This has been broken forever.)
+ * Cope properly with `3.0 (quilt)' with single-debian-patch.
+ Closes:#796016. (Still does not work with wheezy's dpkg-source, so
+ no test case yet.)
+ * With dgit sbuild, pass our -d before the user's arguments, so that
+ the user can override it. Closes:#796019.
+
+ New checks and improved behaviours:
+ * Detect and reject git trees containing debian/source/local-options
+ or debian/source/local-patch-header.
+ * In --dry-run mode, _do_ actually run dpkg-source --commit so that we
+ actually do construct the quilt fixup commit; instead, honour
+ --dry-run by avoiding pulling it back to your HEAD.
+ * quilt-fixup checks that the git tree is clean, as for build-prep.
+
+ Documentation:
+ * In dgit(7), discuss binaries and documentation present in upstream but
+ removed by rules clean.
+
+ Test suite:
+ * Run quilt-fixup with -wgf in distropatches-reject,
+ so that we don't need build-depends.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Sat, 22 Aug 2015 15:31:02 +0100
+
+dgit (1.3) unstable; urgency=high
+
+ Important bugfixes:
+ * In option parser test `@ARGV' not `length @ARGV'. Closes:#795710.
+ * Properly quote package name when constructing regexp in
+ complete_file_from_dsc. Closes:#795736. Also, grep the code for
+ likely similar problems elsewhere and improve a (harmless) instance in
+ dgit-repos-server.
+
+ Other improvements:
+ * If a .orig in .. is a symlink, hardlink the link target into our
+ private unpack directory, rather than the link itself (since latter
+ won't work if the symlink is relative). Closes:#795665.
+ * Test suite: Fix t-restriction-x-dgit-schroot-build in non-adt mode.
+ * Infrastructure: Improve an error message in dgit-repos-policy-debian.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Sun, 16 Aug 2015 17:51:02 +0100
+
+dgit (1.2) unstable; urgency=high
+
+ Improvements:
+ * Honour *.clean-mode configuration setting for --clean= mode.
+ * No longer require option values to be cuddled: support `--opt val' and
+ `-o val'. Closes:#763332.
+
+ Manpages:
+ * Fix typos.
+ * Document that tags are in DEP-14 format, and that they
+ are used for authenticating pushes.
+ * Correct cross-reference to point to browse.d.d.o.
+ * Move dgit.default.* to main CONFIGURATION section.
+
+ Administrivia:
+ * Add missing close of #793060 to changelog for version 1.1.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Fri, 14 Aug 2015 18:27:20 +0100
+
+dgit (1.1) unstable; urgency=medium
+
+ Bugfixes:
+ * When source package contains things called .git (even files, and even
+ in subdirectories), remove them. Closes:#793671.
+ * Work around curl -sS -I printing `HTTP/1.0 200 Connection established'
+ before the actual header, so dgit works with https_proxy set (!)
+ * --new is needed for read access to packages in NEW, too. Document
+ this, and make it work properly.
+ * Work around #793471 (madness with $SIG{__WARN__} and Perl's system
+ builtin): move $SIG{} setting into setup_sigwarn in Dgit.pm, and
+ check getppid.
+ * When invoking git-buildpackage via dgit gbp-build, consider our
+ command line arguments when massaging the dpkg-buildpackage arguments,
+ so that we don't end up giving dpkg-buildpackage contradictory
+ instructions.
+ * Cope with new git-buildpackage which provides gbp, rather than the
+ eponymous command, on PATH.
+
+ Configurability:
+ * Honour dgit-distros.DISTRO.cmd-CMD and .opts-CMD. Closes:#793427.
+ * Make configuration able to prevent dpkg-mergechangelogs setup.
+ * Provide dgit setup-new-tree (like dpkg-setup-mergechangelogs
+ but only does it if not disabled in config).
+ * Set up git user.email and user.name from distro access config
+ or DEBEMAIL/DEBFULLNAME. Closes:#793410.
+ * When key to use not specified any other way, use the debian/changelog
+ trailer line. Closes:#793423.
+ * Honour --git= (mostly).
+
+ Documentation:
+ * Fix some manpage typos. [ Richard Hartmann ]
+ * Manpage said that --clean=check was -wn but that is --clean=none;
+ correctly document that --clean=check is actually -wc.
+ * Document that up to -DDDD (not just -DD) is meaningfully different.
+ * Document that -cname=value applies only for this run.
+ * Improve manpage comment about defining a new distro.
+ * Document that --quilt=linear is the default for Debian.
+ * Fix a formatting problem in --build-products-dir= doc.
+ * In manpage, do not seem to imply that NMU should be of only one
+ new commit.
+ * Qualify to Debian the manpage comment about how to do NMU.
+ * In discussion on how to start using dgit when already using git, do
+ not imply/assume that existing git history will have identical trees
+ to dgit history.
+ * Remove stray sentence in config section of manpage.
+ * Manpage: Clarify wording of readonly config.
+ * Manpage: Better cross-references for -k and keyid.
+ * dgit(7): No longer say that dgit-repos lives on Alioth.
+
+ Improvements:
+ * Introduce more sophisticated protocol negotiation for rpush.
+ * Do not quote `:' in shellquote.
+ * Print a supplementary message when push fails, giving advice to
+ the user about how to retry. Closes:#793144.
+ * Slurp in entire git config, for better performance.
+ * Rename `git-build' operation to `gbp-build' to make it clearer what
+ it's for. Keep the old name as an alias.
+ * Show `dgit sbuild' in usage message.
+ * When we are using dpkg-buildpackage to clean before using it to also
+ do the build, let it do its cleaning thing as part of its run, rather
+ than running it twice. When we are _not_ supposed to be using
+ dpkg-buildpackage to clean, but we are running it to do the build,
+ pass -nc. Closes:#793060.
+ * Also suppress spurious runs of the clean target when building using
+ git-buildpackage.
+ * When exec fails, always print the program name in the error message.
+
+ Infrastructure:
+ * Infrastructure: Get mirroring right for fresh repos of existing
+ packages (!)
+
+ Packaging, cleanups, debugging and test suite:
+ * Fix Vcs-Git and Vcs-Browse to refer to chiark. (The dgit-repos on
+ alioth aren't suitable right now because the master there can
+ currently only be updated with an actual upload, ie dgit push.)
+ * Make warnings fatal in dpkg-repos-admin-debian, dgit-ssh-dispatch
+ (using setup_sigwarn).
+ * tstunt/dpkg-parsechangelog: Make warnings fatal (directly).
+ * tstunt/dpkg-parsechangelog: Do not complain if PERLLIB is empty.
+ * Test suite: Honour DGIT_TEST_DEBUG=''.
+ * With -DDDD, print out all gitcfg references (copious!)
+ * Fix a debug message in the obsolete sshpsql archive access driver.
+ * Test suite: More automatic enumeration of tests.
+ * Test suite: Provide tests which check that all our various build
+ operations run the right targets as expected (ie, that we are massaging
+ the arguments to dpkg-buildpackage, and suppressing our clean target,
+ etc., correctly).
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Mon, 27 Jul 2015 16:34:31 +0100
+
+dgit (1.0) unstable; urgency=medium
+
+ Improvements:
+ * Switch to new production git repositories for reading.
+ (this can no longer divert to alioth). Public readonly access
+ now works. Closes:#791447.
+ * Memoise git config lookups (big speedup!)
+ * Provide -wdd aka --clean=dpkg-source-d. Closes:#792433.
+ * Provide -wc aka --clean=check.
+
+ Manpage updates:
+ * Remove some obsolete caveats from BUGS.
+ * Reorganise and complete the configuration section.
+ * Remove obselete comment about DMs not being able to push.
+ We have, for now, a way to add keys manually. Closes:#720173.
+
+ Access machinery:
+ * Remove configuration relating to alioth.
+ * Provide for different access mechanisms when pushing.
+ * Provide for configurable git url suffix.
+ * Allow git-url to be '' to force fallback to git-proto etc.
+ * Provide for checking git presence via http[s].
+ * Do some quoting on debug output (needed if the server might not
+ be trustworthy and might send us bad stuff).
+ * Talk to push.dgit.debian.org, rather than the .debian.net alias.
+
+ Infrastructure:
+ * Provide for mirroring git updates to a different server.
+ * Provide cgit-regen-config command for cgi-grnet-01.
+ * Make dgit-ssh-dispatch not spew (harmless) warnings if caller
+ tries for a shell session (ie SSH_ORIGINAL_COMMAND not set).
+
+ Cleanups:
+ * Remove an obsolete comment from the code.
+ * Improve an error message from dgit-repos-policy-debian.
+ * Test suite: Break out t-make-hook-link.
+ * Fix a manpage typo.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Sun, 19 Jul 2015 22:15:53 +0100
+
+dgit (0.30) unstable; urgency=high
+
+ INCOMPATIBLE CHANGES:
+
+ * Client uses new infrastructure:
+ - Check for new dgit git service on dgit-git.debian.net (ie
+ gideon.debian.org), with transition plan based on diversion feature.
+ Closes:#720172.
+ - Old versions of dgit will stop working when the server-side handle is
+ pulled.
+
+ * dgit git trees no longer contain .pc for format `3.0 (quilt)' source
+ packages. Closes:#764606.
+ - It is deleted whenever we find it.
+ - Older versions of dgit will choke on trees without .pc.
+ - (When doing quilt fixup, we recreate a suitable .pc in a temporary
+ directory so that we can do dpkg-source --comit.)
+
+ * All users are urged to upgrade ASAP.
+
+ Other significant improvements:
+
+ * When generating quilt patches, try to linearise the git history into a
+ series of individual new patches for debian/patches. Closes:#770710.
+
+ * When receiving a push with dgit-repos-server, update the server's
+ refs/heads/master if we are pushing to what the distro regards as a
+ relevant branch, and the push would ff master. Closes:#728209.
+
+ * For non-Debian distros, distro version release tags contain distro
+ name a la DEP-14 (rather than hardcoding `debian/').
+
+ * Set up a merge driver for debian/changelog. Closes:#769291.
+
+ * --clean=git and --clean=none cause dgit to pass -nc to
+ dpkg-buildpackage, suppressing calls to the package's clean target.
+ Also, expand the documentation in this area slightly. Closes:#768590.
+
+ * Provide --clean=git-ff (aka -wgf), which is useful for dgit itself (!)
+
+ Minor improvements:
+
+ * Reduce some noise output and improve the clarity of some messages.
+ * Be more careful about tag updates during fetch: only update tags
+ referring to uploads to distro we are trying to fetch from.
+ * Change realpath dependency to `coreutils (>= 8.23-1~) | realpath'
+ (Closes:#786955.)
+
+ Bugfixes:
- -- Ian Jackson <ijackson@chiark.greenend.org.uk> Wed, 27 Aug 2014 00:18:10 +0100
+ * Fix handling of rmadison-based and gitless distros (e.g., Ubuntu).
+ * Add missing `gpgv' to test dependencies in debian/tests/control.
+ * Strip `-b <branch>' from contents of Vcs-Git header, when setting up
+ the vcs-git remote. Closes:#759374.
+ * Do not offer wget as an alternative dependency to curl. We always
+ unconditionally invoke curl and have no code to use wget.
+ Closes:#760805.
+ * Complain about lack of cuddled values for value-taking single-letter
+ options, rather than thinking the user meat an empty value.
+ Closes:#763332.
+ * Reject (rather than ignoring) further options merged witth -wn, -wg,
+ -wd.
+ * Fix inaccurate error message when archive's git hash is not an
+ ancestor of git repo's git hash.
+ * Detect and bomb out on vendor-specific `3.0 (quilt)' patch series.
+ * Fix the rules clean target to remove test results and output.
+
+ Documentation improvements:
+
+ * Break out dgit(7) from dgit(1).
+ * Provide example workflow for dgit rpush. Closes:#763334.
+ (Also part of the fix for #768470.)
+ * Document that dgit repos are cloneable with git, in dgit(1)
+ section MODEL. [Andreas Barth.] Closes:#768470.
+ * Better documentation for quilt series handling.
+ * Document under `dgit push' that it is best to build with dgit too.
+ Closes:#763333.
+ * Other minor clarifications and improvements.
+
+ Behind-the-scenes work:
+
+ * Use ftpmasterapi archive query method.
+ (Closes:#727702. Also partly obsoletes #768470.)
+ * New dgit-infrastructure binary package containing dgit-repos-server et
+ al. Client users probably don't want this stuff. Also, it provides a
+ convenient way to publish the dependencies.
+ * Many many bugfixes to the server side (dpkg-repos-server et al.).
+ * Add :..; prefix to ssh remote commands, for the benefit of future
+ forced command wrappers. Implicitly, this defines a new ssh-based
+ command protocol. Closes:#720174, #720175.
+ * Distro access configuration handling changes (should not be noticeable
+ to most users).
+ * In places, significant restructuring or tidying up.
+ * Turn all perl warnings into errors using $SIG{__WARN__}.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Sun, 05 Jul 2015 01:34:55 +0100
+
+dgit (0.22.1) unstable; urgency=high
+
+ * Use Dpkg::Version::version_compare everywhere, not
+ Dpkg::Version::version_compare_string. The latter is entirely wrong,
+ meaning that dgit would get many version comparisons wrong.
+ Closes:#768038.
+
+ -- Ian Jackson <ijackson@chiark.greenend.org.uk> Tue, 04 Nov 2014 12:46:40 +0000
dgit (0.22) unstable; urgency=medium
diff --git a/debian/control b/debian/control
index 798465b..4d4c304 100644
--- a/debian/control
+++ b/debian/control
@@ -5,12 +5,16 @@ Maintainer: Ian Jackson <ijackson@chiark.greenend.org.uk>
Standards-Version: 3.9.4.0
Build-Depends: debhelper (>= 9)
Testsuite: autopkgtest
-Vcs-Git: git://anonscm.debian.org/dgit-repos/repos/dgit.git
-Vcs-Browser: http://anonscm.debian.org/gitweb/?p=dgit-repos/repos/dgit.git
+Vcs-Git: git://git.chiark.greenend.org.uk/~ianmdlvl/dgit.git
+Vcs-Browser: http://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git/dgit.git/
Package: dgit
Depends: perl, libwww-perl, libdpkg-perl, git-core, devscripts, dpkg-dev,
- ${misc:Depends}, realpath, libdigest-sha-perl, dput, curl | wget
+ ${misc:Depends}, git-buildpackage, liblist-moreutils-perl,
+ coreutils (>= 8.23-1~) | realpath,
+ libdigest-sha-perl, dput, curl, apt,
+ libjson-perl, ca-certificates,
+ libtext-iconv-perl, libtext-glob-perl
Recommends: ssh-client
Suggests: sbuild
Architecture: all
@@ -21,3 +25,15 @@ Description: git interoperability with the Debian archive
dgit push constructs uploads from git commits
.
dgit clone and dgit fetch construct git commits from uploads.
+
+Package: dgit-infrastructure
+Depends: ${misc:Depends}, perl, git-core, gpgv, chiark-utils-bin,
+ libjson-perl, libdigest-sha-perl, libdbd-sqlite3-perl, sqlite3,
+ libwww-perl, libdpkg-perl
+Recommends: dgit
+Architecture: all
+Priority: extra
+Description: dgit server backend infrastructure
+ This package contains tools which are useful for setting up a dgit
+ git repository server. You probably want dgit, the client package,
+ instead of dgit-infrastructure.
diff --git a/debian/copyright b/debian/copyright
index 081ff31..a5b8245 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -1,7 +1,8 @@
dgit
Integration between git and Debian-style archives
-Copyright (C)2013 Ian Jackson
+Copyright (C)2013-2016 Ian Jackson
+Copyright (C)2016 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
@@ -21,3 +22,51 @@ The tests/ directory contains a complete copy of the source code for
the pari-extra 3-1 package. This is a dummy package containing only
Debian metadata, by Bill Alombert, with a licence statement saying
it's GPL (implicitly GPLv3 compatible).
+
+
+Contributions are accepted upstram under the same terms; please sign
+off your patches (by writing an approprite Signed-Off-By tag in your
+commit message or patch submission) to indicate your attestation that
+the Developer Certificate of Origin (version 1.1) applies.
+
+
+-8<-
+
+Developer Certificate of Origin
+Version 1.1
+
+Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
+1 Letterman Drive
+Suite D4700
+San Francisco, CA, 94129
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+
+Developer's Certificate of Origin 1.1
+
+By making a contribution to this project, I certify that:
+
+(a) The contribution was created in whole or in part by me and I
+ have the right to submit it under the open source license
+ indicated in the file; or
+
+(b) The contribution is based upon previous work that, to the best
+ of my knowledge, is covered under an appropriate open source
+ license and I have the right under that license to submit that
+ work with modifications, whether created in whole or in part
+ by me, under the same open source license (unless I am
+ permitted to submit under a different license), as indicated
+ in the file; or
+
+(c) The contribution was provided directly to me by some other
+ person who certified (a), (b) or (c) and I have not modified
+ it.
+
+(d) I understand and agree that this project and the contribution
+ are public and that a record of the contribution (including all
+ personal information I submit with it, including my sign-off) is
+ maintained indefinitely and may be redistributed consistent with
+ this project or the open source license(s) involved.
+
diff --git a/debian/rules b/debian/rules
index fece672..9249f88 100755
--- a/debian/rules
+++ b/debian/rules
@@ -3,7 +3,7 @@
# dgit
# Integration between git and Debian-style archives
#
-# Copyright (C)2013 Ian Jackson
+# Copyright (C)2013-2016 Ian Jackson
#
# 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
@@ -29,3 +29,31 @@ override_dh_gencontrol:
cd debian/dgit; \
v=$$(perl -ne 'print if s/^version:\s+//i' DEBIAN/control); \
perl -i -pe "s/UNRELEASED/$$v/g if m/###substituted###/" usr/bin/dgit
+
+globalperl=/usr/share/perl5
+infraperl=/usr/share/dgit/infra/perl5
+
+override_dh_auto_install:
+ make install prefix=/usr DESTDIR=debian/dgit
+ make install-infra prefix=/usr DESTDIR=debian/dgit-infrastructure \
+ perldir=$(infraperl)
+# # Most of the Perl modules in dgit-infrastructure live in
+# # $(infraperl). The exception is Debian::Dgit::Infra, which
+# # lives in $(globalperl) and adds $(infraperl) to @INC.
+ set -ex; \
+ base=debian/dgit-infrastructure; \
+ mod=Debian/Dgit/Infra.pm; \
+ src=$${base}$(infraperl)/$${mod}; \
+ dst=$${base}$(globalperl)/$${mod}; \
+ mkdir -p $${dst%/*}; \
+ mv -f $$src $$dst; \
+ perl -i -p -e 'next unless m/###substituted###/;' \
+ -e 'next unless s/^# (?=unshift \@INC,)//;' \
+ -e 'die unless s{q\{\S+\}}{q{$(infraperl)}};' \
+ $$dst
+
+debian/tests/control: tests/enumerate-tests debian/tests/control.in
+ $< gencontrol >$@.new && mv -f $@.new $@
+
+debian/tests/control: tests/lib-core tests/lib-restricts
+debian/tests/control: tests/tests $(wildcard tests/tests/*[^~#])
diff --git a/debian/tests/control b/debian/tests/control
index 48dbda1..0df610e 100644
--- a/debian/tests/control
+++ b/debian/tests/control
@@ -1,3 +1,35 @@
-Tests: clone-nogit fetch-localgitonly fetch-somegit-notlast push-newpackage push-nextdgit quilt push-buildproductsdir
+Tests: build-modes-gbp
Tests-Directory: tests/tests
-Depends: @, devscripts
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, git-buildpackage
+
+Tests: clone-reprepro downstream-gitless
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, reprepro
+
+Tests: defdistro-dsd-clone-drs dsd-clone-drs
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential
+Restrictions: x-dgit-intree-only x-dgit-git-only
+
+Tests: gitattributes
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, bsdgames, man-db, git-man
+
+Tests: defdistro-mirror mirror mirror-debnewgit mirror-private
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, rsync
+
+Tests: build-modes-sbuild quilt-gbp-build-modes-sbuild
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, sbuild
+Restrictions: x-dgit-schroot-build
+
+Tests: spelling
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential
+Restrictions: x-dgit-git-only
+
+Tests: absurd-gitapply badcommit-rewrite build-modes build-modes-asplit build-modes-gbp-asplit clone-clogsigpipe clone-gitnosuite clone-nogit debpolicy-dbretry debpolicy-newreject debpolicy-quilt-gbp defdistro-rpush defdistro-setup distropatches-reject drs-clone-nogit drs-push-masterupdate drs-push-rejects dsd-clone-nogit dsd-divert fetch-localgitonly fetch-somegit-notlast gbp-orig gitconfig import-dsc import-maintmangle import-native import-nonnative import-tarbomb inarchivecopy mismatches-contents mismatches-dscchanges multisuite newtag-clone-nogit oldnewtagalt oldtag-clone-nogit orig-include-exclude orig-include-exclude-chkquery overwrite-chkclog overwrite-junk overwrite-splitbrains overwrite-version protocol-compat push-buildproductsdir push-newpackage push-nextdgit quilt quilt-gbp quilt-gbp-build-modes quilt-singlepatch quilt-splitbrains quilt-useremail rpush tag-updates test-list-uptodate trustingpolicy-replay unrepresentable version-opt
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential
+
diff --git a/debian/tests/control.in b/debian/tests/control.in
new file mode 100644
index 0000000..c032ba8
--- /dev/null
+++ b/debian/tests/control.in
@@ -0,0 +1,2 @@
+Tests-Directory: tests/tests
+Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential
diff --git a/dgit b/dgit
index 930a594..caa2d75 100755
--- a/dgit
+++ b/dgit
@@ -2,7 +2,7 @@
# dgit
# Integration between git and Debian-style archives
#
-# Copyright (C)2013 Ian Jackson
+# Copyright (C)2013-2016 Ian Jackson
#
# 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
@@ -19,6 +19,9 @@
use strict;
+use Debian::Dgit;
+setup_sigwarn();
+
use IO::Handle;
use Data::Dumper;
use LWP::UserAgent;
@@ -30,13 +33,21 @@ use Dpkg::Version;
use POSIX;
use IPC::Open2;
use Digest::SHA;
-use Config;
+use Digest::MD5;
+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 = 2;
+our @rpushprotovsn_support = qw(4 3 2); # 4 is new tag format
+our $protovsn;
-our $isuite = 'unstable';
+our $isuite;
our $idistro;
our $package;
our @ropts;
@@ -47,31 +58,63 @@ our $changesfile;
our $buildproductsdir = '..';
our $new_package = 0;
our $ignoredirty = 0;
-our $noquilt = 0;
our $rmonerror = 1;
+our @deliberatelies;
+our %previously;
our $existing_package = 'dpkg';
-our $cleanmode = 'dpkg-source';
+our $cleanmode;
our $changes_since_version;
+our $rmchanges;
+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_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 (@git) = qw(git);
our (@dget) = qw(dget);
-our (@curl) = qw(curl -f);
+our (@curl) = qw(curl);
our (@dput) = qw(dput);
our (@debsign) = qw(debsign);
our (@gpg) = qw(gpg);
-our (@sbuild) = qw(sbuild -A);
+our (@sbuild) = qw(sbuild);
our (@ssh) = 'ssh';
our (@dgit) = qw(dgit);
+our (@aptget) = qw(apt-get);
+our (@aptcache) = qw(apt-cache);
our (@dpkgbuildpackage) = qw(dpkg-buildpackage -i\.git/ -I.git);
our (@dpkgsource) = qw(dpkg-source -i\.git/ -I.git);
our (@dpkggenchanges) = qw(dpkg-genchanges);
our (@mergechanges) = qw(mergechanges -f);
+our (@gbp_build) = ('');
+our (@gbp_pq) = ('gbp pq');
our (@changesopts) = ('');
our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
@@ -82,36 +125,68 @@ our %opts_opt_map = ('dget' => \@dget, # accept for compatibility
'sbuild' => \@sbuild,
'ssh' => \@ssh,
'dgit' => \@dgit,
+ 'git' => \@git,
+ 'apt-get' => \@aptget,
+ 'apt-cache' => \@aptcache,
'dpkg-source' => \@dpkgsource,
'dpkg-buildpackage' => \@dpkgbuildpackage,
'dpkg-genchanges' => \@dpkggenchanges,
+ 'gbp-build' => \@gbp_build,
+ 'gbp-pq' => \@gbp_pq,
'ch' => \@changesopts,
'mergechanges' => \@mergechanges);
-our %opts_opt_cmdonly = ('gpg' => 1);
+our %opts_opt_cmdonly = ('gpg' => 1, 'git' => 1);
+our %opts_cfg_insertpos = map {
+ $_,
+ scalar @{ $opts_opt_map{$_} }
+} keys %opts_opt_map;
-our $keyid;
+sub parseopts_late_defaults();
+sub setup_gitattrs(;$);
+sub check_gitattrs($$);
-our $debug = 0;
-open DEBUG, ">/dev/null" or die $!;
+our $keyid;
autoflush STDOUT 1;
+our $supplementary_message = '';
+our $need_split_build_invocation = 0;
+our $split_brain = 0;
+
+END {
+ local ($@, $?);
+ return unless forkcheck_mainprocess();
+ print STDERR "! $_\n" foreach $supplementary_message =~ m/^.+$/mg;
+}
+
our $remotename = 'dgit';
our @ourdscfield = qw(Dgit Vcs-Dgit-Master);
-our $branchprefix = 'dgit';
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) = @_;
+ return "$distro/".dep14_version_mangle $v;
+}
+
+sub madformat ($) { $_[0] eq '3.0 (quilt)' }
sub lbranch () { return "$branchprefix/$csuite"; }
my $lbranch_re = '^refs/heads/'.$branchprefix.'/([^/.]+)$';
sub lref () { return "refs/heads/".lbranch(); }
-sub lrref () { return "refs/remotes/$remotename/$branchprefix/$csuite"; }
-sub rrref () { return "refs/$branchprefix/$csuite"; }
-sub debiantag ($) {
- my ($v) = @_;
- $v =~ y/~:/_%/;
- return "debian/$v";
-}
+sub lrref () { return "refs/remotes/$remotename/".server_branch($csuite); }
+sub rrref () { return server_ref($csuite); }
sub stripepoch ($) {
my ($vsn) = @_;
@@ -119,68 +194,109 @@ sub stripepoch ($) {
return $vsn;
}
+sub srcfn ($$) {
+ my ($vsn,$sfx) = @_;
+ return "${package}_".(stripepoch $vsn).$sfx
+}
+
sub dscfn ($) {
my ($vsn) = @_;
- return "${package}_".(stripepoch $vsn).".dsc";
+ return srcfn($vsn,".dsc");
+}
+
+sub changespat ($;$) {
+ my ($vsn, $arch) = @_;
+ return "${package}_".(stripepoch $vsn)."_".($arch//'*').".changes";
+}
+
+sub upstreamversion ($) {
+ my ($vsn) = @_;
+ $vsn =~ s/-[^-]+$//;
+ return $vsn;
}
our $us = 'dgit';
-our $debugprefix = '';
+initdebug('');
our @end;
END {
local ($?);
+ return unless forkcheck_mainprocess();
foreach my $f (@end) {
eval { $f->(); };
- warn "$us: cleanup: $@" if length $@;
+ print STDERR "$us: cleanup: $@" if length $@;
}
};
-our @signames = split / /, $Config{sig_name};
-
-sub waitstatusmsg () {
- if (!$?) {
- return "terminated, reporting successful completion";
- } elsif (!($? & 255)) {
- return "failed with error exit status ".WEXITSTATUS($?);
- } elsif (WIFSIGNALED($?)) {
- my $signum=WTERMSIG($?);
- return "died due to fatal signal ".
- ($signames[$signum] // "number $signum").
- ($? & 128 ? " (core dumped)" : ""); # POSIX(3pm) has no WCOREDUMP
- } else {
- return "failed with unknown wait status ".$?;
- }
-}
-
-sub printdebug { print DEBUG $debugprefix, @_ or die $!; }
+sub badcfg { print STDERR "$us: invalid configuration: @_\n"; exit 12; }
-sub fail {
- die $us.($we_are_responder ? " (build host)" : "").": @_\n";
+sub forceable_fail ($$) {
+ my ($forceoptsl, $msg) = @_;
+ fail $msg unless grep { $forceopts{$_} } @$forceoptsl;
+ print STDERR "warning: overriding problem due to --force:\n". $msg;
}
-sub badcfg { print STDERR "$us: invalid configuration: @_\n"; exit 12; }
+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 fetchspec () {
- local $csuite = '*';
- return "+".rrref().":".lrref();
-}
-
sub changedir ($) {
my ($newdir) = @_;
printdebug "CD $newdir\n";
- chdir $newdir or die "chdir: $newdir: $!";
+ chdir $newdir or confess "chdir: $newdir: $!";
+}
+
+sub deliberately ($) {
+ my ($enquiry) = @_;
+ return !!grep { $_ eq "--deliberately-$enquiry" } @deliberatelies;
+}
+
+sub deliberately_not_fast_forward () {
+ foreach (qw(not-fast-forward fresh-repo)) {
+ return 1 if deliberately($_) || deliberately("TEST-dgit-only-$_");
+ }
+}
+
+sub quiltmode_splitbrain () {
+ $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;
}
#---------- remote protocol support, common ----------
# remote push initiator/responder protocol:
-# < dgit-remote-push-ready [optional extra info ignored by old initiators]
+# $ dgit remote-push-build-host <n-rargs> <rargs>... <push-args>...
+# where <rargs> is <push-host-dir> <supported-proto-vsn>,... ...
+# < dgit-remote-push-ready <actual-proto-vsn>
+#
+# occasionally:
+#
+# > progress NBYTES
+# [NBYTES message]
+#
+# > supplementary-message NBYTES # $protovsn >= 3
+# [NBYTES message]
+#
+# main sequence:
#
# > file parsed-changelog
# [indicates that output of dpkg-parsechangelog follows]
@@ -195,7 +311,13 @@ sub changedir ($) {
# > file changes
# [etc]
#
-# > param head HEAD
+# > param head DGIT-VIEW-HEAD
+# > param csuite SUITE
+# > param tagformat old|new
+# > param maint-view MAINT-VIEW-HEAD
+#
+# > previously REFNAME=OBJNAME # if --deliberately-not-fast-forward
+# # goes into tag, for replay prevention
#
# > want signed-tag
# [indicates that signed tag is wanted]
@@ -275,7 +397,7 @@ sub protocol_send_file ($$) {
sub protocol_read_bytes ($$) {
my ($fh, $nbytes) = @_;
- $nbytes =~ m/^[1-9]\d{0,5}$/ or badproto \*RO, "bad byte count";
+ $nbytes =~ m/^[1-9]\d{0,5}$|^0$/ or badproto \*RO, "bad byte count";
my $d;
my $got = read $fh, $d, $nbytes;
$got==$nbytes or badproto_badread $fh, "data block";
@@ -365,43 +487,9 @@ sub url_get {
our ($dscdata,$dscurl,$dsc,$dsc_checked,$skew_warning_vsn);
-sub shellquote {
- my @out;
- local $_;
- foreach my $a (@_) {
- $_ = $a;
- if (m{[^-=_./0-9a-z]}i) {
- s{['\\]}{'\\$&'}g;
- push @out, "'$_'";
- } else {
- push @out, $_;
- }
- }
- return join ' ', @out;
-}
-
-sub printcmd {
- my $fh = shift @_;
- my $intro = shift @_;
- print $fh $intro," " or die $!;
- print $fh shellquote @_ or die $!;
- print $fh "\n" or die $!;
-}
-
-sub failedcmd {
- { local ($!); printcmd \*STDERR, "$us: failed command:", @_ or die $!; };
- if ($!) {
- fail "failed to fork/exec: $!";
- } elsif ($?) {
- fail "subprocess ".waitstatusmsg();
- } else {
- fail "subprocess produced invalid output";
- }
-}
-
sub runcmd {
- printcmd(\*DEBUG,$debugprefix."+",@_) if $debug>0;
- $!=0; $?=0;
+ debugcmd "+",@_;
+ $!=0; $?=-1;
failedcmd @_ if system @_;
}
@@ -410,33 +498,12 @@ 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)";
}
}
-sub cmdoutput_errok {
- die Dumper(\@_)." ?" if grep { !defined } @_;
- printcmd(\*DEBUG,$debugprefix."|",@_) if $debug>0;
- open P, "-|", @_ or die $!;
- my $d;
- $!=0; $?=0;
- { local $/ = undef; $d = <P>; }
- die $! if P->error;
- if (!close P) { printdebug "=>!$?\n" if $debug>0; return undef; }
- chomp $d;
- $d =~ m/^.*/;
- printdebug "=> \`$&'",(length $' ? '...' : ''),"\n" if $debug>0; #';
- return $d;
-}
-
-sub cmdoutput {
- my $d = cmdoutput_errok @_;
- defined $d or failedcmd @_;
- return $d;
-}
-
sub dryrun_report {
printcmd(\*STDERR,$debugprefix."#",@_);
}
@@ -466,7 +533,8 @@ our $helpmsg = <<END;
main usages:
dgit [dgit-opts] clone [dgit-opts] package [suite] [./dir|/dir]
dgit [dgit-opts] fetch|pull [dgit-opts] [suite]
- dgit [dgit-opts] build [git-buildpackage-opts|dpkg-buildpackage-opts]
+ dgit [dgit-opts] build [dpkg-buildpackage-opts]
+ dgit [dgit-opts] sbuild [sbuild-opts]
dgit [dgit-opts] push [dgit-opts] [suite]
dgit [dgit-opts] rpush build-host:build-dir ...
important dgit options:
@@ -500,18 +568,55 @@ sub cmd_help () {
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-distro.debian.git-host' => 'git.debian.org',
- 'dgit-distro.debian.git-proto' => 'git+ssh://',
- 'dgit-distro.debian.git-path' => '/git/dgit-repos/repos',
- 'dgit-distro.debian.git-check' => 'ssh-cmd',
- 'dgit-distro.debian.git-create' => 'ssh-cmd',
- 'dgit-distro.debian.sshpsql-host' => 'mirror.ftp-master.debian.org',
- 'dgit-distro.debian.sshpsql-dbname' => 'service=projectb',
+ 'dgit.default.archive-query' => 'madison:',
+ 'dgit.default.sshpsql-dbname' => 'service=projectb',
+ '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"
+ # hist means "repo server may have old pushes without new tag"
+ # ("hist" is implied by "old")
+ 'dgit-distro.debian.archive-query' => 'ftpmasterapi:',
+ 'dgit-distro.debian.git-check' => 'url',
+ 'dgit-distro.debian.git-check-suffix' => '/info/refs',
+ 'dgit-distro.debian.new-private-pushers' => 't',
+ 'dgit-distro.debian/push.git-url' => '',
+ 'dgit-distro.debian/push.git-host' => 'push.dgit.debian.org',
+ 'dgit-distro.debian/push.git-user-force' => 'dgit',
+ 'dgit-distro.debian/push.git-proto' => 'git+ssh://',
+ 'dgit-distro.debian/push.git-path' => '/dgit/debian/repos',
+ 'dgit-distro.debian/push.git-create' => 'true',
+ 'dgit-distro.debian/push.git-check' => 'ssh-cmd',
+ 'dgit-distro.debian.archive-query-url', 'https://api.ftp-master.debian.org/',
+# 'dgit-distro.debian.archive-query-tls-key',
+# '/etc/ssl/certs/%HOST%.pem:/etc/dgit/%HOST%.pem',
+# ^ this does not work because curl is broken nowadays
+# Fixing #790093 properly will involve providing providing the key
+# in some pacagke and maybe updating these paths.
+#
+# 'dgit-distro.debian.archive-query-tls-curl-args',
+# '--ca-path=/etc/ssl/ca-debian',
+# ^ this is a workaround but works (only) on DSA-administered machines
+ 'dgit-distro.debian.git-url' => 'https://git.dgit.debian.org',
+ 'dgit-distro.debian.git-url-suffix' => '',
'dgit-distro.debian.upload-host' => 'ftp-master', # for dput
'dgit-distro.debian.mirror' => 'http://ftp.debian.org/debian/',
+ 'dgit-distro.debian-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',
@@ -523,45 +628,112 @@ our %defcfg = ('dgit.default.distro' => 'debian',
'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' => "dummycat:$td/aq",
+ '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 %gitcfgs;
+our @gitcfgsources = qw(cmdline local global system);
+
+sub git_slurp_config () {
+ local ($debuglevel) = $debuglevel-2;
+ local $/="\0";
+
+ # 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
+
+ my @cmd = (@git, qw(config -z --get-regexp), "--$src", qw(.*));
+ debugcmd "|",@cmd;
+
+ open GITS, "-|", @cmd or die $!;
+ while (<GITS>) {
+ chomp or die;
+ printdebug "=> ", (messagequote $_), "\n";
+ m/\n/ or die "$_ ?";
+ push @{ $gitcfgs{$src}{$`} }, $'; #';
+ }
+ $!=0; $?=0;
+ close GITS
+ or ($!==0 && $?==256)
+ or failedcmd @cmd;
+ }
+}
+
+sub git_get_config ($) {
+ my ($c) = @_;
+ 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/;
- my @cmd = (@git, qw(config --), $c);
- my $v;
- {
- local ($debug) = $debug-1;
- $v = cmdoutput_errok @cmd;
- };
- if ($?==0) {
- return $v;
- } elsif ($?!=256) {
- failedcmd @cmd;
- }
+ 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 access_basedistro__noalias () {
if (defined $idistro) {
- return cfg("dgit-distro.basedistro.distro",
- "dgit-suite.$isuite.distro",
- 'RETURN-UNDEF') // $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, quirk-specific info)
+ # returns (quirk name, distro to use instead or undef, quirk-specific info)
my $basedistro = access_basedistro();
my $backports_quirk = cfg("dgit-distro.$basedistro.backports-quirk",
'RETURN-UNDEF');
@@ -575,25 +747,136 @@ sub access_quirk () {
return ('backports',"$basedistro-backports",$1);
}
}
- return ('none',$basedistro);
+ return ('none',undef);
+}
+
+our $access_forpush;
+
+sub parse_cfg_bool ($$$) {
+ my ($what,$def,$v) = @_;
+ $v //= $def;
+ return
+ $v =~ m/^[ty1]/ ? 1 :
+ $v =~ m/^[fn0]/ ? 0 :
+ badcfg "$what needs t (true, y, 1) or f (false, n, 0) not \`$v'";
+}
+
+sub access_forpush_config () {
+ my $d = access_basedistro();
+
+ return 1 if
+ $new_package &&
+ parse_cfg_bool('new-private-pushers', 0,
+ cfg("dgit-distro.$d.new-private-pushers",
+ 'RETURN-UNDEF'));
+
+ my $v = cfg("dgit-distro.$d.readonly", 'RETURN-UNDEF');
+ $v //= 'a';
+ return
+ $v =~ m/^[ty1]/ ? 0 : # force readonly, forpush = 0
+ $v =~ m/^[fn0]/ ? 1 : # force nonreadonly, forpush = 1
+ $v =~ m/^[a]/ ? '' : # auto, forpush = ''
+ badcfg "readonly needs t (true, y, 1) or f (false, n, 0) or a (auto)";
}
-sub access_distro () {
- return (access_quirk())[1];
+sub access_forpush () {
+ $access_forpush //= access_forpush_config();
+ return $access_forpush;
+}
+
+sub pushing () {
+ die "$access_forpush ?" if ($access_forpush // 1) ne 1;
+ badcfg "pushing but distro is configured readonly"
+ if access_forpush_config() eq '0';
+ $access_forpush = 1;
+ $supplementary_message = <<'END' unless $we_are_responder;
+Push failed, before we got started.
+You can retry the push, after fixing the problem, if you like.
+END
+ parseopts_late_defaults();
+}
+
+sub notpushing () {
+ parseopts_late_defaults();
+}
+
+sub supplementary_message ($) {
+ my ($msg) = @_;
+ if (!$we_are_responder) {
+ $supplementary_message = $msg;
+ return;
+ } elsif ($protovsn >= 3) {
+ responder_send_command "supplementary-message ".length($msg)
+ or die $!;
+ print PO $msg or die $!;
+ }
+}
+
+sub access_distros () {
+ # Returns list of distros to try, in order
+ #
+ # We want to try:
+ # 0. `instead of' distro name(s) we have been pointed to
+ # 1. the access_quirk distro, if any
+ # 2a. the user's specified distro, or failing that } basedistro
+ # 2b. the distro calculated from the suite }
+ my @l = access_basedistro();
+
+ my (undef,$quirkdistro) = access_quirk();
+ unshift @l, $quirkdistro;
+ unshift @l, $instead_distro;
+ @l = grep { defined } @l;
+
+ push @l, access_nomdistro();
+
+ if (access_forpush()) {
+ @l = map { ("$_/push", $_) } @l;
+ }
+ @l;
+}
+
+sub access_cfg_cfgs (@) {
+ my (@keys) = @_;
+ my @cfgs;
+ # The nesting of these loops determines the search order. We put
+ # the key loop on the outside so that we search all the distros
+ # for each key, before going on to the next key. That means that
+ # if access_cfg is called with a more specific, and then a less
+ # specific, key, an earlier distro can override the less specific
+ # without necessarily overriding any more specific keys. (If the
+ # distro wants to override the more specific keys it can simply do
+ # so; whereas if we did the loop the other way around, it would be
+ # impossible to for an earlier distro to override a less specific
+ # key but not the more specific ones without restating the unknown
+ # values of the more specific keys.
+ my @realkeys;
+ my @rundef;
+ # We have to deal with RETURN-UNDEF specially, so that we don't
+ # terminate the search prematurely.
+ foreach (@keys) {
+ if (m/RETURN-UNDEF/) { push @rundef, $_; last; }
+ push @realkeys, $_
+ }
+ foreach my $d (access_distros()) {
+ push @cfgs, map { "dgit-distro.$d.$_" } @realkeys;
+ }
+ push @cfgs, map { "dgit.default.$_" } @realkeys;
+ push @cfgs, @rundef;
+ return @cfgs;
}
sub access_cfg (@) {
my (@keys) = @_;
- my $basedistro = access_basedistro();
- my $distro = $idistro || access_distro();
- my $value = cfg(map {
- ("dgit-distro.$distro.$_",
- "dgit-distro.$basedistro.$_",
- "dgit.default.$_")
- } @keys);
+ my (@cfgs) = access_cfg_cfgs(@keys);
+ my $value = cfg(@cfgs);
return $value;
}
+sub access_cfg_bool ($$) {
+ my ($def, @keys) = @_;
+ parse_cfg_bool($keys[0], $def, access_cfg(@keys, 'RETURN-UNDEF'));
+}
+
sub string_to_ssh ($) {
my ($spec) = @_;
if ($spec =~ m/\s/) {
@@ -612,9 +895,16 @@ sub access_cfg_ssh () {
}
}
+sub access_runeinfo ($) {
+ my ($info) = @_;
+ return ": dgit ".access_basedistro()." $info ;";
+}
+
sub access_someuserhost ($) {
my ($some) = @_;
- my $user = access_cfg("$some-user",'username');
+ my $user = access_cfg("$some-user-force", 'RETURN-UNDEF');
+ defined($user) && length($user) or
+ $user = access_cfg("$some-user",'username');
my $host = access_cfg("$some-host");
return length($user) ? "$user\@$host" : $host;
}
@@ -623,15 +913,22 @@ sub access_gituserhost () {
return access_someuserhost('git');
}
-sub access_giturl () {
+sub access_giturl (;$) {
+ my ($optional) = @_;
my $url = access_cfg('git-url','RETURN-UNDEF');
- if (!defined $url) {
+ my $suffix;
+ if (!length $url) {
+ my $proto = access_cfg('git-proto', 'RETURN-UNDEF');
+ return undef unless defined $proto;
$url =
- access_cfg('git-proto').
+ $proto.
access_gituserhost().
access_cfg('git-path');
+ } else {
+ $suffix = access_cfg('git-url-suffix','RETURN-UNDEF');
}
- return "$url/$package.git";
+ $suffix //= '.git';
+ return "$url/$package$suffix";
}
sub parsecontrolfh ($$;$) {
@@ -661,10 +958,10 @@ sub parsecontrolfh ($$;$) {
}
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;
@@ -674,11 +971,11 @@ sub getfield ($$) {
my ($dctrl,$field) = @_;
my $v = $dctrl->{$field};
return $v if defined $v;
- fail "missing field $field in ".$v->get_option('name');
+ fail "missing field $field in ".$dctrl->get_option('name');
}
sub parsechangelog {
- my $c = Dpkg::Control::Hash->new();
+ my $c = Dpkg::Control::Hash->new(name => 'parsed changelog');
my $p = new IO::Handle;
my @cmd = (qw(dpkg-parsechangelog), @_);
open $p, '-|', @cmd or die $!;
@@ -687,21 +984,17 @@ sub parsechangelog {
return $c;
}
-sub git_get_ref ($) {
- my ($refname) = @_;
- my $got = cmdoutput_errok @git, qw(show-ref --), $refname;
- if (!defined $got) {
- $?==256 or fail "git show-ref failed (status $?)";
- printdebug "ref $refname= [show-ref exited 1]\n";
- return '';
- }
- if ($got =~ m/^(\w+) \Q$refname\E$/m) {
- printdebug "ref $refname=$1\n";
- return $1;
- } else {
- printdebug "ref $refname= [no match]\n";
- return '';
- }
+sub commit_getclogp ($) {
+ # Returns the parsed changelog hashref for a particular commit
+ my ($objid) = @_;
+ our %commit_getclogp_memo;
+ my $memo = $commit_getclogp_memo{$objid};
+ return $memo if $memo;
+ mkpath '.git/dgit';
+ my $mclog = ".git/dgit/clog-$objid";
+ runcmd shell_cmd "exec >$mclog", @git, qw(cat-file blob),
+ "$objid:debian/changelog";
+ $commit_getclogp_memo{$objid} = parsechangelog("-l$mclog");
}
sub must_getcwd () {
@@ -710,25 +1003,29 @@ sub must_getcwd () {
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');
- if (!defined $query) {
- my $distro = access_basedistro();
- if ($distro eq 'debian') {
- $query = "sshpsql:".
- access_someuserhost('sshpsql').':'.
- access_cfg('sshpsql-dbname');
- } else {
- $query = "madison:$distro";
- }
- }
$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 ($$) {
@@ -737,17 +1034,338 @@ sub pool_dsc_subpath ($$) {
return "/pool/$component/$prefix/$package/".dscfn($vsn);
}
-sub archive_query_madison ($$) {
+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 = (@curl, qw(-sS));
+ my $url = access_cfg('archive-query-url');
+ if ($url =~ m#^https://([-.0-9a-z]+)/#) {
+ my $host = $1;
+ my $keys = access_cfg('archive-query-tls-key','RETURN-UNDEF') //'';
+ foreach my $key (split /\:/, $keys) {
+ $key =~ s/\%HOST\%/$host/g;
+ if (!stat $key) {
+ fail "for $url: stat $key: $!" unless $!==ENOENT;
+ next;
+ }
+ fail "config requested specific TLS key but do not know".
+ " how to get curl to use exactly that EE key ($key)";
+# push @cmd, "--cacert", $key, "--capath", "/dev/enoent";
+# # Sadly the above line does not work because of changes
+# # to gnutls. The real fix for #790093 may involve
+# # new curl options.
+ last;
+ }
+ # Fixing #790093 properly will involve providing a value
+ # for this on clients.
+ my $kargs = access_cfg('archive-query-tls-curl-ca-args','RETURN-UNDEF');
+ push @cmd, split / /, $kargs if defined $kargs;
+ }
+ push @cmd, $url.$subpath;
+ return @cmd;
+}
+
+sub api_query ($$;$) {
+ use JSON;
+ 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 {
+ my ($proto,$data) = @_;
+ my $suites = api_query($data, 'suites');
+ my @matched;
+ foreach my $entry (@$suites) {
+ next unless grep {
+ my $v = $entry->{$_};
+ defined $v && $v eq $isuite;
+ } qw(codename name);
+ push @matched, $entry;
+ }
+ fail "unknown suite $isuite" unless @matched;
+ my $cn;
+ eval {
+ @matched==1 or die "multiple matches for suite $isuite\n";
+ $cn = "$matched[0]{codename}";
+ defined $cn or die "suite $isuite info has no codename\n";
+ $cn =~ m/^$suite_re$/ or die "suite $isuite maps to bad codename\n";
+ };
+ die "bad ftpmaster api response: $@\n".Dumper(\@matched)
+ if length $@;
+ return $cn;
+}
+
+sub archive_query_ftpmasterapi {
+ my ($proto,$data) = @_;
+ my $info = api_query($data, "dsc_in_suite/$isuite/$package");
+ my @rows;
+ my $digester = Digest::SHA->new(256);
+ foreach my $entry (@$info) {
+ eval {
+ my $vsn = "$entry->{version}";
+ my ($ok,$msg) = version_check $vsn;
+ die "bad version: $msg\n" unless $ok;
+ my $component = "$entry->{component}";
+ $component =~ m/^$component_re$/ or die "bad component";
+ my $filename = "$entry->{filename}";
+ $filename && $filename !~ m#[^-+:._~0-9a-zA-Z/]|^[/.]|/[/.]#
+ or die "bad filename";
+ my $sha256sum = "$entry->{sha256sum}";
+ $sha256sum =~ m/^[0-9a-f]+$/ or die "bad sha256sum";
+ push @rows, [ $vsn, "/pool/$component/$filename",
+ $digester, $sha256sum ];
+ };
+ die "bad ftpmaster api response: $@\n".Dumper($entry)
+ if length $@;
+ }
+ @rows = sort { -version_compare($a->[0],$b->[0]) } @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 archive_query_prepend_mirror
+ map { [ @$_[0..1] ] } madison_get_parse(@_);
+}
+
+sub madison_get_parse {
my ($proto,$data) = @_;
die unless $proto eq 'madison';
- $rmad{$package} ||= cmdoutput
+ if (!length $data) {
+ $data= access_cfg('madison-distro','RETURN-UNDEF');
+ $data //= access_basedistro();
+ }
+ $rmad{$proto,$data,$package} ||= cmdoutput
qw(rmadison -asource),"-s$isuite","-u$data",$package;
- my $rmad = $rmad{$package};
- return madison_parse($rmad);
-}
+ my $rmad = $rmad{$proto,$data,$package};
-sub madison_parse ($) {
- my ($rmad) = @_;
my @out;
foreach my $l (split /\n/, $rmad) {
$l =~ m{^ \s*( [^ \t|]+ )\s* \|
@@ -766,12 +1384,12 @@ sub madison_parse ($) {
$5 eq 'source' or die "$rmad ?";
push @out, [$vsn,pool_dsc_subpath($vsn,$component),$newsuite];
}
- return sort { -version_compare_string($a->[0],$b->[0]); } @out;
+ return sort { -version_compare($a->[0],$b->[0]); } @out;
}
-sub canonicalise_suite_madison ($$) {
+sub canonicalise_suite_madison {
# madison canonicalises for us
- my @r = archive_query_madison($_[0],$_[1]);
+ my @r = madison_get_parse(@_);
@r or fail
"unable to canonicalise suite using package $package".
" which does not appear to exist in suite $isuite;".
@@ -779,18 +1397,28 @@ sub canonicalise_suite_madison ($$) {
return $r[0][2];
}
-sub sshpsql ($$) {
- my ($data,$sql) = @_;
+sub file_in_archive_madison { return undef; }
+
+#---------- `sshpsql' archive query method ----------
+
+sub sshpsql ($$$) {
+ my ($data,$runeinfo,$sql) = @_;
+ if (!length $data) {
+ $data= access_someuserhost('sshpsql').':'.
+ access_cfg('sshpsql-dbname');
+ }
$data =~ m/:/ or badcfg "invalid sshpsql method string \`$data'";
my ($userhost,$dbname) = ($`,$'); #';
my @rows;
my @cmd = (access_cfg_ssh, $userhost,
- "export LANG=C; ".shellquote qw(psql -A), $dbname, qw(-c), $sql);
- printcmd(\*DEBUG,$debugprefix."|",@cmd) if $debug>0;
+ access_runeinfo("ssh-psql $runeinfo").
+ " export LC_MESSAGES=C; export LC_CTYPE=C;".
+ " ".shellquote qw(psql -A), $dbname, qw(-c), $sql);
+ debugcmd "|",@cmd;
open P, "-|", @cmd or die $!;
while (<P>) {
chomp or die;
- printdebug("$debugprefix>|$_|\n");
+ printdebug(">|$_|\n");
push @rows, $_;
}
$!=0; $?=0; close P or failedcmd @cmd;
@@ -805,13 +1433,13 @@ sub sshpsql ($$) {
}
sub sql_injection_check {
- foreach (@_) { die "$_ $& ?" if m/[']/; }
+ foreach (@_) { die "$_ $& ?" if m{[^-+=:_.,/0-9a-zA-Z]}; }
}
sub archive_query_sshpsql ($$) {
my ($proto,$data) = @_;
sql_injection_check $isuite, $package;
- my @rows = sshpsql($data, <<END);
+ my @rows = sshpsql($data, "archive-query $isuite $package", <<END);
SELECT source.version, component.name, files.filename, files.sha256sum
FROM source
JOIN src_associations ON source.id = src_associations.source
@@ -824,19 +1452,19 @@ sub archive_query_sshpsql ($$) {
AND source.source='$package'
AND files.filename LIKE '%.dsc';
END
- @rows = sort { -version_compare_string($a->[0],$b->[0]) } @rows;
+ @rows = sort { -version_compare($a->[0],$b->[0]) } @rows;
my $digester = Digest::SHA->new(256);
@rows = map {
my ($vsn,$component,$filename,$sha256sum) = @$_;
[ $vsn, "/pool/$component/$filename",$digester,$sha256sum ];
} @rows;
- return @rows;
+ return archive_query_prepend_mirror @rows;
}
sub canonicalise_suite_sshpsql ($$) {
my ($proto,$data) = @_;
sql_injection_check $isuite;
- my @rows = sshpsql($data, <<END);
+ my @rows = sshpsql($data, "canonicalise-suite $isuite", <<END);
SELECT suite.codename
FROM suite where suite_name='$isuite' or codename='$isuite';
END
@@ -846,6 +1474,10 @@ END
return $rows[0];
}
+sub file_in_archive_sshpsql ($$$) { return undef; }
+
+#---------- `dummycat' archive query method ----------
+
sub canonicalise_suite_dummycat ($$) {
my ($proto,$data) = @_;
my $dpath = "$data/suite.$isuite";
@@ -882,15 +1514,73 @@ sub archive_query_dummycat ($$) {
}
C->error and die "$dpath: $!";
close C;
- return sort { -version_compare_string($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".
+ " to use tag format $tagformat_want->[0] ($tagformat_want->[1])".
+ " - no way to proceed"
+ if $tagformat_want && $tagformat_want->[0] ne $fmt;
+ $tagformat_want = [$fmt, $why, $tagformat_want->[2] // 0];
+}
+
+sub select_tagformat () {
+ # sets $tagformatfn
+ return if $tagformatfn && !$tagformat_want;
+ die 'bug' if $tagformatfn && $tagformat_want;
+ # ... $tagformat_want assigned after previous select_tagformat
+
+ my (@supported) = grep { $_ =~ m/^(?:old|new)$/ } access_cfg_tagformats();
+ printdebug "select_tagformat supported @supported\n";
+
+ $tagformat_want //= [ $supported[0], "distro access configuration", 0 ];
+ printdebug "select_tagformat specified @$tagformat_want\n";
+
+ my ($fmt,$why,$override) = @$tagformat_want;
+
+ fail "target distro supports tag formats @supported".
+ " but have to use $fmt ($why)"
+ unless $override
+ or grep { $_ eq $fmt } @supported;
+
+ $tagformat_want = undef;
+ $tagformat = $fmt;
+ $tagformatfn = ${*::}{"debiantag_$fmt"};
+
+ fail "trying to use unknown tag format \`$fmt' ($why) !"
+ unless $tagformatfn;
}
+#---------- archive query entrypoints and rest of program ----------
+
sub canonicalise_suite () {
return if defined $csuite;
fail "cannot operate on $isuite suite" if $isuite eq 'UNRELEASED';
$csuite = archive_query('canonicalise_suite');
if ($isuite ne $csuite) {
progress "canonical suite name for $isuite is $csuite";
+ } else {
+ progress "canonical suite name is $csuite";
}
}
@@ -898,8 +1588,8 @@ sub get_archive_dsc () {
canonicalise_suite();
my @vsns = archive_query('archive_query');
foreach my $vinfo (@vsns) {
- my ($vsn,$subpath,$digester,$digest) = @$vinfo;
- $dscurl = access_cfg('mirror').$subpath;
+ 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;
@@ -913,29 +1603,62 @@ sub get_archive_dsc () {
fail "$dscurl has hash $got but".
" archive told us to expect $digest";
}
- my $dscfh = new IO::File \$dscdata, '<' or die $!;
- printdebug Dumper($dscdata) if $debug>1;
- $dsc = parsecontrolfh($dscfh,$dscurl,1);
- printdebug Dumper($dsc) if $debug>1;
+ 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;
}
$dsc = undef;
+ printdebug "get_archive_dsc: nothing in archive, returning undef\n";
}
+sub check_for_git ();
sub check_for_git () {
# returns 0 or 1
my $how = access_cfg('git-check');
if ($how eq 'ssh-cmd') {
my @cmd =
(access_cfg_ssh, access_gituserhost(),
+ access_runeinfo("git-check $package").
" set -e; cd ".access_cfg('git-path').";".
" if test -d $package.git; then echo 1; else echo 0; fi");
my $r= cmdoutput @cmd;
- failedcmd @cmd unless $r =~ m/^[01]$/;
+ if (defined $r and $r =~ m/^divert (\w+)$/) {
+ my $divert=$1;
+ my ($usedistro,) = access_distros();
+ # NB that if we are pushing, $usedistro will be $distro/push
+ $instead_distro= cfg("dgit-distro.$usedistro.diverts.$divert");
+ $instead_distro =~ s{^/}{ access_basedistro()."/" }e;
+ progress "diverting to $divert (using config for $instead_distro)";
+ return check_for_git();
+ }
+ failedcmd @cmd unless defined $r and $r =~ m/^[01]$/;
return $r+0;
+ } elsif ($how eq 'url') {
+ my $prefix = access_cfg('git-check-url','git-url');
+ my $suffix = access_cfg('git-check-suffix','git-suffix',
+ 'RETURN-UNDEF') // '.git';
+ my $url = "$prefix/$package$suffix";
+ 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
+ # HTTP/1.0 200 Connection established
+ $result =~ m/^\S+ (404|200) /s or
+ fail "unexpected results from git check query - ".
+ Dumper($prefix, $result);
+ my $code = $1;
+ if ($code eq '404') {
+ return 0;
+ } elsif ($code eq '200') {
+ return 1;
+ } else {
+ die;
+ }
} elsif ($how eq 'true') {
return 1;
} elsif ($how eq 'false') {
@@ -950,6 +1673,7 @@ sub create_remote_git_repo () {
if ($how eq 'ssh-cmd') {
runcmd_ordryrun
(access_cfg_ssh, access_gituserhost(),
+ access_runeinfo("git-create $package").
"set -e; cd ".access_cfg('git-path').";".
" cp -a _template $package.git");
} elsif ($how eq 'true') {
@@ -959,38 +1683,90 @@ sub create_remote_git_repo () {
}
}
-our ($dsc_hash,$lastpush_hash);
+our ($dsc_hash,$lastpush_mergeinput);
+our ($dsc_distro, $dsc_hint_tag, $dsc_hint_url);
our $ud = '.git/dgit/unpack';
-sub prep_ud () {
- rmtree($ud);
+sub prep_ud (;$) {
+ my ($d) = @_;
+ $d //= $ud;
+ rmtree($d);
mkpath '.git/dgit';
- mkdir $ud or die $!;
+ mkdir $d or die $!;
}
-sub mktree_in_ud_from_only_subdir () {
- # changes into the subdir
- my (@dirs) = <*/.>;
- die unless @dirs==1;
- $dirs[0] =~ m#^([^/]+)/\.$# or die;
- my $dir = $1;
- changedir $dir;
- fail "source package contains .git directory" if stat '.git';
- die $! unless $!==&ENOENT;
+sub mktree_in_ud_here () {
runcmd qw(git init -q);
+ runcmd qw(git config gc.auto 0);
+ foreach my $copy (qw(user.email user.name user.useConfigOnly)) {
+ my $v = $gitcfgs{local}{$copy};
+ next unless $v;
+ runcmd qw(git config), $copy, $_ foreach @$v;
+ }
rmtree('.git/objects');
symlink '../../../../objects','.git/objects' or die $!;
- runcmd @git, qw(add -Af);
+ setup_gitattrs(1);
+}
+
+sub git_write_tree () {
my $tree = cmdoutput @git, qw(write-tree);
$tree =~ m/^\w+$/ or die "$tree ?";
+ return $tree;
+}
+
+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 $what: ",
+ (messagequote $_), "\n";
+ rmtree $_;
+ }
+ }
+ $!=0; $?=0; close GITS or failedcmd @gitscmd;
+}
+
+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($what);
+ mktree_in_ud_here();
+ if (!$raw) {
+ my ($format, $fopts) = get_source_format();
+ if (madformat($format)) {
+ rmtree '.pc';
+ }
+ }
+
+ 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;
@@ -1018,9 +1794,175 @@ sub dsc_files () {
map { $_->{Filename} } dsc_files_info();
}
-sub is_orig_file ($) {
- local ($_) = @_;
- m/\.orig(?:-\w+)?\.tar\.\w+$/;
+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;
+ # One file means no origs, and the filename doesn't have a "what
+ # part of dsc" component. (Consider versions ending `.orig'.)
+ return 0 unless $f =~ m/\.$orig_f_tail_re$/o;
+ return 1;
+}
+
+sub is_orig_file_of_vsn ($$) {
+ my ($f, $upstreamvsn) = @_;
+ my $base = srcfn $upstreamvsn, '';
+ return 0 unless $f =~ m/^\Q$base\E\.$orig_f_tail_re$/;
+ 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 ($) {
@@ -1028,223 +1970,1150 @@ sub make_commit ($) {
return cmdoutput @git, qw(hash-object -w -t commit), $file;
}
+sub make_commit_text ($) {
+ my ($text) = @_;
+ my ($out, $in);
+ my @cmd = (@git, qw(hash-object -w -t commit --stdin));
+ debugcmd "|",@cmd;
+ print Dumper($text) if $debuglevel > 1;
+ my $child = open2($out, $in, @cmd) or die $!;
+ my $h;
+ eval {
+ print $in $text or die $!;
+ close $in or die $!;
+ $h = <$out>;
+ $h =~ m/^\w+$/ or die;
+ $h = $&;
+ printdebug "=> $h\n";
+ };
+ close $out;
+ waitpid $child, 0 == $child or die "$child $!";
+ $? and failedcmd @cmd;
+ return $h;
+}
+
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/^[^<>]+ \<\S+\> \d+ [-+]\d+$/ or
+ $authline =~ m/$git_authline_re/o or
fail "unexpected commit author line format \`$authline'".
" (was generated from changelog Maintainer field)";
+ return ($1,$2,$3) if wantarray;
return $authline;
}
-sub generate_commit_from_dsc () {
+sub vendor_patches_distro ($$) {
+ my ($checkdistro, $what) = @_;
+ return unless defined $checkdistro;
+
+ my $series = "debian/patches/\L$checkdistro\E.series";
+ printdebug "checking for vendor-specific $series ($what)\n";
+
+ if (!open SERIES, "<", $series) {
+ die "$series $!" unless $!==ENOENT;
+ return;
+ }
+ while (<SERIES>) {
+ next unless m/\S/;
+ next if m/^\s+\#/;
+
+ print STDERR <<END;
+
+Unfortunately, this source package uses a feature of dpkg-source where
+the same source package unpacks to different source code on different
+distros. dgit cannot safely operate on such packages on affected
+distros, because the meaning of source packages is not stable.
+
+Please ask the distro/maintainer to remove the distro-specific series
+files and use a different technique (if necessary, uploading actually
+different packages, if different distros are supposed to have
+different code).
+
+END
+ fail "Found active distro-specific series file for".
+ " $checkdistro ($what): $series, cannot continue";
+ }
+ die "$series $!" if SERIES->error;
+ close SERIES;
+}
+
+sub check_for_vendor_patches () {
+ # This dpkg-source feature doesn't seem to be documented anywhere!
+ # But it can be found in the changelog (reformatted):
+
+ # commit 4fa01b70df1dc4458daee306cfa1f987b69da58c
+ # Author: Raphael Hertzog <hertzog@debian.org>
+ # Date: Sun Oct 3 09:36:48 2010 +0200
+
+ # dpkg-source: correctly create .pc/.quilt_series with alternate
+ # series files
+ #
+ # If you have debian/patches/ubuntu.series and you were
+ # unpacking the source package on ubuntu, quilt was still
+ # directed to debian/patches/series instead of
+ # debian/patches/ubuntu.series.
+ #
+ # debian/changelog | 3 +++
+ # scripts/Dpkg/Source/Package/V3/quilt.pm | 4 +++-
+ # 2 files changed, 6 insertions(+), 1 deletion(-)
+
+ use Dpkg::Vendor;
+ vendor_patches_distro($ENV{DEB_VENDOR}, "DEB_VENDOR");
+ vendor_patches_distro(Dpkg::Vendor::get_current_vendor(),
+ "Dpkg::Vendor \`current vendor'");
+ vendor_patches_distro(access_basedistro(),
+ "(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;
- foreach my $fi (dsc_files_info()) {
+ my @dfi = dsc_files_info();
+ foreach my $fi (@dfi) {
my $f = $fi->{Filename};
die "$f ?" if $f =~ m#/|^\.|\.dsc$|\.tmp$#;
+ my $upper_f = "../../../../$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 "../../../$f", $f
- or $!==&ENOENT
- or die "$f $!";
+ my $refetched;
+ complete_file_from_dsc('.', $fi, \$refetched)
+ or next;
+
+ 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";
+ }
+ }
- complete_file_from_dsc('.', $fi);
+ # We unpack and record the orig tarballs first, so that we only
+ # need disk space for one private copy of the unpacked source.
+ # But we can't make them into commits until we have the metadata
+ # from the debian/changelog, so we record the tree objects now and
+ # make them into commits later.
+ my @tartrees;
+ my $upstreamv = upstreamversion $dsc->{version};
+ my $orig_f_base = srcfn $upstreamv, '';
- if (is_orig_file($f)) {
- link $f, "../../../../$f"
- or $!==&EEXIST
- or die "$f $!";
+ foreach my $fi (@dfi) {
+ # We actually import, and record as a commit, every tarball
+ # (unless there is only one file, in which case there seems
+ # little point.
+
+ my $f = $fi->{Filename};
+ printdebug "import considering $f ";
+ (printdebug "only one dfi\n"), next if @dfi == 1;
+ (printdebug "not tar\n"), next unless $f =~ m/\.tar(\.\w+)?$/;
+ (printdebug "signature\n"), next if $f =~ m/$orig_f_sig_re$/o;
+ my $compr_ext = $1;
+
+ my ($orig_f_part) =
+ $f =~ m/^\Q$orig_f_base\E\.([^._]+)?\.tar(?:\.\w+)?$/;
+
+ printdebug "Y ", (join ' ', map { $_//"(none)" }
+ $compr_ext, $orig_f_part
+ ), "\n";
+
+ my $input = new IO::File $f, '<' or die "$f $!";
+ my $compr_pid;
+ my @compr_cmd;
+
+ if (defined $compr_ext) {
+ my $cname =
+ Dpkg::Compression::compression_guess_from_filename $f;
+ fail "Dpkg::Compression cannot handle file $f in source package"
+ if defined $compr_ext && !defined $cname;
+ my $compr_proc =
+ new Dpkg::Compression::Process compression => $cname;
+ my @compr_cmd = $compr_proc->get_uncompress_cmdline();
+ my $compr_fh = new IO::Handle;
+ my $compr_pid = open $compr_fh, "-|" // die $!;
+ if (!$compr_pid) {
+ open STDIN, "<&", $input or die $!;
+ exec @compr_cmd;
+ die "dgit (child): exec $compr_cmd[0]: $!\n";
+ }
+ $input = $compr_fh;
+ }
+
+ 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 $!;
+ open STDIN, "<&", $input or die $!;
+ exec @tarcmd;
+ die "dgit (child): exec $tarcmd[0]: $!";
}
+ $!=0; (waitpid $tar_pid, 0) == $tar_pid or die $!;
+ !$? or failedcmd @tarcmd;
+
+ close $input or
+ (@compr_cmd ? 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";
+ 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, {
+ Orig => !!$orig_f_part,
+ Sort => (!$orig_f_part ? 2 :
+ $orig_f_part =~ m/-/g ? 1 :
+ 0),
+ F => $f,
+ Tree => $tree,
+ };
}
+ @tartrees = sort {
+ # put any without "_" first (spec is not clear whether files
+ # are always in the usual order). Tarballs without "_" are
+ # the main orig or the debian tarball.
+ $a->{Sort} <=> $b->{Sort} or
+ $a->{F} cmp $b->{F}
+ } @tartrees;
+
+ my $any_orig = grep { $_->{Orig} } @tartrees;
+
my $dscfn = "$package.dsc";
+ my $treeimporthow = 'package';
+
open D, ">", $dscfn or die "$dscfn: $!";
print D $dscdata or die "$dscfn: $!";
close D or die "$dscfn: $!";
my @cmd = qw(dpkg-source);
push @cmd, '--no-check' if $dsc_checked;
+ if (madformat $dsc->{format}) {
+ push @cmd, '--skip-patches';
+ $treeimporthow = 'unpatched';
+ }
push @cmd, qw(-x --), $dscfn;
runcmd @cmd;
- my ($tree,$dir) = mktree_in_ud_from_only_subdir();
- runcmd qw(sh -ec), 'dpkg-parsechangelog >../changelog.tmp';
- my $clogp = parsecontrol('../changelog.tmp',"commit's changelog");
+ my ($tree,$dir) = mktree_in_ud_from_only_subdir("source package");
+ if (madformat $dsc->{format}) {
+ check_for_vendor_patches();
+ }
+
+ my $dappliedtree;
+ if (madformat $dsc->{format}) {
+ my @pcmd = qw(dpkg-source --before-build .);
+ runcmd shell_cmd 'exec >/dev/null', @pcmd;
+ rmtree '.pc';
+ $dappliedtree = git_add_write_tree();
+ }
+
+ my @clogcmd = qw(dpkg-parsechangelog --format rfc822 --all);
+ debugcmd "|",@clogcmd;
+ open CLOGS, "-|", @clogcmd or die $!;
+
+ my $clogp;
+ my $r1clogp;
+
+ printdebug "import clog search...\n";
+
+ for (;;) {
+ my $stanzatext = do { local $/=""; <CLOGS>; };
+ printdebug "import clogp ".Dumper($stanzatext) if $debuglevel>1;
+ last if !defined $stanzatext;
+
+ my $desc = "package changelog, entry no.$.";
+ open my $stanzafh, "<", \$stanzatext or die;
+ my $thisstanza = parsecontrolfh $stanzafh, $desc, 1;
+ $clogp //= $thisstanza;
+
+ printdebug "import clog $thisstanza->{version} $desc...\n";
+
+ last if !$any_orig; # we don't need $r1clogp
+
+ # We look for the first (most recent) changelog entry whose
+ # version number is lower than the upstream version of this
+ # package. Then the last (least recent) previous changelog
+ # entry is treated as the one which introduced this upstream
+ # version and used for the synthetic commits for the upstream
+ # tarballs.
+
+ # One might think that a more sophisticated algorithm would be
+ # necessary. But: we do not want to scan the whole changelog
+ # file. Stopping when we see an earlier version, which
+ # necessarily then is an earlier upstream version, is the only
+ # realistic way to do that. Then, either the earliest
+ # changelog entry we have seen so far is indeed the earliest
+ # upload of this upstream version; or there are only changelog
+ # entries relating to later upstream versions (which is not
+ # possible unless the changelog and .dsc disagree about the
+ # version). Then it remains to choose between the physically
+ # last entry in the file, and the one with the lowest version
+ # number. If these are not the same, we guess that the
+ # versions were created in a non-monotic order rather than
+ # that the changelog entries have been misordered.
+
+ printdebug "import clog $thisstanza->{version} vs $upstreamv...\n";
+
+ last if version_compare($thisstanza->{version}, $upstreamv) < 0;
+ $r1clogp = $thisstanza;
+
+ printdebug "import clog $r1clogp->{version} becomes r1\n";
+ }
+ die $! if CLOGS->error;
+ 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) {
+ $r1clogp //= $clogp; # maybe there's only one entry;
+ my $r1authline = clogp_authline $r1clogp;
+ # Strictly, r1authline might now be wrong if it's going to be
+ # unused because !$any_orig. Whatever.
+
+ printdebug "import tartrees authline $authline\n";
+ printdebug "import tartrees r1authline $r1authline\n";
+
+ foreach my $tt (@tartrees) {
+ printdebug "import tartree $tt->{F} $tt->{Tree}\n";
+
+ $tt->{Commit} = make_commit_text($tt->{Orig} ? <<END_O : <<END_T);
+tree $tt->{Tree}
+author $r1authline
+committer $r1authline
+
+Import $tt->{F}
+
+[dgit import orig $tt->{F}]
+END_O
+tree $tt->{Tree}
+author $authline
+committer $authline
+
+Import $tt->{F}
+
+[dgit import tarball $package $cversion $tt->{F}]
+END_T
+ }
+ }
+
+ printdebug "import main commit\n";
+
open C, ">../commit.tmp" or die $!;
print C <<END or die $!;
tree $tree
+END
+ print C <<END or die $! foreach @tartrees;
+parent $_->{Commit}
+END
+ print C <<END or die $!;
author $authline
committer $authline
$changes
-# imported from the archive
+[dgit import $treeimporthow $package $cversion]
END
+
close C or die $!;
- my $outputhash = make_commit qw(../commit.tmp);
- my $cversion = getfield $clogp, 'Version';
+ my $rawimport_hash = make_commit qw(../commit.tmp);
+
+ if (madformat $dsc->{format}) {
+ printdebug "import apply patches...\n";
+
+ # regularise the state of the working tree so that
+ # the checkout of $rawimport_hash works nicely.
+ my $dappliedcommit = make_commit_text(<<END);
+tree $dappliedtree
+author $authline
+committer $authline
+
+[dgit dummy commit]
+END
+ 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;
+
+ 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";
+ }
+
+ 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;
+ };
+ last unless $@;
+ }
+ if ($@) {
+ { local $@; eval { runcmd qw(cat ../../gbp-pq-output); }; }
+ die $@;
+ }
+ }
+
progress "synthesised git commit from .dsc $cversion";
- if ($lastpush_hash) {
- runcmd @git, qw(reset --hard), $lastpush_hash;
- runcmd qw(sh -ec), 'dpkg-parsechangelog >>../changelogold.tmp';
- my $oldclogp = parsecontrol('../changelogold.tmp','previous changelog');
+
+ my $rawimport_mergeinput = {
+ Commit => $rawimport_hash,
+ Info => "Import of source package",
+ };
+ my @output = ($rawimport_mergeinput);
+
+ if ($lastpush_mergeinput) {
+ my $oldclogp = mergeinfo_getclogp($lastpush_mergeinput);
my $oversion = getfield $oldclogp, 'Version';
my $vcmp =
- version_compare_string($oversion, $cversion);
+ version_compare($oversion, $cversion);
if ($vcmp < 0) {
- # git upload/ is earlier vsn than archive, use archive
- open C, ">../commit2.tmp" or die $!;
- print C <<END or die $!;
-tree $tree
-parent $lastpush_hash
-parent $outputhash
-author $authline
-committer $authline
-
+ @output = ($rawimport_mergeinput, $lastpush_mergeinput,
+ { Message => <<END, ReverseParents => 1 });
Record $package ($cversion) in archive suite $csuite
END
- $outputhash = make_commit qw(../commit2.tmp);
} elsif ($vcmp > 0) {
print STDERR <<END or die $!;
-Version actually in archive: $cversion (older)
-Last allegedly pushed/uploaded: $oversion (newer or same)
+Version actually in archive: $cversion (older)
+Last version pushed with dgit: $oversion (newer or same)
$later_warning_msg
END
- $outputhash = $lastpush_hash;
+ @output = $lastpush_mergeinput;
} else {
- $outputhash = $lastpush_hash;
+ # Same version. Use what's in the server git branch,
+ # discarding our own import. (This could happen if the
+ # server automatically imports all packages into git.)
+ @output = $lastpush_mergeinput;
}
}
changedir '../../../..';
- runcmd @git, qw(update-ref -m),"dgit fetch import $cversion",
- 'DGIT_ARCHIVE', $outputhash;
- cmdoutput @git, qw(log -n2), $outputhash;
- # ... gives git a chance to complain if our commit is malformed
rmtree($ud);
- return $outputhash;
+ 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;
- if (stat $tf) {
- progress "using existing $f";
+ my $got;
+ my $checkhash = sub {
+ open F, "<", "$tf" or die "$tf: $!";
+ $fi->{Digester}->reset();
+ $fi->{Digester}->addfile(*F);
+ F->error and die $!;
+ my $got = $fi->{Digester}->hexdigest();
+ return $got eq $fi->{Hash};
+ };
+
+ if (stat_exists $tf) {
+ 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 {
- die "$tf $!" unless $!==&ENOENT;
-
- my $furl = $dscurl;
- $furl =~ s{/[^/]+$}{};
- $furl .= "/$f";
- die "$f ?" unless $f =~ m/^${package}_/;
- die "$f ?" if $f =~ m#/#;
- runcmd_ordryrun_local @curl,qw(-o),$tf,'--',"$furl";
- next if !act_local();
- $downloaded = 1;
+ printdebug "$tf does not exist, need to fetch\n";
}
- 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
+ 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 ensure_we_have_orig () {
- foreach my $fi (dsc_files_info()) {
+ my @dfi = dsc_files_info();
+ foreach my $fi (@dfi) {
my $f = $fi->{Filename};
- next unless is_orig_file($f);
- complete_file_from_dsc('..', $fi);
+ next unless is_orig_file_in_dsc($f, \@dfi);
+ complete_file_from_dsc('..', $fi)
+ or next;
}
}
-sub rev_parse ($) {
- return cmdoutput @git, qw(rev-parse), "$_[0]~0";
+#---------- git fetch ----------
+
+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 is_fast_fwd ($$) {
- my ($ancestor,$child) = @_;
- my @cmd = (@git, qw(merge-base), $ancestor, $child);
- my $mb = cmdoutput_errok @cmd;
- if (defined $mb) {
- return rev_parse($mb) eq rev_parse($ancestor);
- } else {
- $?==256 or failedcmd @cmd;
- return 0;
+sub git_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 *,
+ # 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
+ # has refs/refs/something. (See, for example, Debian #NNNN.)
+ # - if there is no matching remote ref, it bombs out the whole
+ # fetch.
+ # 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
+ # own qairks. Notably, it has the absurd multi-tail-matching
+ # 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
+ # 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;
+ my $wildcard = $x =~ s/\\\*$/.*/;
+ die if $wildcard && $supplementary;
+ "(?:refs/$x)";
+ } @specs;
+ printdebug "git_lrfetch_sane specre=$specre\n";
+ my $wanted_rref = sub {
+ local ($_) = @_;
+ 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), $url, @look);
+ debugcmd "|",@lcmd;
+
+ my %wantr;
+ open GITLS, "-|", @lcmd or die $!;
+ while (<GITLS>) {
+ printdebug "=> ", $_;
+ m/^(\w+)\s+(\S+)\n/ or die "ls-remote $_ ?";
+ my ($objid,$rrefname) = ($1,$2);
+ if (!$wanted_rref->($rrefname)) {
+ print STDERR <<END;
+warning: git ls-remote @look reported $rrefname; this is silly, ignoring it.
+END
+ next;
+ }
+ $wantr{$rrefname} = $objid;
+ }
+ $!=0; $?=0;
+ close GITLS or failedcmd @lcmd;
+
+ # OK, now %want is exactly what we want for refs in @specs
+ my @fspecs = map {
+ !m/\*$/ && !exists $wantr{"refs/$_"} ? () :
+ "+refs/$_:".lrfetchrefs."/$_";
+ } @specs;
+
+ printdebug "git_lrfetch_sane fspecs @fspecs\n";
+
+ my @fcmd = (@git, qw(fetch -p -n -q), $url, @fspecs);
+ runcmd_ordryrun_local @fcmd if @fspecs;
+
+ if (!$supplementary) {
+ %lrfetchrefs_f = ();
+ }
+ my %objgot;
+
+ git_for_each_ref(lrfetchrefs, sub {
+ my ($objid,$objtype,$lrefname,$reftail) = @_;
+ $lrfetchrefs_f{$lrefname} = $objid;
+ $objgot{$objid} = 1;
+ });
+
+ 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.
+END
+ } else {
+ print STDERR <<END
+warning: git fetch @fspecs created $lrefname; this is silly, deleting it.
+END
+ }
+ runcmd_ordryrun_local @git, qw(update-ref -d), $lrefname;
+ delete $lrfetchrefs_f{$lrefname};
+ next;
+ }
+ }
+ foreach my $rrefname (sort keys %wantr) {
+ my $lrefname = lrfetchrefs.substr($rrefname, 4);
+ my $got = $lrfetchrefs_f{$lrefname} // '<none>';
+ my $want = $wantr{$rrefname};
+ next if $got eq $want;
+ if (!defined $objgot{$want}) {
+ print STDERR <<END;
+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: 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
+END
+ runcmd_ordryrun_local @git, qw(update-ref -m),
+ "dgit fetch git fetch fixup", $lrefname, $want;
+ $lrfetchrefs_f{$lrefname} = $want;
+ }
+ last;
+ }
+
+ 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 () {
- runcmd_ordryrun_local @git, qw(fetch),access_giturl(),fetchspec();
+ # 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_nomdistro);
+
+ git_for_each_ref([map { "refs/tags/$_" } @tagpats], sub {
+ my ($objid,$objtype,$fullrefname,$reftail) = @_;
+ printdebug "currently $fullrefname=$objid\n";
+ $here{$fullrefname} = $objid;
+ });
+ git_for_each_ref([map { lrfetchrefs."/tags/".$_ } @tagpats], sub {
+ my ($objid,$objtype,$fullrefname,$reftail) = @_;
+ my $lref = "refs".substr($fullrefname, length(lrfetchrefs));
+ printdebug "offered $lref=$objid\n";
+ if (!defined $here{$lref}) {
+ my @upd = (@git, qw(update-ref), $lref, $objid, '');
+ runcmd_ordryrun_local @upd;
+ lrfetchref_used $fullrefname;
+ } elsif ($here{$lref} eq $objid) {
+ lrfetchref_used $fullrefname;
+ } else {
+ 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) = @_;
+ $mi->{Clogp} = commit_getclogp($mi->{Commit});
+}
+
+sub mergeinfo_version ($) {
+ 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 () {
- # ensures that lrref() is what is actually in the archive,
- # one way or another
+ 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.
+ # If there is nothing in the archive, leaves lrref alone and
+ # returns undef. git_fetch_us must have already been called.
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";
}
- $lastpush_hash = git_get_ref(lrref());
+ # If the archive's .dsc has a Dgit field, there are three
+ # relevant git commitids we need to choose between and/or merge
+ # together:
+ # 1. $dsc_hash: the Dgit field from the archive
+ # 2. $lastpush_hash: the suite branch on the dgit git server
+ # 3. $lastfetch_hash: our local tracking brach for the suite
+ #
+ # These may all be distinct and need not be in any fast forward
+ # relationship:
+ #
+ # If the dsc was pushed to this suite, then the server suite
+ # branch will have been updated; but it might have been pushed to
+ # a different suite and copied by the archive. Conversely a more
+ # recent version may have been pushed with dgit but not appeared
+ # in the archive (yet).
+ #
+ # $lastfetch_hash may be awkward because archive imports
+ # (particularly, imports of Dgit-less .dscs) are performed only as
+ # needed on individual clients, so different clients may perform a
+ # different subset of them - and these imports are only made
+ # public during push. So $lastfetch_hash may represent a set of
+ # imports different to a subsequent upload by a different dgit
+ # client.
+ #
+ # Our approach is as follows:
+ #
+ # As between $dsc_hash and $lastpush_hash: if $lastpush_hash is a
+ # descendant of $dsc_hash, then it was pushed by a dgit user who
+ # had based their work on $dsc_hash, so we should prefer it.
+ # Otherwise, $dsc_hash was installed into this suite in the
+ # archive other than by a dgit push, and (necessarily) after the
+ # last dgit push into that suite (since a dgit push would have
+ # been descended from the dgit server git branch); thus, in that
+ # case, we prefer the archive's version (and produce a
+ # pseudo-merge to overwrite the dgit server git branch).
+ #
+ # (If there is no Dgit field in the archive's .dsc then
+ # generate_commit_from_dsc uses the version numbers to decide
+ # whether the suite branch or the archive is newer. If the suite
+ # branch is newer it ignores the archive's .dsc; otherwise it
+ # generates an import of the .dsc, and produces a pseudo-merge to
+ # overwrite the suite branch with the archive contents.)
+ #
+ # The outcome of that part of the algorithm is the `public view',
+ # and is same for all dgit clients: it does not depend on any
+ # unpublished history in the local tracking branch.
+ #
+ # As between the public view and the local tracking branch: The
+ # local tracking branch is only updated by dgit fetch, and
+ # whenever dgit fetch runs it includes the public view in the
+ # local tracking branch. Therefore if the public view is not
+ # descended from the local tracking branch, the local tracking
+ # branch must contain history which was imported from the archive
+ # but never pushed; and, its tip is now out of date. So, we make
+ # a pseudo-merge to overwrite the old imports and stitch the old
+ # history in.
+ #
+ # Finally: we do not necessarily reify the public view (as
+ # described above). This is so that we do not end up stacking two
+ # pseudo-merges. So what we actually do is figure out the inputs
+ # to any public view pseudo-merge and put them in @mergeinputs.
+
+ my @mergeinputs;
+ # $mergeinputs[]{Commit}
+ # $mergeinputs[]{Info}
+ # $mergeinputs[0] is the one whose tree we use
+ # @mergeinputs is in the order we use in the actual commit)
+ #
+ # Also:
+ # $mergeinputs[]{Message} is a commit message to use
+ # $mergeinputs[]{ReverseParents} if def specifies that parent
+ # list should be in opposite order
+ # Such an entry has no Commit or Info. It applies only when found
+ # in the last entry. (This ugliness is to support making
+ # identical imports to previous dgit versions.)
+
+ my $lastpush_hash = git_get_ref(lrfetchref());
printdebug "previous reference hash=$lastpush_hash\n";
- my $hash;
+ $lastpush_mergeinput = $lastpush_hash && {
+ Commit => $lastpush_hash,
+ Info => "dgit suite branch on dgit git server",
+ };
+
+ my $lastfetch_hash = git_get_ref(lrref());
+ printdebug "fetch_from_archive: lastfetch=$lastfetch_hash\n";
+ my $lastfetch_mergeinput = $lastfetch_hash && {
+ Commit => $lastfetch_hash,
+ Info => "dgit client's archive history view",
+ };
+
+ my $dsc_mergeinput = $dsc_hash && {
+ Commit => $dsc_hash,
+ Info => "Dgit field in .dsc from archive",
+ };
+
+ my $cwd = getcwd();
+ my $del_lrfetchrefs = sub {
+ changedir $cwd;
+ my $gur;
+ printdebug "del_lrfetchrefs...\n";
+ foreach my $fullrefname (sort keys %lrfetchrefs_d) {
+ my $objid = $lrfetchrefs_d{$fullrefname};
+ printdebug "del_lrfetchrefs: $objid $fullrefname\n";
+ if (!$gur) {
+ $gur ||= new IO::Handle;
+ open $gur, "|-", qw(git update-ref --stdin) or die $!;
+ }
+ printf $gur "delete %s %s\n", $fullrefname, $objid;
+ }
+ if ($gur) {
+ close $gur or failedcmd "git update-ref delete lrfetchrefs";
+ }
+ };
+
if (defined $dsc_hash) {
- fail "missing remote git history even though dsc has hash -".
- " could not find ref ".lrref().
- " (should have been fetched from ".access_giturl()."#".rrref().")"
- unless $lastpush_hash;
- $hash = $dsc_hash;
ensure_we_have_orig();
- if ($dsc_hash eq $lastpush_hash) {
+ if (!$lastpush_hash || $dsc_hash eq $lastpush_hash) {
+ @mergeinputs = $dsc_mergeinput
} elsif (is_fast_fwd($dsc_hash,$lastpush_hash)) {
print STDERR <<END or die $!;
Git commit in archive is behind the last version allegedly pushed/uploaded.
-Commit referred to by archive: $dsc_hash
-Last allegedly pushed/uploaded: $lastpush_hash
+Commit referred to by archive: $dsc_hash
+Last version pushed with dgit: $lastpush_hash
$later_warning_msg
END
- $hash = $lastpush_hash;
+ @mergeinputs = ($lastpush_mergeinput);
} else {
- fail "archive's .dsc refers to ".$dsc_hash.
- " but this is an ancestor of ".$lastpush_hash;
+ # Archive has .dsc which is not a descendant of the last dgit
+ # push. This can happen if the archive moves .dscs about.
+ # Just follow its lead.
+ if (is_fast_fwd($lastpush_hash,$dsc_hash)) {
+ progress "archive .dsc names newer git commit";
+ @mergeinputs = ($dsc_mergeinput);
+ } else {
+ progress "archive .dsc names other git commit, fixing up";
+ @mergeinputs = ($dsc_mergeinput, $lastpush_mergeinput);
+ }
}
} elsif ($dsc) {
- $hash = generate_commit_from_dsc();
+ @mergeinputs = generate_commits_from_dsc();
+ # We have just done an import. Now, our import algorithm might
+ # have been improved. But even so we do not want to generate
+ # a new different import of the same package. So if the
+ # version numbers are the same, just use our existing version.
+ # If the version numbers are different, the archive has changed
+ # (perhaps, rewound).
+ if ($lastfetch_mergeinput &&
+ !version_compare( (mergeinfo_version $lastfetch_mergeinput),
+ (mergeinfo_version $mergeinputs[0]) )) {
+ @mergeinputs = ($lastfetch_mergeinput);
+ }
} elsif ($lastpush_hash) {
# only in git, not in the archive yet
- $hash = $lastpush_hash;
+ @mergeinputs = ($lastpush_mergeinput);
print STDERR <<END or die $!;
Package not found in the archive, but has allegedly been pushed using dgit.
@@ -1261,24 +3130,131 @@ But we were not able to obtain any version from the archive or git.
END
}
- return 0;
+ unshift @end, $del_lrfetchrefs;
+ return undef;
}
- printdebug "current hash=$hash\n";
- if ($lastpush_hash) {
- fail "not fast forward on last upload branch!".
- " (archive's version left in DGIT_ARCHIVE)"
- unless is_fast_fwd($lastpush_hash, $hash);
+
+ if ($lastfetch_hash &&
+ !grep {
+ my $h = $_->{Commit};
+ $h and is_fast_fwd($lastfetch_hash, $h);
+ # If true, one of the existing parents of this commit
+ # is a descendant of the $lastfetch_hash, so we'll
+ # be ff from that automatically.
+ } @mergeinputs
+ ) {
+ # Otherwise:
+ push @mergeinputs, $lastfetch_mergeinput;
+ }
+
+ printdebug "fetch mergeinfos:\n";
+ foreach my $mi (@mergeinputs) {
+ if ($mi->{Info}) {
+ printdebug " commit $mi->{Commit} $mi->{Info}\n";
+ } else {
+ printdebug sprintf " ReverseParents=%d Message=%s",
+ $mi->{ReverseParents}, $mi->{Message};
+ }
}
+
+ my $compat_info= pop @mergeinputs
+ if $mergeinputs[$#mergeinputs]{Message};
+
+ @mergeinputs = grep { defined $_->{Commit} } @mergeinputs;
+
+ my $hash;
+ if (@mergeinputs > 1) {
+ # here we go, then:
+ my $tree_commit = $mergeinputs[0]{Commit};
+
+ my $tree = cmdoutput @git, qw(cat-file commit), $tree_commit;
+ $tree =~ m/\n\n/; $tree = $`;
+ $tree =~ m/^tree (\w+)$/m or die "$dsc_hash tree ?";
+ $tree = $1;
+
+ # We use the changelog author of the package in question the
+ # author of this pseudo-merge. This is (roughly) correct if
+ # this commit is simply representing aa non-dgit upload.
+ # (Roughly because it does not record sponsorship - but we
+ # don't have sponsorship info because that's in the .changes,
+ # which isn't in the archivw.)
+ #
+ # But, it might be that we are representing archive history
+ # updates (including in-archive copies). These are not really
+ # the responsibility of the person who created the .dsc, but
+ # there is no-one whose name we should better use. (The
+ # author of the .dsc-named commit is clearly worse.)
+
+ my $useclogp = mergeinfo_getclogp $mergeinputs[0];
+ my $author = clogp_authline $useclogp;
+ my $cversion = getfield $useclogp, 'Version';
+
+ my $mcf = ".git/dgit/mergecommit";
+ open MC, ">", $mcf or die "$mcf $!";
+ print MC <<END or die $!;
+tree $tree
+END
+
+ my @parents = grep { $_->{Commit} } @mergeinputs;
+ @parents = reverse @parents if $compat_info->{ReverseParents};
+ print MC <<END or die $! foreach @parents;
+parent $_->{Commit}
+END
+
+ print MC <<END or die $!;
+author $author
+committer $author
+
+END
+
+ if (defined $compat_info->{Message}) {
+ print MC $compat_info->{Message} or die $!;
+ } else {
+ print MC <<END or die $!;
+Record $package ($cversion) in archive suite $csuite
+
+Record that
+END
+ my $message_add_info = sub {
+ my ($mi) = (@_);
+ my $mversion = mergeinfo_version $mi;
+ printf MC " %-20s %s\n", $mversion, $mi->{Info}
+ or die $!;
+ };
+
+ $message_add_info->($mergeinputs[0]);
+ print MC <<END or die $!;
+should be treated as descended from
+END
+ $message_add_info->($_) foreach @mergeinputs[1..$#mergeinputs];
+ }
+
+ close MC or die $!;
+ $hash = make_commit $mcf;
+ } else {
+ $hash = $mergeinputs[0]{Commit};
+ }
+ printdebug "fetch hash=$hash\n";
+
+ my $chkff = sub {
+ my ($lasth, $what) = @_;
+ return unless $lasth;
+ die "$lasth $hash $what ?" unless is_fast_fwd($lasth, $hash);
+ };
+
+ $chkff->($lastpush_hash, 'dgit repo server tip (last push)')
+ if $lastpush_hash;
+ $chkff->($lastfetch_hash, 'local tracking tip (last fetch)');
+
+ fetch_from_archive_record_1($hash);
+
if (defined $skew_warning_vsn) {
mkpath '.git/dgit';
printdebug "SKEW CHECK WANT $skew_warning_vsn\n";
- my $clogf = ".git/dgit/changelog.tmp";
- runcmd shell_cmd "exec >$clogf",
- @git, qw(cat-file blob), "$hash:debian/changelog";
- my $gotclogp = parsechangelog("-l$clogf");
+ my $gotclogp = commit_getclogp($hash);
my $got_vsn = getfield $gotclogp, 'Version';
printdebug "SKEW CHECK GOT $got_vsn\n";
- if (version_compare_string($got_vsn, $skew_warning_vsn) < 0) {
+ if (version_compare($got_vsn, $skew_warning_vsn) < 0) {
print STDERR <<END or die $!;
Warning: archive skew detected. Using the available version:
@@ -1288,30 +3264,364 @@ We were able to obtain only $got_vsn
END
}
}
- if ($lastpush_hash ne $hash) {
- my @upd_cmd = (@git, qw(update-ref -m), 'dgit fetch', lrref(), $hash);
- if (act_local()) {
- cmdoutput @upd_cmd;
- } else {
- dryrun_report @upd_cmd;
+
+ if ($lastfetch_hash ne $hash) {
+ fetch_from_archive_record_2($hash);
+ }
+
+ lrfetchref_used lrfetchref();
+
+ check_gitattrs($hash, "fetched source tree");
+
+ unshift @end, $del_lrfetchrefs;
+ return $hash;
+}
+
+sub set_local_git_config ($$) {
+ my ($k, $v) = @_;
+ runcmd @git, qw(config), $k, $v;
+}
+
+sub setup_mergechangelogs (;$) {
+ my ($always) = @_;
+ return unless $always || access_cfg_bool(1, 'setup-mergechangelogs');
+
+ my $driver = 'dpkg-mergechangelogs';
+ my $cb = "merge.$driver";
+ my $attrs = '.git/info/attributes';
+ ensuredir '.git/info';
+
+ open NATTRS, ">", "$attrs.new" or die "$attrs.new $!";
+ if (!open ATTRS, "<", $attrs) {
+ $!==ENOENT or die "$attrs: $!";
+ } else {
+ while (<ATTRS>) {
+ chomp;
+ next if m{^debian/changelog\s};
+ print NATTRS $_, "\n" or die $!;
}
+ ATTRS->error and die $!;
+ close ATTRS;
+ }
+ print NATTRS "debian/changelog merge=$driver\n" or die $!;
+ close NATTRS;
+
+ set_local_git_config "$cb.name", 'debian/changelog merge driver';
+ set_local_git_config "$cb.driver", 'dpkg-mergechangelogs -m %O %A %B %A';
+
+ rename "$attrs.new", "$attrs" or die "$attrs: $!";
+}
+
+sub setup_useremail (;$) {
+ my ($always) = @_;
+ return unless $always || access_cfg_bool(1, 'setup-useremail');
+
+ my $setup = sub {
+ my ($k, $envvar) = @_;
+ my $v = access_cfg("user-$k", 'RETURN-UNDEF') // $ENV{$envvar};
+ return unless defined $v;
+ set_local_git_config "user.$k", $v;
+ };
+
+ $setup->('email', 'DEBEMAIL');
+ $setup->('name', 'DEBFULLNAME');
+}
+
+sub 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_gitattrs () {
+ my $gai = new IO::File ".git/info/attributes"
+ or $!==ENOENT
+ or die "open .git/info/attributes: $!";
+ return $gai;
+}
+
+sub is_gitattrs_setup () {
+ my $gai = open_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 = ".git/info/attributes";
+ open GAO, "> $af.new" or die $!;
+ print GAO <<END or die $!;
+* dgit-defuse-attrs
+[attr]dgit-defuse-attrs -text -eol -crlf -ident -filter
+# ^ see GITATTRIBUTES in dgit(7) and dgit setup-new-tree in dgit(1)
+END
+ my $gai = open_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]) {
+ 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 ($) {
my ($dstdir) = @_;
- canonicalise_suite();
badusage "dry run makes no sense with clone" unless act_local();
- mkdir $dstdir or die "$dstdir $!";
+
+ my $multi_fetched = fork_for_multisuite(sub {
+ printdebug "multi clone before fetch merge\n";
+ changedir $dstdir;
+ });
+ if ($multi_fetched) {
+ printdebug "multi clone after fetch merge\n";
+ clone_set_head();
+ clone_finish($dstdir);
+ exit 0;
+ }
+ 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);
- runcmd @git, qw(config), "remote.$remotename.fetch", fetchspec();
- open H, "> .git/HEAD" or die $!;
- print H "ref: ".lref()."\n" or die $!;
- close H or die $!;
- runcmd @git, qw(remote add), 'origin', access_giturl();
- if (check_for_git()) {
+ setup_new_tree();
+ clone_set_head();
+ my $giturl = access_giturl(1);
+ if (defined $giturl) {
+ runcmd @git, qw(remote add), 'origin', $giturl;
+ }
+ if ($hasgit) {
progress "fetching existing git history";
git_fetch_us();
runcmd_ordryrun_local @git, qw(fetch origin);
@@ -1321,13 +3631,14 @@ sub clone ($) {
fetch_from_archive() or no_such_package;
my $vcsgiturl = $dsc->{'Vcs-Git'};
if (length $vcsgiturl) {
+ $vcsgiturl =~ s/\s+-b\s+\S+//g;
runcmd @git, qw(remote add vcs-git), $vcsgiturl;
}
- 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();
}
@@ -1336,25 +3647,40 @@ sub fetch () {
}
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";
}
sub check_not_dirty () {
+ foreach my $f (qw(local-options local-patch-header)) {
+ if (stat_exists "debian/source/$f") {
+ fail "git tree contains debian/source/$f";
+ }
+ }
+
return if $ignoredirty;
+
my @cmd = (@git, qw(diff --quiet HEAD));
- printcmd(\*DEBUG,$debugprefix."+",@cmd) if $debug>0;
- $!=0; $?=0; system @cmd;
- return if !$! && !$?;
- if (!$! && $?==256) {
+ debugcmd "+",@cmd;
+ $!=0; $?=-1; system @cmd;
+ return if !$?;
+ if ($?==256) {
fail "working tree is dirty (does not match HEAD)";
} else {
failedcmd @cmd;
}
}
+sub commit_admin ($) {
+ my ($m) = @_;
+ progress "$m";
+ runcmd_ordryrun_local @git, qw(commit -m), $m;
+}
+
sub commit_quilty_patch () {
my $output = cmdoutput @git, qw(status --porcelain);
my %adds;
@@ -1364,41 +3690,297 @@ sub commit_quilty_patch () {
$adds{$1}++;
}
}
+ delete $adds{'.pc'}; # if there wasn't one before, don't add it
if (!%adds) {
progress "nothing quilty to commit, ok.";
return;
}
- runcmd_ordryrun_local @git, qw(add), sort keys %adds;
- my $m = "Commit Debian 3.0 (quilt) metadata";
- progress "$m";
- runcmd_ordryrun_local @git, qw(commit -m), $m;
+ my @adds = map { s/[][*?\\]/\\$&/g; $_; } sort keys %adds;
+ runcmd_ordryrun_local @git, qw(add -f), @adds;
+ commit_admin <<END
+Commit Debian 3.0 (quilt) metadata
+
+[dgit ($our_version) quilt-fixup]
+END
+}
+
+sub get_source_format () {
+ my %options;
+ if (open F, "debian/source/options") {
+ while (<F>) {
+ next if m/^\s*\#/;
+ next unless m/\S/;
+ s/\s+$//; # ignore missing final newline
+ if (m/\s*\#\s*/) {
+ my ($k, $v) = ($`, $'); #');
+ $v =~ s/^"(.*)"$/$1/;
+ $options{$k} = $v;
+ } else {
+ $options{$_} = 1;
+ }
+ }
+ F->error and die $!;
+ close F;
+ } else {
+ die $! unless $!==&ENOENT;
+ }
+
+ if (!open F, "debian/source/format") {
+ die $! unless $!==&ENOENT;
+ return '';
+ }
+ $_ = <F>;
+ F->error and die $!;
+ chomp;
+ return ($_, \%options);
}
-sub madformat ($) {
+sub madformat_wantfixup ($) {
my ($format) = @_;
return 0 unless $format eq '3.0 (quilt)';
- progress "Format \`$format', urgh";
- if ($noquilt) {
- progress "Not doing any fixup of \`$format' due to --no-quilt-fixup";
+ our $quilt_mode_warned;
+ if ($quilt_mode eq 'nocheck') {
+ progress "Not doing any fixup of \`$format' due to".
+ " ----no-quilt-fixup or --quilt=nocheck"
+ unless $quilt_mode_warned++;
return 0;
}
+ progress "Format \`$format', need to check/update patch stack"
+ unless $quilt_mode_warned++;
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 ../../../..",
+ @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)
+
+sub infopair_cond_equal ($$) {
+ my ($x,$y) = @_;
+ $x->[0] eq $y->[0] or fail <<END;
+$x->[1] ($x->[0]) not equal to $y->[1] ($y->[0])
+END
+};
+
+sub infopair_lrf_tag_lookup ($$) {
+ my ($tagnames, $what) = @_;
+ # $tagname may be an array ref
+ my @tagnames = ref $tagnames ? @$tagnames : ($tagnames);
+ printdebug "infopair_lrfetchref_tag_lookup $what @tagnames\n";
+ foreach my $tagname (@tagnames) {
+ my $lrefname = lrfetchrefs."/tags/$tagname";
+ my $tagobj = $lrfetchrefs_f{$lrefname};
+ next unless defined $tagobj;
+ printdebug "infopair_lrfetchref_tag_lookup $tagobj $tagname $what\n";
+ return [ git_rev_parse($tagobj), $what ];
+ }
+ fail @tagnames==1 ? <<END : <<END;
+Wanted tag $what (@tagnames) on dgit server, but not found
+END
+Wanted tag $what (one of: @tagnames) on dgit server, but not found
+END
+}
+
+sub infopair_cond_ff ($$) {
+ my ($anc,$desc) = @_;
+ is_fast_fwd($anc->[0], $desc->[0]) or fail <<END;
+$anc->[1] ($anc->[0]) .. $desc->[1] ($desc->[0]) is not fast forward
+END
+};
+
+sub pseudomerge_version_check ($$) {
+ my ($clogp, $archive_hash) = @_;
+
+ my $arch_clogp = commit_getclogp $archive_hash;
+ my $i_arch_v = [ (getfield $arch_clogp, 'Version'),
+ 'version currently in archive' ];
+ if (defined $overwrite_version) {
+ if (length $overwrite_version) {
+ infopair_cond_equal([ $overwrite_version,
+ '--overwrite= version' ],
+ $i_arch_v);
+ } 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 $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
+ }
+ }
+
+ printdebug "pseudomerge_version_check i_arch_v @$i_arch_v\n";
+ return $i_arch_v;
+}
+
+sub pseudomerge_make_commit ($$$$ $$) {
+ my ($clogp, $dgitview, $archive_hash, $i_arch_v,
+ $msg_cmd, $msg_msg) = @_;
+ progress "Declaring that HEAD inciudes all changes in $i_arch_v->[0]...";
+
+ my $tree = cmdoutput qw(git rev-parse), "${dgitview}:";
+ my $authline = clogp_authline $clogp;
+
+ chomp $msg_msg;
+ $msg_cmd .=
+ !defined $overwrite_version ? ""
+ : !length $overwrite_version ? " --overwrite"
+ : " --overwrite=".$overwrite_version;
+
+ mkpath '.git/dgit';
+ my $pmf = ".git/dgit/pseudomerge";
+ open MC, ">", $pmf or die "$pmf $!";
+ print MC <<END or die $!;
+tree $tree
+parent $dgitview
+parent $archive_hash
+author $authline
+committer $authline
+
+$msg_msg
+
+[$msg_cmd]
+END
+ close MC or die $!;
+
+ return make_commit($pmf);
+}
+
+sub splitbrain_pseudomerge ($$$$) {
+ my ($clogp, $maintview, $dgitview, $archive_hash) = @_;
+ # => $merged_dgitview
+ printdebug "splitbrain_pseudomerge...\n";
+ #
+ # We: debian/PREVIOUS HEAD($maintview)
+ # expect: o ----------------- o
+ # \ \
+ # o o
+ # a/d/PREVIOUS $dgitview
+ # $archive_hash \
+ # If so, \ \
+ # we do: `------------------ o
+ # 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);
+
+ if (!defined $overwrite_version) {
+ progress "Checking that HEAD inciudes all changes in archive...";
+ }
+
+ return $dgitview if is_fast_fwd $archive_hash, $dgitview;
+
+ 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 $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;
+}
+
+sub plain_overwrite_pseudomerge ($$$) {
+ my ($clogp, $head, $archive_hash) = @_;
+
+ printdebug "plain_overwrite_pseudomerge...";
+
+ my $i_arch_v = pseudomerge_version_check($clogp, $archive_hash);
+
+ return $head if is_fast_fwd $archive_hash, $head;
+
+ my $m = "Declare fast forward from $i_arch_v->[0]";
+
+ my $r = pseudomerge_make_commit
+ $clogp, $head, $archive_hash, $i_arch_v,
+ "dgit", $m;
+
+ runcmd @git, qw(update-ref -m), $m, 'HEAD', $r, $head;
+
+ progress "Make pseudo-merge of $i_arch_v->[0] into your HEAD.";
+ return $r;
+}
+
sub push_parse_changelog ($) {
my ($clogpfn) = @_;
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);
- 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);
- return ($clogp, $cversion, $tag, $dscfn);
+ return ($clogp, $cversion, $dscfn);
}
sub push_parse_dsc ($$$) {
@@ -1411,13 +3993,57 @@ sub push_parse_dsc ($$$) {
" but debian/changelog is for $package $cversion";
}
-sub push_mktag ($$$$$$$) {
- my ($head,$clogp,$tag,
- $dscfn,
+sub push_tagwants ($$$$) {
+ my ($cversion, $dgithead, $maintviewhead, $tfbase) = @_;
+ my @tagwants;
+ push @tagwants, {
+ TagFn => \&debiantag,
+ Objid => $dgithead,
+ TfSuffix => '',
+ View => 'dgit',
+ };
+ if (defined $maintviewhead) {
+ push @tagwants, {
+ TagFn => \&debiantag_maintview,
+ Objid => $maintviewhead,
+ TfSuffix => '-maintview',
+ View => 'maint',
+ };
+ } 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_nomdistro);
+ $tw->{Tfn} = sub { $tfbase.$tw->{TfSuffix}.$_[0]; };
+ }
+ printdebug 'push_tagwants: ', Dumper(\@_, \@tagwants);
+ return @tagwants;
+}
+
+sub push_mktags ($$ $$ $) {
+ my ($clogp,$dscfn,
$changesfile,$changesfilewhat,
- $tfn) = @_;
+ $tagwants) = @_;
+
+ die unless $tagwants->[0]{View} eq 'dgit';
- $dsc->{$ourdscfield[0]} = $head;
+ 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);
@@ -1433,35 +4059,67 @@ sub push_mktag ($$$$$$$) {
# We make the git tag by hand because (a) that makes it easier
# to control the "tagger" (b) we can do remote signing
my $authline = clogp_authline $clogp;
- open TO, '>', $tfn->('.tmp') or die $!;
- print TO <<END or die $!;
+ my $delibs = join(" ", "",@deliberatelies);
+
+ my $mktag = sub {
+ my ($tw) = @_;
+ my $tfn = $tw->{Tfn};
+ my $head = $tw->{Objid};
+ my $tag = $tw->{Tag};
+
+ open TO, '>', $tfn->('.tmp') or die $!;
+ print TO <<END or die $!;
object $head
type commit
tag $tag
tagger $authline
+END
+ if ($tw->{View} eq 'dgit') {
+ print TO <<END or die $!;
$package release $cversion for $clogsuite ($csuite) [dgit]
+[dgit distro=$declaredistro$delibs]
END
- close TO or die $!;
-
- my $tagobjfn = $tfn->('.tmp');
- if ($sign) {
- if (!defined $keyid) {
- $keyid = access_cfg('keyid','RETURN-UNDEF');
+ foreach my $ref (sort keys %previously) {
+ print TO <<END or die $!;
+[dgit previously:$ref=$previously{$ref}]
+END
+ }
+ } elsif ($tw->{View} eq 'maint') {
+ print TO <<END or die $!;
+$package release $cversion for $clogsuite ($csuite)
+(maintainer view tag generated by dgit --quilt=$quilt_mode)
+END
+ } else {
+ die Dumper($tw)."?";
}
- unlink $tfn->('.tmp.asc') or $!==&ENOENT or die $!;
- my @sign_cmd = (@gpg, qw(--detach-sign --armor));
- push @sign_cmd, qw(-u),$keyid if defined $keyid;
- push @sign_cmd, $tfn->('.tmp');
- runcmd_ordryrun @sign_cmd;
- if (act_scary()) {
- $tagobjfn = $tfn->('.signed.tmp');
- runcmd shell_cmd "exec >$tagobjfn", qw(cat --),
- $tfn->('.tmp'), $tfn->('.tmp.asc');
+
+ close TO or die $!;
+
+ my $tagobjfn = $tfn->('.tmp');
+ if ($sign) {
+ if (!defined $keyid) {
+ $keyid = access_cfg('keyid','RETURN-UNDEF');
+ }
+ if (!defined $keyid) {
+ $keyid = getfield $clogp, 'Maintainer';
+ }
+ unlink $tfn->('.tmp.asc') or $!==&ENOENT or die $!;
+ my @sign_cmd = (@gpg, qw(--detach-sign --armor));
+ push @sign_cmd, qw(-u),$keyid if defined $keyid;
+ push @sign_cmd, $tfn->('.tmp');
+ runcmd_ordryrun @sign_cmd;
+ if (act_scary()) {
+ $tagobjfn = $tfn->('.signed.tmp');
+ runcmd shell_cmd "exec >$tagobjfn", qw(cat --),
+ $tfn->('.tmp'), $tfn->('.tmp.asc');
+ }
}
- }
+ return $tagobjfn;
+ };
- return ($tagobjfn);
+ my @r = map { $mktag->($_); } @$tagwants;
+ return @r;
}
sub sign_changes ($) {
@@ -1477,21 +4135,46 @@ sub sign_changes ($) {
sub dopush () {
printdebug "actually entering push\n";
+
+ supplementary_message(<<'END');
+Push failed, while checking state of the archive.
+You can retry the push, after fixing the problem, if you like.
+END
+ if (check_for_git()) {
+ git_fetch_us();
+ }
+ my $archive_hash = fetch_from_archive();
+ if (!$archive_hash) {
+ $new_package or
+ fail "package appears to be new in this suite;".
+ " if this is intentional, use --new";
+ }
+
+ supplementary_message(<<'END');
+Push failed, while preparing your push.
+You can retry the push, after fixing the problem, if you like.
+END
+
+ need_tagformat 'new', "quilt mode $quilt_mode"
+ if quiltmode_splitbrain;
+
prep_ud();
access_giturl(); # check that success is vaguely likely
+ rpush_handle_protovsn_bothends() if $we_are_initiator;
+ select_tagformat();
my $clogpfn = ".git/dgit/changelog.822.tmp";
runcmd shell_cmd "exec >$clogpfn", qw(dpkg-parsechangelog);
responder_send_file('parsed-changelog', $clogpfn);
- my ($clogp, $cversion, $tag, $dscfn) =
+ my ($clogp, $cversion, $dscfn) =
push_parse_changelog("$clogpfn");
my $dscpath = "$buildproductsdir/$dscfn";
- stat $dscpath or
- fail "looked for .dsc $dscfn, but $!;".
+ stat_exists $dscpath or
+ fail "looked for .dsc $dscpath, but $!;".
" maybe you forgot to build";
responder_send_file('dsc', $dscpath);
@@ -1500,87 +4183,188 @@ sub dopush () {
my $format = getfield $dsc, 'Format';
printdebug "format $format\n";
- if (madformat($format)) {
- commit_quilty_patch();
+
+ my $actualhead = git_rev_parse('HEAD');
+ 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()) {
+ changedir $ud;
+ quilt_make_fake_dsc($upstreamversion);
+ my $cachekey;
+ ($dgithead, $cachekey) =
+ quilt_check_splitbrain_cache($actualhead, $upstreamversion);
+ $dgithead or fail
+ "--quilt=$quilt_mode but no cached dgit view:
+ perhaps tree changed since dgit build[-source] ?";
+ $split_brain = 1;
+ $dgithead = splitbrain_pseudomerge($clogp,
+ $actualhead, $dgithead,
+ $archive_hash);
+ $maintviewhead = $actualhead;
+ changedir '../../../..';
+ prep_ud(); # so _only_subdir() works, below
+ } else {
+ commit_quilty_patch();
+ }
+ }
+
+ if (defined $overwrite_version && !defined $maintviewhead) {
+ $dgithead = plain_overwrite_pseudomerge($clogp,
+ $dgithead,
+ $archive_hash);
}
+
check_not_dirty();
+
+ my $forceflag = '';
+ if ($archive_hash) {
+ if (is_fast_fwd($archive_hash, $dgithead)) {
+ # ok
+ } elsif (deliberately_not_fast_forward) {
+ $forceflag = '+';
+ } else {
+ fail "dgit push: HEAD is not a descendant".
+ " of the archive's version.\n".
+ "To overwrite the archive's contents,".
+ " pass --overwrite[=VERSION].\n".
+ "To rewind history, if permitted by the archive,".
+ " use --deliberately-not-fast-forward.";
+ }
+ }
+
changedir $ud;
progress "checking that $dscfn corresponds to HEAD";
runcmd qw(dpkg-source -x --),
$dscpath =~ m#^/# ? $dscpath : "../../../$dscpath";
- my ($tree,$dir) = mktree_in_ud_from_only_subdir();
+ my ($tree,$dir) = mktree_in_ud_from_only_subdir("source package");
+ check_for_vendor_patches() if madformat($dsc->{format});
changedir '../../../..';
- my $diffopt = $debug>0 ? '--exit-code' : '--quiet';
- my @diffcmd = (@git, qw(diff), $diffopt, $tree);
- printcmd \*DEBUG,$debugprefix."+",@diffcmd;
- $!=0; $?=0;
+ 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 $diffs = cmdoutput @git, qw(diff --stat), $tree, $dgithead;
+ 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 HEAD
+END
} else {
failedcmd @diffcmd;
}
}
-#fetch from alioth
-#do fast forward check and maybe fake merge
-# if (!is_fast_fwd(mainbranch
-# runcmd @git, qw(fetch -p ), "$alioth_git/$package.git",
-# map { lref($_).":".rref($_) }
-# (uploadbranch());
- my $head = rev_parse('HEAD');
if (!$changesfile) {
- my $multi = "$buildproductsdir/".
- "${package}_".(stripepoch $cversion)."_multi.changes";
- if (stat "$multi") {
- $changesfile = $multi;
- } else {
- $!==&ENOENT or die "$multi: $!";
- my $pat = "${package}_".(stripepoch $cversion)."_*.changes";
- my @cs = glob "$buildproductsdir/$pat";
- fail "failed to find unique changes file".
- " (looked for $pat in $buildproductsdir, or $multi);".
- " perhaps you need to use dgit -C"
- unless @cs==1;
- ($changesfile) = @cs;
- }
+ my $pat = changespat $cversion;
+ my @cs = glob "$buildproductsdir/$pat";
+ fail "failed to find unique changes file".
+ " (looked for $pat in $buildproductsdir);".
+ " perhaps you need to use dgit -C"
+ unless @cs==1;
+ ($changesfile) = @cs;
} else {
$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 $head");
+ 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");
+ }
- my $tfn = sub { ".git/dgit/tag$_[0]"; };
- my $tagobjfn;
+ if (deliberately_not_fast_forward) {
+ git_for_each_ref(lrfetchrefs, sub {
+ my ($objid,$objtype,$lrfetchrefname,$reftail) = @_;
+ my $rrefname= substr($lrfetchrefname, length(lrfetchrefs) + 1);
+ responder_send_command("previously $rrefname=$objid");
+ $previously{$rrefname} = $objid;
+ });
+ }
+
+ my @tagwants = push_tagwants($cversion, $dgithead, $maintviewhead,
+ ".git/dgit/tag");
+ my @tagobjfns;
+ supplementary_message(<<'END');
+Push failed, while signing the tag.
+You can retry the push, after fixing the problem, if you like.
+END
+ # If we manage to sign but fail to record it anywhere, it's fine.
if ($we_are_responder) {
- $tagobjfn = $tfn->('.signed.tmp');
- responder_receive_files('signed-tag', $tagobjfn);
+ @tagobjfns = map { $_->{Tfn}('.signed-tmp') } @tagwants;
+ responder_receive_files('signed-tag', @tagobjfns);
} else {
- $tagobjfn =
- push_mktag($head,$clogp,$tag,
- $dscpath,
- $changesfile,$changesfile,
- $tfn);
+ @tagobjfns = push_mktags($clogp,$dscpath,
+ $changesfile,$changesfile,
+ \@tagwants);
}
+ supplementary_message(<<'END');
+Push failed, *after* signing the tag.
+If you want to try again, you should use a new version number.
+END
+
+ pairwise { $a->{TagObjFn} = $b } @tagwants, @tagobjfns;
- my $tag_obj_hash = cmdoutput @git, qw(hash-object -w -t tag), $tagobjfn;
- runcmd_ordryrun @git, qw(verify-tag), $tag_obj_hash;
- runcmd_ordryrun_local @git, qw(update-ref), "refs/tags/$tag", $tag_obj_hash;
- runcmd_ordryrun @git, qw(tag -v --), $tag;
+ foreach my $tw (@tagwants) {
+ my $tag = $tw->{Tag};
+ my $tagobjfn = $tw->{TagObjFn};
+ my $tag_obj_hash =
+ cmdoutput @git, qw(hash-object -w -t tag), $tagobjfn;
+ runcmd_ordryrun @git, qw(verify-tag), $tag_obj_hash;
+ runcmd_ordryrun_local
+ @git, qw(update-ref), "refs/tags/$tag", $tag_obj_hash;
+ }
+ supplementary_message(<<'END');
+Push failed, while updating the remote git repository - see messages above.
+If you want to try again, you should use a new version number.
+END
if (!check_for_git()) {
create_remote_git_repo();
}
- runcmd_ordryrun @git, qw(push),access_giturl(),
- "HEAD:".rrref(), "refs/tags/$tag";
- runcmd_ordryrun @git, qw(update-ref -m), 'dgit push', lrref(), 'HEAD';
+ my @pushrefs = $forceflag.$dgithead.":".rrref();
+ foreach my $tw (@tagwants) {
+ push @pushrefs, $forceflag."refs/tags/$tw->{Tag}";
+ }
+
+ 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, 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";
responder_receive_files('signed-dsc-changes',
@@ -1595,11 +4379,19 @@ sub dopush () {
sign_changes $changesfile;
}
+ supplementary_message(<<END);
+Push failed, while uploading package(s) to the archive server.
+You can retry the upload of exactly these same files with dput of:
+ $changesfile
+If that .changes file is broken, you will need to use a new version
+number for your next attempt at the upload.
+END
my $host = access_cfg('upload-host','RETURN-UNDEF');
my @hostarg = defined($host) ? ($host,) : ();
runcmd_ordryrun @dput, @hostarg, $changesfile;
printdone "pushed and uploaded $cversion";
+ supplementary_message('');
responder_send_command("complete");
}
@@ -1619,12 +4411,11 @@ sub cmd_clone {
} else {
badusage "incorrect arguments to dgit clone";
}
- $dstdir ||= "$package";
+ notpushing();
- if (stat $dstdir) {
+ $dstdir ||= "$package";
+ if (stat_exists $dstdir) {
fail "$dstdir already exists";
- } elsif ($! != &ENOENT) {
- die "$dstdir: $!";
}
my $cwd_remove;
@@ -1636,7 +4427,14 @@ sub cmd_clone {
return if $!==&ENOENT;
die "chdir $cwd_remove: $!";
}
- rmtree($dstdir) or die "remove $dstdir: $!\n";
+ printdebug "clone rmonerror removing $dstdir\n";
+ if (stat $dstdir) {
+ rmtree($dstdir) or die "remove $dstdir: $!\n";
+ } elsif (grep { $! == $_ }
+ (ENOENT, ENOTDIR, EACCES, EPERM, ELOOP)) {
+ } else {
+ print STDERR "check whether to remove $dstdir: $!\n";
+ }
};
}
@@ -1645,7 +4443,12 @@ sub cmd_clone {
}
sub branchsuite () {
- my $branch = cmdoutput_errok @git, qw(symbolic-ref HEAD);
+ my @cmd = (@git, qw(symbolic-ref -q HEAD));
+ my $branch = cmdoutput_errok @cmd;
+ if (!defined $branch) {
+ $?==256 or failedcmd @cmd;
+ return undef;
+ }
if ($branch =~ m#$lbranch_re#o) {
return $1;
} else {
@@ -1659,30 +4462,37 @@ sub fetchpullargs () {
$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();
}
@@ -1700,29 +4510,19 @@ sub cmd_push {
badusage "incorrect arguments to dgit push";
}
$isuite = getfield $clogp, 'Distribution';
+ pushing();
if ($new_package) {
local ($package) = $existing_package; # this is a hack
canonicalise_suite();
- }
- if (defined $specsuite && $specsuite ne $isuite) {
+ } else {
canonicalise_suite();
- $csuite eq $specsuite or
+ }
+ if (defined $specsuite &&
+ $specsuite ne $isuite &&
+ $specsuite ne $csuite) {
fail "dgit push: changelog specifies $isuite ($csuite)".
" but command line specifies $specsuite";
}
- if (check_for_git()) {
- git_fetch_us();
- }
- if (fetch_from_archive()) {
- is_fast_fwd(lrref(), 'HEAD') or
- fail "dgit push: HEAD is not a descendant".
- " of the archive's version.\n".
- "$us: To overwrite it, use git merge -s ours ".lrref().".";
- } else {
- $new_package or
- fail "package appears to be new in this suite;".
- " if this is intentional, use --new";
- }
dopush();
}
@@ -1739,6 +4539,7 @@ sub cmd_remote_push_build_host {
# offered several)
$debugprefix = ' ';
$we_are_responder = 1;
+ $us .= " (build host)";
open PI, "<&STDIN" or die $!;
open STDIN, "/dev/null" or die $!;
@@ -1748,12 +4549,16 @@ sub cmd_remote_push_build_host {
autoflush STDOUT 1;
$vsnwant //= 1;
- fail "build host has dgit rpush protocol version".
- " $rpushprotovsn but invocation host has $vsnwant"
- unless grep { $rpushprotovsn eq $_ } split /,/, $vsnwant;
+ ($protovsn) = grep {
+ $vsnwant =~ m{^(?:.*,)?$_(?:,.*)?$}
+ } @rpushprotovsn_support;
- responder_send_command("dgit-remote-push-ready $rpushprotovsn");
+ fail "build host has dgit rpush protocol versions ".
+ (join ",", @rpushprotovsn_support).
+ " but invocation host has $vsnwant"
+ unless defined $protovsn;
+ responder_send_command("dgit-remote-push-ready $protovsn");
changedir $dir;
&cmd_push;
}
@@ -1762,6 +4567,13 @@ 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)
+sub rpush_handle_protovsn_bothends () {
+ if ($protovsn < 4) {
+ need_tagformat 'old', "rpush negotiated protocol $protovsn";
+ }
+ select_tagformat();
+}
+
our $i_tmp;
sub i_cleanup {
@@ -1779,7 +4591,10 @@ sub i_cleanup {
}
}
-END { i_cleanup(); }
+END {
+ return unless forkcheck_mainprocess();
+ i_cleanup();
+}
sub i_method {
my ($base,$selector,@args) = @_;
@@ -1797,14 +4612,17 @@ sub cmd_rpush {
$dir = nextarg;
}
$dir =~ s{^-}{./-};
- my @rargs = ($dir,$rpushprotovsn);
+ my @rargs = ($dir);
+ push @rargs, join ",", @rpushprotovsn_support;
my @rdgit;
push @rdgit, @dgit;
push @rdgit, @ropts;
push @rdgit, qw(remote-push-build-host), (scalar @rargs), @rargs;
push @rdgit, @ARGV;
my @cmd = (@ssh, $host, shellquote @rdgit);
- printcmd \*DEBUG,$debugprefix."+",@cmd;
+ debugcmd "+",@cmd;
+
+ $we_are_initiator=1;
if (defined $initiator_tempdir) {
rmtree $initiator_tempdir;
@@ -1815,7 +4633,10 @@ sub cmd_rpush {
}
$i_child_pid = open2(\*RO, \*RI, @cmd);
changedir $i_tmp;
- initiator_expect { m/^dgit-remote-push-ready/ };
+ ($protovsn) = initiator_expect { m/^dgit-remote-push-ready (\S+)/ };
+ die "$protovsn ?" unless grep { $_ eq $protovsn } @rpushprotovsn_support;
+ $supplementary_message = '' unless $protovsn >= 3;
+
for (;;) {
my ($icmd,$iargs) = initiator_expect {
m/^(\S+)(?: (.*))?$/;
@@ -1831,6 +4652,11 @@ sub i_resp_progress ($) {
progress $msg;
}
+sub i_resp_supplementary_message ($) {
+ my ($rhs) = @_;
+ $supplementary_message = protocol_read_bytes \*RO, $rhs;
+}
+
sub i_resp_complete {
my $pid = $i_child_pid;
$i_child_pid = undef; # prevents killing some other process with same pid
@@ -1848,7 +4674,8 @@ sub i_resp_file ($) {
my ($keyword) = @_;
my $localname = i_method "i_localname", $keyword;
my $localpath = "$i_tmp/$localname";
- stat $localpath and badproto \*RO, "file $keyword ($localpath) twice";
+ stat_exists $localpath and
+ badproto \*RO, "file $keyword ($localpath) twice";
protocol_receive_file \*RO, $localpath;
i_method "i_file", $keyword;
}
@@ -1860,11 +4687,31 @@ sub i_resp_param ($) {
$i_param{$1} = $2;
}
+sub i_resp_previously ($) {
+ $_[0] =~ m#^(refs/tags/\S+)=(\w+)$#
+ or badproto \*RO, "bad previously spec";
+ my $r = system qw(git check-ref-format), $1;
+ die "bad previously ref spec ($r)" if $r;
+ $previously{$1} = $2;
+}
+
our %i_wanted;
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) {
@@ -1873,13 +4720,13 @@ sub i_resp_want ($) {
print RI "files-end\n" or die $!;
}
-our ($i_clogp, $i_version, $i_tag, $i_dscfn, $i_changesfn);
+our ($i_clogp, $i_version, $i_dscfn, $i_changesfn);
sub i_localname_parsed_changelog {
return "remote-changelog.822";
}
sub i_file_parsed_changelog {
- ($i_clogp, $i_version, $i_tag, $i_dscfn) =
+ ($i_clogp, $i_version, $i_dscfn) =
push_parse_changelog "$i_tmp/remote-changelog.822";
die if $i_dscfn =~ m#/|^\W#;
}
@@ -1906,17 +4753,26 @@ sub i_want_signed_tag {
my $head = $i_param{'head'};
die if $head =~ m/[^0-9a-f]/ || $head !~ m/^../;
+ my $maintview = $i_param{'maint-view'};
+ die if defined $maintview && $maintview =~ m/[^0-9a-f]/;
+
+ select_tagformat();
+ if ($protovsn >= 4) {
+ my $p = $i_param{'tagformat'} // '<undef>';
+ $p eq $tagformat
+ or badproto \*RO, "tag format mismatch: $p vs. $tagformat";
+ }
+
die unless $i_param{'csuite'} =~ m/^$suite_re$/;
$csuite = $&;
push_parse_dsc $i_dscfn, 'remote dsc', $i_version;
- my $tagobjfn =
- push_mktag $head, $i_clogp, $i_tag,
- $i_dscfn,
- $i_changesfn, 'remote changes',
- sub { "tag$_[0]"; };
+ my @tagwants = push_tagwants $i_version, $head, $maintview, "tag";
- return $tagobjfn;
+ return
+ push_mktags $i_clogp, $i_dscfn,
+ $i_changesfn, 'remote changes',
+ \@tagwants;
}
sub i_want_signed_dsc_changes {
@@ -1931,59 +4787,905 @@ our $version;
our $sourcechanges;
our $dscfn;
+#----- `3.0 (quilt)' handling -----
+
our $fakeeditorenv = 'DGIT_FAKE_EDITOR_QUILT';
-sub build_maybe_quilt_fixup () {
- if (!open F, "debian/source/format") {
- die $! unless $!==&ENOENT;
- return;
- }
- $_ = <F>;
- F->error and die $!;
- chomp;
- return unless madformat($_);
- # sigh
-
- my @cmd = (@git, qw(ls-files --exclude-standard -iodm));
- my $problems = cmdoutput @cmd;
- if (length $problems) {
- print STDERR "problematic files:\n";
- print STDERR " $_\n" foreach split /\n/, $problems;
- fail "Cannot do quilt fixup in tree containing ignored files. ".
- "Perhaps your package's clean target is broken, in which".
- " case -wg (which says to use git-clean -xdf) may help.";
- }
+sub quiltify_dpkg_commit ($$$;$) {
+ my ($patchname,$author,$msg, $xinfo) = @_;
+ $xinfo //= '';
- my $clogp = parsechangelog();
- my $version = getfield $clogp, 'Version';
- my $author = getfield $clogp, 'Maintainer';
- my $headref = rev_parse('HEAD');
- my $time = time;
- my $ncommits = 3;
- my $patchname = "auto-$version-$headref-$time";
- my $msg = cmdoutput @git, qw(log), "-n$ncommits";
mkpath '.git/dgit';
my $descfn = ".git/dgit/quilt-description.tmp";
open O, '>', $descfn or die "$descfn: $!";
- $msg =~ s/\n/\n /g;
- $msg =~ s/^\s+$/ ./mg;
+ $msg =~ s/\n+/\n\n/;
print O <<END or die $!;
-Description: Automatically generated patch ($clogp->{Version})
- Last (up to) $ncommits git changes, FYI:
- .
- $msg
-Author: $author
-
+From: $author
+${xinfo}Subject: $msg
---
END
close O or die $!;
+
{
local $ENV{'EDITOR'} = cmdoutput qw(realpath --), $0;
local $ENV{'VISUAL'} = $ENV{'EDITOR'};
local $ENV{$fakeeditorenv} = cmdoutput qw(realpath --), $descfn;
- runcmd_ordryrun_local @dpkgsource, qw(--commit .), $patchname;
+ runcmd @dpkgsource, qw(--commit --include-removal .), $patchname;
+ }
+}
+
+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 -z));
+ 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\n"
+ unless $newmode =~ m/^10\d{4}$/ ||
+ $oldmode =~ m/^10\d{4}$/;
+ if ($oldmode =~ m/[^0]/ &&
+ $newmode =~ m/[^0]/) {
+ die "mode changed\n" if $oldmode ne $newmode;
+ } else {
+ die "non-default mode\n"
+ unless $newmode =~ m/^100644$/ ||
+ $oldmode =~ m/^100644$/;
+ }
+ };
+ 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;
+ }
+ printdebug "quiltify_trees_differ $x $y => $r\n";
+ return $r;
+}
+
+sub quiltify_tree_sentinelfiles ($) {
+ # lists the `sentinel' files present in the tree
+ my ($x) = @_;
+ my $r = cmdoutput @git, qw(ls-tree --name-only), $x,
+ qw(-- debian/rules debian/control);
+ $r =~ s/\n/,/g;
+ return $r;
+}
+
+sub quiltify_splitbrain_needed () {
+ if (!$split_brain) {
+ progress "dgit view: changes are required...";
+ runcmd @git, qw(checkout -q -b dgit-view);
+ $split_brain = 1;
+ }
+}
+
+sub quiltify_splitbrain ($$$$$$) {
+ my ($clogp, $unapplied, $headref, $diffbits,
+ $editedignores, $cachekey) = @_;
+ if ($quilt_mode !~ m/gbp|dpm/) {
+ # treat .gitignore just like any other upstream file
+ $diffbits = { %$diffbits };
+ $_ = !!$_ foreach values %$diffbits;
+ }
+ # We would like any commits we generate 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];
+
+ if ($quilt_mode =~ m/gbp|unapplied/ &&
+ ($diffbits->{O2H} & 01)) {
+ my $msg =
+ "--quilt=$quilt_mode specified, implying patches-unapplied git tree\n".
+ " but git tree differs from orig in upstream files.";
+ if (!stat_exists "debian/patches") {
+ $msg .=
+ "\n ... debian/patches is missing; perhaps this is a patch queue branch?";
+ }
+ fail $msg;
+ }
+ if ($quilt_mode =~ m/dpm/ &&
+ ($diffbits->{H2A} & 01)) {
+ fail <<END;
+--quilt=$quilt_mode specified, implying patches-applied git tree
+ but git tree differs from result of applying debian/patches to upstream
+END
+ }
+ if ($quilt_mode =~ m/gbp|unapplied/ &&
+ ($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_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);
+ }
+ if ($quilt_mode =~ m/gbp|dpm/ &&
+ ($diffbits->{O2A} & 02)) {
+ fail <<END
+--quilt=$quilt_mode specified, implying that HEAD is for use with a
+ tool which does not create patches for changes to upstream
+ .gitignores: but, such patches exist in debian/patches.
+END
+ }
+ 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";
+ ensuredir "debian/patches";
+ my $gipatch = "debian/patches/auto-gitignore";
+ open GIPATCH, ">>", "$gipatch" or die "$gipatch: $!";
+ stat GIPATCH or die "$gipatch: $!";
+ fail "$gipatch already exists; but want to create it".
+ " to record .gitignore changes" if (stat _)[7];
+ print GIPATCH <<END or die "$gipatch: $!";
+Subject: Update .gitignore from Debian packaging branch
+
+The Debian packaging git branch contains these updates to the upstream
+.gitignore file(s). This patch is autogenerated, to provide these
+updates to users of the official Debian archive view of the package.
+
+[dgit ($our_version) update-gitignore]
+---
+END
+ close GIPATCH or die "$gipatch: $!";
+ runcmd shell_cmd "exec >>$gipatch", @git, qw(diff),
+ $unapplied, $headref, "--", sort keys %$editedignores;
+ open SERIES, "+>>", "debian/patches/series" or die $!;
+ defined seek SERIES, -1, 2 or $!==EINVAL or die $!;
+ my $newline;
+ defined read SERIES, $newline, 1 or die $!;
+ print SERIES "\n" or die $! unless $newline eq "\n";
+ print SERIES "auto-gitignore\n" or die $!;
+ close SERIES or die $!;
+ runcmd @git, qw(add -- debian/patches/series), $gipatch;
+ commit_admin <<END
+Commit patch to update .gitignore
+
+[dgit ($our_version) update-gitignore-quilt-fixup]
+END
+ }
+
+ my $dgitview = git_rev_parse 'HEAD';
+
+ changedir '../../../..';
+ # When we no longer need to support squeeze, use --create-reflog
+ # instead of this:
+ ensuredir ".git/logs/refs/dgit-intern";
+ my $makelogfh = new IO::File ".git/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;
+
+ changedir '.git/dgit/unpack/work';
+
+ my $saved = maybe_split_brain_save $headref, $dgitview, "converted";
+ progress "dgit view: created ($saved)";
+}
+
+sub quiltify ($$$$) {
+ my ($clogp,$target,$oldtiptree,$failsuggestion) = @_;
+
+ # Quilt patchification algorithm
+ #
+ # We search backwards through the history of the main tree's HEAD
+ # (T) looking for a start commit S whose tree object is identical
+ # to to the patch tip tree (ie the tree corresponding to the
+ # current dpkg-committed patch series). For these purposes
+ # `identical' disregards anything in debian/ - this wrinkle is
+ # necessary because dpkg-source treates debian/ specially.
+ #
+ # We can only traverse edges where at most one of the ancestors'
+ # trees differs (in changes outside in debian/). And we cannot
+ # handle edges which change .pc/ or debian/patches. To avoid
+ # going down a rathole we avoid traversing edges which introduce
+ # debian/rules or debian/control. And we set a limit on the
+ # number of edges we are willing to look at.
+ #
+ # If we succeed, we walk forwards again. For each traversed edge
+ # PC (with P parent, C child) (starting with P=S and ending with
+ # C=T) to we do this:
+ # - git checkout C
+ # - dpkg-source --commit with a patch name and message derived from C
+ # After traversing PT, we git commit the changes which
+ # should be contained within debian/patches.
+
+ # The search for the path S..T is breadth-first. We maintain a
+ # todo list containing search nodes. A search node identifies a
+ # commit, and looks something like this:
+ # $p = {
+ # Commit => $git_commit_id,
+ # Child => $c, # or undef if P=T
+ # Whynot => $reason_edge_PC_unsuitable, # in @nots only
+ # Nontrivial => true iff $p..$c has relevant changes
+ # };
+
+ my @todo;
+ my @nots;
+ my $sref_S;
+ my $max_work=100;
+ my %considered; # saves being exponential on some weird graphs
+
+ my $t_sentinels = quiltify_tree_sentinelfiles $target;
+
+ my $not = sub {
+ my ($search,$whynot) = @_;
+ printdebug " search NOT $search->{Commit} $whynot\n";
+ $search->{Whynot} = $whynot;
+ push @nots, $search;
+ no warnings qw(exiting);
+ next;
+ };
+
+ push @todo, {
+ Commit => $target,
+ };
+
+ while (@todo) {
+ my $c = shift @todo;
+ next if $considered{$c->{Commit}}++;
+
+ $not->($c, "maximum search space exceeded") if --$max_work <= 0;
+
+ printdebug "quiltify investigate $c->{Commit}\n";
+
+ # are we done?
+ if (!quiltify_trees_differ $c->{Commit}, $oldtiptree) {
+ printdebug " search finished hooray!\n";
+ $sref_S = $c;
+ last;
+ }
+
+ if ($quilt_mode eq 'nofix') {
+ fail "quilt fixup required but quilt mode is \`nofix'\n".
+ "HEAD commit $c->{Commit} differs from tree implied by ".
+ " debian/patches (tree object $oldtiptree)";
+ }
+ if ($quilt_mode eq 'smash') {
+ printdebug " search quitting smash\n";
+ last;
+ }
+
+ my $c_sentinels = quiltify_tree_sentinelfiles $c->{Commit};
+ $not->($c, "has $c_sentinels not $t_sentinels")
+ if $c_sentinels ne $t_sentinels;
+
+ my $commitdata = cmdoutput @git, qw(cat-file commit), $c->{Commit};
+ $commitdata =~ m/\n\n/;
+ $commitdata =~ $`;
+ my @parents = ($commitdata =~ m/^parent (\w+)$/gm);
+ @parents = map { { Commit => $_, Child => $c } } @parents;
+
+ $not->($c, "root commit") if !@parents;
+
+ foreach my $p (@parents) {
+ $p->{Nontrivial}= quiltify_trees_differ $p->{Commit},$c->{Commit};
+ }
+ my $ndiffers = grep { $_->{Nontrivial} } @parents;
+ $not->($c, "merge ($ndiffers nontrivial parents)") if $ndiffers > 1;
+
+ foreach my $p (@parents) {
+ printdebug "considering C=$c->{Commit} P=$p->{Commit}\n";
+
+ my @cmd= (@git, qw(diff-tree -r --name-only),
+ $p->{Commit},$c->{Commit}, qw(-- debian/patches .pc));
+ my $patchstackchange = cmdoutput @cmd;
+ if (length $patchstackchange) {
+ $patchstackchange =~ s/\n/,/g;
+ $not->($p, "changed $patchstackchange");
+ }
+
+ printdebug " search queue P=$p->{Commit} ",
+ ($p->{Nontrivial} ? "NT" : "triv"),"\n";
+ push @todo, $p;
+ }
+ }
+
+ if (!$sref_S) {
+ printdebug "quiltify want to smash\n";
+
+ my $abbrev = sub {
+ my $x = $_[0]{Commit};
+ $x =~ s/(.*?[0-9a-z]{8})[0-9a-z]*$/$1/;
+ return $x;
+ };
+ my $reportnot = sub {
+ my ($notp) = @_;
+ my $s = $abbrev->($notp);
+ my $c = $notp->{Child};
+ $s .= "..".$abbrev->($c) if $c;
+ $s .= ": ".$notp->{Whynot};
+ return $s;
+ };
+ if ($quilt_mode eq 'linear') {
+ print STDERR "$us: quilt fixup cannot be linear. Stopped at:\n";
+ foreach my $notp (@nots) {
+ print STDERR "$us: ", $reportnot->($notp), "\n";
+ }
+ print STDERR "$us: $_\n" foreach @$failsuggestion;
+ fail "quilt fixup naive history linearisation failed.\n".
+ "Use dpkg-source --commit by hand; or, --quilt=smash for one ugly patch";
+ } elsif ($quilt_mode eq 'smash') {
+ } elsif ($quilt_mode eq 'auto') {
+ progress "quilt fixup cannot be linear, smashing...";
+ } else {
+ die "$quilt_mode ?";
+ }
+
+ my $time = $ENV{'GIT_COMMITTER_DATE'} || time;
+ $time =~ s/\s.*//; # trim timezone from GIT_COMMITTER_DATE
+ my $ncommits = 3;
+ my $msg = cmdoutput @git, qw(log), "-n$ncommits";
+
+ quiltify_dpkg_commit "auto-$version-$target-$time",
+ (getfield $clogp, 'Maintainer'),
+ "Automatically generated patch ($clogp->{Version})\n".
+ "Last (up to) $ncommits git changes, FYI:\n\n". $msg;
+ return;
+ }
+
+ progress "quiltify linearisation planning successful, executing...";
+
+ for (my $p = $sref_S;
+ my $c = $p->{Child};
+ $p = $p->{Child}) {
+ printdebug "quiltify traverse $p->{Commit}..$c->{Commit}\n";
+ next unless $p->{Nontrivial};
+
+ my $cc = $c->{Commit};
+
+ my $commitdata = cmdoutput @git, qw(cat-file commit), $cc;
+ $commitdata =~ m/\n\n/ or die "$c ?";
+ $commitdata = $`;
+ my $msg = $'; #';
+ $commitdata =~ m/^author (.*) \d+ [-+0-9]+$/m or die "$cc ?";
+ my $author = $1;
+
+ 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;
+ 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 "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);
+ }
+ 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";
+ $index++) { }
+ $!==ENOENT or die "$patchname$index $!";
+
+ runcmd @git, qw(checkout -q), $cc;
+
+ # We use the tip's changelog so that dpkg-source doesn't
+ # produce complaining messages from dpkg-parsechangelog. None
+ # of the information dpkg-source gets from the changelog is
+ # actually relevant - it gets put into the original message
+ # which dpkg-source provides our stunt editor, and then
+ # overwritten.
+ runcmd @git, qw(checkout -q), $target, qw(debian/changelog);
+
+ quiltify_dpkg_commit "$patchname$index", $author, $msg,
+ "Date: $commitdate\n".
+ "X-Dgit-Generated: $clogp->{Version} $cc\n";
+
+ runcmd @git, qw(checkout -q), $cc, qw(debian/changelog);
+ }
+
+ runcmd @git, qw(checkout -q master);
+}
+
+sub build_maybe_quilt_fixup () {
+ my ($format,$fopts) = get_source_format;
+ return unless madformat_wantfixup $format;
+ # sigh
+
+ check_for_vendor_patches();
+
+ if (quiltmode_splitbrain) {
+ 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;
+
+ my $upstreamversion = upstreamversion $version;
+
+ if ($fopts->{'single-debian-patch'}) {
+ quilt_fixup_singlepatch($clogp, $headref, $upstreamversion);
+ } else {
+ quilt_fixup_multipatch($clogp, $headref, $upstreamversion);
+ }
+
+ die 'bug' if $split_brain && !$need_split_build_invocation;
+
+ changedir '../../../..';
+ runcmd_ordryrun_local
+ @git, qw(pull --ff-only -q .git/dgit/unpack/work master);
+}
+
+sub quilt_fixup_mkwork ($) {
+ my ($headref) = @_;
+
+ mkdir "work" or die $!;
+ changedir "work";
+ mktree_in_ud_here();
+ runcmd @git, qw(reset -q --hard), $headref;
+}
+
+sub quilt_fixup_linkorigs ($$) {
+ my ($upstreamversion, $fn) = @_;
+ # calls $fn->($leafname);
+
+ foreach my $f (<../../../../*>) { #/){
+ my $b=$f; $b =~ s{.*/}{};
+ {
+ local ($debuglevel) = $debuglevel-1;
+ printdebug "QF linkorigs $b, $f ?\n";
+ }
+ next unless is_orig_file_of_vsn $b, $upstreamversion;
+ printdebug "QF linkorigs $b, $f Y\n";
+ link_ltarget $f, $b or die "$b $!";
+ $fn->($b);
}
+}
+
+sub quilt_fixup_delete_pc () {
+ runcmd @git, qw(rm -rqf .pc);
+ commit_admin <<END
+Commit removal of .pc (quilt series tracking data)
+
+[dgit ($our_version) upgrade quilt-remove-pc]
+END
+}
+
+sub quilt_fixup_singlepatch ($$$) {
+ my ($clogp, $headref, $upstreamversion) = @_;
+
+ progress "starting quiltify (single-debian-patch)";
+
+ # dpkg-source --commit generates new patches even if
+ # single-debian-patch is in debian/source/options. In order to
+ # get it to generate debian/patches/debian-changes, it is
+ # necessary to build the source package.
+
+ quilt_fixup_linkorigs($upstreamversion, sub { });
+ quilt_fixup_mkwork($headref);
+
+ rmtree("debian/patches");
+
+ runcmd @dpkgsource, qw(-b .);
+ changedir "..";
+ runcmd @dpkgsource, qw(-x), (srcfn $version, ".dsc");
+ rename srcfn("$upstreamversion", "/debian/patches"),
+ "work/debian/patches";
+
+ changedir "work";
+ commit_quilty_patch();
+}
+
+sub quilt_make_fake_dsc ($) {
+ my ($upstreamversion) = @_;
+
+ my $fakeversion="$upstreamversion-~~DGITFAKE";
+
+ my $fakedsc=new IO::File 'fake.dsc', '>' or die $!;
+ print $fakedsc <<END or die $!;
+Format: 3.0 (quilt)
+Source: $package
+Version: $fakeversion
+Files:
+END
+
+ my $dscaddfile=sub {
+ my ($b) = @_;
+
+ my $md = new Digest::MD5;
+
+ my $fh = new IO::File $b, '<' or die "$b $!";
+ stat $fh or die $!;
+ my $size = -s _;
+
+ $md->addfile($fh);
+ print $fakedsc " ".$md->hexdigest." $size $b\n" or die $!;
+ };
+
+ quilt_fixup_linkorigs($upstreamversion, $dscaddfile);
+
+ my @files=qw(debian/source/format debian/rules
+ debian/control debian/changelog);
+ foreach my $maybe (qw(debian/patches debian/source/options
+ debian/tests/control)) {
+ next unless stat_exists "../../../$maybe";
+ push @files, $maybe;
+ }
+
+ my $debtar= srcfn $fakeversion,'.debian.tar.gz';
+ runcmd qw(env GZIP=-1n tar -zcf), "./$debtar", qw(-C ../../..), @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.
+ # Computes the cache key and looks in the cache.
+ # Returns ($dgit_view_commitid, $cachekey) or (undef, $cachekey)
+
+ my $splitbrain_cachekey;
+
+ progress
+ "dgit: split brain (separate dgit view) may be needed (--quilt=$quilt_mode).";
+ # we look in the reflog of dgit-intern/quilt-cache
+ # we look for an entry whose message is the key for the cache lookup
+ my @cachekey = (qw(dgit), $our_version);
+ push @cachekey, $upstreamversion;
+ push @cachekey, $quilt_mode;
+ push @cachekey, $headref;
+
+ push @cachekey, hashfile('fake.dsc');
+
+ my $srcshash = Digest::SHA->new(256);
+ my %sfs = ( %INC, '$0(dgit)' => $0 );
+ foreach my $sfk (sort keys %sfs) {
+ 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(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") {
+ $! == ENOENT or die $!;
+ printdebug ">(no reflog)\n";
+ exit 0;
+ }
+ exec @cmd; die $!;
+ }
+ while (<GC>) {
+ chomp;
+ printdebug ">| ", $_, "\n" if $debuglevel > 1;
+ next unless m/^(\w+) (\S.*\S)$/ && $2 eq $splitbrain_cachekey;
+
+ 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 ($saved)";
+ runcmd @git, qw(checkout -q -b dgit-view), $cachehit;
+ $split_brain = 1;
+ return ($cachehit, $splitbrain_cachekey);
+ }
+ progress "dgit view: found cached, no changes required";
+ return ($headref, $splitbrain_cachekey);
+ }
+ die $! if GC->error;
+ failedcmd unless close GC;
+
+ printdebug "splitbrain cache miss\n";
+ return (undef, $splitbrain_cachekey);
+}
+
+sub quilt_fixup_multipatch ($$$) {
+ my ($clogp, $headref, $upstreamversion) = @_;
+
+ progress "examining quilt state (multiple patches, $quilt_mode mode)";
+
+ # Our objective is:
+ # - honour any existing .pc in case it has any strangeness
+ # - determine the git commit corresponding to the tip of
+ # the patch stack (if there is one)
+ # - if there is such a git commit, convert each subsequent
+ # git commit into a quilt patch with dpkg-source --commit
+ # - otherwise convert all the differences in the tree into
+ # a single git commit
+ #
+ # To do this we:
+
+ # Our git tree doesn't necessarily contain .pc. (Some versions of
+ # dgit would include the .pc in the git tree.) If there isn't
+ # one, we need to generate one by unpacking the patches that we
+ # have.
+ #
+ # We first look for a .pc in the git tree. If there is one, we
+ # will use it. (This is not the normal case.)
+ #
+ # Otherwise need to regenerate .pc so that dpkg-source --commit
+ # can work. We do this as follows:
+ # 1. Collect all relevant .orig from parent directory
+ # 2. Generate a debian.tar.gz out of
+ # debian/{patches,rules,source/format,source/options}
+ # 3. Generate a fake .dsc containing just these fields:
+ # Format Source Version Files
+ # 4. Extract the fake .dsc
+ # Now the fake .dsc has a .pc directory.
+ # (In fact we do this in every case, because in future we will
+ # want to search for a good base commit for generating patches.)
+ #
+ # Then we can actually do the dpkg-source --commit
+ # 1. Make a new working tree with the same object
+ # store as our main tree and check out the main
+ # tree's HEAD.
+ # 2. Copy .pc from the fake's extraction, if necessary
+ # 3. Run dpkg-source --commit
+ # 4. If the result has changes to debian/, then
+ # - git add them them
+ # - git add .pc if we had a .pc in-tree
+ # - git commit
+ # 5. If we had a .pc in-tree, delete it, and git commit
+ # 6. Back in the main tree, fast forward to the new HEAD
+
+ # Another situation we may have to cope with is gbp-style
+ # patches-unapplied trees.
+ #
+ # 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).
+ #
+ # That is indistinguishable in contents from a patches-unapplied
+ # tree. And looking at the history to distinguish them is not
+ # useful because the user might have made a confusing-looking git
+ # history structure (which ought to produce an error if dgit can't
+ # cope, not a silent reintroduction of an unwanted patch).
+ #
+ # So gbp users will have to pass an option. But we can usually
+ # detect their failure to do so: if the tree is not a clean
+ # patches-applied tree, quilt linearisation fails, but the tree
+ # _is_ a clean patches-unapplied tree, we can suggest that maybe
+ # they want --quilt=unapplied.
+ #
+ # To help detect this, when we are extracting the fake dsc, we
+ # first extract it with --skip-patches, and then apply the patches
+ # afterwards with dpkg-source --before-build. That lets us save a
+ # tree object corresponding to .origs.
+
+ my $splitbrain_cachekey;
+
+ quilt_make_fake_dsc($upstreamversion);
+
+ if (quiltmode_splitbrain()) {
+ my $cachehit;
+ ($cachehit, $splitbrain_cachekey) =
+ quilt_check_splitbrain_cache($headref, $upstreamversion);
+ return if $cachehit;
+ }
+
+ runcmd qw(sh -ec),
+ 'exec dpkg-source --no-check --skip-patches -x fake.dsc >/dev/null';
+
+ my $fakexdir= $package.'-'.(stripepoch $upstreamversion);
+ rename $fakexdir, "fake" or die "$fakexdir $!";
+
+ changedir 'fake';
+
+ remove_stray_gits("source package");
+ mktree_in_ud_here();
+
+ rmtree '.pc';
+
+ runcmd @git, qw(checkout -f), $headref, qw(-- debian);
+ my $unapplied=git_add_write_tree();
+ printdebug "fake orig tree object $unapplied\n";
+
+ ensuredir '.pc';
+
+ 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 '..';
+
+ quilt_fixup_mkwork($headref);
+
+ my $mustdeletepc=0;
+ if (stat_exists ".pc") {
+ -d _ or die;
+ progress "Tree already contains .pc - will use it then delete it.";
+ $mustdeletepc=1;
+ } else {
+ rename '../fake/.pc','.pc' or die $!;
+ }
+
+ changedir '../fake';
+ rmtree '.pc';
+ my $oldtiptree=git_add_write_tree();
+ printdebug "fake o+d/p tree object $unapplied\n";
+ changedir '../work';
+
+
+ # We calculate some guesswork now about what kind of tree this might
+ # 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
+ 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(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->{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 ?";
+
+ if (quiltmode_splitbrain()) {
+ quiltify_splitbrain($clogp, $unapplied, $headref,
+ $diffbits, \%editedignores,
+ $splitbrain_cachekey);
+ return;
+ }
+
+ progress "starting quiltify (multiple patches, $quilt_mode mode)";
+ quiltify($clogp,$headref,$oldtiptree,\@failsuggestion);
if (!open P, '>>', ".pc/applied-patches") {
$!==&ENOENT or die $!;
@@ -1992,6 +5694,10 @@ END
}
commit_quilty_patch();
+
+ if ($mustdeletepc) {
+ quilt_fixup_delete_pc();
+ }
}
sub quilt_fixup_editor () {
@@ -2013,11 +5719,56 @@ sub quilt_fixup_editor () {
exit 0;
}
+sub maybe_apply_patches_dirtily () {
+ return unless $quilt_mode =~ m/gbp|unapplied/;
+ print STDERR <<END or die $!;
+
+dgit: Building, or cleaning with rules target, in patches-unapplied tree.
+dgit: Have to apply the patches - making the tree dirty.
+dgit: (Consider specifying --clean=git and (or) using dgit sbuild.)
+
+END
+ $patches_applied_dirtily = 01;
+ $patches_applied_dirtily |= 02 unless stat_exists '.pc';
+ runcmd qw(dpkg-source --before-build .);
+}
+
+sub maybe_unapply_patches_again () {
+ progress "dgit: Unapplying patches again to tidy up the tree."
+ if $patches_applied_dirtily;
+ runcmd qw(dpkg-source --after-build .)
+ if $patches_applied_dirtily & 01;
+ rmtree '.pc'
+ if $patches_applied_dirtily & 02;
+ $patches_applied_dirtily = 0;
+}
+
+#----- other building -----
+
+our $clean_using_builder;
+# ^ tree is to be cleaned by dpkg-source's builtin idea that it should
+# clean the tree before building (perhaps invoked indirectly by
+# whatever we are using to run the build), rather than separately
+# and explicitly by us.
+
sub clean_tree () {
+ return if $clean_using_builder;
if ($cleanmode eq 'dpkg-source') {
+ maybe_apply_patches_dirtily();
runcmd_ordryrun_local @dpkgbuildpackage, qw(-T clean);
+ } elsif ($cleanmode eq 'dpkg-source-d') {
+ maybe_apply_patches_dirtily();
+ runcmd_ordryrun_local @dpkgbuildpackage, qw(-d -T clean);
} elsif ($cleanmode eq 'git') {
runcmd_ordryrun_local @git, qw(clean -xdf);
+ } elsif ($cleanmode eq 'git-ff') {
+ runcmd_ordryrun_local @git, qw(clean -xdff);
+ } elsif ($cleanmode eq 'check') {
+ my $leftovers = cmdoutput @git, qw(clean -xdn);
+ if (length $leftovers) {
+ print STDERR $leftovers, "\n" or die $!;
+ fail "tree contains uncommitted files and --clean=check specified";
+ }
} elsif ($cleanmode eq 'none') {
} else {
die "$cleanmode ?";
@@ -2026,22 +5777,44 @@ sub clean_tree () {
sub cmd_clean () {
badusage "clean takes no additional arguments" if @ARGV;
+ notpushing();
clean_tree();
+ maybe_unapply_patches_again();
}
-sub build_prep () {
+sub build_prep_early () {
+ our $build_prep_early_done //= 0;
+ return if $build_prep_early_done++;
badusage "-p is not allowed when building" if defined $package;
- check_not_dirty();
- clean_tree();
my $clogp = parsechangelog();
$isuite = getfield $clogp, 'Distribution';
$package = getfield $clogp, 'Source';
$version = getfield $clogp, 'Version';
+ notpushing();
+ check_not_dirty();
+}
+
+sub build_prep () {
+ build_prep_early();
+ clean_tree();
build_maybe_quilt_fixup();
+ if ($rmchanges) {
+ my $pat = changespat $version;
+ foreach my $f (glob "$buildproductsdir/$pat") {
+ if (act_local()) {
+ unlink $f or fail "remove old changes file $f: $!";
+ } else {
+ progress "would remove $f";
+ }
+ }
+ }
}
-sub changesopts () {
+sub changesopts_initial () {
my @opts =@changesopts[1..$#changesopts];
+}
+
+sub changesopts_version () {
if (!defined $changes_since_version) {
my @vsns = archive_query('archive_query');
my @quirk = access_quirk();
@@ -2053,7 +5826,7 @@ sub changesopts () {
}
if (@vsns) {
@vsns = map { $_->[0] } @vsns;
- @vsns = sort { -version_compare_string($a, $b) } @vsns;
+ @vsns = sort { -version_compare($a, $b) } @vsns;
$changes_since_version = $vsns[0];
progress "changelog will contain changes since $vsns[0]";
} else {
@@ -2062,44 +5835,290 @@ sub changesopts () {
}
}
if ($changes_since_version ne '_') {
- unshift @opts, "-v$changes_since_version";
+ return ("-v$changes_since_version");
+ } else {
+ return ();
+ }
+}
+
+sub changesopts () {
+ return (changesopts_initial(), changesopts_version());
+}
+
+sub massage_dbp_args ($;$) {
+ my ($cmd,$xargs) = @_;
+ # We need to:
+ #
+ # - if we're going to split the source build out so we can
+ # do strange things to it, massage the arguments to dpkg-buildpackage
+ # so that the main build doessn't build source (or add an argument
+ # to stop it building source by default).
+ #
+ # - add -nc to stop dpkg-source cleaning the source tree,
+ # unless we're not doing a split build and want dpkg-source
+ # as cleanmode, in which case we can do nothing
+ #
+ # return values:
+ # 0 - source will NOT need to be built separately by caller
+ # +1 - source will need to be built separately by caller
+ # +2 - source will need to be built separately by caller AND
+ # dpkg-buildpackage should not in fact be run at all!
+ debugcmd '#massaging#', @$cmd if $debuglevel>1;
+#print STDERR "MASS0 ",Dumper($cmd, $xargs, $need_split_build_invocation);
+ if ($cleanmode eq 'dpkg-source' && !$need_split_build_invocation) {
+ $clean_using_builder = 1;
+ return 0;
+ }
+ # -nc has the side effect of specifying -b if nothing else specified
+ # and some combinations of -S, -b, et al, are errors, rather than
+ # later simply overriding earlie. So we need to:
+ # - search the command line for these options
+ # - pick the last one
+ # - perhaps add our own as a default
+ # - perhaps adjust it to the corresponding non-source-building version
+ my $dmode = '-F';
+ foreach my $l ($cmd, $xargs) {
+ next unless $l;
+ @$l = grep { !(m/^-[SgGFABb]$/s and $dmode=$_) } @$l;
+ }
+ push @$cmd, '-nc';
+#print STDERR "MASS1 ",Dumper($cmd, $xargs, $dmode);
+ my $r = 0;
+ if ($need_split_build_invocation) {
+ printdebug "massage split $dmode.\n";
+ $r = $dmode =~ m/[S]/ ? +2 :
+ $dmode =~ y/gGF/ABb/ ? +1 :
+ $dmode =~ m/[ABb]/ ? 0 :
+ die "$dmode ?";
+ }
+ printdebug "massage done $r $dmode.\n";
+ push @$cmd, $dmode;
+#print STDERR "MASS2 ",Dumper($cmd, $xargs, $r);
+ 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";
}
- return @opts;
}
sub cmd_build {
- build_prep();
- runcmd_ordryrun_local @dpkgbuildpackage, qw(-us -uc), changesopts(), @ARGV;
- printdone "build successful\n";
+ 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();
+ }
+ if ($wantsrc < 2) {
+ push @dbp, changesopts_version();
+ maybe_apply_patches_dirtily();
+ runcmd_ordryrun_local @dbp;
+ }
+ maybe_unapply_patches_again();
+ postbuild_mergechanges_vanilla $wantsrc;
}
-sub cmd_git_build {
- build_prep();
- my @cmd =
- (qw(git-buildpackage -us -uc --git-no-sign-tags),
- "--git-builder=@dpkgbuildpackage");
- unless (grep { m/^--git-debian-branch|^--git-ignore-branch/ } @ARGV) {
- canonicalise_suite();
- push @cmd, "--git-debian-branch=".lbranch();
+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;
}
- push @cmd, changesopts();
- runcmd_ordryrun_local @cmd, @ARGV;
- printdone "build successful\n";
+
+ my @dbp = @dpkgbuildpackage;
+
+ my $wantsrc = massage_dbp_args \@dbp, \@ARGV;
+
+ 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=@dbp");
+
+ if ($gbp_make_orig) {
+ ensuredir '.git/dgit';
+ my $ok = '.git/dgit/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 $ok .git/dgit/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;
+ }
+ }
+
+ if ($wantsrc > 0) {
+ build_source();
+ midbuild_checkchanges_vanilla $wantsrc;
+ } else {
+ if (!$clean_using_builder) {
+ push @cmd, '--git-cleaner=true';
+ }
+ build_prep();
+ }
+ maybe_unapply_patches_again();
+ if ($wantsrc < 2) {
+ push @cmd, changesopts();
+ runcmd_ordryrun_local @cmd, @ARGV;
+ }
+ postbuild_mergechanges_vanilla $wantsrc;
}
+sub cmd_git_build { cmd_gbp_build(); } # compatibility with <= 1.0
sub build_source {
+ build_prep_early();
+ 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();
- $sourcechanges = "${package}_".(stripepoch $version)."_source.changes";
+ $sourcechanges = changespat $version,'source';
+ if (act_local()) {
+ unlink "../$sourcechanges" or $!==ENOENT
+ or fail "remove $sourcechanges: $!";
+ }
$dscfn = dscfn($version);
- if ($cleanmode eq 'dpkg-source') {
- runcmd_ordryrun_local (@dpkgbuildpackage, qw(-us -uc -S)),
+ 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();
} else {
- my $pwd = must_getcwd();
- my $leafdir = basename $pwd;
- changedir "..";
- runcmd_ordryrun_local @dpkgsource, qw(-b --), $leafdir;
- changedir $pwd;
+ 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",
@@ -2108,46 +6127,268 @@ sub build_source {
}
sub cmd_build_source {
+ build_prep_early();
badusage "build-source takes no additional arguments" if @ARGV;
build_source();
+ maybe_unapply_patches_again();
printdone "source built, results in $dscfn and $sourcechanges";
}
sub cmd_sbuild {
build_source();
- changedir "..";
- my $pat = "${package}_".(stripepoch $version)."_*.changes";
- if (act_local()) {
- stat $dscfn or fail "$dscfn (in parent directory): $!";
- stat $sourcechanges or fail "$sourcechanges (in parent directory): $!";
- foreach my $cf (glob $pat) {
- next if $cf eq $sourcechanges;
- unlink $cf or fail "remove $cf: $!";
+ 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): $!";
}
- }
- runcmd_ordryrun_local @sbuild, @ARGV, qw(-d), $isuite, $dscfn;
- my @changesfiles = glob $pat;
- @changesfiles = sort {
- ($b =~ m/_source\.changes$/ <=> $a =~ m/_source\.changes$/)
- or $a cmp $b
- } @changesfiles;
- fail "wrong number of different changes files (@changesfiles)"
- unless @changesfiles;
- runcmd_ordryrun_local @mergechanges, @changesfiles;
- my $multichanges = "${package}_".(stripepoch $version)."_multi.changes";
- if (act_local()) {
- stat $multichanges or fail "$multichanges: $!";
- }
- printdone "build successful, results in $multichanges\n" or die $!;
+ runcmd_ordryrun_local @sbuild, qw(-d), $isuite, @ARGV, $dscfn;
+ };
+ maybe_unapply_patches_again();
+ 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';
+ 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";
+ next if lstat $here;
+ 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 "cannot import $dscfn which seems to not have a basename";
+ $there .= "/$f";
+ 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 cmd_archive_api_query {
+ badusage "need only 1 subpath argument" unless @ARGV==1;
+ my ($subpath) = @ARGV;
+ 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 cmd_clone_dgit_repos_server {
+ badusage "need destination argument" unless @ARGV==1;
+ my ($destdir) = @ARGV;
+ my $url = repos_server_url();
+ my @cmd = (@git, qw(clone), $url, $destdir);
+ debugcmd ">",@cmd;
+ exec @cmd or fail "exec git clone: $!\n";
+}
+
+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 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-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();
+}
+
#---------- argument parsing and main program ----------
sub cmd_version {
@@ -2155,6 +6396,65 @@ sub cmd_version {
exit 0;
}
+our (%valopts_long, %valopts_short);
+our (%funcopts_long);
+our @rvalopts;
+our (@modeopt_cfgs);
+
+sub defvalopt ($$$$) {
+ my ($long,$short,$val_re,$how) = @_;
+ my $oi = { Long => $long, Short => $short, Re => $val_re, How => $how };
+ $valopts_long{$long} = $oi;
+ $valopts_short{$short} = $oi;
+ # $how subref should:
+ # do whatever assignemnt or thing it likes with $_[0]
+ # if the option should not be passed on to remote, @rvalopts=()
+ # or $how can be a scalar ref, meaning simply assign the value
+}
+
+defvalopt '--since-version', '-v', '[^_]+|_', \$changes_since_version;
+defvalopt '--distro', '-d', '.+', \$idistro;
+defvalopt '', '-k', '.+', \$keyid;
+defvalopt '--existing-package','', '.*', \$existing_package;
+defvalopt '--build-products-dir','','.*', \$buildproductsdir;
+defvalopt '--clean', '', $cleanmode_re, \$cleanmode;
+defvalopt '--package', '-p', $package_re, \$package;
+defvalopt '--quilt', '', $quilt_modes_re, \$quilt_mode;
+
+defvalopt '', '-C', '.+', sub {
+ ($changesfile) = (@_);
+ if ($changesfile =~ s#^(.*)/##) {
+ $buildproductsdir = $1;
+ }
+};
+
+defvalopt '--initiator-tempdir','','.*', sub {
+ ($initiator_tempdir) = (@_);
+ $initiator_tempdir =~ m#^/# or
+ badusage "--initiator-tempdir must be used specify an".
+ " absolute, not relative, directory."
+};
+
+sub 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;
@@ -2164,6 +6464,27 @@ sub parseopts () {
@ssh = ($ENV{'GIT_SSH'});
}
+ my $oi;
+ my $val;
+ my $valopt = sub {
+ my ($what) = @_;
+ @rvalopts = ($_);
+ if (!defined $val) {
+ badusage "$what needs a value" unless @ARGV;
+ $val = shift @ARGV;
+ push @rvalopts, $val;
+ }
+ badusage "bad value \`$val' for $what" unless
+ $val =~ m/^$oi->{Re}$(?!\n)/s;
+ my $how = $oi->{How};
+ if (ref($how) eq 'SCALAR') {
+ $$how = $val;
+ } else {
+ $how->($val);
+ }
+ push @ropts, @rvalopts;
+ };
+
while (@ARGV) {
last unless $ARGV[0] =~ m/^-/;
$_ = shift @ARGV;
@@ -2185,10 +6506,7 @@ sub parseopts () {
} elsif (m/^--new$/) {
push @ropts, $_;
$new_package=1;
- } elsif (m/^--since-version=([^_]+|_)$/) {
- push @ropts, $_;
- $changes_since_version = $1;
- } elsif (m/^--([-0-9a-z]+)=(.*)/s &&
+ } elsif (m/^--([-0-9a-z]+)=(.+)/s &&
($om = $opts_opt_map{$1}) &&
length $om->[0]) {
push @ropts, $_;
@@ -2198,34 +6516,68 @@ sub parseopts () {
($om = $opts_opt_map{$1})) {
push @ropts, $_;
push @$om, $2;
- } elsif (m/^--existing-package=(.*)/s) {
- push @ropts, $_;
- $existing_package = $1;
- } elsif (m/^--initiator-tempdir=(.*)/s) {
- $initiator_tempdir = $1;
- $initiator_tempdir =~ m#^/# or
- badusage "--initiator-tempdir must be used specify an".
- " absolute, not relative, directory."
- } elsif (m/^--distro=(.*)/s) {
- push @ropts, $_;
- $idistro = $1;
- } elsif (m/^--build-products-dir=(.*)/s) {
- push @ropts, $_;
- $buildproductsdir = $1;
- } elsif (m/^--clean=(dpkg-source|git|none)$/s) {
- push @ropts, $_;
- $cleanmode = $1;
- } elsif (m/^--clean=(.*)$/s) {
- badusage "unknown cleaning mode \`$1'";
+ } elsif (m/^--(gbp|dpm)$/s) {
+ push @ropts, "--quilt=$1";
+ $quilt_mode = $1;
} elsif (m/^--ignore-dirty$/s) {
push @ropts, $_;
$ignoredirty = 1;
} elsif (m/^--no-quilt-fixup$/s) {
push @ropts, $_;
- $noquilt = 1;
+ $quilt_mode = 'nocheck';
} 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, $_;
+ $tagformat_want = [ $1, 'command line', 1 ];
+ # 1 menas overrides distro configuration
+ } elsif (m/^--always-split-source-build$/s) {
+ # 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 \`$_'";
}
@@ -2241,42 +6593,44 @@ sub parseopts () {
cmd_help();
} elsif (s/^-D/-/) {
push @ropts, $&;
- open DEBUG, ">&STDERR" or die $!;
- autoflush DEBUG 1;
- $debug++;
+ $debuglevel++;
+ enabledebug();
} elsif (s/^-N/-/) {
push @ropts, $&;
$new_package=1;
- } elsif (s/^-v([^_]+|_)$//s) {
- push @ropts, $&;
- $changes_since_version = $1;
} elsif (m/^-m/) {
push @ropts, $&;
push @changesopts, $_;
$_ = '';
- } elsif (s/^-c(.*=.*)//s) {
- push @ropts, $&;
- push @git, '-c', $1;
- } elsif (s/^-d(.*)//s) {
- push @ropts, $&;
- $idistro = $1;
- } elsif (s/^-C(.*)//s) {
- push @ropts, $&;
- $changesfile = $1;
- if ($changesfile =~ s#^(.*)/##) {
- $buildproductsdir = $1;
- }
- } elsif (s/^-k(.*)//s) {
- $keyid=$1;
- } elsif (s/^-wn//s) {
+ } elsif (s/^-wn$//s) {
push @ropts, $&;
$cleanmode = 'none';
- } elsif (s/^-wg//s) {
+ } elsif (s/^-wg$//s) {
push @ropts, $&;
$cleanmode = 'git';
- } elsif (s/^-wd//s) {
+ } elsif (s/^-wgf$//s) {
+ push @ropts, $&;
+ $cleanmode = 'git-ff';
+ } elsif (s/^-wd$//s) {
push @ropts, $&;
$cleanmode = 'dpkg-source';
+ } elsif (s/^-wdd$//s) {
+ push @ropts, $&;
+ $cleanmode = 'dpkg-source-d';
+ } 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;
+ $valopt->($oi->{Short});
+ $_ = '';
} else {
badusage "unknown short option \`$_'";
}
@@ -2285,11 +6639,108 @@ sub parseopts () {
}
}
+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};
+
+ my $v = access_cfg("cmd-$k", 'RETURN-UNDEF');
+ if (defined $v) {
+ badcfg "cannot set command for $k"
+ unless length $om->[0];
+ $om->[0] = $v;
+ }
+
+ foreach my $c (access_cfg_cfgs("opts-$k")) {
+ my @vl =
+ map { $_ ? @$_ : () }
+ map { $gitcfgs{$_}{$c} }
+ reverse @gitcfgsources;
+ printdebug "CL $c ", (join " ", map { shellquote } @vl),
+ "\n" if $debuglevel >= 4;
+ next unless @vl;
+ badcfg "cannot configure options for $k"
+ if $opts_opt_cmdonly{$k};
+ my $insertpos = $opts_cfg_insertpos{$k};
+ @$om = ( @$om[0..$insertpos-1],
+ @vl,
+ @$om[$insertpos..$#$om] );
+ }
+ }
+
+ if (!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}) {
+ git_slurp_config();
quilt_fixup_editor();
}
parseopts();
+check_env_sanity();
+git_slurp_config();
+
print STDERR "DRY RUN ONLY\n" if $dryrun_level > 1;
print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n"
if $dryrun_level == 1;
@@ -2300,6 +6751,9 @@ if (!@ARGV) {
my $cmd = shift @ARGV;
$cmd =~ y/-/_/;
+my $pre_fn = ${*::}{"pre_$cmd"};
+$pre_fn->() if $pre_fn;
+
my $fn = ${*::}{"cmd_$cmd"};
$fn or badusage "unknown operation $cmd";
$fn->();
diff --git a/dgit-badcommit-fixup b/dgit-badcommit-fixup
new file mode 100755
index 0000000..8b202c0
--- /dev/null
+++ b/dgit-badcommit-fixup
@@ -0,0 +1,315 @@
+#!/usr/bin/perl -w
+#
+# Script to help with fallout from #849041.
+#
+# usage:
+# dgit-badcommit-fixup --check
+# dgit-badcommit-fixup --test
+# dgit-badcommit-fixup --real
+
+# Update procedure, from server operator's point of view:
+#
+# 1. Test in an offline tree that this DTRT
+#
+# 2. Announce a transition time. Tell everyone that between
+# the transition time and their next upload, they must
+# run this script.
+#
+# 3. At the transition time, run this script in every repo.
+#
+# 4. Run the mirror script to push changes, if necessary.
+
+
+use strict;
+
+use POSIX;
+use IPC::Open2;
+use Data::Dumper;
+
+our $our_version = 'UNRELEASED'; ###substituted###
+
+my $real;
+
+foreach my $a (@ARGV) {
+ if ($a eq '--test') {
+ $real = 0;
+ } elsif ($a eq '--real') {
+ $real = 1;
+ } elsif ($a eq '--check') {
+ $real = -1;
+ } else {
+ die "$a ?";
+ }
+}
+
+die unless defined $real;
+
+my $gcfpid = open2 \*GCFO, \*GCFI, 'git cat-file --batch' or die $!;
+
+our %count;
+
+no warnings qw(recursion);
+
+sub runcmd {
+ system @_ and die "@_ $! $?";
+}
+
+$!=0; $?=0;
+my $bare = `git rev-parse --is-bare-repository`;
+die "$? $!" if $?;
+chomp $bare or die;
+
+sub getobj ($$) {
+ my ($obj, $type) = @_;
+ print GCFI $obj, "\n" or die $!;
+ my $x = <GCFO>;
+ my ($gtype, $gsize) = $x =~ m/^\w+ (\w+) (\d+)\n/ or die "$obj ?";
+ $gtype eq $type or die "$obj $gtype != $type ?";
+ my $gdata;
+ (read GCFO, $gdata, $gsize) == $gsize or die "$obj $!";
+ $x = <GCFO>;
+ $x eq "\n" or die "$obj ($_) $!";
+ $count{inspected}++;
+ return $gdata;
+}
+
+sub hashobj ($$) {
+ my ($data,$type) = @_;
+ my $gwopid = open2 \*GWO, \*GWI,
+ "git hash-object -w -t $type --stdin"
+ or die $!;
+ print GWI $data or die $!;
+ close GWI or die $!;
+ $_ = <GWO>;
+ close GWO or die $!;
+ waitpid $gwopid,0 == $gwopid or die $!;
+ die $? if $?;
+ m/^(\w+)\n/ or die "$_ ?";
+ $count{"rewritten $type"}++;
+ return $1;
+}
+
+our %memo;
+
+sub rewrite_commit ($);
+sub rewrite_commit ($) {
+ my ($obj) = @_;
+ my $m = \ $memo{$obj};
+ return $$m if defined $$m;
+ my $olddata = getobj $obj, 'commit';
+ $olddata =~ m/(?<=\n)(?=\n)/ or die "$obj ?";
+ my $msg = $';
+ local $_ = $`;
+ s{^(parent )(\w+)$}{ $1 . rewrite_commit($2) }gme;
+ $count{'fix overwrite'} += s{^commiter }{committer }gm;
+ if (!m{^author }m && !m{^committer }m) {
+ m{^parent (\w+)}m or die "$obj ?";
+ my $parent = getobj $1, 'commit';
+ $parent =~ m/^(?:.+\n)+(author .*\ncommitter .*\n)/;
+ m/\n$/ or die "$obj ?";
+ $_ .= $1;
+ $count{'fix import'}++;
+ }
+ my $newdata = $_.$msg;
+ my $newobj;
+ if ($newdata eq $olddata) {
+ $newobj = $obj;
+ $count{unchanged}++;
+#print STDERR "UNCHANGED $obj\n";
+ } else {
+ $newobj = hashobj $newdata, 'commit';
+#print STDERR "REWRITTEN $obj $newobj\n";
+ }
+ $$m= $newobj;
+ return $newobj;
+}
+
+our @updates;
+
+sub filter_updates () {
+ @updates = grep { $_->[1] ne $_->[2] } @updates;
+}
+
+sub rewrite_tag ($) {
+ my ($obj) = @_;
+ $_ = getobj $obj, 'tag';
+ m/^type (\w+)\n/m or die "$obj ?";
+ if ($1 ne 'commit') {
+ $count{"oddtags $1"}++;
+ return $obj;
+ }
+ m/^object (\w+)\n/m or die "$obj ?";
+ my $oldref = $1;
+ my $newref = rewrite_commit $oldref;
+ if ($oldref eq $newref) {
+ return $obj;
+ }
+ s/^(object )\w+$/ $1.$newref /me or die "$obj ($_) ?";
+ s/^-----BEGIN PGP SIGNATURE-----\n.*^-----END PGP SIGNATURE-----\n$//sm;
+ return hashobj $_, 'tag';
+}
+
+sub edit_rewrite_map ($) {
+ my ($old) = @_;
+
+ filter_updates();
+ return $old unless @updates;
+
+ my $td = 'dgit-broken-fixup.tmp';
+ runcmd qw(rm -rf), $td;
+ mkdir $td, 0700 or die "$td $!";
+ chdir $td or die $!;
+ runcmd qw(git init -q);
+ runcmd qw(git config gc.auto 0);
+ runcmd qw(rm -rf .git/objects);
+ symlink "../../objects", ".git/objects" or die $!;
+
+ my %map;
+
+ if ($old) {
+ runcmd qw(git checkout -q), $old;
+ open M, "map" or die $!;
+ while (<M>) {
+ m/^(\w+)(?:\s+(\w+))?$/ or die;
+ $map{$1} = $2;
+ $count{rewrite_map_previous}++;
+ }
+ M->error and die $!;
+ close M or die $!;
+ }
+
+ foreach my $oldc (keys %memo) {
+ my $newc = $memo{$oldc};
+ next if $oldc eq $newc;
+ $map{$oldc} = $newc;
+ }
+ foreach my $up (@updates) { # catches tags
+ $map{ $up->[1] } = $up->[2];
+ }
+
+ open M, ">", "map" or die $!;
+ printf M "%s%s\n",
+ $_, (defined $map{$_} ? " $map{$_}" : "")
+ or die $!
+ foreach keys %map;
+ close M or die $!;
+
+ if (!$old) {
+ runcmd qw(git add map);
+ }
+
+ runcmd qw(git commit -q), qw(-m), <<END, qw(map);
+dgit-badcommit-fixup
+
+[dgit-badcommit-fixup $our_version]
+END
+
+ $!=0; $?=0;
+ my $new = `git rev-parse HEAD`;
+ die "$? $!" if $?;
+ chomp $new or die;
+
+ chdir '..' or die $!;
+ runcmd qw(rm -rf), $td;
+
+ $count{rewrite_map_updated}++;
+
+ return $new;
+}
+
+$!=0; $?=0;
+my $refs=`git for-each-ref`;
+die "$? $!" if $?;
+
+chomp $refs;
+
+our $org_rewrite_map;
+
+foreach my $rline (split /\n/, $refs) {
+ my ($obj, $type, $refname) =
+ $rline =~ m/^(\w+)\s+(\w+)\s+(\S.*)/
+ or die "$_ ?";
+ if ($refname eq 'refs/dgit-rewrite/map') {
+ $org_rewrite_map = $obj;
+ next;
+ }
+ next if $refname =~ m{^refs/dgit-(?:badcommit|badfixuptest)/};
+
+ $!=0; $?=0;
+ system qw(sh -ec),
+ 'exec >/dev/null git symbolic-ref -q "$1"', qw(x),
+ $refname;
+ if ($?==0) {
+ $count{symrefs_ignored}++;
+ next;
+ }
+ die "$? $!" unless $?==256;
+
+ my $rewrite;
+ if ($type eq 'commit') {
+ $rewrite = rewrite_commit($obj);
+ } elsif ($type eq 'tag') {
+ $rewrite = rewrite_tag($obj);
+ } else {
+ warn "ref $refname refers to $type\n";
+ next;
+ }
+ push @updates, [ $refname, $obj, $rewrite ];
+}
+
+if ($bare eq 'true') {
+ my $new_rewrite_map = edit_rewrite_map($org_rewrite_map);
+ push @updates, [ 'refs/dgit-rewrite/map',
+ ($org_rewrite_map // '0'x40),
+ ($new_rewrite_map // '0'x40),
+ 1 ];
+}
+
+filter_updates();
+
+if (!@updates) {
+ print Dumper(\%count), "all is well - nothing to do\n";
+ exit 0;
+}
+
+#print Dumper(\@updates);
+
+open U, "|git update-ref -m 'dgit bad commit fixup' --stdin" or die $!
+ if $real >= 0;
+
+for my $up (@updates) {
+ my ($ref, $old, $new, $nobackup) = @$up;
+ my $otherref = $ref;
+ $otherref =~ s{^refs/}{};
+ if ($real > 0) {
+ print U <<END or die $! unless $nobackup;
+create refs/dgit-badcommit/$otherref $old
+END
+ print U <<END or die $!;
+update $ref $new $old
+END
+ } elsif ($real==0) {
+ print U <<END or die $!;
+update refs/dgit-badfixuptest/$otherref $new
+END
+ } else {
+ print "found trouble in history of $ref\n" or die $!;
+ }
+}
+
+if ($real >= 0) {
+ $?=0; $!=0;
+ close U or die "$? $!";
+ die $? if $?;
+}
+
+print Dumper(\%count);
+
+if ($real >= 0) {
+ print "old values saved in refs/dgit-badcommit/\n" or die $!;
+} elsif ($real == 0) {
+ print "testing output saved in refs/dgit-badfixuptest/\n" or die $!;
+} else {
+ print STDERR "found work to do, exiting status 2\n";
+ exit 2;
+}
diff --git a/dgit-maint-gbp.7.pod b/dgit-maint-gbp.7.pod
new file mode 100644
index 0000000..c31dfa5
--- /dev/null
+++ b/dgit-maint-gbp.7.pod
@@ -0,0 +1,126 @@
+=head1 NAME
+
+dgit - tutorial for package maintainers already using git-buildpackage(1)
+
+=head1 INTRODUCTION
+
+This document explains how B<dgit> can be incorporated into a
+git-buildpackage(1) package-maintenance workflow. This should be read
+jointly with git-buildpackage(1)'s documentation. Some reasons why
+you might want to incorporate B<dgit> into your existing workflow:
+
+=over 4
+
+=item
+
+Benefit from dgit's safety catches. In particular, ensure that your
+upload always matches exactly your git HEAD.
+
+=item
+
+Provide a better, more detailed git history to downstream dgit users,
+such as people using dgit to do an NMU (see dgit-nmu-simple(7) and
+dgit-user(7)).
+
+=back
+
+Note that we assume a patches-unapplied repository: the upstream
+source committed to the git repository is unpatched.
+git-buildpackage(1) can work with patched-applied repositories, but is
+normally used with patches-unapplied.
+
+=head1 GIT CONFIGURATION
+
+If you run
+
+=over 4
+
+ % git config dgit.default.quilt-mode gbp
+
+=back
+
+in your repository, you can omit I<--gbp> wherever it occurs below.
+
+Note that this does require that you always work from your gbp master
+branch, never the dgit patches-applied branch.
+
+=head1 BUILDING
+
+You can perform builds like this:
+
+=over 4
+
+ % dgit [--allow-dirty] gbp-build [OPTIONS]
+
+=back
+
+where I<--allow-dirty> is needed for testing uncommitted changes, and
+I<OPTIONS> are any further options to be passed on to
+gbp-buildpackage(1).
+
+When you are ready to build for upload, you will probably want to use
+sbuild(1) or pbuilder(1), or do a source-only upload. Either
+
+=over 4
+
+ % dgit --rm-old-changes --gbp sbuild
+
+=back
+
+or
+
+=over 4
+
+ % dgit --rm-old-changes gbp-build --git-pbuilder
+
+=back
+
+or
+
+=over 4
+
+ % dgit --rm-old-changes --gbp build-source
+
+=back
+
+We use I<--rm-old-changes> to ensure that there is exactly one changes
+file corresponding to this package, so we can be confident we're
+uploading what we intend (though B<dgit push> will do some safety
+checks).
+
+Note that all of the commands in this section are not required to
+upload with dgit. You can invoke gbp-buildpackage(1), pbuilder(1) and
+sbuild(1) directly. However, the defaults for these tools may leave
+you with something that dgit will refuse to upload because it doesn't
+match your git HEAD. As a general rule, leave all signing and tagging
+to dgit.
+
+=head1 UPLOADING
+
+Don't use I<--git-tag>: B<dgit push> will do this for you. To upload:
+
+=over 4
+
+ % dgit --gbp push
+
+=back
+
+This will push your git history to the dgit-repos, but you probably
+want to follow it up with a push to alioth.
+
+You will need to pass I<--overwrite> if the previous upload was not
+performed with dgit.
+
+=head1 INCORPORATING NMUS
+
+B<dgit pull> can't yet incorporate NMUs into patches-unapplied gbp
+branches. You can just apply the NMU diff the traditional way. The
+next upload will require I<--overwrite>.
+
+=head1 SEE ALSO
+
+dgit(1), dgit(7)
+
+=head1 AUTHOR
+
+This tutorial was written and is maintained by Sean Whitton <spwhitton@spwhitton.name>.
diff --git a/dgit-maint-merge.7.pod b/dgit-maint-merge.7.pod
new file mode 100644
index 0000000..3da1b78
--- /dev/null
+++ b/dgit-maint-merge.7.pod
@@ -0,0 +1,439 @@
+=head1 NAME
+
+dgit - tutorial for package maintainers, using a workflow centered around git-merge(1)
+
+=head1 INTRODUCTION
+
+This document describes elements of a workflow for maintaining a
+non-native Debian package using B<dgit>. The workflow makes the
+following opinionated assumptions:
+
+=over 4
+
+=item
+
+Git histories should be the non-linear histories produced by
+git-merge(1), preserving all information about divergent development
+that was later brought together.
+
+=item
+
+Maintaining convenient and powerful git workflows takes priority over
+the usefulness of the raw Debian source package. The Debian archive
+is thought of as an output format.
+
+For example, we don't spend time curating a series of quilt patches.
+However,
+in straightforward cases,
+the information such a series would contain is readily
+available from B<dgit-repos>.
+
+=item
+
+It is more important to have the Debian package's git history be a
+descendent of upstream's git history than to use exactly the orig.tar
+that upstream makes available for download.
+
+=back
+
+This workflow is less suitable for some packages.
+When the Debian delta contains multiple pieces which interact,
+or which you aren't going to be able to upstream soon,
+it might be preferable to
+maintain the delta as a rebasing patch series.
+For such a workflow see for example
+dgit-maint-gbp(7).
+
+=head1 INITIAL DEBIANISATION
+
+This section explains how to start using this workflow with a new
+package. It should be skipped when converting an existing package to
+this workflow.
+
+=head2 When upstream tags releases in git
+
+Suppose that the latest stable upstream release is 1.2.2, and this has
+been tagged '1.2.2' by upstream.
+
+=over 4
+
+ % git clone -oupstream https://some.upstream/foo.git
+ % cd foo
+ % git verify-tag 1.2.2
+ % git reset --hard 1.2.2
+ % git branch --unset-upstream
+
+=back
+
+The final command detaches your master branch from the upstream remote,
+so that git doesn't try to push anything there, or merge unreleased
+upstream commits. If you want to maintain a copy of your packaging
+branch on B<alioth.debian.org> in addition to B<dgit-repos>, you can
+do something like this:
+
+=over 4
+
+ % git remote add -f origin git.debian.org:/git/collab-maint/foo.git
+ % git push --follow-tags -u origin master
+
+=back
+
+Now go ahead and Debianise your package. Just make commits on the
+master branch, adding things in the I<debian/> directory. If you need
+to patch the upstream source, just make commits that change files
+outside of the I<debian/> directory. It is best to separate commits
+that touch I<debian/> from commits that touch upstream source, so that
+the latter can be cherry-picked by upstream.
+
+Note that there is no need to maintain a separate 'upstream' branch,
+unless you also happen to be involved in upstream development. We
+work with upstream tags rather than any branches, except when
+forwarding patches (see FORWARDING PATCHES UPSTREAM, below).
+
+Finally, you need an orig tarball:
+
+=over 4
+
+ % git deborig
+
+=back
+
+See git-deborig(1) if this fails.
+
+This tarball is ephemeral and easily regenerated, so we don't commit
+it anywhere (e.g. with tools like pristine-tar(1)).
+
+=head3 Verifying upstream's tarball releases
+
+=over 4
+
+It can be a good idea to compare upstream's released tarballs with the
+release tags, at least for the first upload of the package. If they
+are different, you might need to add some additional steps to your
+I<debian/rules>, such as running autotools.
+
+A convenient way to perform this check is to import the tarball as
+described in the following section, using a different value for
+'upstream-tag', and then use git-diff(1) to compare the imported
+tarball to the release tag. If they are the same, you can use
+upstream's tarball instead of running git-deborig(1).
+
+=back
+
+=head2 When upstream releases only tarballs
+
+We need a virtual upstream branch with virtual release tags.
+gbp-import-orig(1) can manage this for us. To begin
+
+=over 4
+
+ % mkdir foo
+ % cd foo
+ % git init
+
+=back
+
+Now create I<debian/gbp.conf>:
+
+=over 4
+
+ [DEFAULT]
+ upstream-branch = upstream
+ debian-branch = master
+ upstream-tag = %(version)s
+
+ sign-tags = True
+ pristine-tar = False
+ pristine-tar-commit = False
+
+=back
+
+Then we can import the upstream version:
+
+=over 4
+
+ % git add debian/gbp.conf && git commit -m "create gbp.conf"
+ % gbp import-orig ../foo_1.2.2.orig.tar.xz
+
+=back
+
+You are now ready to proceed as above, making commits to both the
+upstream source and the I<debian/> directory.
+
+If you want to maintain a copy of your repository on
+B<alioth.debian.org>, you should push both the origin and the upstream
+branches:
+
+=over 4
+
+ % git remote add -f origin git.debian.org:/git/collab-maint/foo.git
+ % git push --follow-tags -u origin master upstream
+
+=back
+
+=head1 CONVERTING AN EXISTING PACKAGE
+
+This section explains how to convert an existing Debian package to
+this workflow. It should be skipped when debianising a new package.
+
+=head2 No existing git history
+
+=over 4
+
+ % dgit clone foo
+ % cd foo
+ % git remote add -f upstream https://some.upstream/foo.git
+
+=back
+
+=head2 Existing git history using another workflow
+
+First, dump any existing patch queue:
+
+=over 4
+
+ % git rm -rf debian/patches
+ % git commit -m "drop existing quilt patch queue"
+
+=back
+
+Then make new upstream tags available:
+
+=over 4
+
+ % git remote add -f upstream https://some.upstream/foo.git
+
+=back
+
+Now you simply need to ensure that your git HEAD is dgit-compatible,
+i.e., it is exactly what you would get if you ran B<dpkg-buildpackage
+-i\.git/ -I.git -S> and then unpacked the resultant source package.
+
+To achieve this, you might need to delete
+I<debian/source/local-options>. One way to have dgit check your
+progress is to run B<dgit build-source>.
+
+The first dgit push will require I<--overwrite>.
+
+=head1 SOURCE PACKAGE CONFIGURATION
+
+=head2 debian/source/options
+
+We set some source package options such that dgit can transparently
+handle the "dropping" and "refreshing" of changes to the upstream
+source:
+
+=over 4
+
+ single-debian-patch
+ auto-commit
+
+=back
+
+You don't need to create this file if you are using the version 1.0
+source package format.
+
+=head2 Sample text for debian/source/patch-header
+
+It is a good idea to explain how a user can obtain a breakdown of the
+changes to the upstream source:
+
+=over 4
+
+The Debian packaging of foo is maintained in git,
+using the merging workflow described in dgit-maint-merge(7).
+There isn't a patch queue that can be represented as a quilt series.
+
+A detailed breakdown of the changes is available from their
+canonical representation -
+git commits in the packaging repository.
+For example, to see the changes made by the Debian maintainer in the
+first upload of upstream version 1.2.3, you could use:
+
+=over 4
+
+ % git clone https://git.dgit.debian.org/foo
+ % cd foo
+ % git log --oneline 1.2.3..debian/1.2.3-1 -- . ':!debian'
+
+=back
+
+(If you have dgit, use `dgit clone foo`,
+rather than plain `git clone`.)
+
+A single combined diff, containing all the changes, follows.
+
+=back
+
+Alternatively, this text could be added to README.source. However,
+this might distract from more important information present in the
+latter file.
+
+=head1 BUILDING AND UPLOADING
+
+Use B<dgit build>, B<dgit sbuild>, B<dgit build-source>, and B<dgit
+push> as detailed in dgit(1). If any command fails, dgit will provide
+a carefully-worded error message explaining what you should do. If
+it's not clear, file a bug against dgit. Remember to pass I<--new>
+for the first upload.
+
+As an alternative to B<dgit build> and friends, you can use a tool
+like gitpkg(1). This works because like dgit, gitpkg(1) enforces that
+HEAD has exactly the contents of the source package. gitpkg(1) is
+highly configurable, and one dgit user reports using it to produce and
+test multiple source packages, from different branches corresponding
+to each of the current Debian suites.
+
+If you want to skip dgit's checks while iterating on a problem with
+the package build (for example, you don't want to commit your changes
+to git), you can just run dpkg-buildpackage(1) or debuild(1) instead.
+
+=head1 NEW UPSTREAM RELEASES
+
+=head2 When upstream tags releases in git
+
+It's a good idea to preview the merge of the new upstream release.
+First, just check for any new or deleted files that may need
+accounting for in your copyright file:
+
+=over 4
+
+ % git remote update
+ % git diff --stat master..1.2.3 -- . ':!debian'
+
+=back
+
+You can then review the full merge diff:
+
+=over 4
+
+ % git merge-tree `git merge-base master 1.2.3` master 1.2.3 | $PAGER
+
+=back
+
+Once you're satisfied with what will be merged, update your package:
+
+=over 4
+
+ % git merge 1.2.3
+ % dch -v1.2.3-1 New upstream release.
+ % git add debian/changelog && git commit -m changelog
+ % git deborig
+
+=back
+
+and you are ready to try a build.
+
+=head2 When upstream releases only tarballs
+
+You will need the I<debian/gbp.conf> from "When upstream releases only
+tarballs", above.
+
+Then, either
+
+=over 4
+
+ % gbp import-orig ../foo_1.2.2.orig.tar.xz
+
+=back
+
+or if you have a working watch file
+
+=over 4
+
+ % gbp import-orig --uscan
+
+=back
+
+=head1 HANDLING DFSG-NON-FREE MATERIAL
+
+=head2 When upstream tags releases in git
+
+We create a DFSG-clean tag to merge to master:
+
+=over 4
+
+ % git checkout -b pre-dfsg 1.2.3
+ % git rm evil.bin
+ % git commit -m "upstream version 1.2.3 DFSG-cleaned"
+ % git tag -s 1.2.3+dfsg
+ % git checkout master
+ % git branch -D pre-dfsg
+
+=back
+
+Before merging the new 1.2.3+dfsg tag to master, you should first
+determine whether it would be legally dangerous for the non-free
+material to be publicly accessible in the git history on
+B<dgit-repos>.
+
+If it would be dangerous, there is a big problem;
+in this case please consult your archive administrators
+(for Debian this is the dgit administrator dgit-owner@debian.org
+and the ftpmasters ftpmaster@ftp-master.debian.org).
+
+=head2 When upstream releases only tarballs
+
+The easiest way to handle this is to add a B<Files-Excluded> field to
+I<debian/copyright>, and a B<uversionmangle> setting in
+I<debian/watch>. See uscan(1). Alternatively, see the I<--filter>
+option detailed in gbp-import-orig(1).
+
+=head1 FORWARDING PATCHES UPSTREAM
+
+The basic steps are:
+
+=over 4
+
+=item 1.
+
+Create a new branch based off upstream's master branch.
+
+=item 2.
+
+git-cherry-pick(1) commits from your master branch onto your new
+branch.
+
+=item 3.
+
+Push the branch somewhere and ask upstream to merge it, or use
+git-format-patch(1) or git-request-pull(1).
+
+=back
+
+For example (and it is only an example):
+
+=over 4
+
+ % # fork foo.git on GitHub
+ % git remote add -f fork git@github.com:spwhitton/foo.git
+ % git checkout -b fix-error upstream/master
+ % git config branch.fix-error.pushRemote fork
+ % git cherry-pick master^2
+ % git push
+ % # submit pull request on GitHub
+
+=back
+
+Note that when you merge an upstream release containing your forwarded
+patches, git and dgit will transparently handle "dropping" the patches
+that have been forwarded, "retaining" the ones that haven't.
+
+=head1 INCORPORATING NMUS
+
+=over 4
+
+ % dgit pull
+
+=back
+
+Alternatively, you can apply the NMU diff to your repository. The
+next push will then require I<--overwrite>.
+
+=head1 SEE ALSO
+
+dgit(1), dgit(7)
+
+=head1 AUTHOR
+
+This tutorial was written and is maintained by Sean Whitton <spwhitton@spwhitton.name>. It contains contributions from other dgit contributors too - see the dgit copyright file.
diff --git a/dgit-maint-native.7.pod b/dgit-maint-native.7.pod
new file mode 100644
index 0000000..03aee59
--- /dev/null
+++ b/dgit-maint-native.7.pod
@@ -0,0 +1,116 @@
+=head1 NAME
+
+dgit - tutorial for package maintainers of Debian-native packages
+
+=head1 INTRODUCTION
+
+This document describes elements of a workflow for using B<dgit> to
+maintain a Debian package that uses one of the native source formats
+("1.0" & "3.0 (native)").
+
+=over 4
+
+=item
+
+We expect that your git history is fast-forwarding.
+
+=item
+
+You should be prepared to tolerate a small amount of
+ugliness in your git history
+in the form of merges which stitch
+the dgit-generated archive view
+into your maintainer history.
+
+This is to handle uploads that were not made with dgit,
+such as the uploads you made before switching to this workflow,
+or NMUs.
+
+=back
+
+=head2 Benefits
+
+=over 4
+
+=item
+
+Benefit from dgit's safety catches. In particular, ensure that your
+upload always matches exactly your git HEAD.
+
+=item
+
+Provide a better,
+more detailed history
+to downstream dgit users.
+
+=item
+
+Incorporate an NMU with one command.
+
+=back
+
+=head1 FIRST PUSH WITH DGIT
+
+You do not need to do anything special to your tree
+to push with dgit.
+
+Simply prepare your git tree in the usual way, and then:
+
+=over 4
+
+ % dgit -wgf sbuild -A -c sid
+ % dgit -wgf --overwrite push
+
+=back
+
+(Do not make any tags yourself: dgit push will do that.)
+
+The --overwrite option tells dgit that you are expecting
+that your git history is not a descendant of the
+history which dgit synthesised from the previous
+non-dgit uploads.
+
+dgit will make a merge commit
+on your branch
+but without making any code changes
+(ie, a pseudo-merge)
+so that your history,
+which will be pushed to the dgit git server,
+is fast forward from the dgit archive view.
+
+=head1 SUBSEQUENT PUSHES
+
+=over 4
+
+ % dgit -wgf push
+
+=back
+
+That's it.
+
+=head1 INCORPORATING AN NMU
+
+=over 4
+
+ % dgit pull
+
+=back
+
+That's it.
+
+Or, if you would prefer to review the changes,
+you can do this:
+
+=over 4
+
+ % dgit fetch
+ % dgit diff HEAD..dgit/dgit/sid
+
+=back
+
+If you do not merge the NMU into your own git history,
+the next push will then require I<--overwrite>.
+
+=head1 SEE ALSO
+
+dgit(1), dgit(7)
diff --git a/dgit-nmu-simple.7.pod b/dgit-nmu-simple.7.pod
new file mode 100644
index 0000000..0d2525f
--- /dev/null
+++ b/dgit-nmu-simple.7.pod
@@ -0,0 +1,139 @@
+=head1 NAME
+
+dgit-nmu-simple - tutorial for DDs wanting to NMU with git
+
+=head1 INTRODUCTION AND SCOPE
+
+This tutorial describes how a Debian Developer can do
+a straightforward NMU
+of a package in Debian, using dgit.
+
+This document won't help you decide whether
+an NMU is a good idea or
+whether it be well received.
+The Debian Developers' Reference has some
+(sometimes questionable) guidance on this.
+
+Conversely, you do not need to know anything
+about the usual maintainer's git workflow.
+If appropriate, you can work on many different packages,
+making similar changes,
+without worrying about the individual maintainers' git practices.
+
+This tutorial only covers changes which
+can sensibly be expressed as a
+reasonably small number of linear commits
+(whether to Debian packaging or to upstream files or both).
+
+If you want to do a new upstream version,
+you probably want to do as the maintainer would have done.
+You'll need to find out what the maintainer's
+git practices are
+and
+consult the appropriate C<dgit-maint-*(7)> workflow tutorial,
+
+=head1 SUMMARY
+
+=over 4
+
+ % dgit clone glibc jessie
+ % cd glibc
+ % git am ~/glibc-security-fix.diff
+ % dch --nmu "Apply upstream's fix for foo bug."
+ % git add debian/changelog && git commit -m"NMU changelog entry"
+ % dpkg-buildpackage -uc -b
+ [ run your tests ]
+ % dch -r && git add debian/changelog && git commit -m"Finalise NMU"
+ % dgit -wgf sbuild -A -c jessie
+ [ final tests on generated .debs ]
+ % dgit -wgf [--delayed=5] push jessie
+ [ enter your gnupg passphrase as prompted ]
+ [ see that push and upload are successful ]
+ [ prepare and email NMU diff (git-diff, git-format-patch) ]
+
+=back
+
+=head1 WHAT KIND OF CHANGES AND COMMITS TO MAKE
+
+When preparing an NMU, the git commits you make on the dgit branch
+should be simple linear series of commits with good commit messages.
+The commit messages will be published in various ways,
+including perhaps being used as the cover messages for
+generated quilt patches.
+
+Do not make merge commits.
+Do not try to rebase to drop patches - if you need to revert a
+change which is actually a Debian patch,
+use git-revert.
+
+If you need to modify a Debian patch,
+make a new commit which fixes what needs fixing,
+and explain in the commit message which patch it should be
+squashed with
+(perhaps by use of a commit message in C<git rebase --autosquash -i>
+format).
+
+(Of course if you have specific instructions from the maintainer,
+you can follow those instead.
+But the procedure in this tutorial is legitimate for any maintainer,
+in the sense that it should generate an upload to which the
+maintainer cannot reasonably object.)
+
+=head1 RELEVANT BRANCHES
+
+dgit clone will put you on a branch like C<dgit/sid>.
+There is a pseudo-remote called C<dgit> which also contains a branch
+like C<dgit/sid>, so you do things like
+C<git diff dgit/dgit/sid>
+to see what changes you have made.
+
+=head1 KEEPING YOUR WORKING TREE TIDY
+
+Don't forget to C<git add> any new files you create.
+Otherwise git clean
+(which is requested with the C<-wgf> option in the recipe above)
+will delete them.
+
+Many package builds leave dirty git trees.
+So, commit before building.
+That way you can use C<git reset --hard>.
+
+If you follow this approach
+you don't need to care about the build dirtying the
+tree.
+It also means you don't care about the package clean target,
+which is just as well because many package clean targets are broken.
+
+=head1 OTHER GIT BRANCHES
+
+The dgit git history
+(visible in gitk and git log)
+is not necessarily related to the maintainer's
+or upstream's git history (if any).
+
+If the maintainer has advertised a git repo with
+Vcs-Git
+dgit will set up a remote for it,
+so you can do
+
+=over 4
+
+ % git fetch vcs-git
+
+=back
+
+You can cherry pick changes from there, for example.
+Note that the maintainer's git history may not be
+suitable for use with dgit.
+For example, it might be a patches-unapplied branch
+or even contain only a debian/ directory.
+
+=head1 UPLOADING TO DELAYED
+
+You can use dgit's I<--delayed> option
+to upload to the DELAYED queue.
+However, you should read the warning about this option in dgit(1).
+
+=head1 SEE ALSO
+
+dgit(1), dgit(7), dgit-maint-*(7)
diff --git a/dgit-repos-server b/dgit-repos-server
deleted file mode 100755
index 3e74217..0000000
--- a/dgit-repos-server
+++ /dev/null
@@ -1,627 +0,0 @@
-#!/usr/bin/perl -w
-# dgit-repos-server
-#
-# usages:
-# .../dgit-repos-server SUITES KEYRING-AUTH-SPEC DGIT-REPOS-DIR --ssh
-# internal usage:
-# .../dgit-repos-server --pre-receive-hook PACKAGE
-#
-# Invoked as the ssh restricted command
-#
-# Works like git-receive-pack
-#
-# SUITES is the name of a file which lists the permissible suites
-# one per line (#-comments and blank lines ignored)
-#
-# KEYRING-AUTH-SPEC is a :-separated list of
-# KEYRING.GPG,AUTH-SPEC
-# where AUTH-SPEC is one of
-# a
-# mDM.TXT
-
-use strict;
-
-# What we do is this:
-# - extract the destination repo name
-# - make a hardlink clone of the destination repo
-# - provide the destination with a stunt pre-receive hook
-# - run actual git-receive-pack with that new destination
-# as a result of this the stunt pre-receive hook runs; it does this:
-# + understand what refs we are allegedly updating and
-# check some correspondences:
-# * we are updating only refs/tags/debian/* and refs/dgit/*
-# * and only one of each
-# * and the tag does not already exist
-# and
-# * recover the suite name from the destination refs/dgit/ ref
-# + disassemble the signed tag into its various fields and signature
-# including:
-# * parsing the first line of the tag message to recover
-# the package name, version and suite
-# * checking that the package name corresponds to the dest repo name
-# * checking that the suite name is as recovered above
-# + verify the signature on the signed tag
-# and if necessary check that the keyid and package are listed in dm.txt
-# + check various correspondences:
-# * the suite is one of those permitted
-# * the signed tag must refer to a commit
-# * the signed tag commit must be the refs/dgit value
-# * the name in the signed tag must correspond to its ref name
-# * the tag name must be debian/<version> (massaged as needed)
-# * the signed tag has a suitable name
-# * the commit is a fast forward
-# + push the signed tag and new dgit branch to the actual repo
-#
-# If the destination repo does not already exist, we need to make
-# sure that we create it reasonably atomically, and also that
-# we don't every have a destination repo containing no refs at all
-# (because such a thing causes git-fetch-pack to barf). So then we
-# do as above, except:
-# - before starting, we take out our own lock for the destination repo
-# - we create a prospective new destination repo by making a copy
-# of _template
-# - we use the prospective new destination repo instead of the
-# actual new destination repo (since the latter doesn't exist)
-# - we set up a post-receive hook as well, which
-# + touches a stamp file
-# - after git-receive-pack exits, we
-# + check that the prospective repo contains a tag and head
-# + rename the prospective destination repo into place
-#
-# Cleanup strategy:
-# - We are crash-only
-# - Temporary working trees and their locks are cleaned up
-# opportunistically by a program which tries to take each lock and
-# if successful deletes both the tree and the lockfile
-# - Prospective working trees and their locks are cleaned up by
-# a program which tries to take each lock and if successful
-# deletes any prospective working tree and the lock (but not
-# of course any actual tree)
-# - It is forbidden to _remove_ the lockfile without removing
-# the corresponding temporary tree, as the lockfile is also
-# a stampfile whose presence indicates that there may be
-# cleanup to do
-
-use POSIX;
-use Fcntl qw(:flock);
-use File::Path qw(rmtree);
-
-open DEBUG, ">/dev/null" or die $!;
-
-our $package_re = '[0-9a-z][-+.0-9a-z]+';
-
-our $func;
-our $dgitrepos;
-our $package;
-our $suitesfile;
-our $realdestrepo;
-our $destrepo;
-our $workrepo;
-our $keyrings;
-our @lockfhs;
-our $debug='';
-
-#----- utilities -----
-
-sub debug {
- print DEBUG "$debug @_\n";
-}
-
-sub acquirelock ($$) {
- my ($lock, $must) = @_;
- my $fh;
- printf DEBUG "$debug locking %s %d\n", $lock, $must;
- for (;;) {
- close $fh if $fh;
- $fh = new IO::File $lock, ">" or die "open $lock: $!";
- my $ok = flock $fh, $must ? LOCK_EX : (LOCK_EX|LOCK_NB);
- if (!$ok) {
- die "flock $lock: $!" if $must;
- debug " locking $lock failed";
- return undef;
- }
- if (!stat $lock) {
- next if $! == ENOENT;
- die "stat $lock: $!";
- }
- my $want = (stat _)[1];
- stat $fh or die $!;
- my $got = (stat _)[1];
- last if $got == $want;
- }
- return $fh;
-}
-
-sub acquiretree ($$) {
- my ($tree, $must) = @_;
- my $fh = acquirelock("$tree.lock", $must);
- if ($fh) {
- push @lockfhs, $fh;
- rmtree $tree;
- }
- return $fh;
-}
-
-sub mkrepotmp () {
- my $tmpdir = "$dgitrepos/_tmp";
- return if mkdir $tmpdir;
- return if $! == EEXIST;
- die $!;
-}
-
-sub recorderror ($) {
- my ($why) = @_;
- my $w = $ENV{'DGIT_DRS_WORK'}; # we are in stunthook
- if (defined $w) {
- chomp $why;
- open ERR, ">", "$w/drs-error" or die $!;
- print ERR $why, "\n" or die $!;
- close ERR or die $!;
- return 1;
- }
- return 0;
-}
-
-sub reject ($) {
- my ($why) = @_;
- recorderror "reject: $why";
- die "dgit-repos-server: reject: $why\n";
-}
-
-sub debugcmd {
- if ($debug) {
- use Data::Dumper;
- local $Data::Dumper::Indent = 0;
- local $Data::Dumper::Terse = 1;
- debug "|".Dumper(\@_);
- }
-}
-
-sub runcmd {
- debugcmd @_;
- $!=0; $?=0;
- my $r = system @_;
- die "@_ $? $!" if $r;
-}
-
-#----- git-receive-pack -----
-
-sub fixmissing__git_receive_pack () {
- mkrepotmp();
- $destrepo = "$dgitrepos/_tmp/${package}_prospective";
- acquiretree($destrepo, 1);
- my $template = "$dgitrepos/_template";
- debug "fixmissing copy tempalate $template -> $destrepo";
- my $r = system qw(cp -a --), $template, $destrepo;
- !$r or die "create new repo failed failed: $r $!";
-}
-
-sub makeworkingclone () {
- mkrepotmp();
- $workrepo = "$dgitrepos/_tmp/${package}_incoming$$";
- acquiretree($workrepo, 1);
- runcmd qw(git clone -l -q --mirror), $destrepo, $workrepo;
-}
-
-sub setupstunthook () {
- my $prerecv = "$workrepo/hooks/pre-receive";
- my $fh = new IO::File $prerecv, O_WRONLY|O_CREAT|O_TRUNC, 0777
- or die "$prerecv: $!";
- print $fh <<END or die "$prerecv: $!";
-#!/bin/sh
-set -e
-exec $0 --pre-receive-hook $package
-END
- close $fh or die "$prerecv: $!";
- $ENV{'DGIT_DRS_WORK'}= $workrepo;
- $ENV{'DGIT_DRS_DEST'}= $destrepo;
- debug " stunt hook set up $prerecv";
-}
-
-sub maybeinstallprospective () {
- return if $destrepo eq $realdestrepo;
-
- if (open REJ, "<", "$workrepo/drs-error") {
- local $/ = undef;
- my $msg = <REJ>;
- REJ->error and die $!;
- print STDERR $msg;
- exit 1;
- } else {
- $!==&ENOENT or die $!;
- }
-
- debug " show-ref ($destrepo) ...";
-
- my $child = open SR, "-|";
- defined $child or die $!;
- if (!$child) {
- chdir $destrepo or die $!;
- exec qw(git show-ref);
- die $!;
- }
- my %got = qw(tag 0 head 0);
- while (<SR>) {
- chomp or die;
- debug " show-refs| $_";
- s/^\S*[1-9a-f]\S* (\S+)$/$1/ or die;
- my $wh =
- m{^refs/tags/} ? 'tag' :
- m{^refs/dgit/} ? 'head' :
- die;
- die if $got{$wh}++;
- }
- $!=0; $?=0; close SR or $?==256 or die "$? $!";
-
- debug "installprospective ?";
- die Dumper(\%got)." -- missing refs in new repo"
- if grep { !$_ } values %got;
-
- debug "install $destrepo => $realdestrepo";
- rename $destrepo, $realdestrepo or die $!;
- remove "$destrepo.lock" or die $!;
-}
-
-sub main__git_receive_pack () {
- makeworkingclone();
- setupstunthook();
- runcmd qw(git receive-pack), $workrepo;
- maybeinstallprospective();
-}
-
-#----- stunt post-receive hook -----
-
-our ($tagname, $tagval, $suite, $oldcommit, $commit);
-our ($version, %tagh);
-
-sub readupdates () {
- debug " updates ...";
- while (<STDIN>) {
- chomp or die;
- debug " upd.| $_";
- m/^(\S+) (\S+) (\S+)$/ or die "$_ ?";
- my ($old, $sha1, $refname) = ($1, $2, $3);
- if ($refname =~ m{^refs/tags/(?=debian/)}) {
- reject "pushing multiple tags!" if defined $tagname;
- $tagname = $'; #';
- $tagval = $sha1;
- reject "tag $tagname already exists -".
- " not replacing previously-pushed version"
- if $old =~ m/[^0]/;
- } elsif ($refname =~ m{^refs/dgit/}) {
- reject "pushing multiple heads!" if defined $suite;
- $suite = $'; #';
- $oldcommit = $old;
- $commit = $sha1;
- } else {
- reject "pushing unexpected ref!";
- }
- }
- STDIN->error and die $!;
-
- reject "push is missing tag ref update" unless defined $tagname;
- reject "push is missing head ref update" unless defined $suite;
- debug " updates ok.";
-}
-
-sub parsetag () {
- debug " parsetag...";
- open PT, ">dgit-tmp/plaintext" or die $!;
- open DS, ">dgit-tmp/plaintext.asc" or die $!;
- open T, "-|", qw(git cat-file tag), $tagval or die $!;
- for (;;) {
- $!=0; $_=<T>; defined or die $!;
- print PT or die $!;
- if (m/^(\S+) (.*)/) {
- push @{ $tagh{$1} }, $2;
- } elsif (!m/\S/) {
- last;
- } else {
- die;
- }
- }
- $!=0; $_=<T>; defined or die $!;
- m/^($package_re) release (\S+) for \S+ \((\S+)\) \[dgit\]$/ or
- reject "tag message not in expected format";
-
- die unless $1 eq $package;
- $version = $2;
- die "$3 != $suite " unless $3 eq $suite;
-
- for (;;) {
- print PT or die $!;
- $!=0; $_=<T>; defined or die "missing signature? $!";
- last if m/^-----BEGIN PGP/;
- }
- for (;;) {
- print DS or die $!;
- $!=0; $_=<T>;
- last if !defined;
- }
- T->error and die $!;
- close PT or die $!;
- close DS or die $!;
- debug " parsetag ok.";
-}
-
-sub checksig_keyring ($) {
- my ($keyringfile) = @_;
- # returns primary-keyid if signed by a key in this keyring
- # or undef if not
- # or dies on other errors
-
- my $ok = undef;
-
- debug " checksig keyring $keyringfile...";
-
- our @cmd = (qw(gpgv --status-fd=1 --keyring),
- $keyringfile,
- qw(dgit-tmp/plaintext.asc dgit-tmp/plaintext));
- debugcmd @cmd;
-
- open P, "-|", @cmd
- or die $!;
-
- while (<P>) {
- next unless s/^\[GNUPG:\] //;
- chomp or die;
- debug " checksig| $_";
- my @l = split / /, $_;
- if ($l[0] eq 'NO_PUBKEY') {
- last;
- } elsif ($l[0] eq 'VALIDSIG') {
- my $sigtype = $l[9];
- $sigtype eq '00' or reject "signature is not of type 00!";
- $ok = $l[10];
- die unless defined $ok;
- last;
- }
- }
- close P;
-
- debug sprintf " checksig ok=%d", !!$ok;
-
- return $ok;
-}
-
-sub dm_txt_check ($$) {
- my ($keyid, $dmtxtfn) = @_;
- debug " dm_txt_check $keyid $dmtxtfn";
- open DT, '<', $dmtxtfn or die "$dmtxtfn $!";
- while (<DT>) {
- m/^fingerprint:\s+$keyid$/oi
- ..0 or next;
- if (s/^allow:/ /i..0) {
- } else {
- m/^./
- or reject "key $keyid missing Allow section in permissions!";
- next;
- }
- # in right stanza...
- s/^[ \t]+//
- or reject "package $package not allowed for key $keyid";
- # in allow field...
- s/\([^()]+\)//;
- s/\,//;
- chomp or die;
- debug " dm_txt_check allow| $_";
- foreach my $p (split /\s+/) {
- if ($p eq $package) {
- # yay!
- debug " dm_txt_check ok";
- return;
- }
- }
- }
- DT->error and die $!;
- close DT or die $!;
- reject "key $keyid not in permissions list although in keyring!";
-}
-
-sub verifytag () {
- foreach my $kas (split /:/, $keyrings) {
- debug "verifytag $kas...";
- $kas =~ s/^([^,]+),// or die;
- my $keyid = checksig_keyring $1;
- if (defined $keyid) {
- if ($kas =~ m/^a$/) {
- debug "verifytag a ok";
- return; # yay
- } elsif ($kas =~ m/^m([^,]+)$/) {
- dm_txt_check($keyid, $1);
- debug "verifytag m ok";
- return;
- } else {
- die;
- }
- }
- }
- reject "key not found in keyrings";
-}
-
-sub checksuite () {
- debug "checksuite ($suitesfile)";
- open SUITES, "<", $suitesfile or die $!;
- while (<SUITES>) {
- chomp;
- next unless m/\S/;
- next if m/^\#/;
- s/\s+$//;
- return if $_ eq $suite;
- }
- die $! if SUITES->error;
- reject "unknown suite";
-}
-
-sub tagh1 ($) {
- my ($tag) = @_;
- my $vals = $tagh{$tag};
- reject "missing header $tag in signed tag object" unless $vals;
- reject "multiple headers $tag in signed tag object" unless @$vals == 1;
- return $vals->[0];
-}
-
-sub checks () {
- debug "checks";
- checksuite();
- tagh1('type') eq 'commit' or reject "tag refers to wrong kind of object";
- tagh1('object') eq $commit or reject "tag refers to wrong commit";
- tagh1('tag') eq $tagname or reject "tag name in tag is wrong";
-
- my $v = $version;
- $v =~ y/~:/_%/;
-
- debug "translated version $v";
- $tagname eq "debian/$v" or die;
-
- # check that our ref is being fast-forwarded
- debug "oldcommit $oldcommit";
- if ($oldcommit =~ m/[^0]/) {
- $?=0; $!=0; my $mb = `git merge-base $commit $oldcommit`;
- chomp $mb;
- $mb eq $oldcommit or reject "not fast forward on dgit branch";
- }
-}
-
-sub onwardpush () {
- my @cmd = (qw(git send-pack), $destrepo,
- "$commit:refs/dgit/$suite",
- "$tagval:refs/tags/$tagname");
- debugcmd @cmd;
- $!=0;
- my $r = system @cmd;
- !$r or die "onward push failed: $r $!";
-}
-
-sub stunthook () {
- debug "stunthook";
- chdir $workrepo or die "chdir $workrepo: $!";
- mkdir "dgit-tmp" or $!==EEXIST or die $!;
- readupdates();
- parsetag();
- verifytag();
- checks();
- onwardpush();
- debug "stunthook done.";
-}
-
-#----- git-upload-pack -----
-
-sub fixmissing__git_upload_pack () {
- $destrepo = "$dgitrepos/_empty";
- my $lfh = acquiretree($destrepo,1);
- return if stat $destrepo;
- die $! unless $!==ENOENT;
- rmtree "$destrepo.new";
- umask 022;
- runcmd qw(git init --bare --quiet), "$destrepo.new";
- rename "$destrepo.new", $destrepo or die $!;
- unlink "$destrepo.lock" or die $!;
- close $lfh;
-}
-
-sub main__git_upload_pack () {
- runcmd qw(git upload-pack), $destrepo;
-}
-
-#----- arg parsing and main program -----
-
-sub argval () {
- die unless @ARGV;
- my $v = shift @ARGV;
- die if $v =~ m/^-/;
- return $v;
-}
-
-sub parseargsdispatch () {
- die unless @ARGV;
-
- delete $ENV{'GIT_DIR'}; # if not run via ssh, our parent git process
- delete $ENV{'GIT_PREFIX'}; # sets these and they mess things up
-
- if ($ENV{'DGIT_DRS_DEBUG'}) {
- $debug='=';
- open DEBUG, ">&STDERR" or die $!;
- }
-
- if ($ARGV[0] eq '--pre-receive-hook') {
- if ($debug) { $debug.="="; }
- shift @ARGV;
- @ARGV == 1 or die;
- $package = shift @ARGV;
- defined($suitesfile = $ENV{'DGIT_DRS_SUITES'}) or die;
- defined($workrepo = $ENV{'DGIT_DRS_WORK'}) or die;
- defined($destrepo = $ENV{'DGIT_DRS_DEST'}) or die;
- defined($keyrings = $ENV{'DGIT_DRS_KEYRINGS'}) or die $!;
- open STDOUT, ">&STDERR" or die $!;
- eval {
- stunthook();
- };
- if ($@) {
- recorderror "$@" or die;
- die $@;
- }
- exit 0;
- }
-
- $ENV{'DGIT_DRS_SUITES'} = argval();
- $ENV{'DGIT_DRS_KEYRINGS'} = argval();
- $dgitrepos = argval();
-
- die unless @ARGV==1 && $ARGV[0] eq '--ssh';
-
- my $cmd = $ENV{'SSH_ORIGINAL_COMMAND'};
- $cmd =~ m{
- ^
- (?: \S* / )?
- ( [-0-9a-z]+ )
- \s+
- (?: \S* / )?
- ($package_re) \.git
- $
- }ox
- or reject "command string not understood";
- my $method = $1;
- $package = $2;
- $realdestrepo = "$dgitrepos/$package.git";
-
- my $funcn = $method;
- $funcn =~ y/-/_/;
- my $mainfunc = $main::{"main__$funcn"};
-
- reject "unknown method" unless $mainfunc;
-
- if (stat $realdestrepo) {
- $destrepo = $realdestrepo;
- } else {
- $! == ENOENT or die "stat dest repo $destrepo: $!";
- debug " fixmissing $funcn";
- my $fixfunc = $main::{"fixmissing__$funcn"};
- &$fixfunc;
- }
-
- debug " running main $funcn";
- &$mainfunc;
-}
-
-sub unlockall () {
- while (my $fh = pop @lockfhs) { close $fh; }
-}
-
-sub cleanup () {
- unlockall();
- if (!chdir "$dgitrepos/_tmp") {
- $!==ENOENT or die $!;
- return;
- }
- foreach my $lf (<*.lock>) {
- my $tree = $lf;
- $tree =~ s/\.lock$//;
- next unless acquiretree($tree, 0);
- remove $lf or warn $!;
- unlockall();
- }
-}
-
-parseargsdispatch();
-cleanup();
diff --git a/dgit-sponsorship.7.pod b/dgit-sponsorship.7.pod
new file mode 100644
index 0000000..8d5b72d
--- /dev/null
+++ b/dgit-sponsorship.7.pod
@@ -0,0 +1,313 @@
+=head1 NAME
+
+dgit-sponsorship - tutorial for Debian upload sponsorship, using git
+
+=head1 INTRODUCTION AND SCOPE
+
+This tutorial describes how a Debian sponsored contributor
+and
+a sponsoring DD (or DM)
+can collaborate and publish using git.
+
+The sponsor must to be intending to use dgit for the upload.
+(If the sponsor does not use dgit,
+it is not possible to properly publish
+a sponsee's git branch.)
+
+It is best if the sponsee also uses dgit;
+but also covered (later on) is the case where
+the sponsee provides a proposed upload in source package form,
+but the sponsor would like to work in git.
+
+This tutorial does not provide a checklist for the sponsor's review.
+Both contributors are expected to be familiar with Debian
+packaging and Debian's processes, and with git.
+
+=head1 SPONSEE WORKFLOW
+
+This section is addressed to the sponsee:
+
+=head2 General
+
+You should prepare the package as if you were going
+to upload it with C<dgit push> yourself.
+
+For a straightforward NMU, consult L<dgit-nmu-simple(7)>.
+
+If you are the (prospective) maintainer,
+you can adopt any suitable (dgit-compatible)
+git workflow.
+The L<dgit-maint-*(7)> tutorials describe some of the possibilities.
+
+=head2 Upload preparation
+
+You should go through all of the steps
+a self-uploading maintainer would do,
+including building for ad hoc tests,
+and checking via a formal build (eg using C<dgit sbuild>)
+that the package builds on sid (or the target release).
+
+At the point where you would,
+if you were a DD,
+do the actual upload
+by running dgit push,
+you hand off to your sponsor.
+
+If you were going to use one of the
+C<--quilt=>
+options to dgit, or
+C<dgit --gbp> or C<dgit --dpm>,
+you must specify that in your handoff email - see below.
+
+=head2 git+origs based handoff
+
+The elements of the handoff consists of:
+
+=over
+
+=item *
+
+The git branch.
+
+=item *
+
+Any .orig tarballs which will be needed,
+or sample git-archive(1)
+or gbp-buildpackage(1)
+command(s) to generate them.
+
+=item *
+
+A sample dgit push command, containing
+any dgit --quilt=, --gbp or --dpm option needed
+
+=item *
+
+Plus of course all the usual information about the state
+of the package,
+any caveats or areas you would like the sponsor to focus their review,
+constraints about upload timing, etc.
+
+=back
+
+If the handoff is done by email,
+the elements above should be a in a single, signed, message.
+This could be an RFS submission
+against the sponsorship-requests pseudo-package.
+
+=head3 git branch
+
+=over 4
+
+The sponsee should push their HEAD as a git branch
+to any suitable git server.
+They can use their own git server;
+alioth is another possibility.
+
+The branch names used by the sponsee on their local machine,
+and on the server, do not matter.
+
+Instead, the sponsee should include the
+git commit id of their HEAD
+in their handover email.
+
+=back
+
+=head3 orig tarballs
+
+=over 4
+
+If there are any .origs that are not in the archive already,
+the sponsor will need them as part of the upload.
+
+If the sponsee generated these tarballs with git-archive(1)
+or gbp-buildpackage(1),
+they can simply include a sample invocation of git-archive(1)
+or ensure that a suitable gbp.conf is present
+in the source package
+to generate the tarball.
+
+Otherwise, the simplest approach is to
+commit the orig tarballs
+with pristine-tar(1), e.g.
+
+=over 4
+
+ % pristine-tar commit ../foo_1.2.3.orig.tar.xz upstream/1.2.3
+
+=back
+
+and be sure to push the pristine-tar branch.
+If you are using git-buildpackage(1), just pass
+I<--git-pristine-tar> and I<--git-pristine-tar-commit>.
+
+Alternatively,
+the sponsee can put them on a suitable webserver,
+or attach to the e-mail,
+if they are small.
+
+The sponsee should quote sha256sums of the .origs in their
+handoff email,
+unless they supplied commands to generate them.
+
+=back
+
+=head3 quilt options
+
+=over 4
+
+Some workflows involve git branches which are not natively
+dgit-compatible.
+Normally dgit will convert them as needed, during push.
+
+Supply a sample "dgit push" command
+including any
+C<--gbp> (aka C<--quilt=gbp>),
+C<--dpm> (aka C<--quilt=dpm>),
+or other C<--quilt=> option
+they need to use.
+e.g.
+
+=over 4
+
+ % dgit --gbp push
+
+=back
+
+=back
+
+=head1 SPONSOR WORKFLOW
+
+This part is addressed to the sponsor:
+
+=head2 Receiving and validating the sponsorship request
+
+You should check the signature on the email.
+
+Use C<git fetch> or C<git clone> to obtain the git branch
+prepared by your sponsee,
+and obtain any .origs mentioned by the sponsee
+(to extract .origs committed with pristine-tar,
+you can use origtargz(1),
+or use "gbp clone --pristine-tar".)
+
+Check the git commit ID of the sponsee's branch tip,
+and the sha256sums of the .origs,
+against the handoff email.
+
+Now you can check out the branch tip,
+and do your substantive review.
+
+=head2 Dealing with branches that want --quilt=
+
+If your sponsee mentioned a C<--quilt>
+option, and you don't want to grapple with their preferred tree format,
+you can convert their tree into the standard dgit view:
+
+=over 4
+
+ % dgit -wgf --quilt=foo --dgit-view-save=unquilted quilt-fixup
+ % git checkout unquilted
+
+=back
+
+You should check that what you're looking at is a descendant of
+the sponsee's branch.
+
+=head2 Some hints which may help the review
+
+C<dgit fetch sid> will get you an up-to-date
+C<refs/remotes/dgit/dgit/sid>
+showing what's in the archive already.
+
+C<dgit -wgf --damp-run push>
+will check that dgit can build an appropriate source package.
+
+There is no need to run debdiff.
+dgit will not upload anything that doesn't unpack
+to exactly the git commit you are pushing,
+so you can rely on what you see in C<git diff>.
+
+=head2 Doing the upload
+
+When you have completed your source review,
+and use
+C<dgit -wgf [--quilt=...] sbuild -A -C>
+or similar, to to the build, and then
+C<dgit -wgf [--quilt=...] push>
+to do the upload.
+
+Check whether the sponsee made a debian/I<version> tag.
+If they did,
+ensure you have their tag in the repository you are pushing from,
+or pass C<--no-dep14tag>.
+This avoids identically named, non-identical tags,
+which can be confusing.
+
+(It is possible to upload from
+the quilt-cache dgit view.
+If you want to do this,
+B<do not> pass the C<--quilt> or C<--gbp> or C<--dpm> options again,
+and B<do> pass C<--no-dep14tag>,
+since the debian/I<version> tag
+should go on the sponsee's branch.)
+
+If this was the first upload done with dgit,
+you may need to pass
+C<--overwrite>
+to dgit.
+
+
+=head1 SPONSORING A NON-GIT-USING SPONSEE
+
+This part is addressed to the sponsor:
+
+If your sponsee does not use git,
+you can still do your review with git,
+and use dgit for the upload.
+
+Your sponsee will provide you with a source package:
+that is, a .dsc and the files it refers to.
+Obtain these files, and check signatures as appropriate.
+Then:
+
+=over 4
+
+ % dgit clone PACKAGE
+ % cd PACKAGE
+ % dgit import-dsc /path/to/sponsee's.dsc +sponsee
+ % git checkout sponsee
+
+=back
+
+Or for an entirely new package:
+
+=over 4
+
+ % mkdir PACKAGE
+ % cd PACKAGE
+ % git init
+ % dgit -pPACKAGE import-dsc /path/to/sponsee's.dsc +sponsee
+
+=back
+
+This will leave you looking at the sponsee's package,
+formatted as a dgit branch.
+
+When you have finished your review and your tests,
+you can do the
+dgit sbuild and
+dgit push directly from the "sponsee" branch.
+
+You will need to pass
+C<--overwrite>
+to dgit push for every successive upload.
+This disables a safety catch which would normally spot
+situations where changes are accidentally lost.
+When your sponsee is sending you source packages -
+perhaps multiple source pacakges with the same version number -
+these safety catches are inevitably ineffective.
+
+=head1 SEE ALSO
+
+dgit(1), dgit(7), dgit-nmu-simple(7), dgit-maint-*(7)
diff --git a/dgit-user.7.pod b/dgit-user.7.pod
new file mode 100644
index 0000000..aacdf4d
--- /dev/null
+++ b/dgit-user.7.pod
@@ -0,0 +1,432 @@
+=head1 NAME
+
+dgit-user - making and sharing changes to Debian packages, with git
+
+=head1 INTRODUCTION
+
+dgit lets you fetch the source code to every package on your
+system
+as if your distro used git to maintain all of it.
+
+You can then edit it,
+build updated binary packages (.debs)
+and install and run them.
+You can also share your work with others.
+
+This tutorial gives some recipes and hints for this.
+It assumes you have basic familiarity with git.
+It does not assume any initial familiarity with
+Debian's packaging processes.
+
+If you are a package maintainer within Debian; a DM or DD;
+and/or a sponsee:
+this tutorial is not for you.
+Try L<dgit-nmu-simple(7)>, L<dgit-maint-*(7)>,
+or L<dgit(1)> and L<dgit(7)>.
+
+=head1 SUMMARY
+
+(These runes will be discussed later.)
+
+=over 4
+
+ % dgit clone glibc jessie,-security
+ % cd glibc
+ % wget 'https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=28250;mbox=yes;msg=89' | patch -p1 -u
+ % git commit -a -m 'Fix libc lost output bug'
+ % gbp dch -S --since=dgit/dgit/sid --ignore-branch --commit
+ % sudo apt-get build-dep glibc
+ % dpkg-buildpackage -uc -b
+ % sudo dpkg -i ../libc6_*.deb
+
+=back
+
+Occasionally:
+
+=over 4
+
+ % git clean -xdf
+ % git reset --hard
+
+=back
+
+Later:
+
+=over 4
+
+ % cd glibc
+ % dgit pull jessie,-security
+ % gbp dch -S --since=dgit/dgit/sid --ignore-branch --commit
+ % dpkg-buildpackage -uc -b
+ % sudo dpkg -i ../libc6_*.deb
+
+=back
+
+=head1 FINDING THE RIGHT SOURCE CODE - DGIT CLONE
+
+=over 4
+
+ % dgit clone glibc jessie,-security
+ % cd glibc
+
+=back
+
+dgit clone needs to be told the source package name
+(which might be different to the binary package name,
+which was the name you passed to "apt-get install")
+and the codename or alias of the Debian release
+(this is called the "suite").
+
+=head2 Finding the source package name
+
+For many packages, the source package name is obvious.
+Otherwise, if you know a file that's in the package,
+you can look it up with dpkg:
+
+=over 4
+
+ % dpkg -S /lib/i386-linux-gnu/libc.so.6
+ libc6:i386: /lib/i386-linux-gnu/libc.so.6
+ % dpkg -s libc6:i386
+ Package: libc6
+ Status: install ok installed
+ ...
+ Source: glibc
+
+=back
+
+(In this example,
+libc6 is a "multi-arch: allowed" package,
+ which means that it exists in several different builds
+ for different architectures.
+That's where C<:i386> comes from.)
+
+=head2 Finding the Debian release (the "suite")
+
+Internally,
+Debian (and derived) distros normally refer to their releases by codenames.
+Debian also has aliases which refer to the current stable release etc.
+So for example, at the time of writing
+Debian C<jessie> (Debian 8) is Debian C<stable>; and
+the current version of Ubuntu is C<yakkety> (Yakkety Yak, 16.10).
+You can specify either
+the codename C<jessie> or the alias C<stable>.
+If you don't say, you get C<sid>,
+which is Debian C<unstable> - the main work-in progress branch.
+
+If you don't know what you're running, try this:
+
+=over 4
+
+ % grep '^deb' /etc/apt/sources.list
+ deb http://the.earth.li/debian/ jessie main non-free contrib
+ ...
+ %
+
+=back
+
+For Debian, you should add C<,-security>
+to the end of the suite name,
+unless you're on unstable or testing.
+Hence, in our example
+C<jessie> becomes C<jessie,-security>.
+(Yes, with a comma.)
+
+=head1 WHAT DGIT CLONE PRODUCES
+
+=head2 What branches are there
+
+dgit clone will give you a new working tree,
+and arrange for you to be on a branch named like
+C<dgit/jessie,-security> (yes, with a comma in the branch name).
+
+For each release (like C<jessie>)
+there is a tracking branch for the contents of the archive, called
+C<remotes/dgit/dgit/jessie>
+(and similarly for other suites). This can be updated with
+C<dgit fetch jessie>.
+This, the I<remote suite branch>,
+is synthesized by your local copy of dgit.
+It is fast forwarding.
+
+Debian separates out the security updates, into C<*-security>.
+Telling dgit C<jessie,-security> means that it should include
+any updates available in C<jessie-security>.
+The comma notation is a request to dgit to track jessie,
+or jessie-security if there is an update for the package there.
+
+(You can also dgit fetch in a tree that wasn't made by dgit clone.
+If there's no C<debian/changelog>
+you'll have to supply a C<-p>I<package> option to dgit fetch.)
+
+=head2 What kind of source tree do you get
+
+If the Debian package is based on some upstream release,
+the code layout should be like the upstream version.
+You should find C<git grep> helpful to find where to edit.
+
+The package's Debian metadata and the scripts for building binary
+packages are under C<debian/>.
+C<debian/control>, C<debian/changelog> and C<debian/rules> are the
+starting points.
+The Debian Policy Manual has most of the in-depth
+technical details.
+
+For many Debian packages,
+there will also be some things in C<debian/patches/>.
+It is best to ignore these.
+Insofar as they are relevant
+the changes there will have been applied to the actual files,
+probably by means of actual comments in the git history.
+The contents of debian/patches are ignored
+when building binaries
+from dgitish git branches.
+
+(For Debian afficionados:
+the git trees that come out of dgit are
+"patches-applied packaging branches
+without a .pc directory".)
+
+=head2 What kind of history you get
+
+If you're lucky, the history will be a version of,
+or based on,
+the Debian maintainer's own git history,
+or upstream's git history.
+
+But for many packages the real git history
+does not exist,
+or has not been published in a dgitish form.
+So you may find that the history is a rather short
+history invented by dgit.
+
+dgit histories often contain automatically-generated commits,
+including commits which make no changes but just serve
+to make a rebasing branch fast-forward.
+This is particularly true of
+combining branches like
+C<jessie,-security>.
+
+If the package maintainer is using git then
+after dgit clone
+you may find that there is a useful C<vcs-git> remote
+referring to the Debian package maintainer's repository
+for the package.
+You can see what's there with C<git fetch vcs-git>.
+But use what you find there with care:
+Debian maintainers' git repositories often have
+contents which are very confusing and idiosyncratic.
+In particular, you may need to manually apply the patches
+that are in debian/patches before you do anything else!
+
+=head1 BUILDING
+
+=head2 Always commit before building
+
+=over 4
+
+ % wget 'https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=28250;mbox=yes;msg=89' | patch -p1 -u
+ % git commit -a -m 'Fix libc lost output bug'
+
+=back
+
+Debian package builds are often quite messy:
+they may modify files which are also committed to git,
+or leave outputs and temporary files not covered by C<.gitignore>.
+
+If you always commit,
+you can use
+
+=over 4
+
+ % git clean -xdf
+ % git reset --hard
+
+=back
+
+to tidy up after a build.
+(If you forgot to commit, don't use those commands;
+instead, you may find that you can use C<git add -p>
+to help commit what you actually wanted to keep.)
+
+These are destructive commands which delete all new files
+(so you B<must> remember to say C<git add>)
+and throw away edits to every file
+(so you B<must> remember to commit).
+
+=head2 Update the changelog (at least once) before building
+
+=over 4
+
+ % gbp dch -S --since=dgit/dgit/sid --ignore-branch --commit
+
+=back
+
+The binaries you build will have a version number which ultimately
+comes from the C<debian/changelog>.
+You want to be able to tell your
+binaries apart from your distro's.
+
+So you should update C<debian/changelog>
+to add a new stanza at the top,
+for your build.
+
+This rune provides an easy way to do this.
+It adds a new changelog
+entry with an uninformative message and a plausible version number
+(containing a bit of your git commit id).
+
+If you want to be more sophisticated,
+the package C<dpkg-dev-el> has a good Emacs mode
+for editing changelogs.
+Alternatively, you could edit the changelog with another text editor,
+or run C<dch> or C<gbp dch> with different options.
+Choosing a good version number is slightly tricky and
+a complete treatment is beyond the scope of this tutorial.
+
+=head2 Actually building
+
+=over 4
+
+ % sudo apt-get build-dep glibc
+ % dpkg-buildpackage -uc -b
+
+=back
+
+apt-get build-dep installs the build dependencies according to the
+official package, not your modified one. So if you've changed the
+build dependencies you might have to install some of them by hand.
+
+dpkg-buildpackage is the primary tool for building a Debian source
+package.
+C<-uc> means not to pgp-sign the results.
+C<-b> means build all binary packages,
+but not to build a source package.
+
+=head1 INSTALLING
+
+=head2 Debian Jessie or older
+
+=over 4
+
+ % sudo dpkg -i ../libc6_*.deb
+
+=back
+
+You can use C<dpkg -i> to install the
+.debs that came out of your package.
+
+If the dependencies aren't installed,
+you will get an error, which can usually be fixed with
+C<apt-get -f install>.
+
+=head2 Debian Stretch or newer
+
+=over 4
+
+ % sudo apt install ../libc6_*.deb
+
+=back
+
+=head1 Multiarch
+
+If you're working on a library package and your system has multiple
+architectures enabled,
+you may see something like this:
+
+=over 4
+
+ dpkg: error processing package libpcre3-dev:amd64 (--configure):
+ package libpcre3-dev:amd64 2:8.39-3~3.gbp8f25f5 cannot be configured because libpcre3-dev:i386 is at a different version (2:8.39-2)
+
+=back
+
+The multiarch system used by Debian requires each package which is
+present for multiple architectures to be exactly the same across
+all the architectures for which it is installed.
+
+The proper solution
+is to build the package for all the architectures you
+have enabled.
+You'll need a chroot for each of the secondary architectures.
+This is somewhat tiresome,
+even though Debian has excellent tools for managing chroots.
+C<sbuild-createchroot> from the sbuild package is a
+good starting point.
+
+Otherwise you could deinstall the packages of interest
+for those other architectures
+with something like C<dpkg --remove libpcre3:i386>.
+
+If neither of those are an option,
+your desperate last resort is to try
+using the same version number
+as the official package for your own package.
+(The version is controlled by C<debian/changelog> - see above).
+This is not ideal because it makes it hard to tell what is installed,
+and because it will mislead and confuse apt.
+
+With the "same number" approach you may still get errors like
+
+=over 4
+
+trying to overwrite shared '/usr/include/pcreposix.h', which is different from other instances of package libpcre3-dev
+
+=back
+
+but passing C<--force-overwrite> to dpkg will help
+- assuming you know what you're doing.
+
+=head1 SHARING YOUR WORK
+
+The C<dgit/jessie,-security> branch (or whatever) is a normal git branch.
+You can use C<git push> to publish it on any suitable git server.
+
+Anyone who gets that git branch from you
+will be able to build binary packages (.deb)
+just as you did.
+
+If you want to contribute your changes back to Debian,
+you should probably send them as attachments to
+an email to the
+L<Debian Bug System|https://bugs.debian.org/>
+(either a followup to an existing bug, or a new bug).
+Patches in C<git-format-patch> format are usually very welcome.
+
+=head2 Source packages
+
+The
+git branch is not sufficient to build a source package
+the way Debian does.
+Source packages are somewhat awkward to work with.
+Indeed many plausible git histories or git trees
+cannot be converted into a suitable source package.
+So I recommend you share your git branch instead.
+
+If a git branch is not enough, and
+you need to provide a source package
+but don't care about its format/layout
+(for example because some software you have consumes source packages,
+not git histories)
+you can use this recipe to generate a C<3.0 (native)>
+source package, which is just a tarball
+with accompanying .dsc metadata file:
+
+=over 4
+
+ % echo '3.0 (native)' >debian/source/format
+ % git commit -m 'switch to native source format' debian/source/format
+ % dgit -wgf build-source
+
+=back
+
+If you need to provide a good-looking source package,
+be prepared for a lot more work.
+You will need to read much more, perhaps starting with
+L<dgit-nmu-simple(7)>,
+L<dgit-sponsorship(7)> or
+L<dgit-maint-*(7)>
+
+=head1 SEE ALSO
+
+dgit(1), dgit(7)
diff --git a/dgit.1 b/dgit.1
index 4b27cda..2233d95 100644
--- a/dgit.1
+++ b/dgit.1
@@ -1,3 +1,4 @@
+'\" t
.TH dgit 1 "" "Debian Project" "dgit"
.SH NAME
dgit \- git integration with the Debian archive
@@ -27,20 +28,26 @@ dgit \- git integration with the Debian archive
[\fIdgit\-opts\fP] \fIaction\fR ...
.SH DESCRIPTION
.B dgit
-treats the Debian archive as a version control system, and
-bidirectionally gateways between the archive and git. The git view of
-the package can contain the usual upstream git history, and will be
-augmented by commits representing uploads done by other developers not
-using dgit. This git history is stored in a canonical location known
-as
-.B dgit-repos
-which lives outside the Debian archive (currently, on Alioth).
-
-The usual workflow is: 1. clone or fetch; 2. make and commit changes
-in git as desired; 3. run dgit build, dgit sbuild or dgit
-build-source, or generate the source and binary packages for upload
-some other way; 4. do pre-upload tests of the proposed upload; 5. run
-dgit push.
+allows you to treat the Debian archive as if it were a git
+repository.
+
+This is the command line reference.
+Please read the tutorial(s):
+.TS
+lb l.
+dgit-user(7) for users: editing, building and sharing packages
+dgit-nmu-simple(7) for DDs: doing a straightforward NMU
+dgit-maint-native(7) for maintainers of Debian-native packages
+dgit-maint-merge(7) for maintainers who want a pure git workflow
+dgit-maint-gbp(7) for maintainers already using git-buildpackage
+dgit-sponsorship(7) for sponsors and sponsored contributors
+.TE
+.LP
+See \fBdgit(7)\fP for detailed information about the data
+model,
+common problems likely to arise with certain kinds of package,
+etc.
+.SH OPERATIONS
.TP
\fBdgit clone\fR \fIpackage\fP [\fIsuite\fP] [\fB./\fP\fIdir|\fB/\fP\fIdir\fR]
Consults the archive and dgit-repos to construct the git view of
@@ -66,6 +73,11 @@ for the distro to which
.I suite
belongs.
+.I suite
+may be a combination of several underlying suites in the form
+.IR mainsuite \fB,\fR subsuite ...;
+see COMBINED SUITES in dgit(7).
+
For your convenience, the
.B vcs-git
remote will be set up from the package's Vcs-Git field, if there is
@@ -83,6 +95,7 @@ then dgit fetch defaults to
.IR suite ;
otherwise it parses debian/changelog and uses the suite specified
there.
+suite may be a combined suite, as for clone.
.TP
\fBdgit pull\fR [\fIsuite\fP]
Does dgit fetch, and then merges the new head of the remote tracking
@@ -93,7 +106,7 @@ into the current branch.
\fBdgit build\fR ...
Runs
.B dpkg-buildpackage
-with some suitable options. Options and argumments after build
+with some suitable options. Options and arguments after build
will be passed on to dpkg-buildpackage. It is not necessary to use
dgit build when using dgit; it is OK to use any approach which ensures
that the generated source package corresponds to the relevant git
@@ -123,30 +136,42 @@ Print a usage summary.
Constructs the source package, uses
.B sbuild
to do a binary build, and uses mergechanges to merge the source and
-binary changes files. Options and argumments after sbuild will be
-passed on to sbuild. Changes files matching
-.IB package _ version _*.changes
-in the parent directory will be removed; the output is left in
+binary changes files. Options and arguments after sbuild will be
+passed on to sbuild.
+The output is left in
.IR package \fB_\fR version \fB_multi.changes\fR.
-
+.IP
+Note that by default
+sbuild does not build arch-independent packages.
+You probably want to pass -A, to request those.
+.IP
Tagging, signing and actually uploading should be left to dgit push.
.TP
-\fBdgit git-build\fR ...
+\fBdgit gbp-build\fR ...
Runs
.B git-buildpackage
-with some suitable options. Options and argumments after git-build
+with some suitable options. Options and arguments after gbp-build
will be passed on to git-buildpackage.
+By default this uses \-\-quilt=gbp, so HEAD should be a
+git-buildpackage style branch, not a patches-applied branch.
+
Tagging, signing and actually uploading should be left to dgit push.
.TP
\fBdgit push\fR [\fIsuite\fP]
Does an `upload', pushing the current HEAD to the archive (as a source
package) and to dgit-repos (as git commits). The package must already
have been built ready for upload, with the .dsc and .changes
-left in the parent directory.
+left in the parent directory. It is normally best to do the build
+with dgit too (eg with dgit sbuild): some existing build tools pass
+unhelpful options to dpkg-source et al by default, which can result in
+the built source package not being identical to the git tree.
In more detail: dgit push checks that the current HEAD corresponds to
the .dsc. It then pushes the HEAD to the suite's dgit-repos branch,
+adjusts the .changes to include any .origs which the archive lacks
+and exclude .origs which the archive has
+(so -sa and -sd are not needed when building for dgit push),
makes a signed git tag, edits the .dsc to contain the dgit metadata
field, runs debsign to sign the upload (.dsc and .changes), pushes the
signed tag, and finally uses dput to upload the .changes to the
@@ -155,16 +180,22 @@ archive.
dgit push always uses the package, suite and version specified in the
debian/changelog and the .dsc, which must agree. If the command line
specifies a suite then that must match too.
-
-If dgit push fails while uploading, it is fine to simply retry the
-dput on the .changes file at your leisure.
.TP
\fBdgit rpush\fR \fIbuild-host\fR\fB:\fR\fIbuild-dir\fR [\fIpush args...\fR]
Pushes the contents of the specified directory on a remote machine.
This is like running dgit push on build-host with build-dir as the
current directory; however, signing operations are done on the
invoking host. This allows you to do a push when the system which has
-the source code and the build outputs has no access to the key.
+the source code and the build outputs has no access to the key:
+
+.TS
+l l.
+1. Clone on build host (dgit clone)
+2. Edit code on build host (edit, git commit)
+3. Build package on build host (dgit build)
+4. Test package on build host or elsewhere (dpkg -i, test)
+5. Upload by invoking dgit rpush on host with your GPG key.
+.TE
However, the build-host must be able to ssh to the dgit repos. If
this is not already the case, you must organise it separately, for
@@ -182,25 +213,145 @@ You will need similar enough versions of dgit on the build-host and
the invocation host. The build-host needs gnupg installed, with your
public key in its keyring (but not your private key, obviously).
.TP
+.B dgit setup-new-tree
+Configure the current working tree the way that dgit clone would have
+set it up. Like running
+.BR "dgit setup-useremail" ,
+.B setup-mergechangelogs
+and
+.B setup-gitattributes
+(but only does each thing if dgit is configured to do it automatically).
+You can use these in any git repository, not just ones used with
+the other dgit operations.
+.TP
+.B dgit setup-useremail
+Set the working tree's user.name and user.email from the
+distro-specific dgit configuration
+.RB ( dgit-distro. \fIdistro\fR .user-name " and " .user-email ),
+or DEBFULLNAME or DEBEMAIL.
+.TP
+.B dgit setup-mergechangelogs
+Configures a git merge helper for the file
+.B debian/changelog
+which uses
+.BR dpkg-mergechangelogs .
+.TP
+.B dgit setup-gitattributes
+Set up the working tree's
+.B .git/info/attributes
+to disable all transforming attributes for all files.
+This is done by defining a macro attribute
+.B dgit-defuse-attrs
+and applying it to
+.BR * .
+For why, see
+.B GITATTRIBUTES
+in
+.BR dgit(7) .
+
+(If there is already a macro attribute line
+.B [attr]dgit-defuse-attrs
+in .git/info/attributes
+(whatever its effects),
+this operation does nothing further.
+This fact can be used to defeat or partially defeat
+dgit setup-gitattributes
+and hence
+dgit setup-new-tree.)
+.TP
.B dgit quilt-fixup
-Looks to see if the tree is one which dpkg-source cannot properly
-represent. If it isn't, dgit will fix it up for you (in quilt terms,
-by making a new debian/ patch containing your unquilty changes) and
-make a commit of the changes it has made.
+`3.0 (quilt)' format source packages need changes representing not
+only in-tree but also as patches in debian/patches. dgit quilt-fixup
+checks whether this has been done; if not, dgit will make appropriate
+patches in debian/patches and also commit the resulting changes to
+git.
This is normally done automatically by dgit build and dgit push.
+
+dgit will try to turn each relevant commit in your git history into a
+new quilt patch. dgit cannot convert nontrivial merges, or certain
+other kinds of more exotic history. If dgit can't find a suitable
+linearisation of your history, by default it will fail, but you can
+ask it to generate a single squashed patch instead.
+.TP
+\fBdgit import-dsc\fR [\fIsub-options\fR] \fI../path/to/.dsc\fR [\fB+\fR|\fB..\fR]branch
+Import a Debian-format source package,
+specified by its .dsc,
+into git,
+the way dgit fetch would do.
+
+This does about half the work of dgit fetch:
+it will convert the .dsc into a new, orphan git branch.
+Since dgit has no access to a corresponding source package archive
+or knowledge of the history
+it does not consider whether this version is newer
+than any previous import
+or corresponding git branches;
+and it therefore does not
+make a pseudomerge to bind the import
+into any existing git history.
+
+Because a .dsc can contain a Dgit field naming a git commit
+(which you might not have),
+and specifying where to find that commit
+(and any history rewrite table),
+import-dsc might need online access.
+If this is a problem
+(or dgit's efforts to find the commit fail),
+consider --no-chase-dsc-distro
+or --force-import-dsc-with-dgit-field.
+
+There is only only sub-option:
+
+.B --require-valid-signature
+causes dgit to insist that the signature on the .dsc is valid
+(using the same criteria as dpkg-source -x).
+Otherwise, dgit tries to verify the signature but
+the outcome is reported only as messages to stderr.
+
+If
+.I branch
+is prefixed with
+.B +
+then if it already exists, it will be simply ovewritten,
+no matter its existing contents.
+If
+.I branch
+is prefixed with
+.B ..
+then if it already exists
+and dgit actually imports the dsc
+(rather than simply reading the git commit out of the Dgit field),
+dgit will make a pseudomerge
+so that the result is necessarily fast forward
+from the existing branch.
+Otherwise, if branch already exists,
+dgit will stop with an error message.
+
+If
+.I branch
+does not start with refs/, refs/heads/ is prepended.
.TP
.B dgit version
Prints version information and exits.
+.TP
+.BI "dgit clone-dgit-repos-server" " destdir"
+Tries to fetch a copy of the source code for the dgit-repos-server,
+as actually being used on the dgit git server, as a git tree.
+.TP
+.BI "dgit print-dgit-repos-server-source-url"
+Prints the url used by dgit clone-dgit-repos-server.
+This is hopefully suitable for use as a git remote url.
+It may not be useable in a browser.
.SH OPTIONS
.TP
-.BR --dry-run | -n
+.BR --dry-run " | " -n
Go through the motions, fetching all information needed, but do not
actually update the output(s). For push, dgit does
the required checks and leaves the new .dsc in a temporary file,
but does not sign, tag, push or upload.
.TP
-.BR --damp-run | -L
+.BR --damp-run " | " -L
Go through many more of the motions: do everything that doesn't
involve either signing things, or making changes on the public
servers.
@@ -208,7 +359,11 @@ servers.
.BI -k keyid
Use
.I keyid
-for signing the tag and the upload.
+for signing the tag and the upload. The default comes from the
+distro's
+.B keyid
+config setting (see CONFIGURATION, below), or failing that, the
+uploader trailer line in debian/changelog.
.TP
.BR --no-sign
does not sign tags or uploads (meaningful only with push).
@@ -220,47 +375,390 @@ Specifies that we should process source package
rather than looking in debian/control or debian/changelog.
Valid with dgit fetch and dgit pull, only.
.TP
-.BR --clean=git | -wg
-The source tree should be cleaned, before building a source package
-with one of the build options, using
-.BR "git clean -xdf" .
+.BR --clean=git " | " -wg
+Use
+.BR "git clean -xdf"
+to clean the working tree,
+rather than running the package's rules clean target.
+
This will delete all files which are not tracked by git.
+(Including any files you forgot to git add.)
+
+.BI --clean= ...
+options other than dpkg-source
+are useful when the package's clean target is troublesome, or
+to avoid needing the build-dependencies.
.TP
-.BR --clean=none | -wn
-Do not clean the tree before building a source package. If there are
-files which are not in git, a subsequent dgit push will fail.
-.TP
-.BR --clean=dpkg-source | -wd
+.BR --clean=git-ff " | " -wgf
+Use
+.BR "git clean -xdff"
+to clean the working tree.
+Like
+git clean -xdf
+but it also removes any subdirectories containing different git
+trees (which only unusual packages are likely to create).
+.TP
+.BR --clean=check " | " -wc
+Merely check that the tree is clean (does not contain uncommitted
+files).
+Avoids running rules clean,
+and can avoid needing the build-dependencies.
+.TP
+.BR --clean=none " | " -wn
+Do not clean the tree, nor check that it is clean.
+Avoids running rules clean,
+and can avoid needing the build-dependencies.
+If there are
+files which are not in git, or if the build creates such files, a
+subsequent dgit push will fail.
+.TP
+.BR --clean=dpkg-source " | " -wd
Use dpkg-buildpackage to do the clean, so that the source package
is cleaned by dpkg-source running the package's clean target.
-This is the default. It requires the package's build dependencies.
+This is the default.
+Requires the package's build dependencies.
+.TP
+.BR --clean=dpkg-source-d " | " -wdd
+Use
+.B dpkg-buildpackage -d
+to do the clean,
+so that the source package
+is cleaned by dpkg-source running the package's clean target.
+The build-dependencies are not checked (due to
+.BR -d ),
+which violates policy, but may work in practice.
.TP
-.BR -N | --new
-The package may be new in this suite. Without this, dgit will
-refuse to push.
+.BR -N " | " --new
+The package is or may be new in this suite. Without this, dgit will
+refuse to push. It may (for Debian, will) be unable to access the git
+history for any packages which have been newly pushed and have not yet
+been published.
.TP
.BR --ignore-dirty
Do not complain if the working tree does not match your git HEAD.
This can be useful with build, if you plan to commit later. (dgit
push will still ensure that the .dsc you upload and the git tree
you push are identical, so this option won't make broken pushes.)
+.TP
+.BR --overwrite [=\fIprevious-version\fR]
+Declare that even though your git branch is not a descendant
+of the version in the archive
+according to the revision history,
+it really does contain
+all the (wanted) changes from that version.
+
+This option is useful if you are the maintainer, and you have
+incorporated NMU changes into your own git workflow in a way that
+doesn't make your branch a fast forward from the NMU.
+
+.I previous-version
+ought to be the version currently in the archive. If
+.I previous-version
+is not
+specified, dgit will check that the version in the archive is
+mentioned in your debian/changelog.
+(This will avoid losing
+changes unless someone committed to git a finalised changelog
+entry, and then made later changes to that version.)
+
+dgit push --overwrite
+will make a
+pseudo-merge (that is, something that looks like the result
+of git merge -s ours) to stitch the archive's version into your own
+git history, so that your push is a fast forward from the archive.
+
+(In quilt mode
+.BR gbp ", " dpm " or " unpatched ,
+implying a split between the dgit view and the
+maintainer view, the pseudo-merge will appear only in the dgit view.)
+.TP
+.BR --delayed =\fIdays\fR
+Upload to a DELAYED queue.
+
+.B WARNING:
+If the maintainer responds by cancelling
+your upload from the queue,
+and does not make an upload of their own,
+this will not rewind the git branch on the dgit git server.
+Other dgit users will then see your push
+(with a warning message from dgit)
+even though the maintainer wanted to abolish it.
+Such users might unwittingly reintroduce your changes.
+
+If this situation arises,
+someone should make a suitable dgit push
+to update the contents of dgit-repos
+to a version without the controversial changes.
+.TP
+.BR --no-chase-dsc-distro
+Tells dgit not to look online
+for additional git repositories
+containing information about a particular .dsc being imported.
+Chasing is the default.
+
+For most operations
+(such as fetch and pull),
+disabling chasing
+means dgit will access only the git server
+for the distro you are directly working with,
+even if the .dsc was copied verbatim from another distro.
+For import-dsc,
+disabling chasing
+means dgit will work completely offline.
+
+Disabling chasing can be hazardous:
+if the .dsc names a git commit which has been rewritten
+by those in charge of the distro,
+this option may prevent that rewrite from being effective.
+Also,
+it can mean that
+dgit fails to find necessary git commits.
+.TP
+.BR --dgit-view-save= \fIbranch\fR|\fIref\fR
+Specifies that when a split view quilt mode is in operation,
+and dgit calculates
+(or looks up in its cache)
+a dgit view corresponding to your HEAD,
+the dgit view will be left in
+.IR ref .
+The specified ref is unconditionally overwritten,
+so don't specify a branch you want to keep.
+
+This option is effective only with the following operations:
+quilt-fixup; push; all builds.
+And it is only effective with
+--[quilt=]gbp,
+--[quilt=]dpm,
+--quilt=unpatched.
+
+If ref does not start with refs/
+it is taken to to be a branch -
+i.e. refs/heads/ is prepended.
+.TP
+.BI --deliberately- something
+Declare that you are deliberately doing
+.IR something .
+This can be used to override safety catches, including safety catches
+which relate to distro-specific policies.
+The use of --deliberately is declared and published in the signed tags
+generated for you by dgit,
+so that the archive software can give effect to your intent,
+and
+for the benefit humans looking at the history.
+The meanings of
+.IR something s
+understood in the context of Debian are discussed below:
+.TP
+.BR --deliberately-not-fast-forward
+Declare that you are deliberately rewinding history. When pushing to
+Debian, use this when you are making a renewed upload of an entirely
+new source package whose previous version was not accepted for release
+from NEW because of problems with copyright or redistributibility.
+
+In split view quilt modes,
+this also prevents the construction by dgit of a pseudomerge
+to make the dgit view fast forwarding.
+Normally only one of
+--overwrite (which creates a suitable pseudomerge)
+and
+--deliberately-not-fast-forward
+(which suppresses the pseudomerge and the fast forward checks)
+should be needed;
+--overwrite is usually better.
+.TP
+.BR --deliberately-include-questionable-history
+Declare that you are deliberately including, in the git history of
+your current push, history which contains a previously-submitted
+version of this package which was not approved (or has not yet been
+approved) by the ftpmasters. When pushing to Debian, only use this
+option after verifying that: none of the rejected-from-NEW (or
+never-accepted) versions in the git history of your current push, were
+rejected by ftpmaster for copyright or redistributability reasons.
+.TP
+.BR --deliberately-fresh-repo
+Declare that you are deliberately rewinding history and want to
+throw away the existing repo. Not relevant when pushing to Debian,
+as the Debian server will do this automatically when necessary.
+.TP
+.BR --quilt=linear
+When fixing up source format `3.0 (quilt)' metadata, insist on
+generating a linear patch stack: one new patch for each relevant
+commit.
+If such a stack cannot be generated, fail.
+This is the default for Debian.
+
+HEAD should be a series of plain commits
+(not touching debian/patches/),
+and pseudomerges,
+with as ancestor a patches-applied branch.
+.TP
+.BR --quilt=auto
+When fixing up source format `3.0 (quilt)' metadata, prefer to
+generate a linear patch stack
+(as with --quilt=auto)
+but if that doesn't seem possible,
+try to generate a single squashed patch for all the changes made in git
+(as with --quilt=smash).
+This is not a good idea for an NMU in Debian.
+.TP
+.BR --quilt=smash
+When fixing up source format `3.0 (quilt)' metadata,
+generate a single additional patch for all the changes made in git.
+This is not a good idea for an NMU in Debian.
+
+(If HEAD has any in-tree patches already, they must apply cleanly.
+This will be the case for any trees produced by dgit fetch or clone;
+if you do not change the upstream version
+nor make changes in debian/patches,
+it will remain true.)
+.TP
+.BR --quilt=nofix
+Check whether source format `3.0 (quilt)' metadata would need fixing
+up, but, if it does, fail. You must then fix the metadata yourself
+somehow before pushing. (NB that dpkg-source --commit will not work
+because the dgit git tree does not have a
+.B .pc
+directory.)
+.TP
+.BR --quilt=nocheck " | " --no-quilt-fixup
+Do not check whether up source format `3.0 (quilt)' metadata needs
+fixing up. If you use this option and the metadata did in fact need
+fixing up, dgit push will fail.
+.TP
+.BR -- [ quilt= ] gbp " | " -- [ quilt= ] dpm " | " --quilt=unapplied
+Tell dgit that you are using a nearly-dgit-compatible git branch,
+aka a
+.BR "maintainer view" ,
+and
+do not want your branch changed by dgit.
+
+.B --gbp
+(short for
+.BR --quilt=gbp )
+is for use with git-buildpackage.
+Your HEAD is expected to be
+a patches-unapplied git branch, except that it might contain changes
+to upstream .gitignore files. This is the default for dgit gbp-build.
+
+.B --dpm
+(short for
+.BR --quilt=dpm )
+is for use with git-dpm.
+Your HEAD is expected to be
+a patches-applied git branch,
+except that it might contain changes to upstream .gitignore files.
+
+.B --quilt=unapplied
+specifies that your HEAD is a patches-unapplied git branch (and
+that any changes to upstream .gitignore files are represented as
+patches in debian/patches).
+
+With --quilt=gbp|dpm|unapplied,
+dgit push (or precursors like quilt-fixup and build) will automatically
+generate a conversion of your git branch into the right form.
+dgit push will push the
+dgit-compatible form (the
+.BR "dgit view" )
+to the dgit git server.
+The dgit view will be visible to you
+in the dgit remote tracking branches, but your own branch will
+not be modified.
+dgit push will create a tag
+.BI debian/ version
+for the maintainer view, and the dgit tag
+.BI archive/debian/ version
+for the dgit view.
+dgit quilt-fixup will merely do some checks,
+and cache the maintainer view.
+
+.B If you have a branch like this it is essential to specify the appropriate --quilt= option!
+This is because it is not always possible to tell: a patches-unapplied
+git branch of a package with one patch, for example, looks very like
+a patches-applied branch where the user has used git revert to
+undo the patch, expecting to actually revert it.
+However, if you fail to specify the right \-\-quilt option,
+and you aren't too lucky, dgit will notice the problem and stop,
+with a useful hint.
+.TP
+.BR -d "\fIdistro\fR | " --distro= \fIdistro\fR
+Specifies that the suite to be operated on is part of distro
+.IR distro .
+This overrides the default value found from the git config option
+.BR dgit-suite. \fIsuite\fR .distro .
+The only effect is that other configuration variables (used
+for accessing the archive and dgit-repos) used are
+.BR dgit-distro. \fIdistro\fR .* .
+
+If your suite is part of a distro that dgit already knows about, you
+can use this option to make dgit work even if your dgit doesn't know
+about the suite. For example, specifying
+.B -ddebian
+will work when the suite is an unknown suite in the Debian archive.
-This option may not work properly on `3.0 (quilt)' packages, as in
-that case dgit needs to use and perhaps commit parts of your working
-tree.
+To define a new distro it is necessary to define methods and URLs
+for fetching (and, for dgit push, altering) a variety of information both
+in the archive and in dgit-repos.
+How to set this up is not yet documented.
+.TP
+.BI -C changesfile
+Specifies the .changes file which is to be uploaded. By default
+dgit push looks for single .changes file in the parent directory whose
+filename suggests it is for the right package and version.
+
+If the specified
+.I changesfile
+pathname contains slashes, the directory part is also used as
+the value for
+.BR --build-products-dir ;
+otherwise, the changes file is expected in that directory (by
+default, in
+.BR .. ).
+.TP
+.B --rm-old-changes
+When doing a build, delete any changes files matching
+.IB package _ version _*.changes
+before starting. This ensures that
+dgit push (and dgit sbuild) will be able to unambigously
+identify the relevant changes files from the most recent build, even
+if there have been previous builds with different tools or options.
+The default is not to remove, but
+.B \-\-no-rm-old-changes
+can be used to override a previous \-\-rm-old-changes
+or the .rm-old-changes configuration setting.
+.TP
+.BI --build-products-dir= directory
+Specifies where to find the built files to be uploaded.
+By default, dgit looks in the parent directory
+.RB ( .. ).
+.TP
+.BI --no-rm-on-error
+Do not delete the destination directory if clone fails.
.TP
-.BR --no-quilt-fixup
-Do not fix up source format `3.0 (quilt)' metadata. If you use this
-option and the package did in fact need fixing up, dgit push will
-fail.
+.BI --dep14tag
+Generates a DEP-14 tag (eg
+.BR debian/ \fIversion\fR)
+as well as a dgit tag (eg
+.BR archive/debian/ \fIversion\fR)
+where possible. This is the default.
+.TP
+.BI --no-dep14tag
+Do not generate a DEP-14 tag, except in split quilt view mode.
+(On servers where only the old tag format is supported,
+the dgit tag will have the DEP-14 name.
+This option does not prevent that.)
+.TP
+.BI --dep14tag-always
+Insist on generating a DEP-14 tag
+as well as a dgit tag.
+If the server does not support that, dgit push will fail.
.TP
.BI -D
Prints debugging information to stderr. Repeating the option produces
-more output (currently, up to -DD is meaningfully different).
+more output (currently, up to -DDDD is meaningfully different).
.TP
.BI -c name = value
-Specifies a git configuration option. dgit itself is also controlled
-by git configuration options.
+Specifies a git configuration option, to be used for this run.
+dgit itself is also controlled by git configuration options.
.TP
.RI \fB-v\fR version "|\fB_\fR | " \fB--since-version=\fR version |\fB_\fR
Specifies the
@@ -283,8 +781,63 @@ Passed to dpkg-genchanges (eventually).
.RI \fB--ch:\fR option
Specifies a single additional option to pass, eventually, to
dpkg-genchanges.
+
+Options which are safe to pass include
+.BR -C
+(and also
+.BR "-si -sa -sd"
+although these should never be necessary with Debian since dgit
+automatically calculates whether .origs need to be uploaded.)
+
+For other options the caveat below applies.
.TP
-.RI \fB--curl=\fR program |\fB--dput=\fR program |...
+.RI \fB--curl:\fR option " | \fB--dput:\fR" option " |..."
+Specifies a single additional option to pass to
+.BR curl ,
+.BR dput ,
+.BR debsign ,
+.BR dpkg-source ,
+.BR dpkg-buildpackage ,
+.BR dpkg-genchanges ,
+.BR sbuild ,
+.BR ssh ,
+.BR dgit ,
+.BR apt-get ,
+.BR apt-cache ,
+.BR gbp-pq ,
+.BR gbp-build ,
+or
+.BR mergechanges .
+Can be repeated as necessary.
+
+Use of this ability should not normally be necessary.
+It is provided for working around bugs,
+or other unusual situations.
+If you use these options,
+you may violate dgit's assumptions
+about the behaviour of its subprograms
+and cause lossage.
+
+For dpkg-buildpackage, dpkg-genchanges, mergechanges and sbuild,
+the option applies only when the program is invoked directly by dgit.
+Usually, for passing options to dpkg-genchanges, you should use
+.BR --ch: \fIoption\fR.
+
+Specifying --git is not effective for some lower-level read-only git
+operations performed by dgit, and also not when git is invoked by
+another program run by dgit.
+
+See notes below regarding ssh and dgit.
+
+NB that --gpg:option is not supported (because debsign does not
+have that facility).
+But see
+.B -k
+and the
+.B keyid
+distro config setting.
+.TP
+.RI \fB--curl=\fR program " | \fB--dput=\fR" program " |..."
Specifies alternative programs to use instead of
.BR curl ,
.BR dput ,
@@ -296,18 +849,43 @@ Specifies alternative programs to use instead of
.BR gpg ,
.BR ssh ,
.BR dgit ,
+.BR apt-get ,
+.BR apt-cache ,
+.BR git ,
+.BR gbp-pq ,
+.BR gbp-build ,
or
.BR mergechanges .
-For dpkg-buildpackage, dpkg-genchanges, mergechanges and sbuild,
+For
+.BR dpkg-buildpackage ,
+.BR dpkg-genchanges ,
+.B mergechanges
+and
+.BR sbuild ,
this applies only when the program is invoked directly by dgit.
-For dgit, specifies the command to run on the remote host when dgit
+For
+.BR dgit ,
+specifies the command to run on the remote host when dgit
rpush needs to invoke a remote copy of itself. (dgit also reinvokes
itself as the EDITOR for dpkg-source --commit; this is done using
argv[0], and is not affected by --dgit=).
-For ssh, the default value is taken from the
+.BR gbp-build 's
+value
+is used instead of gbp build or git-buildpackage. (The default is
+the latter unless the former exists on PATH.)
+.BR gbp-pq 's
+value
+is used instead of gbp pq.
+In both cases,
+unusually, the specified value is split on whitespace
+to produce a command and possibly some options and/or arguments.
+
+For
+.BR ssh ,
+the default value is taken from the
.B DGIT_SSH
or
.B GIT_SSH
@@ -322,72 +900,6 @@ git to access dgit-repos, only git's idea of what ssh to use (eg,
.BR GIT_SSH )
is relevant.
.TP
-.RI \fB--curl:\fR option |\fB--dput:\fR option |...
-Specifies a single additional option to pass to
-.BR curl ,
-.BR dput ,
-.BR debsign ,
-.BR dpkg-source ,
-.BR dpkg-buildpackage ,
-.BR dpkg-genchanges ,
-.BR sbuild ,
-.BR ssh ,
-.BR dgit ,
-or
-.BR mergechanges .
-Can be repeated as necessary.
-
-For dpkg-buildpackage, dpkg-genchanges, mergechanges and sbuild,
-this applies only when the program is invoked directly by dgit.
-Usually, for passing options to dpkg-genchanges, you should use
-.BR --ch: \fIoption\fR.
-
-See notes above regarding ssh and dgit.
-
-NB that --gpg:option is not supported (because debsign does not
-have that facility). But see -k.
-.TP
-.BR -d "\fIdistro\fR | " --distro= \fIdistro\fR
-Specifies that the suite to be operated on is part of distro
-.IR distro .
-This overrides the default value found from the git config option
-.BR dgit-suite. \fIsuite\fR .distro .
-The only effect is that other configuration variables (used
-for accessing the archive and dgit-repos) used are
-.BR dgit-distro. \fIdistro\fR .* .
-
-If your suite is part of a distro that dgit already knows about, you
-can use this option to make dgit work even if your dgit doesn't know
-about the suite. For example, specifying
-.B -ddebian
-will work when the suite is an unknown suite in the Debian archive.
-
-To define a new distro it is necessary to define methods and URLs
-for fetching (and, for dgit push, altering) a variety of information both
-in the archive and in dgit-repos. How to do this is not yet
-documented, and currently the arrangements are unpleasant. See
-BUGS.
-.TP
-.BI -C changesfile
-Specifies the .changes file which is to be uploaded. By default
-dgit push looks for single .changes file in the parent directory whose
-filename suggests it is for the right package and version - or,
-if there is a _multi.changes file, dgit uses that.
-
-If the specified
-.I changesfile
-pathname contains slashes, the directory part is also used as
-the value for
-.BR --build-products-dir ;
-otherwise, the changes file is expected in that directory (by
-default, in
-.BR .. ).
-.TP
-.BI --build-products-dir= directory
-Specifies where to find the built files to be uploaded.
-By default, dgit looks in the parent directory
-.BR .. ).
-.TP
.BI --existing-package= package
dgit push needs to canonicalise the suite name. Sometimes, dgit
lacks a way to ask the archive to do this without knowing the
@@ -410,258 +922,204 @@ removed and recreated before dgit starts, rather than removed
after dgit finishes. The directory specified must be an absolute
pathname.
.TP
-.BI --no-rm-on-error
-Do not delete the destination directory if clone fails.
-.SH WORKFLOW - SIMPLE
-It is always possible with dgit to clone or fetch a package, make
-changes in git (using git-commit) on the suite branch
-.RB ( "git checkout dgit/" \fIsuite\fR)
-and then dgit push. You can use whatever gitish techniques you like
-to construct the commit to push; the only requirement is that it is a
-descendant of the state of the archive, as provided by dgit in the
-remote tracking branch
-.BR remotes/dgit/dgit/ \fIsuite\fR.
-
-If you are lucky the other uploaders have also used dgit and
-integrated the other relevant git history; if not you can fetch it
-into your tree and cherry-pick etc. as you wish.
-.SH WORKFLOW - INTEGRATING BETWEEN DGIT AND OTHER GIT HISTORY
-If you are the maintainer of a package dealing with uploads made
-without dgit, you will probably want to merge the synthetic commits
-(made by dgit to represent the uploads) into your git history.
-Normally you can just merge the dgit branch into your own master, or
-indeed if you do your work on the dgit local suite branch
-.BI dgit/ suite
-you can just use dgit pull.
-
-However the first time dgit is used it will generate a new origin
-commit from the archive which won't be linked into the rest of your
-git history. You will need to merge this.
-
-If last upload was in fact made with git, you should usually proceed
-as follows: identify the commit which was actually used to build the
-package. (Hopefully you have a tag for this.) Check out the dgit
-branch
-.RB ( "git checkout dgit/" \fIsuite\fR)
-and merge that other commit
-.RB ( "git merge debian/" \fIversion\fR).
-Hopefully this merge will be trivial because the two trees should
-be the same. The resulting branch head can be merged into your
-working branches
-.RB ( "git checkout master && git merge dgit/" \fIsuite\fR).
-
-If last upload was not made with git, a different approach is required
-to start using dgit. First, do
-.B dgit fetch
-(or clone) to obtain a git history representation of what's in the
-archive and record it in the
-.BI remotes/dgit/dgit/ suite
-tracking branch. Then somehow, using your other git history
-plus appropriate diffs and cherry picks from the dgit remote tracking
-branch, construct a git commit whose tree corresponds to the tree to use for the
-next upload. If that commit-to-be-uploaded is not a descendant of the
-dig remote tracking branch, check it out and say
-.BR "git merge -s ours remotes/dgit/dgit/" \fIsuite\fR;
-that tells git that we are deliberately throwing away any differences
-between what's in the archive and what you intend to upload.
-Then run
-.BR "dgit push"
-to actually upload the result.
-.SH MODEL
-You may use any suitable git workflow with dgit, provided you
-satisfy dgit's requirements:
-
-dgit maintains a pseudo-remote called
-.BR dgit ,
-with one branch per suite. This remote cannot be used with
-plain git.
-
-The
-.B dgit-repos
-repository for each package contains one ref per suite named
-\fBrefs/dgit/\fR\fIsuite\fR. These should be pushed to only by
-dgit. They are fast forwarding. Each push on this branch
-corresponds to an upload (or attempted upload).
-
-However, it is perfectly fine to have other branches in dgit-repos;
-normally the dgit-repos repo for the package will be accessible via
-the remote name `origin'.
-
-dgit push will also (by default) make signed tags called
-.BI debian/ version
-and push them to dgit-repos, but nothing depends on these tags
-existing.
-
-dgit push can operate on any commit which is a descendant of the
-current dgit/suite tip in dgit-repos.
-
-Uploads made by dgit contain an additional field
-.B Dgit
-in the source package .dsc. (This is added by dgit push.)
-This specifies a commit (an ancestor of the dgit/suite
-branch) whose tree is identical to the unpacked source upload.
-
-Uploads not made by dgit are represented in git by commits which are
-synthesised by dgit. The tree of each such commit corresponds to the
-unpacked source; there is an origin commit with the contents, and a
-psuedo-merge from last known upload - that is, from the contents of
-the dgit/suite branch.
-
-dgit expects repos that it works with to have a
-.B dgit
-remote. This refers to the well-known dgit-repos location
-(currently, the dgit-repos project on Alioth). dgit fetch updates
-the remote tracking branch for dgit/suite.
-
-dgit does not (currently) represent the orig tarball(s) in git. The
-orig tarballs are downloaded (by dgit clone) into the parent
-directory, as with a traditional (non-gitish) dpkg-source workflow.
-You need to retain these tarballs in the parent directory for dgit
-build and dgit push.
-
-To a user looking at the archive, changes pushed using dgit look like
-changes made in an NMU: in a `3.0 (quilt)' package the delta from the
-previous upload is recorded in a new patch constructed by dpkg-source.
-.SH READ-ONLY DISTROS
-Distros which do not maintain a set of dgit history git repositories
-can still be used in a read-only mode with dgit. Currently Ubuntu
-is configured this way.
-.SH PACKAGE SOURCE FORMATS
-If you are not the maintainer, you do not need to worry about the
-source format of the package. You can just make changes as you like
-in git. If the package is a `3.0 (quilt)' package, the patch stack
-will usually not be represented in the git history.
-
-If you are the maintainer of a non-native package, you currently have
-two sensible options:
-
-Firstly, you can regard your git history as primary, and the archive
-as an export format. For example, you could maintain topic branches
-in git and a fast-forwarding release branch; or you could do your work
-directly in a merging way on the
-.BI dgit/ suite
-branches. If you do this you should probably use a `1.0' format
-source package if you can. In the archive, the delta between upstream
-will be represented in the single Debian patch.
-
-Secondly, you can use `3.0 (quilt)', and regard your quiltish patch
-stack in the archive as primary. You will have to use other tools
-besides dgit to import and export this patch stack. But see below:
-.SH FORMAT 3.0 (QUILT)
-For a format `3.0 (quilt)' source package, dgit may have to make a
-commit on your current branch to contain metadata used by quilt and
-dpkg-source.
-
-This is because (i) the `3.0 (quilt)' source format cannot represent
-certain trees, and (ii) packing up a tree in `3.0 (quilt)' and then
-unpacking it does not always yield the same tree. Instead,
-dpkg-source insists on the trees having extra quilty metadata and
-patch files in the debian/ and .pc/ directories, which dpkg-source
-sometimes modifies.
-
-dgit will automatically work around this braindamage for you when
-building and pushing. The only thing you need to know is that dgit
-build, sbuild, etc., may make a new commit on your HEAD. If you're
-not a quilt user this commit won't contain any changes to files you
-care about.
-
-You can explicitly request that dgit do just this fixup, by running
-dgit quilt-fixup.
-
-We recommend against the use of `3.0 (quilt)'.
-.SH FILES IN THE SOURCE PACKAGE BUT NOT IN GIT
-This section is mainly of interest to maintainers who want to use dgit
-with their existing git history for the Debian package.
-
-Some developers like to have an extra-clean git tree which lacks files
-which are normally found in source tarballs and therefore in Debian
-source packages. For example, it is conventional to ship ./configure
-in the source tarball, but some people prefer not to have it present
-in the git view of their project.
-
-dgit requires that the source package unpacks to exactly the same
-files as are in the git commit on which dgit push operates. So if you
-just try to dgit push directly from one of these extra-clean git
-branches, it will fail.
-
-As the maintainer you therefore have the following options:
-.TP
-\(bu
-Persuade upstream that the source code in their git history and the
-source they ship as tarballs should be identical. Of course simply
-removing the files from the tarball may make the tarball hard for
-people to use.
-.IP
-One answer is to commit the (maybe autogenerated)
-files, perhaps with some simple automation to deal with conflicts and
-spurious changes. This has the advantage that someone who clones
-the git repository finds the program just as easy to build as someone
-who uses the tarball.
-.TP
-\(bu
-Have separate git branches which do contain the extra files, and after
-regenerating the extra files (whenever you would have to anyway),
-commit the result onto those branches.
-.TP
-\(bu
-Provide source packages which lack the files you don't want
-in git, and arrange for your package build to create them as needed.
-This may mean not using upstream source tarballs and makes the Debian
-source package less useful for people without Debian build
-infrastructure.
-.LP
-Of course it may also be that the differences are due to build system
-bugs, which cause unintended files to end up in the source package.
-dgit will notice this and complain. You may have to fix these bugs
-before you can unify your existing git history with dgit's.
+.BI --force- something
+Instructs dgit to try to proceed despite detecting
+what it thinks is going to be a fatal problem.
+.B This is probably not going to work.
+These options are provided as an escape hatch,
+in case dgit is confused.
+(They might also be useful for testing error cases.)
+.TP
+.B --force-import-dsc-with-dgit-field
+Tell dgit import-dsc to treat a .dsc with a Dgit field
+like one without it.
+The result is a fresh import,
+discarding the git history
+that the person who pushed that .dsc was working with.
+.TP
+.B --force-unrepresentable
+Carry on even if
+dgit thinks that your git tree contains changes
+(relative to your .orig tarballs)
+which dpkg-source is not able to represent.
+Your build or push will probably fail later.
+.TP
+.B --force-changes-origs-exactly
+Use the set of .origs specified in your .changes, exactly,
+without regard to what is in the archive already.
+The archive may well reject your upload.
+.TP
+.B --force-unsupported-source-format
+Carry on despite dgit not understanding your source package format.
+dgit will probably mishandle it.
+.TP
+.B --force-dsc-changes-mismatch
+Do not check whether .dsc and .changes match.
+The archive will probably reject your upload.
+.TP
+.BR --force-import-gitapply-absurd " | " --force-import-gitapply-no-absurd
+Force on or off the use of the absurd git-apply emulation
+when running gbp pq import
+when importing a package from a .dsc.
+See Debian bug #841867.
.SH CONFIGURATION
-dgit looks at the following git config keys to control its behaviour.
-You may set them with git-config (either in system-global or per-tree
+dgit can be configured via the git config system.
+You may set keys with git-config (either in system-global or per-tree
configuration), or provide
.BI -c key = value
on the dgit command line.
+.LP
+Settings likely to be useful for an end user include:
+.TP
+.BR dgit-suite. \fIsuite\fR .distro " \fIdistro\fR"
+Specifies the distro for a suite. dgit keys off the suite name (which
+appears in changelogs etc.), and uses that to determine the distro
+which is involved. The config used is thereafter that for the distro.
+
+.I suite
+may be a glob pattern.
+.TP
+.BI dgit.default.distro " distro"
+The default distro for an unknown suite.
.TP
-.BI dgit-suite. suite .distro
+.BI dgit.default.default-suite " suite"
+The default suite (eg for clone).
.TP
-.BI dgit.default.distro
+.BR dgit.default. *
+for each
+.BR dgit-distro. \fIdistro\fR . *,
+the default value used if there is no distro-specific setting.
+.TP
+.BR dgit-distro. \fIdistro\fR .clean-mode
+One of the values for the command line --clean= option; used if
+--clean is not specified.
+.TP
+.BR dgit-distro. \fIdistro\fR .quilt-mode
+One of the values for the command line --quilt= option; used if
+--quilt is not specified.
+.TP
+.BR dgit-distro. \fIdistro\fR .rm-old-changes
+Boolean, used if neither \-\-rm-old-changes nor \-\-no-rm-old-changes
+is specified. The default is not to remove.
+.TP
+.BR dgit-distro. \fIdistro\fR .readonly " " auto | a " | " true | t | y | 1 " | " false | f | n | 0
+Whether you have push access to the distro.
+For Debian, it is OK to use auto, which uses readonly mode if you are
+not pushing right now;
+but, setting this to false will avoid relying on the mirror of the dgit
+git repository server.
+.TP
+.BI dgit-distro. distro .keyid
+See also
+.BR -k .
+.TP
+.BI dgit-distro. distro .mirror " url"
.TP
.BI dgit-distro. distro .username
+Not relevant for Debian.
.TP
-.BI dgit-distro. distro .git-url
+.BI dgit-distro. distro .upload-host
+Might be useful if you have an intermediate queue server.
+.TP
+.BI dgit-distro. distro .user-name " " dgit-distro. distro .user-email
+Values to configure for user.name and user.email in new git trees. If
+not specified, the DEBFULLNAME and DEBEMAIL environment variables are
+used, respectively. Only used if .setup-usermail is not disabled.
+.TP
+.BI dgit-distro. distro .setup-useremail
+Whether to set user.name and user.email in new git trees.
+True by default. Ignored for dgit setup-setup-useremail, which does it anyway.
+.TP
+.BI dgit-distro. distro .setup-mergechangelogs
+Whether to setup a merge driver which uses dpkg-mergechangelogs for
+debian/changelog. True by default. Ignored for dgit
+setup-mergechangelogs, which does it anyway.
+.TP
+.BI dgit-distro. distro .setup-gitattributes
+Whether to configure .git/info/attributes
+to suppress checkin/checkout file content transformations
+in new git trees.
+True by default. Ignored for dgit setup-gitattributes, which does it anyway.
+.TP
+.BI dgit-distro. distro .cmd- cmd
+Program to use instead of
+.IR cmd .
+Works like
+.BR -- \fIcmd\fR = "... ."
+.TP
+.BI dgit-distro. distro .opts- cmd
+Extra options to pass to
+.IR cmd .
+Works like
+.BR -- \fIcmd\fR : "... ."
+To pass several options, configure multiple values in git config
+(with git config --add). The options for
+.BI dgit.default.opts- cmd
+.BI dgit-distro. distro /push.opts- cmd
+and are all used, followed by options from dgit's command line.
+.SH ACCESS CONFIGURATION
+There are many other settings which specify how a particular distro's
+services (archive and git) are provided. These should not normally be
+adjusted, but are documented for the benefit of distros who wish to
+adopt dgit.
+.TP
+.BI dgit-distro. distro .nominal-distro
+Shown in git tags, Dgit fields, and so on.
+.TP
+.BI dgit-distro. distro .alias-canon
+Used for all access configuration lookup.
+.TP
+.BR dgit-distro. \fIdistro\fR /push. *
+If set, overrides corresponding non \fB/push\fR config when
+.BR readonly=false ,
+or when pushing and
+.BR readonly=auto .
.TP
-.BI dgit-distro. distro .git-user
+.BI dgit-distro. distro .git-url
.TP
-.BI dgit-distro. distro .git-host
+.BR dgit-distro. \fIdistro\fR .git-url [ -suffix ]
.TP
.BI dgit-distro. distro .git-proto
.TP
.BI dgit-distro. distro .git-path
.TP
-.BI dgit-distro. distro .git-check
+.BR dgit-distro. \fIdistro\fR .git-check " " true | false | url | ssh-cmd
.TP
-.BI dgit-distro. distro .git-create
+.BI dgit-distro. distro .git-check-suffix
.TP
-.BI dgit-distro. distro .upload-host
+.BR dgit-distro. \fIdistro\fR .diverts.divert " " new-distro | / \fIdistro-suffix\fR
+.TP
+.BI dgit-distro. distro .git-create " " ssh-cmd | true
.TP
-.BI dgit-distro. distro .mirror
+.BR dgit-distro. \fIdistro\fR .archive-query " " ftpmasterapi: " | " madison: "\fIdistro\fR | " dummycat: "\fI/path\fR | " sshpsql: \fIuser\fR @ \fIhost\fR : \fIdbname\fR
.TP
-.BI dgit-distro. distro .archive-query
+.BR dgit-distro. \fIdistro\fR .archive-query- ( url | tls-key | curl-ca-args )
+.TP
+.BI dgit-distro. distro .madison-distro
.TP
.BI dgit-distro. distro .archive-query-default-component
.TP
-.BI dgit-distro. distro .sshpsql-user
+.BI dgit-distro. distro .dgit-tag-format
.TP
-.BI dgit-distro. distro .sshpsql-host
+.BR dgit-distro. \fIdistro\fR .dep14tag " " want | no | always
+.TP
+.BI dgit-distro. distro .ssh
.TP
.BI dgit-distro. distro .sshpsql-dbname
.TP
-.BI dgit-distro. distro .ssh
+.BR dgit-distro. \fIdistro\fR . ( git | sshpsql ) - ( user | host | user-force )
.TP
-.BI dgit-distro. distro .keyid
+.BI dgit-distro. distro .backports-quirk
.TP
-.BR dgit.default. *
-for each
-.BR dgit-distro. \fIdistro\fR . *
+.BI dgit-distro. distro .rewrite-map-enable
+.TP
+.BI dgit.default.old-dsc-distro
+.TP
+.BI dgit.dsc-url-proto-ok. protocol
+.TP
+.BI dgit.dsc-url-proto-ok.bad-syntax
+.TP
+.BI dgit.default.dsc-url-proto-ok
.SH ENVIRONMENT VARIABLES
.TP
.BR DGIT_SSH ", " GIT_SSH
@@ -673,71 +1131,40 @@ interprets it the same way as git does.
See
also the --ssh= and --ssh: options.
.TP
+.BR DEBEMAIL ", " DEBFULLNAME
+Default git user.email and user.name for new trees. See
+.BR "dgit setup-new-tree" .
+.TP
.BR gpg ", " dpkg- "..., " debsign ", " git ", " curl ", " dput ", " LWP::UserAgent
and other subprograms and modules used by dgit are affected by various
environment variables. Consult the documentaton for those programs
for details.
.SH BUGS
-We should be using some kind of vhost/vpath setup for the git repos on
-alioth, so that they can be moved later if and when this turns out to
-be a good idea.
-
-dgit push should perhaps do `git push origin', or something similar,
-by default.
-
-Debian does not have a working rmadison server, so to find out what
-version of a package is in the archive, or to canonicalise suite
-names, we ssh directly into the ftpmaster server and run psql there to
-access the database.
-
-The mechanism for checking for and creating per-package repos on
-alioth is a hideous bodge. One consequence is that dgit currently
-only works for people with push access.
-
-Debian Maintainers are currently not able to push, as there is not
-currently any mechanism for determining and honouring the archive's
-ideas about access control. Currently only DDs can push.
-
-dgit's representation of format `3.0 (quilt)' source packages does not
-represent the patch stack. Currently the patch series representation
-cannot round trip through the archive. Ideally dgit would represent a
-quilty package with an origin commit of some kind followed by the
-patch stack as a series of commits followed by a pseudo-merge (to make
-the branch fast-forwarding). This would also mean a new `dgit
-rebase-prep' command or some such to turn such a fast-forwarding
-branch back into a rebasing patch stack, and a `force' option to dgit
-push (perhaps enabled automatically by a note left by rebase-prep)
-which will make the required pseudo-merge.
-
-If the dgit push fails halfway through, it should be restartable and
-idempotent. However this is not true for the git tag operation.
-Also, it would be good to check that the proposed signing key is
-available before starting work.
+There should be
+a `dgit rebase-prep' command or some such to turn a
+fast-forwarding branch containing pseudo-merges
+back into a rebasing patch stack.
+It might have to leave a note
+for a future dgit push.
-dgit's handling of .orig.tar.gz is not very sophisticated. Ideally
-the .orig.tar.gz could be transported via the git repo as git tags.
-Doing this is made more complicated by the possibility of a `3.0
-(quilt)' package with multiple .orig tarballs.
+If the dgit push fails halfway through,
+it is not necessarily restartable and
+idempotent.
+It would be good to check that the proposed signing key is
+available before starting work.
-dgit's build functions, and dgit push, should not make any changes to
+dgit's build functions, and dgit push, may make changes to
your current HEAD. Sadly this is necessary for packages in the `3.0
-(quilt)' source format. This is ultimately due to design problems in
-quilt and dpkg-source.
-
-There should be an option which arranges for the `3.0 (quilt)'
-autocommit to not appear on your HEAD, but instead only in the
-remote tracking suite branch.
-
-The option parser requires values to be cuddled to the option name.
-
-dgit assumes knowledge of the archive database. (The information dgit
-needs is not currently available via any public online service with a
-well-defined interface, let alone a secure one.)
+(quilt)' source format. This is ultimately due to what I consider
+design problems in quilt and dpkg-source.
--dry-run does not always work properly, as not doing some of the git
fetches may result in subsequent actions being different. Doing a
non-dry-run dgit fetch first will help.
+--damp-run is likely to work much better.
.SH SEE ALSO
+\fBdgit\fP(7),
+\fBdgit-*\fP(7),
\fBcurl\fP(1),
\fBdput\fP(1),
\fBdebsign\fP(1),
@@ -745,4 +1172,4 @@ non-dry-run dgit fetch first will help.
\fBgit-buildpackage\fP(1),
\fBdpkg-buildpackage\fP(1),
.br
-https://wiki.debian.org/Alioth
+https://browse.dgit.debian.org/
diff --git a/dgit.7 b/dgit.7
new file mode 100644
index 0000000..70165a1
--- /dev/null
+++ b/dgit.7
@@ -0,0 +1,425 @@
+.TH dgit 7 "" "Debian Project" "dgit"
+.SH NAME
+dgit \- principles of operation
+.SH SUMMARY
+.B dgit
+treats the Debian archive as a version control system, and
+bidirectionally gateways between the archive and git. The git view of
+the package can contain the usual upstream git history, and will be
+augmented by commits representing uploads done by other developers not
+using dgit. This git history is stored in a canonical location known
+as
+.B dgit-repos
+which lives on a dedicated git server.
+
+git branches suitable for use with dgit
+can be edited directly in git,
+and used directly for building binary packages.
+They can be shared using all conventional means for sharing git
+branches.
+It is not necessary to use dgit to work with dgitish git branches.
+However, dgit is (usually) needed in order to convert to or from
+Debian-format source packages.
+.SH SEE ALSO
+.TP
+\fBdgit\fP(1)
+Reference manual and documentation catalogue.
+.TP
+\fBdgit-*\fB(7)
+Tutorials and workflow guides. See dgit(1) for a list.
+.SH MODEL
+You may use any suitable git workflow with dgit, provided you
+satisfy dgit's requirements:
+
+dgit maintains a pseudo-remote called
+.BR dgit ,
+with one branch per suite. This remote cannot be used with
+plain git.
+
+The
+.B dgit-repos
+repository for each package contains one ref per suite named
+\fBrefs/dgit/\fR\fIsuite\fR. These should be pushed to only by
+dgit. They are fast forwarding. Each push on this branch
+corresponds to an upload (or attempted upload).
+
+However, it is perfectly fine to have other branches in dgit-repos;
+normally the dgit-repos repo for the package will be accessible via
+the remote name `origin'.
+
+dgit push will also make signed tags called
+.BI archive/debian/ version
+(with version encoded a la DEP-14)
+and push them to dgit-repos. These are used at the
+server to authenticate pushes.
+
+Uploads made by dgit contain an additional field
+.B Dgit
+in the source package .dsc. (This is added by dgit push.)
+This specifies: a commit (an ancestor of the dgit/suite
+branch) whose tree is identical to the unpacked source upload;
+the distro to which the upload was made;
+a tag name which can be used to fetch the git commits;
+and
+a url to use as a hint for the dgit git server for that distro.
+
+Uploads not made by dgit are represented in git by commits which are
+synthesised by dgit. The tree of each such commit corresponds to the
+unpacked source; there is a
+commit with the contents,
+and a
+pseudo-merge from last known upload - that is, from the contents of
+the dgit/suite branch.
+Depending on the source package format,
+the contents commit may have a more complex structure,
+but ultimately it will be a convergence of stubby branches
+from origin commits representing the components of the source package.
+
+dgit expects trees that it works with to have a
+.B dgit
+(pseudo) remote. This refers to the dgit-created git view of
+the corresponding archive.
+
+The dgit archive tracking view is synthesised locally,
+on demand,
+by each copy of dgit.
+The tracking view is always a descendant of the
+dgit-repos suite branch (if one exists),
+but may be ahead of it if uploads have been done without dgit.
+The archive tracking view is always fast forwarding within
+each suite.
+
+dgit push can operate on any commit which is a descendant of
+the suite tracking branch.
+
+dgit does not make a systematic record of
+its imports of orig tarball(s).
+So it does not work by finding git tags or branches
+referring to orig tarball(s).
+The
+orig tarballs are downloaded (by dgit clone) into the parent
+directory, as with a traditional (non-gitish) dpkg-source workflow.
+You need to retain these tarballs in the parent directory for dgit
+build and dgit push.
+(They are not needed for purely-git-based workflows.)
+
+dgit repositories could be cloned with standard (git) methods.
+However,
+the dgit repositories do not contain uploads not made with dgit.
+And
+for sourceful builds / uploads the orig
+tarball(s) will need to be present in the parent directory.
+
+To a user looking at the archive, changes pushed
+in a simple NMU
+using dgit look like
+reasonable
+changes made in an NMU: in a `3.0 (quilt)' package the delta from the
+previous upload is recorded in new patch(es) constructed by dpkg-source.
+.SH COMBINED SUITES
+dgit can synthesize a combined view of several underlying suites.
+This is requested by specifying, for
+.I suite,
+a comma-separated list:
+.IP
+.IR mainsuite \fB,\fR subsuite ...
+.LP
+This facility is available with dgit clone, fetch and pull, only.
+
+dgit will fetch the same package from each specified underlying suite,
+separately (as if with dgit fetch).
+dgit will then generate a pseudomerge commit
+on the tracking branch
+.BI remotes/dgit/dgit/ suite
+which has the tip of each of the underlying suites
+as an ancestor,
+and which contains the same as the suite which
+has the highest version of the package.
+
+The package must exist in mainsuite,
+but need not exist in the subsuites.
+
+If a specified subsuite starts with
+.B -
+then mainsuite is prepended.
+
+So, for example,
+.B stable,-security
+means to look for the package in stable, and stable-security,
+taking whichever is newer.
+If stable is currently jessie,
+dgit clone would leave you on the branch
+.BR dgit/jessie,-security .
+
+Combined suites are not supported by the dgit build operations.
+This is because those options are intended for building for
+uploading source packages,
+and look in the changelog to find the relevant suite.
+It does not make sense to name a dgit-synthesised combined suite
+in a changelog,
+or to try to upload to it.
+
+When using this facility, it is important to always specify the
+same suites in the same order:
+dgit will not be make a coherent fast-forwarding history
+view otherwise.
+
+The history generated by this feature is not normally suitable
+for merging back into upstreams,
+as it necessarily contains unattractive pseudomerges.
+.SH LIMITATIONS
+Because the synthesis
+of the suite tracking branches
+is done locally based only on the current archive state,
+it will not necessarily see every upload
+not done with dgit.
+Also, different versions of dgit
+(or the software it calls)
+might import the same .dscs differently
+(although we try to minimise this).
+As a consequence, the dgit tracking views of the same
+suite, made by different instances of dgit, may vary.
+They will have the same contents, but may have different history.
+
+There is no uniform linkage between the tracking branches for
+different suites.
+The Debian infrastructure
+does not do any automatic import of uploads made without dgit.
+It would be possible for a distro's infrastructure to do this;
+in that case,
+different dgit client instances
+would see exactly the same history.
+
+There has been no bulk import of historical uploads into
+Debian's dgit infrastructure.
+To do this it would be necessary to decide whether to
+import existing vcs history
+(which might not be faithful to dgit's invariants)
+or previous non-Dgit uploads
+(which would not provide a very rich history).
+.SH READ-ONLY DISTROS
+Distros which do not maintain a set of dgit history git repositories
+can still be used in a read-only mode with dgit. Currently Ubuntu
+is configured this way.
+.SH GITATTRIBUTES
+git has features which can automatically transform files
+as they are being copied between the working tree
+and the git history.
+The attributes can be specified in the source tree itself,
+in
+.BR .gitattributes .
+See \fBgitattributes\fP(5).
+
+These transformations are context-sensitive
+and not, in general, reversible,
+so dgit operates on the principle that
+the dgit git history contains the actual contents of the package.
+(When dgit is manipulating a .dsc,
+it does so in a private area,
+where the transforming gitattributes are defused,
+to achieve this.)
+
+If transforming gitattributes are used,
+they can cause trouble,
+because the working tree files can differ from
+the git revision history
+(and therefore from the source packages).
+dgit warns if it finds a .gitattributes file
+(in a package being fetched or imported),
+unless the transforming gitattributes have been defused.
+
+dgit clone
+and dgit setup-new-tree
+disable transforming gitattributes
+by default,
+by creating a suitable .git/info/attributes.
+See
+.B dgit setup-new-tree
+and
+.B dgit setup-gitattributes
+in dgit(1).
+.SH PACKAGE SOURCE FORMATS
+If you are not the maintainer, you do not need to worry about the
+source format of the package. You can just make changes as you like
+in git. If the package is a `3.0 (quilt)' package, the patch stack
+will usually not be represented in the git history.
+.SH FORMAT 3.0 (QUILT)
+For a format `3.0 (quilt)' source package, dgit may have to make a
+commit on your current branch to contain metadata used by quilt and
+dpkg-source.
+
+This is because `3.0 (quilt)' source format represents the patch stack
+as files in debian/patches/ actually inside the source tree. This
+means that, taking the whole tree (as seen by git or ls) (i)
+dpkg-source cannot represent certain trees, and (ii) packing up a tree
+in `3.0 (quilt)' and then unpacking it does not always yield the same
+tree.
+
+dgit will automatically work around this for you when building and
+pushing. The only thing you need to know is that dgit build, sbuild,
+etc., may make new commits on your HEAD. If you're not a quilt user
+this commit won't contain any changes to files you care about.
+
+You can explicitly request that dgit do just this fixup, by running
+dgit quilt-fixup.
+
+If you are a quilt user you need to know that dgit's git trees are
+`patches applied packaging branches' and do not contain the .pc
+directory (which is used by quilt to record which patches are
+applied). If you want to manipulate the patch stack you probably want
+to be looking at tools like git-dpm.
+.SH SPLIT VIEW QUILT MODE
+When working with git branches intended
+for use with the `3.0 (quilt)' source format
+dgit can automatically convert a suitable
+maintainer-provided git branch
+(in one of a variety of formats)
+into a dgit branch.
+
+When a split view mode is engaged
+dgit build commands and
+dgit push
+will, on each invocation,
+convert the user's HEAD into the dgit view,
+so that it can be built and/or uploaded.
+
+dgit push in split view mode will push the dgit view to the dgit
+git server.
+The dgit view is always a descendant of the maintainer view.
+dgit push will also make a maintainer view tag
+according to DEP-14
+and push that to the dgit git server.
+
+Split view mode must be enabled explicitly
+(by the use of the applicable command line options,
+subcommands, or configuration).
+This is because it is not possible to reliably tell
+(for example)
+whether a git tree for a dpkg-source `3.0 (quilt)' package
+is a patches-applied or patches-unapplied tree.
+
+Split view conversions are cached in the ref
+dgit-intern/quilt-cache.
+This should not be manipulated directly.
+.SH FILES IN THE ORIG TARBALL BUT NOT IN GIT - AUTOTOOLS ETC.
+This section is mainly of interest to maintainers who want to use dgit
+with their existing git history for the Debian package.
+
+Some developers like to have an extra-clean git tree which lacks files
+which are normally found in source tarballs and therefore in Debian
+source packages. For example, it is conventional to ship ./configure
+in the source tarball, but some people prefer not to have it present
+in the git view of their project.
+
+dgit requires that the source package unpacks to exactly the same
+files as are in the git commit on which dgit push operates. So if you
+just try to dgit push directly from one of these extra-clean git
+branches, it will fail.
+
+As the maintainer you therefore have the following options:
+.TP
+\(bu
+Delete the files from your git branches,
+and your Debian source packages,
+and carry the deletion as a delta from upstream.
+(With `3.0 (quilt)' this means represeting the deletions as patches.
+You may need to pass --include-removal to dpkg-source --commit,
+or pass corresponding options to other tools.)
+This can make the Debian
+source package less useful for people without Debian build
+infrastructure.
+.TP
+\(bu
+Persuade upstream that the source code in their git history and the
+source they ship as tarballs should be identical. Of course simply
+removing the files from the tarball may make the tarball hard for
+people to use.
+.IP
+One answer is to commit the (maybe autogenerated)
+files, perhaps with some simple automation to deal with conflicts and
+spurious changes. This has the advantage that someone who clones
+the git repository finds the program just as easy to build as someone
+who uses the tarball.
+.LP
+Of course it may also be that the differences are due to build system
+bugs, which cause unintended files to end up in the source package.
+dgit will notice this and complain. You may have to fix these bugs
+before you can unify your existing git history with dgit's.
+.LP
+.SH FILES IN THE SOURCE PACKAGE BUT NOT IN GIT - DOCS, BINARIES ETC.
+Some upstream tarballs contain build artifacts which upstream expects
+some users not to want to rebuild (or indeed to find hard to rebuild),
+but which in Debian we always rebuild.
+.LP
+Examples sometimes include crossbuild firmware binaries and
+documentation.
+To avoid problems when building updated source
+packages
+(in particular, to avoid trying to represent as changes in
+the source package uninteresting or perhaps unrepresentable changes
+to such files)
+many maintainers arrange for the package clean target
+to delete these files.
+.LP
+dpkg-source does not
+(with any of the commonly used source formats)
+represent deletion of binaries (outside debian/) present in upstream.
+Thus deleting such files in a dpkg-source working tree does not
+actually result in them being deleted from the source package.
+Thus
+deleting the files in rules clean sweeps this problem under the rug.
+.LP
+However, git does always properly record file deletion.
+Since dgit's
+principle is that the dgit git tree is the same of dpkg-source -x,
+that means that a dgit-compatible git tree always contains these
+files.
+.LP
+For the non-maintainer,
+this can be observed in the following suboptimal occurrences:
+.TP
+\(bu
+The package clean target often deletes these files, making the git
+tree dirty trying to build the source package, etc.
+This can be fixed
+by using
+.BR "dgit -wg" " aka " "--clean=git" ,
+so that the package clean target is never run.
+.TP
+\(bu
+The package build modifies these files, so that builds make the git
+tree dirty.
+This can be worked around by using `git reset --hard'
+after each build
+(or at least before each commit or push).
+.LP
+From the maintainer's point of view,
+the main consequence is that to make a dgit-compatible git branch
+it is necessary to commit these files to git.
+The maintainer has a few additional options for mitigation:
+for example,
+it may be possible for the rules file to arrange to do the
+build in a temporary area, which avoids updating the troublesome
+files;
+they can then be left in the git tree without seeing trouble.
+.SH PROBLEMS WITH PACKAGE CLEAN TARGETS ETC.
+A related problem is other unexpected behaviour by a package's
+.B clean
+target.
+If a package's rules
+modify files which are distributed in the package,
+or simply forget to remove certain files,
+dgit will complain that the tree is dirty.
+.LP
+Again, the solution is to use
+.BR "dgit -wg" " aka " "--clean=git" ,
+which instructs dgit to use git clean instead of the package's
+build target,
+along with perhaps
+.B git reset --hard
+before each build.
+.LP
+This is 100% reliable, but has the downside
+that if you forget to git add or to commit, and then use
+.BR "dgit -wg" " or " "git reset --hard" ,
+your changes may be lost.
diff --git a/infra/cgit-regen-config b/infra/cgit-regen-config
new file mode 100755
index 0000000..36228a1
--- /dev/null
+++ b/infra/cgit-regen-config
@@ -0,0 +1,26 @@
+#!/bin/sh
+set -e
+
+root=/srv/dgit.debian.org
+
+repos=$root/unpriv/repos
+outfile=$root/etc/projects.cgit
+lockfile=$outfile.lock
+template=$root/config/cgit-template
+
+flock $lockfile -c '
+ outfile='"$outfile"'
+ repos='"$repos"'
+ exec >"$outfile.tmp"
+ for ff in "$repos"/[0-9a-z]*.git; do
+ f=${ff##*/}
+ p=${f%.git}
+ cat <<END
+repo.url=$f
+repo.path=$repos/$f
+END
+ sed "s/%PACKAGE%/$p/g" <'"$template"'
+ echo
+ done
+ mv -f "$outfile.tmp" "$outfile"
+'
diff --git a/infra/dgit-mirror-rsync b/infra/dgit-mirror-rsync
new file mode 100755
index 0000000..9346489
--- /dev/null
+++ b/infra/dgit-mirror-rsync
@@ -0,0 +1,171 @@
+#!/bin/bash
+#
+# Mirror script for use as a dgit-repos-server mirror hook
+#
+# In addition to updated-hook (invoked by dgit-repos-server),
+# this script also supports the following ACTIONs:
+# MIRROR-HOOK-SCRIPT ... setup [...] create queue dir etc.
+# MIRROR-HOOK-SCRIPT ... backlog [...] do all packages which need it
+# MIRROR-HOOK-SCRIPT ... all [...] do all packages
+# MIRROR-HOOK-SCRIPT ... mirror PACKAGE [...] do just that, longer timeout
+#
+# DISTRO-DIR must contain a file `mirror-settings' which is a bash
+# script fragment assigning the following variables:
+# remoterepos for rsync, in form user@host:/dir
+# and optionally
+# hooktimeout default 30 [sec]
+# rsynctimeout default 900 [sec]
+# rsyncssh default 'ssh -o batchmode=yes'
+# rsync array, default (rsync -rltH --safe-links --delete)
+# repos default DISTRO-DIR/repos
+# (optional settings are all set before mirror-settings is included,
+# so you can modify them with += or some such)
+
+set -e
+set -o pipefail
+shopt -s nullglob
+
+case "$DGIT_DRS_DEBUG" in
+''|0|1) ;;
+*) set -x ;;
+esac
+
+fail () {
+ echo >&2 "dgit-mirror-rsync: $*"; exit 127
+}
+
+if [ $# -lt 2 ]; then fail "too few arguments"; fi
+
+self=$0
+
+case "$self" in
+/*) ;;
+*/*) self="$PWD/$self" ;;
+*) ;;
+esac
+
+distrodir=$1; shift
+action=$1; shift
+package=$1
+
+repos=$distrodir/repos
+
+rsync=(rsync -rltH --safe-links --delete)
+hooktimeout=30
+rsynctimeout=900
+rsyncssh='ssh -o batchmode=yes'
+
+. $distrodir/mirror-settings
+
+# contents of $queue
+# $queue/$package.n - mirror needed
+# $queue/$package.a - being attempted, or attempt failed
+# $queue/$package.lock - lock (with-lock-ex)
+# $queue/$package.err - stderr from failed (or current) run
+# $queue/$package.log - stderr from last successful run
+
+cd $repos
+queue=_mirror-queue
+
+case "$remoterepos" in
+*:/*|/*) ;;
+'') fail "remoterepos config not set" ;;
+*) fail "remoterepos config does not match *:/* or /*" ;;
+esac
+
+actually () {
+ "${rsync[@]}" \
+ --timeout=$rsynctimeout \
+ -e "$rsyncssh" \
+ "$repos/$package.git"/. \
+ "$remoterepos/$package.git"
+}
+
+reinvoke () {
+ newaction="$1"; shift
+
+ exec \
+ "$@" \
+ "$self" "$distrodir" "reinvoke$newaction" "$package"
+}
+
+check-package-mirrorable () {
+ local repo=$repos/$package.git
+ local mode; mode=$(stat -c%a "$repo")
+ case $mode in
+ *5) return 0 ;;
+ *0) return 1 ;;
+ *) echo >&2 "unexpected mode $mode for $repo"; return 1 ;;
+ esac
+}
+
+lock-and-process () {
+ check-package-mirrorable || return 0
+ reinvoke -locked with-lock-ex -w "$queue/$package.lock"
+}
+
+attempt () {
+ exec 3>&2 >"$queue/$package.err" 2>&1
+ if actually; then
+ rm -f "$queue/$package.a"
+ exec 2>&3 2>&1
+ mv -f "$queue/$package.err" "$queue/$package.log"
+ if ! [ -s "$queue/$package.log" ]; then
+ rm "$queue/$package.log"
+ fi
+ rm "$queue/$package.lock"
+ else
+ cat >&3 "$queue/$package.err"
+ exit 127
+ fi
+}
+
+lock-and-process-baseof-f () {
+ package=${f##*/}
+ package=${package%.*}
+ lock-and-process
+}
+
+case "$action" in
+
+updated-hook)
+ check-package-mirrorable || exit 0
+ touch "$queue/$package.n"
+ reinvoke -timed timeout --foreground $hooktimeout
+ ;;
+
+reinvoke-timed)
+ (lock-and-process) >/dev/null 2>&1
+ ;;
+
+mirror)
+ lock-and-process
+ ;;
+
+reinvoke-locked)
+ touch "$queue/$package.a"
+ rm -f "$queue/$package.n"
+ attempt
+ ;;
+
+backlog)
+ for f in $queue/*.[na]; do
+ (lock-and-process-baseof-f ||:)
+ done
+ ;;
+
+all)
+ for f in [a-z0-9]*.git; do
+ (lock-and-process-baseof-f)
+ done
+ ;;
+
+setup)
+ test -d "$queue" || mkdir "$queue"
+ ;;
+
+*)
+ fail "bad action $action"
+ ;;
+
+esac
diff --git a/infra/dgit-repos-admin-debian b/infra/dgit-repos-admin-debian
new file mode 100755
index 0000000..6d1e4d0
--- /dev/null
+++ b/infra/dgit-repos-admin-debian
@@ -0,0 +1,220 @@
+#!/usr/bin/perl -w
+# dgit repos policy admin script for Debian
+#
+# Copyright (C) 2015-2016 Ian Jackson
+#
+# 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
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+use strict;
+
+use Debian::Dgit::Infra; # must precede Debian::Dgit; - can change @INC!
+use Debian::Dgit;
+setup_sigwarn();
+
+our $usage = <<'END';
+usage:
+ dgit-repos-admin-debian [<options>] operation...
+options:
+ --git-dir /path/to/git/repo/or/working/tree
+ --repos /path/to/dgit/repos/directory } alternatives
+ --db /path/to/dgit/repos/policy.sqlite3 }
+ (at least one of above required; if only one, cwd is used for other)
+operations:
+ create-db
+ list-taints
+ taint [--global|<package>] <gitobjid> '<comment>'
+ untaint [--global|<package>] <gitobjid>
+END
+
+use POSIX;
+use DBI;
+
+use Debian::Dgit::Policy::Debian;
+
+sub badusage ($) { die "bad usage: $_[0]\n$usage"; }
+
+use Getopt::Long qw(:config posix_default gnu_compat bundling);
+
+our ($git_dir,$repos_dir,$db_path);
+
+GetOptions("git-dir=s" => \$git_dir,
+ "repos=s" => \$repos_dir,
+ "db=s" => \$db_path)
+ or die $usage;
+
+$db_path //= poldb_path($repos_dir) if defined $repos_dir;
+$db_path // $repos_dir ||
+ die <<'END'.$usage;
+Must supply --git-dir and/or --repos (or --db instead of --repos).
+If only one of --git-dir and --repos is supplied, other is taken to
+be current working directory.
+END
+# /
+
+$git_dir //= '.';
+$repos_dir //= '.';
+
+our $p;
+our $gitobjid;
+
+sub get_package_objid () {
+ $p = shift @ARGV; $p // badusage "operation needs package or --global";
+ if ($p eq '--global') {
+ $p = '';
+ } else {
+ $p =~ m/^$package_re$/ or badusage 'package name or --global needed';
+ }
+ $gitobjid = shift @ARGV;
+ $gitobjid // badusage "operation needs git object id";
+ $gitobjid =~ m/\W/ && badusage "invalid git object id";
+}
+
+sub sort_out_git_dir () {
+ foreach my $sfx ('/.git', '') {
+ my $path = "$git_dir/$sfx";
+ if (stat_exists "$path/objects") {
+ $ENV{GIT_DIR} = $git_dir = $path;
+ return;
+ }
+ }
+ die "git directory $git_dir doesn't seem valid\n";
+}
+
+sub show_taints ($$@) {
+ my ($m, $cond, @condargs) = @_;
+ my $q = $poldbh->prepare
+ ("SELECT package,gitobjid,gitobjtype,time,comment, ".
+ " (gitobjdata IS NOT NULL) hasdata".
+ " FROM taints WHERE $cond".
+ " ORDER BY package, gitobjid, time");
+ $q->execute(@condargs);
+ print "$m:\n" or die $!;
+ my $count = 0;
+ while (my $row = $q->fetchrow_hashref) {
+ my $t = strftime "%Y-%m-%dT%H:%M:%S", gmtime $row->{time};
+ my $objinfo = $row->{gitobjtype}. ($row->{hasdata} ? '+' : ' ');
+ my $comment = $row->{comment};
+ $comment =~ s/\\/\\\\/g; $comment =~ s/\n/\\n/g;
+ printf(" %s %-30s %s %7s %s\n",
+ $t, $row->{package}, $row->{gitobjid},
+ $objinfo, $row->{comment})
+ or die $!;
+ $count++;
+ }
+ return $count;
+}
+
+sub cmd_list_taints ($) {
+ badusage "no args/options" if @ARGV;
+ my $count = show_taints("all taints","1");
+ printf "%d taints listed\n", $count or die $!;
+}
+
+sub cmd_create_db ($) {
+ badusage "no args/options" if @ARGV;
+
+ $poldbh->do(<<END);
+ CREATE TABLE IF NOT EXISTS taints (
+ taint_id INTEGER NOT NULL PRIMARY KEY ASC AUTOINCREMENT,
+ package TEXT NOT NULL,
+ gitobjid TEXT NOT NULL,
+ comment TEXT NOT NULL,
+ time INTEGER,
+ gitobjtype TEXT,
+ gitobjdata TEXT
+ )
+END
+ $poldbh->do(<<END);
+ CREATE INDEX IF NOT EXISTS taints_by_gitobjid
+ ON taints (gitobjid, package)
+END
+ # any one of of the listed deliberatelies will override its taint
+ # the field `deliberately' contains `--deliberately-blah-blah',
+ # not just `blah blah'.
+ $poldbh->do(<<END);
+ CREATE TABLE IF NOT EXISTS taintoverrides (
+ taint_id INTEGER NOT NULL
+ REFERENCES taints (taint_id)
+ ON UPDATE RESTRICT
+ ON DELETE CASCADE
+ DEFERRABLE INITIALLY DEFERRED,
+ deliberately TEXT NOT NULL,
+ PRIMARY KEY (taint_id, deliberately)
+ )
+END
+
+ $poldbh->commit;
+}
+
+sub show_taints_bypackage ($) {
+ my ($m) = @_;
+ show_taints($m, "package = ?", $p);
+}
+
+sub show_taints_bygitobjid ($) {
+ my ($m) = @_;
+ show_taints($m, "gitobjid = ?", $gitobjid);
+}
+
+sub show_relevant_taints ($) {
+ my ($what) = @_;
+ show_taints_bypackage($p ? "$what taints for package $p"
+ : "$what global taints");
+ show_taints_bygitobjid("$what taints for object $gitobjid");
+}
+
+sub cmd_taint () {
+ get_package_objid();
+ my $comment = shift @ARGV;
+ $comment // badusage "operation needs comment";
+ @ARGV && badusage "too many arguments to taint";
+
+ sort_out_git_dir();
+ $!=0; $?=0; my $objtype = `git cat-file -t $gitobjid`;
+ chomp $objtype or die "$? $!";
+
+ $poldbh->do("INSERT INTO taints".
+ " (package, gitobjid, gitobjtype, time, comment)".
+ " VALUES (?,?,?,?,?)", {},
+ $p, $gitobjid, $objtype, time, $comment);
+ $poldbh->commit;
+ print "taint added\n" or die $!;
+ show_relevant_taints("resulting");
+}
+
+sub cmd_untaint () {
+ get_package_objid();
+ @ARGV && badusage "too many arguments to untaint";
+
+ show_relevant_taints("existing");
+ my $affected =
+ $poldbh->do("DELETE FROM taints".
+ " WHERE package = ? AND gitobjid = ?",
+ {}, $p, $gitobjid);
+ $poldbh->commit;
+ printf "%d taints removed\n", $affected or die $!;
+ exit $affected ? 0 : 1;
+}
+
+
+my $cmd = shift @ARGV;
+$cmd // badusage "need operation";
+
+$cmd =~ y/-/_/;
+my $fn = ${*::}{"cmd_$cmd"};
+$fn or badusage "unknown operation $cmd";
+
+poldb_setup($db_path);
+
+$fn->();
diff --git a/infra/dgit-repos-policy-debian b/infra/dgit-repos-policy-debian
new file mode 100755
index 0000000..990abd2
--- /dev/null
+++ b/infra/dgit-repos-policy-debian
@@ -0,0 +1,553 @@
+#!/usr/bin/perl -w
+# dgit repos policy hook script for Debian
+#
+# Copyright (C) 2015-2016 Ian Jackson
+#
+# 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
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+use strict;
+
+use Debian::Dgit::Infra; # must precede Debian::Dgit; - can change @INC!
+use Debian::Dgit qw(:DEFAULT :policyflags);
+setup_sigwarn();
+
+use POSIX;
+use JSON;
+use File::Temp qw(tempfile);
+use DBI;
+use IPC::Open2;
+use Data::Dumper;
+
+use Debian::Dgit::Policy::Debian;
+
+initdebug('%');
+enabledebuglevel $ENV{'DGIT_DRS_DEBUG'};
+
+END { $? = 127; } # deliberate exit uses _exit
+
+our $distro = shift @ARGV // die "need DISTRO";
+our $repos = shift @ARGV // die "need DGIT-REPOS-DIR";
+our $dgitlive = shift @ARGV // die "need DGIT-LIVE-DIR";
+our $distrodir = shift @ARGV // die "need DISTRO-DIR";
+our $action = shift @ARGV // die "need ACTION";
+
+our $publicmode = 02775;
+our $new_upload_propagation_slop = 3600*4 + 100;# fixme config;
+
+our $poldbh;
+our $pkg;
+our $pkgdir;
+our ($pkg_exists,$pkg_secret);
+
+our $stderr;
+
+our ($version,$suite,$tagname);
+our %deliberately;
+
+# We assume that it is not possible for NEW to have a version older
+# than sid.
+
+# Whenever pushing, we check for
+# source-package-local tainted history
+# global tainted history
+# can be overridden by --deliberately except for an admin prohib taint
+#
+# ALL of the following apply only if history is secret:
+#
+# if NEW has no version, or a version which is not in our history[1]
+# (always)
+# check all suites
+# if any suite's version is in our history[1], publish our history
+# otherwise discard our history,
+# tainting --deliberately-include-questionable-history
+#
+# if NEW has a version which is in our history[1]
+# (on push only)
+# require explicit specification of one of
+# --deliberately-include-questionable-history
+# --deliberately-not-fast-forward
+# (latter will taint old NEW version --d-i-q-h)
+# (otherwise)
+# leave it be
+#
+# [1] looking for the relevant git tag for the version number and not
+# caring what that tag refers to.
+#
+# When we are doing a push to a fresh repo, any version will do: in
+# this case, this is the first dgit upload of an existing package,
+# and we trust that the uploader hasn't included in their git
+# history any previous non-dgit uploads.
+#
+# A wrinkle: if we approved a push recently, we treat NEW as having
+# a version which is in our history. This is because the package may
+# still be being uploaded. (We record this using the timestamp of the
+# package's git repo directory.)
+
+# We aim for the following invariants and properties:
+#
+# - .dsc of published dgit package will have corresponding publicly
+# visible dgit-repo (soon)
+#
+# - when a new package is rejected we help maintainer avoid
+# accidentally including bad objects in published dgit history
+#
+# - .dsc of NEW dgit package has corresponding dgit-repo but not
+# publicly readable
+
+sub apiquery ($) {
+ my ($subpath) = @_;
+ local $/=undef;
+ my $dgit = "$dgitlive/dgit";
+ $dgit = "dgit" if !stat_exists $dgit;
+ my $cmd = "$dgit -d$distro \$DGIT_TEST_OPTS";
+ $cmd .= " -".("D" x $debuglevel) if $debuglevel;
+ $cmd .= " archive-api-query $subpath";
+ printdebug "apiquery $cmd\n";
+ $!=0; $?=0; my $json = `$cmd`;
+ defined $json && !$? or die "$subpath $! $?";
+ my $r = decode_json $json;
+ my $d = new Data::Dumper([$r], [qw(r)]);
+ printdebug "apiquery $subpath | ", $d->Dump() if $debuglevel>=2;
+ return $r;
+}
+
+sub vsn_in_our_history ($) {
+ my ($vsn) = @_;
+
+ # Eventually, when we withdraw support for old-format (DEP-14
+ # namespace) tags, we will need to change this to only look
+ # for debiantag_new. See the commit
+ # "Tag change: Update dgit-repos-policy-debian"
+ # (reverting which is a good start for that change).
+
+ my @tagrefs = map { "refs/tags/".$_ } debiantags $vsn, $distro;
+ printdebug " checking history vsn=$vsn tagrefs=@tagrefs\n";
+ open F, "-|", qw(git for-each-ref), @tagrefs;
+ $_ = <F>;
+ close F;
+ return 1 if defined && m/\S/;
+ die "$pkg tagrefs @tagrefs $? $!" if $?;
+ return 0;
+}
+
+sub specific_suite_has_suitable_vsn ($$) {
+ my ($suite, $vsn_check) = @_; # tests $vsn_check->($version)
+ my $in_suite = apiquery "dsc_in_suite/$suite/$pkg";
+ foreach my $entry (@$in_suite) {
+ my $vsn = $entry->{version};
+ die "$pkg ?" unless defined $vsn;
+ printdebug " checking history found suite=$suite vsn=$vsn\n";
+ return 1 if $vsn_check->($vsn);
+ }
+ return 0;
+}
+
+sub new_has_vsn_in_our_history () {
+ return specific_suite_has_suitable_vsn('new', \&vsn_in_our_history);
+}
+
+sub good_suite_has_suitable_vsn ($) {
+ my ($vsn_check) = @_; # as for specific_suite_has_specific_vsn
+ my $suites = apiquery "suites";
+ foreach my $suitei (@$suites) {
+ my $suite = $suitei->{name};
+ die unless defined $suite;
+ next if $suite =~ m/\bnew$/;
+ return 1 if specific_suite_has_suitable_vsn($suite, $vsn_check);
+ }
+ return 0;
+}
+
+sub statpackage () {
+ $pkgdir = "$repos/$pkg.git";
+ if (!stat_exists $pkgdir) {
+ printdebug "statpackage $pkg => ENOENT\n";
+ $pkg_exists = 0;
+ } else {
+ $pkg_exists = 1;
+ $pkg_secret = !!(~(stat _)[2] & 05);
+ printdebug "statpackage $pkg => exists, secret=$pkg_secret.\n";
+ }
+}
+
+sub getpackage () {
+ die unless @ARGV >= 1;
+ $pkg = shift @ARGV;
+ die unless $pkg =~ m/^$package_re$/;
+
+ statpackage();
+}
+
+sub add_taint ($$) {
+ my ($refobj, $reason) = @_;
+
+ printdebug "TAINTING $refobj\n",
+ (map { "\%| $_" } split "\n", $reason),
+ "\n";
+
+ my $tf = new File::Temp or die $!;
+ print $tf "$refobj^0\n" or die $!;
+ flush $tf or die $!;
+ seek $tf,0,0 or die $!;
+
+ my $gcfpid = open GCF, "-|";
+ defined $gcfpid or die $!;
+ if (!$gcfpid) {
+ open STDIN, "<&", $tf or die $!;
+ exec 'git', 'cat-file', '--batch';
+ die $!;
+ }
+
+ close $tf or die $!;
+ $_ = <GCF>;
+ defined $_ or die;
+ m/^(\w+) (\w+) (\d+)\n/ or die "$_ ?";
+ my $gitobjid = $1;
+ my $gitobjtype = $2;
+ my $bytes = $3;
+
+ my $gitobjdata;
+ if ($gitobjtype eq 'commit' or $gitobjtype eq 'tag') {
+ $!=0; read GCF, $gitobjdata, $bytes == $bytes
+ or die "$gitobjid $bytes $!";
+ }
+ close GCF;
+
+ $poldbh->do("INSERT INTO taints".
+ " (package, gitobjid, gitobjtype, gitobjdata, time, comment)".
+ " VALUES (?,?,?,?,?,?)", {},
+ $pkg, $gitobjid, $gitobjtype, $gitobjdata, time, $reason);
+
+ my $taint_id = $poldbh->last_insert_id(undef,undef,"taints","taint_id");
+ die unless defined $taint_id;
+
+ $poldbh->do("INSERT INTO taintoverrides".
+ " (taint_id, deliberately)".
+ " VALUES (?, '--deliberately-include-questionable-history')",
+ {}, $taint_id);
+}
+
+sub add_taint_by_tag ($$) {
+ my ($tagname,$refobjid) = @_;
+ add_taint($refobjid,
+ "tag $tagname referred to this object in git tree but all".
+ " previously pushed versions were found to have been".
+ " removed from NEW (ie, rejected) (or never arrived)");
+}
+
+sub check_package () {
+ return 0 unless $pkg_exists;
+ return 0 unless $pkg_secret;
+
+ printdebug "check_package\n";
+
+ chdir $pkgdir or die "$pkgdir $!";
+
+ stat '.' or die "$pkgdir $!";
+ my $mtime = ((stat _)[9]);
+ my $age = time - $mtime;
+ printdebug "check_package age=$age\n";
+
+ if (good_suite_has_suitable_vsn(\&vsn_in_our_history)) {
+ chmod $publicmode, "." or die $!;
+ $pkg_secret = 0;
+ eval {
+ my $mirror_hook = "$distrodir/mirror-hook";
+ if (stat_exists $mirror_hook) {
+ my @mirror_cmd =
+ ($mirror_hook, $distrodir, "updated-hook", $pkg);
+ debugcmd " (mirror)",@mirror_cmd;
+ system @mirror_cmd and failedcmd @mirror_cmd;
+ }
+ };
+ if (length $@) {
+ chomp $@;
+ print STDERR "policy hook: warning:".
+ " failed to mirror publication of \`$pkg':".
+ " $@\n";
+ }
+ return 0;
+ }
+
+ return 0 if $age < $new_upload_propagation_slop;
+
+ return 0 if new_has_vsn_in_our_history();
+
+ printdebug "check_package secret, deleted, tainting\n";
+
+ git_for_each_ref('refs/tags', sub {
+ my ($objid,$objtype,$fullrefname,$tagname) = @_;
+ add_taint_by_tag($tagname,$objid);
+ });
+
+ return FRESHREPO;
+}
+
+sub action_check_package () {
+ getpackage();
+ return check_package();
+}
+
+sub getpushinfo () {
+ die unless @ARGV >= 4;
+ $version = shift @ARGV;
+ $suite = shift @ARGV;
+ $tagname = shift @ARGV;
+ my $delibs = shift @ARGV;
+ foreach my $delib (split /\,/, $delibs) {
+ $deliberately{$delib} = 1;
+ }
+}
+
+sub deliberately ($) { return $deliberately{"--deliberately-$_[0]"}; }
+
+sub action_push () {
+ getpackage();
+ getpushinfo();
+
+ check_package(); # might make package public, or might add taints
+
+ return 0 unless $pkg_exists;
+ return 0 unless $pkg_secret;
+
+ # we suppose that NEW has a version which is already in our
+ # history, as otherwise the repo would have been blown away
+
+ if (deliberately('not-fast-forward')) {
+ add_taint(server_ref($suite),
+ "rewound suite $suite; --deliberately-not-fast-forward".
+ " specified in signed tag $tagname for upload of".
+ " version $version");
+ return NOFFCHECK|FRESHREPO;
+ }
+ if (deliberately('include-questionable-history')) {
+ return 0;
+ }
+ die "\nPackage is in NEW and has not been accepted or rejected yet;".
+ " use a --deliberately option to specify whether you are".
+ " keeping or discarding the previously pushed history. ".
+ " Please RTFM dgit(1).\n\n";
+}
+
+sub action_push_confirm () {
+ getpackage();
+ getpushinfo();
+ die unless @ARGV >= 1;
+ my $freshrepo = shift @ARGV;
+
+ my $initq = $poldbh->prepare(<<END);
+ SELECT taint_id, gitobjid FROM taints t
+ WHERE (package = ? OR package = '')
+END
+ $initq->execute($pkg);
+
+ my @objscatcmd = qw(git);
+ push @objscatcmd, qw(--git-dir), $freshrepo if length $freshrepo;
+ push @objscatcmd, qw(cat-file --batch);
+ debugcmd '|',@objscatcmd if $debuglevel>=2;
+
+ my @taintids;
+ my $chkinput = tempfile();
+ while (my $taint = $initq->fetchrow_hashref()) {
+ push @taintids, $taint->{taint_id};
+ print $chkinput $taint->{gitobjid}, "\n" or die $!;
+ printdebug '|> ', $taint->{gitobjid}, "\n" if $debuglevel>=2;
+ }
+ flush $chkinput or die $!;
+ seek $chkinput,0,0 or die $!;
+
+ my $checkpid = open CHKOUT, "-|" // die $!;
+ if (!$checkpid) {
+ open STDIN, "<&", $chkinput or die $!;
+ delete $ENV{GIT_ALTERNATE_OBJECT_DIRECTORIES};
+ # ^ recent versions of git set this in the environment of
+ # receive hooks. This can cause us to see things which
+ # the user is trying to abolish.
+ exec @objscatcmd or die $!;
+ }
+
+ my ($taintinfoq,$overridesanyq,$untaintq,$overridesq);
+
+ my $overridesstmt = <<END;
+ SELECT deliberately FROM taintoverrides WHERE (
+ 1=0
+END
+ my @overridesv = sort keys %deliberately;
+ $overridesstmt .= <<END foreach @overridesv;
+ OR deliberately = ?
+END
+ $overridesstmt .= <<END;
+ ) AND taint_id = ?
+ ORDER BY deliberately ASC
+END
+
+ my $mustreject=0;
+
+ while (my $taintid = shift @taintids) {
+ $!=0; $_ = <CHKOUT>;
+ die "($taintid @objscatcmd) $!" unless defined $_;
+ printdebug "|< ", $_ if $debuglevel>=2;
+
+ next if m/^\w+ missing$/;
+ die "($taintid @objscatcmd) $_ ?" unless m/^(\w+) (\w+) (\d+)\s/;
+ my ($objid,$objtype,$nbytes) = ($1,$2,$3);
+
+ my $drop;
+ (read CHKOUT, $drop, $nbytes) == $nbytes
+ or die "($taintid @objscatcmd) $!";
+
+ $!=0; $_ = <CHKOUT>;
+ die "($taintid @objscatcmd) $!" unless defined $_;
+ die "($taintid @objscatcmd) $_ ?" if m/\S/;
+
+ $taintinfoq ||= $poldbh->prepare(<<END);
+ SELECT package, time, comment FROM taints WHERE taint_id = ?
+END
+ $taintinfoq->execute($taintid);
+
+ my $ti = $taintinfoq->fetchrow_hashref();
+ die "($taintid)" unless $ti;
+
+ my $timeshow = defined $ti->{time}
+ ? " at time ".strftime("%Y-%m-%d %H:%M:%S Z", gmtime $ti->{time})
+ : "";
+ my $pkgshow = length $ti->{package}
+ ? "package $ti->{package}"
+ : "any package";
+
+ $stderr .= <<END;
+
+History contains tainted $objtype $objid
+Taint recorded$timeshow for $pkgshow
+Reason: $ti->{comment}
+END
+
+ printdebug "SQL overrides: @overridesv $taintid /\n$overridesstmt\n";
+
+ $overridesq ||= $poldbh->prepare($overridesstmt);
+ $overridesq->execute(@overridesv, $taintid);
+ my ($ovwhy) = $overridesq->fetchrow_array();
+ if (!defined $ovwhy) {
+ $overridesanyq ||= $poldbh->prepare(<<END);
+ SELECT 1 FROM taintoverrides WHERE taint_id = ? LIMIT 1
+END
+ $overridesanyq->execute($taintid);
+ my ($ovany) = $overridesanyq->fetchrow_array();
+ $stderr .= $ovany ? <<END : <<END;
+Could be forced using --deliberately. Consult documentation.
+END
+Uncorrectable error. If confused, consult administrator.
+END
+ $mustreject = 1;
+ } else {
+ $stderr .= <<END;
+Forcing due to --deliberately-$ovwhy
+END
+ $untaintq ||= $poldbh->prepare(<<END);
+ DELETE FROM taints WHERE taint_id = ?
+END
+ $untaintq->execute($taintid);
+ }
+ }
+ close CHKOUT;
+
+ if ($mustreject) {
+ $stderr .= <<END;
+
+Rejecting push due to questionable history.
+END
+ return 1;
+ }
+
+ if (length $freshrepo) {
+ if (!good_suite_has_suitable_vsn(sub { 1; })) {
+ stat $freshrepo or die "$freshrepo $!";
+ my $oldmode = ((stat _)[2]);
+ my $oldwrites = $oldmode & 0222;
+ # remove r and x bits which have corresponding w bits clear
+ my $newmode = $oldmode &
+ (~0555 | ($oldwrites << 1) | ($oldwrites >> 1));
+ printdebug sprintf "chmod %#o (was %#o) %s\n",
+ $newmode, $oldmode, $freshrepo;
+ chmod $newmode, $freshrepo or die $!;
+ utime undef, undef, $freshrepo or die $!;
+ }
+ }
+
+ return 0;
+}
+
+sub action_check_list () {
+ opendir L, "$repos" or die "$repos $!";
+ while (defined (my $dent = readdir L)) {
+ next unless $dent =~ m/^($package_re)\.git$/;
+ $pkg = $1;
+ statpackage();
+ next unless $pkg_exists;
+ next unless $pkg_secret;
+ print "$pkg\n" or die $!;
+ }
+ closedir L or die $!;
+ close STDOUT or die $!;
+ return 0;
+}
+
+$action =~ y/-/_/;
+my $fn = ${*::}{"action_$action"};
+if (!$fn) {
+ printdebug "dgit-repos-policy-debian: unknown action $action\n";
+ exit 0;
+}
+
+my $sleepy=0;
+my $rcode;
+
+my $db_busy_exception= 'Debian::Dgit::Policy::Debian::DB_BUSY';
+
+my @orgargv = @ARGV;
+
+for (;;) {
+ @ARGV = @orgargv;
+ eval {
+ poldb_setup(poldb_path($repos), sub {
+ $poldbh->{HandleError} = sub {
+ return 0 unless $poldbh->err == 5; # SQLITE_BUSY, not in .pm :-(
+ die bless { }, $db_busy_exception;
+ };
+
+ eval ($ENV{'DGIT_RPD_TEST_DBLOOP_HOOK'}//'');
+ die $@ if length $@;
+ # used by tests/tests/debpolicy-dbretry
+ });
+
+ $stderr = '';
+
+ $rcode = $fn->();
+ die unless defined $rcode;
+
+ $poldbh->commit;
+ };
+ last unless length $@;
+ die $@ unless ref $@ eq $db_busy_exception;
+
+ die if $sleepy >= 20;
+ $sleepy++;
+ print STDERR "[policy database busy, retrying (${sleepy}s)]\n";
+
+ eval { $poldbh->rollback; };
+}
+
+print STDERR $stderr or die $!;
+flush STDERR or die $!;
+_exit $rcode;
diff --git a/infra/dgit-repos-policy-trusting b/infra/dgit-repos-policy-trusting
new file mode 100755
index 0000000..b551d50
--- /dev/null
+++ b/infra/dgit-repos-policy-trusting
@@ -0,0 +1,58 @@
+#!/bin/bash
+#
+# This is a genuine policy, not just one for testing.
+#
+# It allows anyone authorised to push to also, on demand:
+# - wipe the repo and replace it with a new one
+# (with --deliberately-fresh-repo)
+# - do non-fast-forward pushes
+# (with --deliberately-not-fast-forward)
+
+set -e
+
+case "$DGIT_DRS_DEBUG" in
+''|0) exec 3>/dev/null ;;
+1) exec 3>&2 ;;
+*) exec 3>&2; set -x ;;
+esac
+
+distro=$1 ; shift
+reposdir=$1 ; shift
+livedir=$1 ; shift
+distrodir=$1 ; shift
+action=$1 ; shift
+
+echo >&3 "dgit-repos-policy-trusting: action=$action"
+
+case "$action" in
+push|push-confirm) ;;
+*) exit 0 ;;
+esac
+
+package=$1 ; shift
+version=$1 ; shift
+suite=$1 ; shift
+tagname=$1 ; shift
+delibs=$1 ; shift
+
+bitmask=0
+
+policyflags () {
+ perl -e '
+ use Debian::Dgit::Infra;
+ use Debian::Dgit qw(:policyflags); print '$1',"\n"
+ '
+}
+
+set -e
+
+case "$action//,$delibs," in
+push//*,--deliberately-fresh-repo,*)
+ bitmask=$(( bitmask | `policyflags 'NOFFCHECK|FRESHREPO'` ))
+ ;;
+push//*,--deliberately-not-fast-forward,*)
+ bitmask=$(( bitmask | `policyflags 'NOFFCHECK'` ))
+ ;;
+esac
+
+exit $bitmask
diff --git a/infra/dgit-repos-server b/infra/dgit-repos-server
new file mode 100755
index 0000000..6131774
--- /dev/null
+++ b/infra/dgit-repos-server
@@ -0,0 +1,1178 @@
+#!/usr/bin/perl -w
+# dgit-repos-server
+#
+# git protocol proxy to check dgit pushes etc.
+#
+# Copyright (C) 2014-2016 Ian Jackson
+#
+# 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
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# usages:
+# dgit-repos-server DISTRO DISTRO-DIR AUTH-SPEC [<settings>] --ssh
+# dgit-repos-server DISTRO DISTRO-DIR AUTH-SPEC [<settings>] --cron
+# settings
+# --repos=GIT-REPOS-DIR default DISTRO-DIR/repos/
+# --suites=SUITES-FILE default DISTRO-DIR/suites
+# --suites-master=SUITES-FILE default DISTRO-DIR/suites-master
+# --policy-hook=POLICY-HOOK default DISTRO-DIR/policy-hook
+# --mirror-hook=MIRROR-HOOK default DISTRO-DIR/mirror-hook
+# --dgit-live=DGIT-LIVE-DIR default DISTRO-DIR/dgit-live
+# (DISTRO-DIR is not used other than as default and to pass to policy
+# and mirror hooks)
+# internal usage:
+# .../dgit-repos-server --pre-receive-hook PACKAGE
+#
+# Invoked as the ssh restricted command
+#
+# Works like git-receive-pack
+#
+# SUITES-FILE is the name of a file which lists the permissible suites
+# one per line (#-comments and blank lines ignored). For --suites-master
+# it is a list of the suite(s) which should, when pushed to, update
+# `master' on the server (if fast forward).
+#
+# AUTH-SPEC is a :-separated list of
+# KEYRING.GPG,AUTH-SPEC
+# where AUTH-SPEC is one of
+# a
+# mDM.TXT
+# (With --cron AUTH-SPEC is not used and may be the empty string.)
+
+use strict;
+
+use Debian::Dgit::Infra; # must precede Debian::Dgit; - can change @INC!
+use Debian::Dgit qw(:DEFAULT :policyflags);
+setup_sigwarn();
+
+# DGIT-REPOS-DIR contains:
+# git tree (or other object) lock (in acquisition order, outer first)
+#
+# _tmp/PACKAGE_prospective ! } SAME.lock, held during receive-pack
+#
+# _tmp/PACKAGE_incoming$$ ! } SAME.lock, held during receive-pack
+# _tmp/PACKAGE_incoming$$_fresh ! }
+#
+# PACKAGE.git } PACKAGE.git.lock
+# PACKAGE_garbage } (also covers executions of
+# PACKAGE_garbage-old } policy hook script for PACKAGE)
+# PACKAGE_garbage-tmp }
+# policy* } (for policy hook script, covered by
+# } lock only when invoked for a package)
+#
+# leaf locks, held during brief operaton only:
+#
+# _empty } SAME.lock
+# _empty.new }
+#
+# _template } SAME.lock
+#
+# locks marked ! may be held during client data transfer
+
+# What we do on push is this:
+# - extract the destination repo name
+# - make a hardlink clone of the destination repo
+# - provide the destination with a stunt pre-receive hook
+# - run actual git-receive-pack with that new destination
+# as a result of this the stunt pre-receive hook runs; it does this:
+# + understand what refs we are allegedly updating and
+# check some correspondences:
+# * we are updating only refs/tags/[archive/]DISTRO/* and refs/dgit/*
+# * and only one of each
+# * and the tag does not already exist
+# and
+# * recover the suite name from the destination refs/dgit/ ref
+# + disassemble the signed tag into its various fields and signature
+# including:
+# * parsing the first line of the tag message to recover
+# the package name, version and suite
+# * checking that the package name corresponds to the dest repo name
+# * checking that the suite name is as recovered above
+# + verify the signature on the signed tag
+# and if necessary check that the keyid and package are listed in dm.txt
+# + check various correspondences:
+# * the signed tag must refer to a commit
+# * the signed tag commit must be the refs/dgit value
+# * the name in the signed tag must correspond to its ref name
+# * the tag name must be [archive/]debian/<version> (massaged as needed)
+# * the suite is one of those permitted
+# * the signed tag has a suitable name
+# * run the "push" policy hook
+# * replay prevention for --deliberately-not-fast-forward
+# * check the commit is a fast forward
+# * handle a request from the policy hook for a fresh repo
+# + push the signed tag and new dgit branch to the actual repo
+#
+# If the destination repo does not already exist, we need to make
+# sure that we create it reasonably atomically, and also that
+# we don't every have a destination repo containing no refs at all
+# (because such a thing causes git-fetch-pack to barf). So then we
+# do as above, except:
+# - before starting, we take out our own lock for the destination repo
+# - we create a prospective new destination repo by making a copy
+# of _template
+# - we use the prospective new destination repo instead of the
+# actual new destination repo (since the latter doesn't exist)
+# - after git-receive-pack exits, we
+# + check that the prospective repo contains a tag and head
+# + rename the prospective destination repo into place
+#
+# Cleanup strategy:
+# - We are crash-only
+# - Temporary working trees and their locks are cleaned up
+# opportunistically by a program which tries to take each lock and
+# if successful deletes both the tree and the lockfile
+# - Prospective working trees and their locks are cleaned up by
+# a program which tries to take each lock and if successful
+# deletes any prospective working tree and the lock (but not
+# of course any actual tree)
+# - It is forbidden to _remove_ the lockfile without removing
+# the corresponding temporary tree, as the lockfile is also
+# a stampfile whose presence indicates that there may be
+# cleanup to do
+#
+# Policy hook scripts are invoked like this:
+# POLICY-HOOK-SCRIPT DISTRO DGIT-REPOS-DIR DGIT-LIVE-DIR DISTRO-DIR ACTION...
+# ie.
+# POLICY-HOOK-SCRIPT ... check-list [...]
+# POLICY-HOOK-SCRIPT ... check-package PACKAGE [...]
+# POLICY-HOOK-SCRIPT ... push PACKAGE \
+# VERSION SUITE TAGNAME DELIBERATELIES [...]
+# POLICY-HOOK-SCRIPT ... push-confirm PACKAGE \
+# VERSION SUITE TAGNAME DELIBERATELIES FRESH-REPO|'' [...]
+#
+# DELIBERATELIES is like this: --deliberately-foo,--deliberately-bar,...
+#
+# Exit status of policy hook is a bitmask.
+# Bit weight constants are defined in Dgit.pm.
+# NOFFCHECK (2)
+# suppress dgit-repos-server's fast-forward check ("push" only)
+# FRESHREPO (4)
+# blow away repo right away (ie, as if before push or fetch)
+# ("check-package" and "push" only)
+# NOCOMMITCHECK (8)
+# suppress dgit-repos-server's check that commits do
+# not lack "committer" info (eg as produced by #849041)
+# ("push" only)
+# any unexpected bits mean failure, and then known set bits are ignored
+# if no unexpected bits set, operation continues (subject to meaning
+# of any expected bits set). So, eg, exit 0 means "continue normally"
+# and would be appropriate for an unknown action.
+#
+# cwd for push and push-confirm is a temporary repo where the incoming
+# objects have been received; TAGNAME is the version-based tag.
+#
+# FRESH-REPO is '' iff the repo for this package already existed, or
+# the pathname of the newly-created repo which will be renamed into
+# place if everything goes well. (NB that this is generally not the
+# same repo as the cwd, because the objects are first received into a
+# temporary repo so they can be examined.) In this case FRESH-REPO
+# contains exactly the objects and refs that will appear in the
+# destination if push-confirm approves.
+#
+# if push requested FRESHREPO, push-confirm happens in the old working
+# repo and FRESH-REPO is guaranteed not to be ''.
+#
+# policy hook for a particular package will be invoked only once at
+# a time - (see comments about DGIT-REPOS-DIR, above)
+#
+# check-list and check-package are invoked via the --cron option.
+# First, without any locking, check-list is called. It should produce
+# a list of package names (one per line). Then check-package will be
+# invoked for each named package, in each case after taking an
+# appropriate lock.
+#
+# If policy hook wants to run dgit (or something else in the dgit
+# package), it should use DGIT-LIVE-DIR/dgit (etc.), or if that is
+# ENOENT, use the installed version.
+#
+# Mirror hook scripts are invoked like this:
+# MIRROR-HOOK-SCRIPT DISTRO-DIR ACTION...
+# and currently there is only one action invoked by dgit-repos-server:
+# MIRROR-HOOK-SCRIPT DISTRO-DIR updated-hook PACKAGE [...]
+#
+# Exit status of the mirror hook is advisory only. The mirror hook
+# runs too late to do anything useful about a problem, so the only
+# effect of a mirror hook exiting nonzero is a warning message to
+# stderr (which the pushing user should end up seeing).
+#
+# If the mirror hook does not exist, it is silently skipped.
+
+use POSIX;
+use Fcntl qw(:flock);
+use File::Path qw(rmtree);
+use File::Temp qw(tempfile);
+
+initdebug('');
+
+our $func;
+our $dgitrepos;
+our $package;
+our $distro;
+our $suitesfile;
+our $suitesformasterfile;
+our $policyhook;
+our $mirrorhook;
+our $dgitlive;
+our $distrodir;
+our $destrepo;
+our $workrepo;
+our $keyrings;
+our @lockfhs;
+
+our @deliberatelies;
+our %previously;
+our $policy;
+our @policy_args;
+
+#----- utilities -----
+
+sub realdestrepo () { "$dgitrepos/$package.git"; }
+
+sub acquirelock ($$) {
+ my ($lock, $must) = @_;
+ my $fh;
+ printdebug sprintf "locking %s %d\n", $lock, $must;
+ for (;;) {
+ close $fh if $fh;
+ $fh = new IO::File $lock, ">" or die "open $lock: $!";
+ my $ok = flock $fh, $must ? LOCK_EX : (LOCK_EX|LOCK_NB);
+ if (!$ok) {
+ die "flock $lock: $!" if $must;
+ printdebug " locking $lock failed\n";
+ return undef;
+ }
+ next unless stat_exists $lock;
+ my $want = (stat _)[1];
+ stat $fh or die $!;
+ my $got = (stat _)[1];
+ last if $got == $want;
+ }
+ return $fh;
+}
+
+sub acquirermtree ($$) {
+ my ($tree, $must) = @_;
+ my $fh = acquirelock("$tree.lock", $must);
+ if ($fh) {
+ push @lockfhs, $fh;
+ rmtree $tree;
+ }
+ return $fh;
+}
+
+sub locksometree ($) {
+ my ($tree) = @_;
+ acquirelock("$tree.lock", 1);
+}
+
+sub lockrealtree () {
+ locksometree(realdestrepo);
+}
+
+sub mkrepotmp () { ensuredir "$dgitrepos/_tmp" };
+
+sub removedtagsfile () { "$dgitrepos/_removed-tags/$package"; }
+
+sub recorderror ($) {
+ my ($why) = @_;
+ my $w = $ENV{'DGIT_DRS_WORK'}; # we are in stunthook
+ if (defined $w) {
+ chomp $why;
+ open ERR, ">", "$w/drs-error" or die $!;
+ print ERR $why, "\n" or die $!;
+ close ERR or die $!;
+ return 1;
+ }
+ return 0;
+}
+
+sub reject ($) {
+ my ($why) = @_;
+ recorderror "reject: $why";
+ die "\ndgit-repos-server: reject: $why\n\n";
+}
+
+sub runcmd {
+ debugcmd '+',@_;
+ $!=0; $?=0;
+ my $r = system @_;
+ die (shellquote @_)." $? $!" if $r;
+}
+
+sub policyhook {
+ my ($policyallowbits, @polargs) = @_;
+ # => ($exitstatuspolicybitmap);
+ die if $policyallowbits & ~0x3e;
+ my @cmd = ($policyhook,$distro,$dgitrepos,$dgitlive,$distrodir,@polargs);
+ debugcmd '+M',@cmd;
+ my $r = system @cmd;
+ die "system: $!" if $r < 0;
+ die "dgit-repos-server: policy hook failed (or rejected) ($?)\n"
+ if $r & ~($policyallowbits << 8);
+ printdebug sprintf "hook => %#x\n", $r;
+ return $r >> 8;
+}
+
+sub mkemptyrepo ($$) {
+ my ($dir,$sharedperm) = @_;
+ runcmd qw(git init --bare --quiet), "--shared=$sharedperm", $dir;
+}
+
+sub mkrepo_fromtemplate ($) {
+ my ($dir) = @_;
+ my $template = "$dgitrepos/_template";
+ my $templatelock = locksometree($template);
+ printdebug "copy template $template -> $dir\n";
+ my $r = system qw(cp -a --), $template, $dir;
+ !$r or die "create new repo $dir failed: $r $!";
+ close $templatelock;
+}
+
+sub movetogarbage () {
+ # realdestrepo must have been locked
+
+ my $real = realdestrepo;
+ return unless stat_exists $real;
+
+ my $garbagerepo = "$dgitrepos/${package}_garbage";
+ # We arrange to always keep at least one old tree, for recovery
+ # from mistakes. This is either $garbage or $garbage-old.
+ if (stat_exists "$garbagerepo") {
+ printdebug "movetogarbage: rmtree $garbagerepo-tmp\n";
+ rmtree "$garbagerepo-tmp";
+ if (rename "$garbagerepo-old", "$garbagerepo-tmp") {
+ printdebug "movetogarbage: $garbagerepo-old -> -tmp, rmtree\n";
+ rmtree "$garbagerepo-tmp";
+ } else {
+ die "$garbagerepo $!" unless $!==ENOENT;
+ printdebug "movetogarbage: $garbagerepo-old -> -tmp\n";
+ }
+ printdebug "movetogarbage: $garbagerepo -> -old\n";
+ rename "$garbagerepo", "$garbagerepo-old" or die "$garbagerepo $!";
+ }
+
+ ensuredir "$dgitrepos/_removed-tags";
+ open PREVIOUS, ">>", removedtagsfile or die removedtagsfile." $!";
+ git_for_each_ref([ map { 'refs/tags/'.$_ } debiantags('*',$distro) ],
+ sub {
+ my ($objid,$objtype,$fullrefname,$reftail) = @_;
+ print PREVIOUS "\n$objid $reftail .\n" or die $!;
+ }, $real);
+ close PREVIOUS or die $!;
+
+ printdebug "movetogarbage: $real -> $garbagerepo\n";
+ rename $real, $garbagerepo
+ or $! == ENOENT
+ or die "$garbagerepo $!";
+}
+
+sub policy_checkpackage () {
+ my $lfh = lockrealtree();
+
+ $policy = policyhook(FRESHREPO,'check-package',$package);
+ if ($policy & FRESHREPO) {
+ movetogarbage();
+ }
+
+ close $lfh;
+}
+
+#----- git-receive-pack -----
+
+sub fixmissing__git_receive_pack () {
+ mkrepotmp();
+ $destrepo = "$dgitrepos/_tmp/${package}_prospective";
+ acquirermtree($destrepo, 1);
+ mkrepo_fromtemplate($destrepo);
+}
+
+sub makeworkingclone () {
+ mkrepotmp();
+ $workrepo = "$dgitrepos/_tmp/${package}_incoming$$";
+ acquirermtree($workrepo, 1);
+ my $lfh = lockrealtree();
+ runcmd qw(git clone -l -q --mirror), $destrepo, $workrepo;
+ close $lfh;
+ rmtree "${workrepo}_fresh";
+}
+
+sub setupstunthook () {
+ my $prerecv = "$workrepo/hooks/pre-receive";
+ my $fh = new IO::File $prerecv, O_WRONLY|O_CREAT|O_TRUNC, 0777
+ or die "$prerecv: $!";
+ print $fh <<END or die "$prerecv: $!";
+#!/bin/sh
+set -e
+exec $0 --pre-receive-hook $package
+END
+ close $fh or die "$prerecv: $!";
+ $ENV{'DGIT_DRS_WORK'}= $workrepo;
+ $ENV{'DGIT_DRS_DEST'}= $destrepo;
+ printdebug " stunt hook set up $prerecv\n";
+}
+
+sub dealwithfreshrepo () {
+ my $freshrepo = "${workrepo}_fresh";
+ return unless stat_exists $freshrepo;
+ $destrepo = $freshrepo;
+}
+
+sub mirrorhook {
+ my @cmd = ($mirrorhook,$distrodir,@_);
+ debugcmd '+',@cmd;
+ return unless stat_exists $mirrorhook;
+ my $r = system @cmd;
+ if ($r) {
+ printf STDERR <<END,
+dgit-repos-server: warning: mirror hook failed: %s
+dgit-repos-server: push complete but may not fully visible.
+END
+ ($r < 0 ? "exec: $!" :
+ $r == (124 << 8) ? "exited status 124 (timeout?)" :
+ !($r & ~0xff00) ? "exited ".($? >> 8) :
+ "wait status $?");
+ }
+}
+
+sub maybeinstallprospective () {
+ return if $destrepo eq realdestrepo;
+
+ if (open REJ, "<", "$workrepo/drs-error") {
+ local $/ = undef;
+ my $msg = <REJ>;
+ REJ->error and die $!;
+ print STDERR $msg;
+ exit 1;
+ } else {
+ $!==&ENOENT or die $!;
+ }
+
+ printdebug " show-ref ($destrepo) ...\n";
+
+ my $child = open SR, "-|";
+ defined $child or die $!;
+ if (!$child) {
+ chdir $destrepo or die $!;
+ exec qw(git show-ref);
+ die $!;
+ }
+ my %got = qw(newtag 0 omtag 0 head 0);
+ while (<SR>) {
+ chomp or die;
+ printdebug " show-refs| $_\n";
+ s/^\S*[1-9a-f]\S* (\S+)$/$1/ or die;
+ next if m{^refs/heads/master$};
+ my $wh =
+ m{^refs/tags/archive/} ? 'newtag' :
+ m{^refs/tags/} ? 'omtag' :
+ m{^refs/dgit/} ? 'head' :
+ die;
+ use Data::Dumper;
+ die if $got{$wh}++;
+ }
+ $!=0; $?=0; close SR or $?==256 or die "$? $!";
+
+ printdebug "installprospective ?\n";
+ die Dumper(\%got)." -- missing refs in new repo"
+ unless $got{head} && grep { m/tag$/ && $got{$_} } keys %got;
+
+ lockrealtree();
+
+ if ($destrepo eq "${workrepo}_fresh") {
+ movetogarbage;
+ }
+
+ printdebug "install $destrepo => ".realdestrepo."\n";
+ rename $destrepo, realdestrepo or die $!;
+ remove realdestrepo.".lock" or die $!;
+}
+
+sub main__git_receive_pack () {
+ makeworkingclone();
+ setupstunthook();
+ runcmd qw(git receive-pack), $workrepo;
+ dealwithfreshrepo();
+ maybeinstallprospective();
+ mirrorhook('updated-hook', $package);
+}
+
+#----- stunt post-receive hook -----
+
+our ($tagname, $tagval, $suite, $oldcommit, $commit);
+our ($version, %tagh);
+our ($maint_tagname, $maint_tagval);
+
+our ($tagexists_error);
+
+sub readupdates () {
+ printdebug " updates ...\n";
+ my %tags;
+ while (<STDIN>) {
+ chomp or die;
+ printdebug " upd.| $_\n";
+ m/^(\S+) (\S+) (\S+)$/ or die "$_ ?";
+ my ($old, $sha1, $refname) = ($1, $2, $3);
+ if ($refname =~ m{^refs/tags/(?=(?:archive/)?$distro/)}) {
+ my $tn = $'; #';
+ $tags{$tn} = $sha1;
+ $tagexists_error= "tag $tn already exists -".
+ " not replacing previously-pushed version"
+ if $old =~ m/[^0]/;
+ } elsif ($refname =~ m{^refs/dgit/}) {
+ reject "pushing multiple heads!" if defined $suite;
+ $suite = $'; #';
+ $oldcommit = $old;
+ $commit = $sha1;
+ } else {
+ reject "pushing unexpected ref!";
+ }
+ }
+ STDIN->error and die $!;
+
+ reject "push is missing tag ref update" unless %tags;
+ my @newtags = grep { m#^archive/# } keys %tags;
+ my @omtags = grep { !m#^archive/# } keys %tags;
+ reject "pushing too many similar tags" if @newtags>1 || @omtags>1;
+ if (@newtags) {
+ ($tagname) = @newtags;
+ ($maint_tagname) = @omtags;
+ } else {
+ ($tagname) = @omtags or die;
+ }
+ $tagval = $tags{$tagname};
+ $maint_tagval = $tags{$maint_tagname // ''};
+
+ reject "push is missing head ref update" unless defined $suite;
+ printdebug " updates ok.\n";
+}
+
+sub parsetag () {
+ printdebug " parsetag...\n";
+ open PT, ">dgit-tmp/plaintext" or die $!;
+ open DS, ">dgit-tmp/plaintext.asc" or die $!;
+ open T, "-|", qw(git cat-file tag), $tagval or die $!;
+ for (;;) {
+ $!=0; $_=<T>; defined or die $!;
+ print PT or die $!;
+ if (m/^(\S+) (.*)/) {
+ push @{ $tagh{$1} }, $2;
+ } elsif (!m/\S/) {
+ last;
+ } else {
+ die;
+ }
+ }
+ $!=0; $_=<T>; defined or die $!;
+ m/^($package_re) release (\S+) for \S+ \((\S+)\) \[dgit\]$/ or
+ reject "tag message not in expected format";
+
+ die unless $1 eq $package;
+ $version = $2;
+ die "$3 != $suite " unless $3 eq $suite;
+
+ my $copyl = $_;
+ for (;;) {
+ print PT $copyl or die $!;
+ $!=0; $_=<T>; defined or die "missing signature? $!";
+ $copyl = $_;
+ if (m/^\[dgit ([^"].*)\]$/) { # [dgit "something"] is for future
+ $_ = $1." ";
+ while (length) {
+ if (s/^distro\=(\S+) //) {
+ die "$1 != $distro" unless $1 eq $distro;
+ } elsif (s/^(--deliberately-$deliberately_re) //) {
+ push @deliberatelies, $1;
+ } elsif (s/^previously:(\S+)=(\w+) //) {
+ die "previously $1 twice" if defined $previously{$1};
+ $previously{$1} = $2;
+ } elsif (s/^[-+.=0-9a-z]\S* //) {
+ } else {
+ die "unknown dgit info in tag ($_)";
+ }
+ }
+ next;
+ }
+ last if m/^-----BEGIN PGP/;
+ }
+ $_ = $copyl;
+ for (;;) {
+ print DS or die $!;
+ $!=0; $_=<T>;
+ last if !defined;
+ }
+ T->error and die $!;
+ close PT or die $!;
+ close DS or die $!;
+ printdebug " parsetag ok.\n";
+}
+
+sub checksig_keyring ($) {
+ my ($keyringfile) = @_;
+ # returns primary-keyid if signed by a key in this keyring
+ # or undef if not
+ # or dies on other errors
+
+ my $ok = undef;
+
+ printdebug " checksig keyring $keyringfile...\n";
+
+ our @cmd = (qw(gpgv --status-fd=1 --keyring),
+ $keyringfile,
+ qw(dgit-tmp/plaintext.asc dgit-tmp/plaintext));
+ debugcmd '|',@cmd;
+
+ open P, "-|", @cmd
+ or die $!;
+
+ while (<P>) {
+ next unless s/^\[GNUPG:\] //;
+ chomp or die;
+ printdebug " checksig| $_\n";
+ my @l = split / /, $_;
+ if ($l[0] eq 'NO_PUBKEY') {
+ last;
+ } elsif ($l[0] eq 'VALIDSIG') {
+ my $sigtype = $l[9];
+ $sigtype eq '00' or reject "signature is not of type 00!";
+ $ok = $l[10];
+ die unless defined $ok;
+ last;
+ }
+ }
+ close P;
+
+ printdebug sprintf " checksig ok=%d\n", !!$ok;
+
+ return $ok;
+}
+
+sub dm_txt_check ($$) {
+ my ($keyid, $dmtxtfn) = @_;
+ printdebug " dm_txt_check $keyid $dmtxtfn\n";
+ open DT, '<', $dmtxtfn or die "$dmtxtfn $!";
+ while (<DT>) {
+ m/^fingerprint:\s+\Q$keyid\E$/oi
+ ..0 or next;
+ if (s/^allow:/ /i..0) {
+ } else {
+ m/^./
+ or reject "key $keyid missing Allow section in permissions!";
+ next;
+ }
+ # in right stanza...
+ s/^[ \t]+//
+ or reject "package $package not allowed for key $keyid";
+ # in allow field...
+ s/\([^()]+\)//;
+ s/\,//;
+ chomp or die;
+ printdebug " dm_txt_check allow| $_\n";
+ foreach my $p (split /\s+/) {
+ if ($p eq $package) {
+ # yay!
+ printdebug " dm_txt_check ok\n";
+ return;
+ }
+ }
+ }
+ DT->error and die $!;
+ close DT or die $!;
+ reject "key $keyid not in permissions list although in keyring!";
+}
+
+sub verifytag () {
+ foreach my $kas (split /:/, $keyrings) {
+ printdebug "verifytag $kas...\n";
+ $kas =~ s/^([^,]+),// or die;
+ my $keyid = checksig_keyring $1;
+ if (defined $keyid) {
+ if ($kas =~ m/^a$/) {
+ printdebug "verifytag a ok\n";
+ return; # yay
+ } elsif ($kas =~ m/^m([^,]+)$/) {
+ dm_txt_check($keyid, $1);
+ printdebug "verifytag m ok\n";
+ return;
+ } else {
+ die;
+ }
+ }
+ }
+ reject "key not found in keyrings";
+}
+
+sub suite_is_in ($) {
+ my ($sf) = @_;
+ printdebug "suite_is_in ($sf)\n";
+ if (!open SUITES, "<", $sf) {
+ $!==ENOENT or die $!;
+ return 0;
+ }
+ while (<SUITES>) {
+ chomp;
+ next unless m/\S/;
+ next if m/^\#/;
+ s/\s+$//;
+ return 1 if $_ eq $suite;
+ }
+ die $! if SUITES->error;
+ return 0;
+}
+
+sub checksuite () {
+ printdebug "checksuite ($suitesfile)\n";
+ return if suite_is_in $suitesfile;
+ reject "unknown suite";
+}
+
+sub checktagnoreplay () {
+ # We need to prevent a replay attack using an earlier signed tag.
+ # We also want to archive in the history the object ids of
+ # anything we remove, even if we get rid of the actual objects.
+ #
+ # So, we check that the signed tag mentions the name and tag
+ # object id of:
+ #
+ # (a) In the case of FRESHREPO: all tags and refs/heads/* in
+ # the repo. That is, effectively, all the things we are
+ # deleting.
+ #
+ # This prevents any tag implying a FRESHREPO push
+ # being replayed into a different state of the repo.
+ #
+ # There is still the folowing risk: If a non-ff push is of a
+ # head which is an ancestor of a previous ff-only push, the
+ # previous push can be replayed.
+ #
+ # So we keep a separate list, as a file in the repo, of all
+ # the tag object ids we have ever seen and removed. Any such
+ # tag object id will be rejected even for ff-only pushes.
+ #
+ # (b) In the case of just NOFFCHECK: all tags referring to the
+ # current head for the suite (there must be at least one).
+ #
+ # This prevents any tag implying a NOFFCHECK push being
+ # replayed to rewind from a different head.
+ #
+ # The possibility of an earlier ff-only push being replayed is
+ # eliminated as follows: the tag from such a push would still
+ # be in our repo, and therefore the replayed push would be
+ # rejected because the set of refs being updated would be
+ # wrong.
+
+ if (!open PREVIOUS, "<", removedtagsfile) {
+ die removedtagsfile." $!" unless $!==ENOENT;
+ } else {
+ # Protocol for updating this file is to append to it, not
+ # write-new-and-rename. So all updates are prefixed with \n
+ # and suffixed with " .\n" so that partial writes can be
+ # ignored.
+ while (<PREVIOUS>) {
+ next unless m/^(\w+) (.*) \.\n/;
+ next unless $1 eq $tagval;
+ reject "Replay of previously-rewound upload ($tagval $2)";
+ }
+ die removedtagsfile." $!" if PREVIOUS->error;
+ close PREVIOUS;
+ }
+
+ return unless $policy & (FRESHREPO|NOFFCHECK);
+
+ my $garbagerepo = "$dgitrepos/${package}_garbage";
+ lockrealtree();
+
+ my $nchecked = 0;
+ my @problems;
+
+ my $check_ref_previously= sub {
+ my ($objid,$objtype,$fullrefname,$reftail) = @_;
+ my $supkey = $fullrefname;
+ $supkey =~ s{^refs/}{} or die "$supkey $objid ?";
+ my $supobjid = $previously{$supkey};
+ if (!defined $supobjid) {
+ printdebug "checktagnoreply - missing\n";
+ push @problems, "does not declare previously $supkey";
+ } elsif ($supobjid ne $objid) {
+ push @problems, "declared previously $supkey=$supobjid".
+ " but actually previously $supkey=$objid";
+ } else {
+ $nchecked++;
+ }
+ };
+
+ if ($policy & FRESHREPO) {
+ foreach my $kind (qw(tags heads)) {
+ git_for_each_ref("refs/$kind", $check_ref_previously);
+ }
+ } else {
+ my $branch= server_branch($suite);
+ my $branchhead= git_get_ref(server_ref($suite));
+ if (!length $branchhead) {
+ # No such branch - NOFFCHECK was unnecessary. Oh well.
+ printdebug "checktagnoreplay - not FRESHREPO, new branch, ok\n";
+ } else {
+ printdebug "checktagnoreplay - not FRESHREPO,".
+ " checking for overwriting refs/$branch=$branchhead\n";
+ git_for_each_tag_referring($branchhead, sub {
+ my ($tagobjid,$refobjid,$fullrefname,$tagname) = @_;
+ $check_ref_previously->($tagobjid,undef,$fullrefname,undef);
+ });
+ printdebug "checktagnoreplay - not FRESHREPO, nchecked=$nchecked";
+ push @problems, "does not declare previously any tag".
+ " referring to branch head $branch=$branchhead"
+ unless $nchecked;
+ }
+ }
+
+ if (@problems) {
+ reject "replay attack prevention check failed:".
+ " signed tag for $version: ".
+ join("; ", @problems).
+ "\n";
+ }
+ printdebug "checktagnoreplay - all ok ($tagval)\n"
+}
+
+sub tagh1 ($) {
+ my ($tag) = @_;
+ my $vals = $tagh{$tag};
+ reject "missing header $tag in signed tag object" unless $vals;
+ reject "multiple headers $tag in signed tag object" unless @$vals == 1;
+ return $vals->[0];
+}
+
+sub checks () {
+ printdebug "checks\n";
+
+ tagh1('type') eq 'commit' or reject "tag refers to wrong kind of object";
+ tagh1('object') eq $commit or reject "tag refers to wrong commit";
+ tagh1('tag') eq $tagname or reject "tag name in tag is wrong";
+
+ my @expecttagnames = debiantags($version, $distro);
+ printdebug "expected tag @expecttagnames\n";
+ grep { $tagname eq $_ } @expecttagnames or die;
+
+ foreach my $othertag (grep { $_ ne $tagname } @expecttagnames) {
+ reject "tag $othertag (pushed with differing dgit version)".
+ " already exists -".
+ " not replacing previously-pushed version"
+ if git_get_ref "refs/tags/".$othertag;
+ }
+
+ lockrealtree();
+
+ @policy_args = ($package,$version,$suite,$tagname,
+ join(",",@deliberatelies));
+ $policy = policyhook(NOFFCHECK|FRESHREPO|NOCOMMITCHECK, 'push', @policy_args);
+
+ if (defined $tagexists_error) {
+ if ($policy & FRESHREPO) {
+ printdebug "ignoring tagexists_error: $tagexists_error\n";
+ } else {
+ reject $tagexists_error;
+ }
+ }
+
+ checktagnoreplay();
+ checksuite();
+
+ # check that our ref is being fast-forwarded
+ printdebug "oldcommit $oldcommit\n";
+ if (!($policy & NOFFCHECK) && $oldcommit =~ m/[^0]/) {
+ $?=0; $!=0; my $mb = `git merge-base $commit $oldcommit`;
+ chomp $mb;
+ $mb eq $oldcommit or reject "not fast forward on dgit branch";
+ }
+
+ # defend against commits generated by #849041
+ if (!($policy & NOCOMMITCHECK)) {
+ my @checks = qw(%ae %at
+ %ce %ct);
+ my @chk = qw(git log -z);
+ push @chk, '--pretty=tformat:%H%n'.
+ (join "", map { $_, '%n' } @checks);
+ push @chk, "^$oldcommit" if $oldcommit =~ m/[^0]/;
+ push @chk, $commit;;
+ printdebug " ~NOCOMMITCHECK @chk\n";
+ open CHK, "-|", @chk or die $!;
+ local $/ = "\0";
+ while (<CHK>) {
+ next unless m/^$/m;
+ m/^\w+(?=\n)/ or die;
+ reject "corrupted object $& (missing metadata)";
+ }
+ $!=0; $?=0; close CHK or $?==256 or die "$? $!";
+ }
+
+ if ($policy & FRESHREPO) {
+ # It's a bit late to be discovering this here, isn't it ?
+ #
+ # What we do is: Generate a fresh destination repo right now,
+ # and arrange to treat it from now on as if it were a
+ # prospective repo.
+ #
+ # The presence of this fresh destination repo is detected by
+ # the parent, which responds by making a fresh master repo
+ # from the template. (If the repo didn't already exist then
+ # $destrepo was _prospective, and we change it here. This is
+ # OK because the parent's check for _fresh persuades it not to
+ # use _prospective.)
+ #
+ $destrepo = "${workrepo}_fresh"; # workrepo lock covers
+ mkrepo_fromtemplate $destrepo;
+ }
+}
+
+sub onwardpush () {
+ my @cmdbase = (qw(git send-pack), $destrepo);
+ push @cmdbase, qw(--force) if $policy & NOFFCHECK;
+
+ my @cmd = @cmdbase;
+ push @cmd, "$commit:refs/dgit/$suite",
+ "$tagval:refs/tags/$tagname";
+ push @cmd, "$maint_tagval:refs/tags/$maint_tagname"
+ if defined $maint_tagname;
+ debugcmd '+',@cmd;
+ $!=0;
+ my $r = system @cmd;
+ !$r or die "onward push to $destrepo failed: $r $!";
+
+ if (suite_is_in $suitesformasterfile) {
+ @cmd = @cmdbase;
+ push @cmd, "$commit:refs/heads/master";
+ debugcmd '+', @cmd;
+ $!=0; my $r = system @cmd;
+ # tolerate errors (might be not ff)
+ !($r & ~0xff00) or die
+ "onward push to $destrepo#master failed: $r $!";
+ }
+}
+
+sub finalisepush () {
+ if ($destrepo eq realdestrepo) {
+ policyhook(0, 'push-confirm', @policy_args, '');
+ onwardpush();
+ } else {
+ # We are to receive the push into a new repo (perhaps
+ # because the policy push hook asked us to with FRESHREPO, or
+ # perhaps because the repo didn't exist before).
+ #
+ # We want to provide the policy push-confirm hook with a repo
+ # which looks like the one which is going to be installed.
+ # The working repo is no good because it might contain
+ # previous history.
+ #
+ # So we push the objects into the prospective new repo right
+ # away. If the hook declines, we decline, and the prospective
+ # repo is never installed.
+ onwardpush();
+ policyhook(0, 'push-confirm', @policy_args, $destrepo);
+ }
+}
+
+sub stunthook () {
+ printdebug "stunthook in $workrepo\n";
+ chdir $workrepo or die "chdir $workrepo: $!";
+ mkdir "dgit-tmp" or $!==EEXIST or die $!;
+ readupdates();
+ parsetag();
+ verifytag();
+ checks();
+ finalisepush();
+ printdebug "stunthook done.\n";
+}
+
+#----- git-upload-pack -----
+
+sub fixmissing__git_upload_pack () {
+ $destrepo = "$dgitrepos/_empty";
+ my $lfh = locksometree($destrepo);
+ return if stat_exists $destrepo;
+ rmtree "$destrepo.new";
+ mkemptyrepo "$destrepo.new", "0644";
+ rename "$destrepo.new", $destrepo or die $!;
+ unlink "$destrepo.lock" or die $!;
+ close $lfh;
+}
+
+sub main__git_upload_pack () {
+ my $lfh = locksometree($destrepo);
+ printdebug "git-upload-pack in $destrepo\n";
+ chdir $destrepo or die "$destrepo: $!";
+ close $lfh;
+ runcmd qw(git upload-pack), ".";
+}
+
+#----- arg parsing and main program -----
+
+sub argval () {
+ die unless @ARGV;
+ my $v = shift @ARGV;
+ die if $v =~ m/^-/;
+ return $v;
+}
+
+our %indistrodir = (
+ # keys are used for DGIT_DRS_XXX too
+ 'repos' => \$dgitrepos,
+ 'suites' => \$suitesfile,
+ 'suites-master' => \$suitesformasterfile,
+ 'policy-hook' => \$policyhook,
+ 'mirror-hook' => \$mirrorhook,
+ 'dgit-live' => \$dgitlive,
+ );
+
+our @hookenvs = qw(distro suitesfile suitesformasterfile policyhook
+ mirrorhook dgitlive keyrings dgitrepos distrodir);
+
+# workrepo and destrepo handled ad-hoc
+
+sub mode_ssh () {
+ die if @ARGV;
+
+ my $cmd = $ENV{'SSH_ORIGINAL_COMMAND'};
+ $cmd =~ m{
+ ^
+ (?: \S* / )?
+ ( [-0-9a-z]+ )
+ \s+
+ '? (?: \S* / )?
+ ($package_re) \.git
+ '?$
+ }ox
+ or reject "command string not understood";
+ my $method = $1;
+ $package = $2;
+
+ my $funcn = $method;
+ $funcn =~ y/-/_/;
+ my $mainfunc = $main::{"main__$funcn"};
+
+ reject "unknown method" unless $mainfunc;
+
+ policy_checkpackage();
+
+ if (stat_exists realdestrepo) {
+ $destrepo = realdestrepo;
+ } else {
+ printdebug " fixmissing $funcn\n";
+ my $fixfunc = $main::{"fixmissing__$funcn"};
+ &$fixfunc;
+ }
+
+ printdebug " running main $funcn\n";
+ &$mainfunc;
+}
+
+sub mode_cron () {
+ die if @ARGV;
+
+ my $listfh = tempfile();
+ open STDOUT, ">&", $listfh or die $!;
+ policyhook(0,'check-list');
+ open STDOUT, ">&STDERR" or die $!;
+
+ seek $listfh, 0, 0 or die $!;
+ while (<$listfh>) {
+ chomp or die;
+ next if m/^\s*\#/;
+ next unless m/\S/;
+ die unless m/^($package_re)$/;
+
+ $package = $1;
+ policy_checkpackage();
+ }
+ die $! if $listfh->error;
+}
+
+sub parseargsdispatch () {
+ die unless @ARGV;
+
+ delete $ENV{'GIT_DIR'}; # if not run via ssh, our parent git process
+ delete $ENV{'GIT_PREFIX'}; # sets these and they mess things up
+
+ if ($ENV{'DGIT_DRS_DEBUG'}) {
+ enabledebug();
+ }
+
+ if ($ARGV[0] eq '--pre-receive-hook') {
+ if ($debuglevel) {
+ $debugprefix.="=";
+ printdebug "in stunthook ".(shellquote @ARGV)."\n";
+ foreach my $k (sort keys %ENV) {
+ printdebug "$k=$ENV{$k}\n" if $k =~ m/^DGIT/;
+ }
+ }
+ shift @ARGV;
+ @ARGV == 1 or die;
+ $package = shift @ARGV;
+ ${ $main::{$_} } = $ENV{"DGIT_DRS_\U$_"} foreach @hookenvs;
+ defined($workrepo = $ENV{'DGIT_DRS_WORK'}) or die;
+ defined($destrepo = $ENV{'DGIT_DRS_DEST'}) or die;
+ open STDOUT, ">&STDERR" or die $!;
+ eval {
+ stunthook();
+ };
+ if ($@) {
+ recorderror "$@" or die;
+ die $@;
+ }
+ exit 0;
+ }
+
+ $distro = argval();
+ $distrodir = argval();
+ $keyrings = argval();
+
+ foreach my $dk (keys %indistrodir) {
+ ${ $indistrodir{$dk} } = "$distrodir/$dk";
+ }
+
+ while (@ARGV && $ARGV[0] =~ m/^--([-0-9a-z]+)=/ && $indistrodir{$1}) {
+ ${ $indistrodir{$1} } = $'; #';
+ shift @ARGV;
+ }
+
+ $ENV{"DGIT_DRS_\U$_"} = ${ $main::{$_} } foreach @hookenvs;
+
+ die unless @ARGV==1;
+
+ my $mode = shift @ARGV;
+ die unless $mode =~ m/^--(\w+)$/;
+ my $fn = ${*::}{"mode_$1"};
+ die unless $fn;
+ $fn->();
+}
+
+sub unlockall () {
+ while (my $fh = pop @lockfhs) { close $fh; }
+}
+
+sub cleanup () {
+ unlockall();
+ if (!chdir "$dgitrepos/_tmp") {
+ $!==ENOENT or die $!;
+ return;
+ }
+ foreach my $lf (<*.lock>) {
+ my $tree = $lf;
+ $tree =~ s/\.lock$//;
+ next unless acquirermtree($tree, 0);
+ remove $lf or warn $!;
+ unlockall();
+ }
+}
+
+parseargsdispatch();
+cleanup();
diff --git a/infra/dgit-ssh-dispatch b/infra/dgit-ssh-dispatch
new file mode 100755
index 0000000..c5861d2
--- /dev/null
+++ b/infra/dgit-ssh-dispatch
@@ -0,0 +1,181 @@
+#!/usr/bin/perl -w
+# wrapper to dispatch git ssh service requests
+#
+# Copyright (C) 2015-2016 Ian Jackson
+#
+# 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
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+use strict;
+
+use Debian::Dgit::Infra; # must precede Debian::Dgit; - can change @INC!
+use Debian::Dgit;
+setup_sigwarn();
+
+use POSIX;
+
+open DEBUG, '>/dev/null' or die $!;
+if (@ARGV && $ARGV[0] eq '-D') {
+ shift @ARGV;
+ open DEBUG, '>&STDERR' or die $!;
+}
+
+die unless @ARGV>=1 && @ARGV<=2 && $ARGV[0] !~ m/^-/;
+our ($dispatchdir,$authrune) = @ARGV;
+
+$authrune //= join ':',
+ '@/keyrings/debian-keyring.gpg,a',
+ '@/keyrings/debian-maintainers.gpg,m@/dm.txt';
+
+our $lre = $package_re;
+our $qre = '["'."']?";
+
+# $dispatchdir/distro=DISTRO should contain
+# dgit-live a clone of dgit (only if not using installed vsns)
+# diverts
+# repos/ } by virtue of
+# suites } dgit-repos-server's defaults relating to
+# policy-hook } dispatch-dir
+# plus files required by the authrune (by default, keyrings/ and dm.txt)
+#
+# diverts should be list of
+# <pat> [<divert-to>]
+# where <pat> is a package name pattern which may contain * or literals.
+# <divert-to> is for `git config dgit-distro.DISTRO.diverts.<divert-to>'
+
+our ($distro,$pkg, $d);
+our ($dgitlive,$repos,$suites,$diverts,$policyhook,$repo);
+
+sub checkdivert ($) {
+ my ($df) = @_;
+ if (!open DIV, '<', $df) {
+ $!==ENOENT or die $!;
+ return undef;
+ } else {
+ while (<DIV>) {
+ s/^\s+//; s/\s+$//;
+ next unless m/\S/;
+ next if m/^\#/;
+ my $divert;
+ if (s/\s+(\S+)$//) { $divert=$1; }
+ s/[^-+._0-9a-zA-Z*]/\\$&/g;
+ s/\*/.*/g;
+ printf DEBUG 'DISPATCH DIVERT ^%s$ %s'."\n",
+ $_, ($divert // '(undef)');
+ if ($pkg =~ m/^$_$/) { return $divert; }
+ }
+ DIV->error and die $!;
+ close DIV;
+ return undef;
+ }
+}
+
+sub finish () {
+ close STDOUT or die $!;
+ exit 0;
+}
+
+sub prl ($) {
+ print @_, "\n" or die $!;
+}
+
+sub selectpackage ($$;$) {
+ my $divertfn;
+ ($distro,$pkg, $divertfn) = @_; # $distro,$pkg must have sane syntax
+
+ $d = "$dispatchdir/distro=$distro";
+
+ if (!stat $d) {
+ die $! unless $!==ENOENT;
+ die "unknown distro ($distro)\n";
+ }
+
+ $dgitlive= "$d/dgit-live";
+ $repos= "$d/repos";
+ $suites= "$d/suites";
+ $policyhook= "$d/policy-hook";
+
+ $authrune =~ s/\@/$d/g;
+
+ my $divert = checkdivert("$d/diverts");
+ if (defined $divert) {
+ $divertfn //= sub {
+ die "diverted to $divert incompletely or too late!\n";
+ };
+ $divertfn->($divert);
+ die;
+ }
+
+ $repo = "$repos/$pkg.git";
+
+ print DEBUG "DISPATCH DISTRO $distro PKG $pkg\n";
+}
+
+sub hasrepo () {
+ if (stat $repo) {
+ -d _ or die;
+ return 1;
+ } else {
+ $!==ENOENT or die $!;
+ return 0;
+ }
+}
+
+sub serve_up ($) {
+ my ($repo) = @_;
+ exec qw(git upload-pack --strict --timeout=1000), $repo;
+ die "exec git: $!";
+}
+
+sub dispatch () {
+ local ($_) = $ENV{'SSH_ORIGINAL_COMMAND'} // '';
+
+ if (m#^: dgit ($lre) git-check ($lre) ;#) {
+ selectpackage $1,$2, sub { prl "divert @_"; finish; };
+ prl hasrepo;
+ finish;
+ } elsif (
+ m#^${qre}git-([-a-z]+) ${qre}/dgit/($lre)/repos/($lre)\.git${qre}$#
+ ) {
+ my $cmd=$1;
+ selectpackage $2,$3;
+ if ($cmd eq 'receive-pack') {
+ $ENV{'PERLLIB'} //= '';
+ $ENV{'PERLLIB'} =~ s#^(?=.)#:#;
+ $ENV{'PERLLIB'} =~ s#^# $ENV{DGIT_TEST_INTREE} // $dgitlive #e;
+ my $s = "$dgitlive/infra/dgit-repos-server";
+ $s = "dgit-repos-server" if !stat_exists $s;
+ exec $s, $distro, $d, $authrune, qw(--ssh);
+ die "exec $s: $!";
+ } elsif ($cmd eq 'upload-pack') {
+ $repo='$repos/_empty' unless hasrepo;
+ serve_up $repo;
+ } else {
+ die "unsupported git operation $cmd ($_)";
+ }
+ } elsif (
+ m#^${qre}git-upload-pack ${qre}/dgit/($lre)/(?:repos/)?_dgit-repos-server\.git${qre}$#
+ ) {
+ my $distro= $1;
+ # if running installed packages, source code should come
+ # some other way
+ serve_up("$dispatchdir/distro=$1/dgit-live/.git");
+ } elsif (m#^${qre}git-upload-pack\s#) {
+ die "unknown repo to serve ($_). use dgit, or for server source ".
+ "git clone here:/dgit/DISTRO/repos/_dgit-repos-server.git";
+ } else {
+ die "unsupported operation ($_)";
+ }
+}
+
+dispatch;
diff --git a/infra/drs-cron-wrap b/infra/drs-cron-wrap
new file mode 100755
index 0000000..52e819b
--- /dev/null
+++ b/infra/drs-cron-wrap
@@ -0,0 +1,14 @@
+#!/bin/sh
+set -e
+umask 002
+
+distro=$1; shift
+
+srvdir=/srv/dgit.debian.org
+dispatchdir=$srvdir/dispatch-dir
+dgitlive=$srvdir/dgit-live
+
+distrodir=$dispatchdir/distro=$distro
+
+PERLLIB="$dgitlive${PERLLIB+:}${PERLLIB}" \
+exec $dgitlive/infra/dgit-repos-server $distro $distrodir '' --cron
diff --git a/infra/get-dm-txt b/infra/get-dm-txt
new file mode 100755
index 0000000..0b9ab10
--- /dev/null
+++ b/infra/get-dm-txt
@@ -0,0 +1,21 @@
+#!/bin/sh
+set -e
+
+cd ${DGIT_INFRA_GETDMTXT_DATADIR-/srv/dgit.debian.org/data}
+${DGIT_INFRA_GETDMTXT_UMASK-umask 002}
+
+file=dm.txt
+server=ftp-master.debian.org
+path=$file
+
+certargs=$(git config dgit-distro.debian.archive-query-tls-curl-ca-args \
+ || (echo >&2 "git config failed"; exit 1))
+
+with-lock-ex -f $file.lock sh -c "
+ if ! curl $certargs \
+ >$file.new https://$server/$path 2>$file.stderr; then
+ cat $file.stderr >&2
+ exit 127
+ fi
+ mv -f $file.new $file
+"
diff --git a/infra/get-suites b/infra/get-suites
new file mode 100755
index 0000000..c5a4c56
--- /dev/null
+++ b/infra/get-suites
@@ -0,0 +1,26 @@
+#!/bin/bash
+set -e
+set -o pipefail
+
+srvdir=/srv/dgit.debian.org
+dgitlive=${DGIT_TEST_INTREE-$srvdir/dgit-live}
+output=${DGIT_GETSUITES_OUTPUT-$srvdir/data/suites}
+
+export PERLLIB="$dgitlive${PERLLIB+:}${PERLLIB}"
+
+$dgitlive/dgit archive-api-query /suites | perl -we '
+ use strict;
+ use JSON;
+ undef $/;
+ my $json = <STDIN>;
+ die $! if STDIN->error;
+ my $items = decode_json $json;
+ foreach my $item (@$items) {
+ next unless ($item->{archive}//"") eq "ftp-master";
+ next unless ($item->{codename});
+ print $item->{codename}, "\n" or die $!;
+ }
+ flush STDOUT or die $!;
+' >$output.new
+
+mv -f $output.new $output
diff --git a/infra/ssh-wrap b/infra/ssh-wrap
new file mode 100755
index 0000000..deccc38
--- /dev/null
+++ b/infra/ssh-wrap
@@ -0,0 +1,10 @@
+#!/bin/sh
+set -e
+umask 002
+
+srvdir=/srv/dgit.debian.org
+dispatchdir=$srvdir/dispatch-dir
+dgitlive=$srvdir/dgit-live
+
+PERLLIB="$dgitlive${PERLLIB+:}${PERLLIB}" \
+exec $dgitlive/infra/dgit-ssh-dispatch $dispatchdir
diff --git a/local-pod-man b/local-pod-man
new file mode 100755
index 0000000..3c3e0ea
--- /dev/null
+++ b/local-pod-man
@@ -0,0 +1,13 @@
+#!/bin/bash
+set -e
+
+case "$#.$1" in
+1.[^-]*) ;;
+*) echo >&2 'usage: ./local-pod-man dgit-something[.7[.pod]]'; exit 16;;
+esac
+base="$1"
+base="${base%.pod}"
+base="${base%.7}"
+
+make "$base.7"
+man -l "$base.7"
diff --git a/tests/Makefile b/tests/Makefile
new file mode 100644
index 0000000..5bd3eee
--- /dev/null
+++ b/tests/Makefile
@@ -0,0 +1,14 @@
+# usage: tests/using-intree make -f tests/Makefile
+# (optionally setting TESTSCRIPTS='tests/tests/foo tests/tests/bar')
+
+TESTSCRIPTS ?= $(shell tests/enumerate-tests)
+TESTNAMES := $(notdir $(TESTSCRIPTS))
+
+all: $(foreach t,$(TESTNAMES),tests/tmp/$t.ok)
+ @echo "ALL PASSED"
+
+tests/tmp:
+ mkdir -p $@
+
+tests/tmp/%.ok: tests/tmp
+ tests/tests/$* >tests/tmp/$*.log 2>&1
diff --git a/tests/adhoc b/tests/adhoc
new file mode 100755
index 0000000..b45251f
--- /dev/null
+++ b/tests/adhoc
@@ -0,0 +1,51 @@
+#!/bin/bash
+#
+# usage:
+# after tests/tests/some-test has been run (and maybe failed)
+# cd tests/tmp/some-test/blah
+# ../../adhoc ../../../dgit some options
+# or
+# after tests/tests/some-test has been run (and maybe failed)
+# cd tests/tmp/some-test/blah
+# ../../adhoc some rune run by some piece of infrastructure
+#
+# effects:
+# directly sets the env variables which arrange to use
+# programs etc. from the working tree
+
+ourname=adhoc
+
+case $0 in
+*/$ourname)
+ : ${DGIT_TEST_INTREE:=$(realpath "${0%/$ourname}/..")}
+ ;;
+*)
+ echo >&2 "$ourname must be invoked as .../$ourname not $0"
+ exit 127
+ ;;
+esac
+
+export DGIT_TEST_INTREE
+
+. $DGIT_TEST_INTREE/tests/lib-core
+
+t-set-intree
+
+pwd=$(realpath "$(pwd)")
+basis=$DGIT_TEST_INTREE/tests/tmp
+case "$pwd" in
+"$basis" | "$basis"/*)
+ testname=${pwd/"$basis/"/}
+ testname=${testname%%/*}
+ tmp="$basis/$testname"
+ ;;
+*)
+ fail "$ourname pwd must be inside some test (in $basis), not $pwd"
+ ;;
+esac
+
+export ADTTMP=$tmp
+
+t-set-using-tmp
+
+exec "$@"
diff --git a/tests/drs-git-ext b/tests/drs-git-ext
index 52e7817..ad27c9b 100755
--- a/tests/drs-git-ext
+++ b/tests/drs-git-ext
@@ -2,12 +2,14 @@
set -e
tmp=$DGIT_TEST_TMP
+: ${DGIT_DRS_DEBUG:=1}
+export DGIT_DRS_DEBUG
echo >&2 '(((((((((((((((((((((((((((((((((((((((('
set -x
export SSH_ORIGINAL_COMMAND="$*"
${DGIT_REPOS_SERVER_TEST-dgit-repos-server} \
- $tmp/suites \
+ test-dummy $tmp/distro=test-dummy \
$tmp/dd.gpg,a:$tmp/dm.gpg,m$tmp/dm.txt \
- $tmp/git \
+ --repos=$tmp/git --suites=$tmp/suites \
--ssh
: '))))))))))))))))))))))))))))))))))))))))'
diff --git a/tests/dsd-ssh b/tests/dsd-ssh
new file mode 100755
index 0000000..d5df5aa
--- /dev/null
+++ b/tests/dsd-ssh
@@ -0,0 +1,18 @@
+#!/bin/sh
+set -e
+
+echo >&2 '(((((((((((((((((((((((((((((((((((((((('
+set -x
+
+tmp=$DGIT_TEST_TMP
+cd /
+userhost="$1"; shift
+export SSH_ORIGINAL_COMMAND="$*"
+
+# undoes PERLLIB so that we rely on dgit-ssh-dispatch setting it
+# we have to compensate with -I so that dgit-ssh-dispatch finds Dgit.pm
+unset PERLLIB
+${DGIT_TEST_INTREE+perl -I}$DGIT_TEST_INTREE \
+${DGIT_SSH_DISPATCH_TEST-dgit-ssh-dispatch} -D $tmp
+
+: '))))))))))))))))))))))))))))))))))))))))'
diff --git a/tests/enumerate-tests b/tests/enumerate-tests
new file mode 100755
index 0000000..2c00f97
--- /dev/null
+++ b/tests/enumerate-tests
@@ -0,0 +1,109 @@
+#!/bin/bash
+
+set -e
+
+. tests/lib-core
+. tests/lib-restricts
+
+mode=$1
+
+test-begin- () {
+ whynots=''
+}
+
+restriction- () {
+ set +e
+ whynot=$(t-restriction-$r)
+ rc=$?
+ whynot="${whynot//
+/ / }"
+ set -e
+ case "$rc.$whynot" in
+ 0.) ;;
+ 1.?*) whynots="$whynots${whynots:+; }$whynot" ;;
+ *) fail "restriction $r for $t gave $rc $whynot !"
+ esac
+}
+
+dependencies- () {
+ :
+}
+
+test-done- () {
+ case "$whynots" in
+ '') echo $t ;;
+ ?*) echo >&2 "SKIP $t $whynots" ;;
+ esac
+}
+
+finish- () {
+ :
+}
+
+test-begin-gencontrol () {
+ restrictions=''
+ dependencies=''
+}
+
+restriction-gencontrol () {
+ restrictions+=" $r"
+}
+
+dependencies-gencontrol () {
+ dependencies+=", $deps"
+}
+
+test-done-gencontrol () {
+ stanza=$(
+ add_Depends="$dependencies" \
+ perl <debian/tests/control.in -wpe '
+ if (/^(\w+):/) {
+ my $h = $1;
+ s{$}{ $ENV{"add_$h"} // "" }e;
+ }
+ '
+ case "$restrictions" in
+ ?*) echo "Restrictions:$restrictions" ;;
+ esac
+ )
+ key=$(printf "%s" "$stanza" | sha256sum)
+ key=${key%% *}
+ eval "
+ stanza_$key=\"\$stanza\"
+ tests_$key+=\" \${t#tests/tests/}\"
+ "
+ keys=" ${keys/ $key /}"
+ keys+=" $key "
+}
+
+finish-gencontrol () {
+ for key in $keys; do
+ eval "
+ stanza=\$stanza_$key
+ tests=\$tests_$key
+ "
+ printf "Tests:%s\n%s\n\n" "$tests" "$stanza"
+ done
+}
+
+seddery () {
+ local seddery=$1
+ sed <$t -n '
+ 20q;
+ /^: t-enumerate-tests-end$/q;
+ '"$seddery"'
+ '
+}
+
+for t in $(run-parts --list tests/tests); do
+ test-begin-$mode
+ for r in $(seddery 's/^t-restrict //p'); do
+ restriction-$mode
+ done
+ for deps in $(seddery 's/^t-dependencies //p'); do
+ dependencies-$mode
+ done
+ test-done-$mode
+done
+
+finish-$mode
diff --git a/tests/lib b/tests/lib
index e17be7f..e7ab334 100644
--- a/tests/lib
+++ b/tests/lib
@@ -2,6 +2,36 @@
exec 2>&1
set -x
+set -o pipefail
+
+. tests/lib-core
+. tests/lib-restricts
+
+t-report-failure () {
+ set +x
+ rc=$1
+ cat <<END >&2
+TEST FAILED
+funcs: ${FUNCNAME[*]}
+lines: ${BASH_LINENO[*]}
+files: ${BASH_SOURCE[*]}
+END
+ exit 16
+}
+
+trap 'test $? = 0 || t-report-failure' EXIT
+
+t-filter-out-git-hyphen-dir
+
+t-set-intree
+
+: ${DGIT_TEST_DEBUG=-D}
+export DGIT_TEST_DEBUG
+
+: ${DGIT_TEST_DISTRO+ ${distro=${DGIT_TEST_DISTRO}}}
+
+export GIT_COMMITTER_DATE='1440253867 +0100'
+export GIT_AUTHOR_DATE='1440253867 +0100'
root=`pwd`
troot=$root/tests
@@ -10,21 +40,25 @@ testname="${DGIT_TEST_TESTNAME-${0##*/}}"
tmp=$ADTTMP
if [ x"$tmp" = x ]; then
mkdir -p tests/tmp
+ tmpbase=$troot/tmp
tmp=tests/tmp/$testname
rm -rf $tmp
mkdir $tmp
+elif [ "x$DGIT_TEST_TMPBASE" != x ]; then
+ tmpbase="$DGIT_TEST_TMPBASE"
fi
cd $tmp
-export HOME=$tmp
tmp=`pwd`
-export DGIT_TEST_DUMMY_DIR=$tmp
+
+t-set-using-tmp
+
+test -f $tmp/.save-env || \
+env -0 >$tmp/.save-env
+
ln -f $troot/ssh ssh
-mkdir -p $tmp/gnupg
-cp $troot/gnupg/* $tmp/gnupg
-chmod go-rw $tmp/gnupg/*
-export GNUPGHOME=$tmp/gnupg
+export DEBCHANGE_VENDOR=dpkg
mkdir -p $tmp/incoming
cat <<END >$tmp/dput.cf
@@ -34,16 +68,97 @@ incoming = $tmp/incoming
run_dinstall = 0
END
-fail () {
- echo >&2 "failed: $*"
- exit 1
+: ${t_archive_method:=aq}
+: ${tagpfx:=archive/test-dummy}
+: ${suitespecs:=sid:unstable}
+
+t-git-next-date () {
+ GIT_COMMITTER_DATE="$(( ${GIT_COMMITTER_DATE%% *} + 1 )) ${GIT_COMMITTER_DATE#* }"
+ GIT_AUTHOR_DATE="$GIT_COMMITTER_DATE"
+}
+
+t-expect-fail () {
+ local mpat="$1"; shift
+
+ set +o pipefail
+ LC_MESSAGES=C "$@" 2>&1 | tee $tmp/t.output
+ local ps="${PIPESTATUS[*]}"
+ set -o pipefail
+
+ case $ps in
+ "0 0") fail "command unexpectedly succeeded (instead of: $mpat)" ;;
+ *" 0") ;;
+ *) fail "tee failed" ;;
+ esac
+
+ t-grep-mpat "$mpat" $tmp/t.output
+}
+
+t-grep-mpat () {
+ local mpat="$1"
+ local file="$2"
+
+ local grepper=fgrep
+ case "$mpat" in
+ [A-Z]:*)
+ case "$mpat" in
+ E:*) grepper=egrep ;;
+ F:*) grepper=fgrep ;;
+ *) fail "bad mpat prefix in $mpat";;
+ esac
+ mpat=${mpat#[A-Z]:}
+ ;;
+ esac
+
+ $grepper -e "$mpat" "$file" ||
+ fail "message not found"
+}
+
+t-expect-push-fail () {
+ local mpat="$1"; shift
+
+ local triedpush; triedpush=`git rev-parse HEAD`
+
+ t-reporefs pre-push
+ t-expect-fail "$mpat" "$@"
+ t-reporefs post-push
+ diff $tmp/show-refs.{pre,post}-push
+
+ t-git-objects-not-present '' $triedpush
+
+ eval "$t_expect_push_fail_hook"
+}
+
+t-git-objects-not-present () {
+ # t-git-objects-not-present GITDIR|'' OBJID [...]
+ # specifying '' means the repo for package $p
+ local gitdir="${1-$dgitrepo}"
+ local obj
+ if ! [ -e "$gitdir" ]; then return; fi
+ for obj in "$@"; do
+ GIT_DIR=$gitdir \
+ t-expect-fail 'unable to find' \
+ git cat-file -t $obj
+ done
+}
+
+t-reporefs () {
+ local whichoutput=$1; shift
+ local whichrepo=${1-$dgitrepo}
+ local outputfile="$tmp/show-refs.$whichoutput"
+ (set -e
+ exec >"$outputfile"
+ if test -d $whichrepo; then
+ cd $whichrepo
+ git show-ref |sort
+ fi)
}
t-untar () {
local tarfile=$1.tar
local edittree=$1.edit
if test -d "$edittree"; then
- cp -al "$edittree"/* .
+ cp -a "$edittree"/* .
else
tar xf "$tarfile"
fi
@@ -54,8 +169,13 @@ t-worktree () {
t-untar $troot/worktrees/${p}_$1
}
-t-git () {
+t-select-package () {
p=$1
+ dgitrepo=$tmp/git/$p.git
+}
+
+t-git () {
+ t-select-package $1
v=$2
mkdir -p $tmp/git
local gs=$troot/git-srcs/${p}_$v.git
@@ -67,56 +187,248 @@ t-git-none () {
(set -e; cd $tmp/git; tar xf $troot/git-template.tar)
}
+t-git-merge-base () {
+ git merge-base $1 $2 || test $? = 1
+}
+
t-has-ancestor () {
- local now=`git rev-parse HEAD`
- local ancestor=`git rev-parse $1^{}`
- local mbase=`git merge-base $ancestor $now`
+ # t-has-ancestor ANCESTOR
+ # (CHILD is implicit, HEAD)
+ local now; now=`git rev-parse HEAD`
+ local ancestor; ancestor=`git rev-parse $1^{}`
+ local mbase; mbase=`t-git-merge-base $ancestor $now`
if [ x$mbase != x$ancestor ]; then
fail "not ff $ancestor..$now, $mbase != $ancestor"
fi
-}
+}
+
+t-has-parent-or-is () {
+ # t-has-parent-or-is CHILD PARENT
+ local child=$1
+ local parent=$2
+ local parents
+ parents=$(git show --pretty=format:' %P %H ' "$child")
+ parent=$(git rev-parse "$parent~0")
+ case "$parents" in
+ *" $parent "*) ;;
+ *) fail "child $child lacks parent $parent" ;;
+ esac
+}
+
+t-prep-newpackage () {
+ t-select-package $1
+ v=$2
+ t-archive-none $p
+ t-git-none
+ t-worktree $v
+ cd $p
+ if ! git show-ref --verify --quiet refs/heads/master; then
+ git branch -m dgit/sid master
+ git remote rm dgit
+ fi
+ cd ..
+}
t-archive-none () {
- p=$1
- mkdir -p $tmp/aq $tmp/mirror
- echo sid >$tmp/aq/suite.unstable
+ t-select-package $1
+ t-archive-none-$t_archive_method
+}
+t-archive-none-aq () {
+ mkdir -p $tmp/aq/dsc_in_suite $tmp/mirror/pool/main
+
+ : >$tmp/aq/suites
+ local jsondelim="["
+
+ local suitespec
+ for suitespec in $suitespecs; do
+ local suite=${suitespec%%:*}
+ local sname=${suitespec#*:}
+
+ >$tmp/aq/package.$suite.$p
+ t-aq-archive-updated $suite $p
+
+ >$tmp/aq/package.new.$p
+ t-aq-archive-updated new $p
+
+ ln -sf $suite $tmp/aq/dsc_in_suite/$sname
+
+ cat <<END >>$tmp/aq/suites
+$jsondelim
+ {
+ "archive" : "ftp-master",
+ "codename" : "$suite",
+ "components" : [
+ "main",
+ "contrib",
+ "non-free"
+ ],
+ "name" : "$sname",
+ "dakname" : "$sname"
+END
+
+ jsondelim=" },"
+
+ done
+ cat <<END >>$tmp/aq/suites
+ }
+]
+END
+}
+
+t-aq-archive-updated () {
+ local suite=$1
+ local p=$2
+ local suitedir=$tmp/aq/dsc_in_suite/$suite
+ mkdir -p $suitedir
+ perl <$tmp/aq/package.$suite.$p >$suitedir/$p -wne '
+ use JSON;
+ use strict;
+ our @v;
+ m{^(\S+) (\w+) ([^ \t/]+)/(\S+)} or die;
+ push @v, {
+ "version" => "$1",
+ "sha256sum" => "$2",
+ "component" => "$3",
+ "filename" => "$4",
+ };
+ END {
+ my $json = JSON->new->canonical();
+ print $json->encode(\@v) or die $!;
+ }
+ '
}
t-archive-process-incoming () {
- mv incoming/${p}_${v}[._]* mirror/
- t-archive-query
+ local suite=$1
+ mv $tmp/incoming/${p}_* $tmp/mirror/pool/main/
+ t-archive-query "$suite"
}
t-archive-query () {
- local dscf=${p}_${v}.dsc
- echo "${v} $dscf" >>$tmp/aq/package.sid.${p}
+ local suite=${1-sid}
+ local dscf=main/${p}_${v}.dsc
+ t-archive-query-$t_archive_method "$suite" "$p" "$v" "$dscf"
+}
+t-archive-query-aq () {
+ local suite=$1
+ local p=$2
+ local v=$3
+ local dscf=$4
+ local sha; sha=`sha256sum <$tmp/mirror/pool/$dscf`
+ echo "${v} ${sha% -} $dscf" >>$tmp/aq/package.$suite.${p}
+ t-aq-archive-updated $suite $p
}
t-archive () {
t-archive-none $1
v=$2
local dscf=${p}_$2.dsc
- rm -f $tmp/mirror/${p}_*
- ln $troot/pkg-srcs/${p}_${2%-*}* $tmp/mirror/
- t-archive-query
+ rm -f $tmp/mirror/pool/main/${p}_*
+ ln $troot/pkg-srcs/${p}_${2%-*}* $tmp/mirror/pool/main/
+ t-archive-query $suite
rm -rf $tmp/extract
mkdir $tmp/extract
- (set -e; cd $tmp/extract; dpkg-source -x ../mirror/$dscf)
+ (set -e; cd $tmp/extract; dpkg-source -x ../mirror/pool/main/$dscf)
+}
+
+t-git-dir-time-passes () {
+ touch -d 'last year' $dgitrepo
+}
+
+t-git-dir-check () {
+ local gitdir=$dgitrepo
+ case "$1" in
+ enoent)
+ if test -e "$gitdir"; then fail "$gitdir exists"; fi
+ return
+ ;;
+ public) wantstat='7[75]5' ;;
+ secret) wantstat='7[70]0' ;;
+ *) fail "$1 t-git-dir-check ?" ;;
+ esac
+ gotstat=`stat -c%a $gitdir`
+ case "$gotstat" in
+ *$wantstat) return ;;
+ *) fail "$gitdir has mode $gotstat, expected $wantstat" ;;
+ esac
+}
+
+t-expect-fsck-fail () {
+ echo >>$tmp/fsck.expected-errors "$1"
+}
+
+t-git-fsck () {
+ set +e
+ LC_MESSAGES=C git fsck --no-dangling --strict 2>&1 \
+ | tee dgit-test-fsck.errs
+ ps="${PIPESTATUS[*]}"
+ set -e
+
+ local pats
+ if [ -f $tmp/fsck.expected-errors ]; then
+ pats=(-w -f $tmp/fsck.expected-errors)
+ else
+ test "$ps" = "0 0"
+ fi
+ pats+=(-e 'notice: HEAD points to an unborn branch')
+ pats+=(-e 'notice: No default references')
+
+ set +e
+ grep -v "${pats[@]}" dgit-test-fsck.errs
+ rc=$?
+ set -e
+ case $rc in
+ 1) ;; # no unexpected errors
+ 0) fail "unexpected messages from git-fsck" ;;
+ *) fail "grep of git-fsck failed" ;;
+ esac
+}
+
+t-fscks () {
+ (
+ shopt -s nullglob
+ for d in $tmp/*/.git $tmp/git/*.git; do
+ cd "$d"
+ t-git-fsck
+ done
+ )
+}
+
+t-ok () {
+ : '========================================'
+ t-fscks
+ echo ok.
+}
+
+t-rm-dput-dropping () {
+ rm -f $tmp/${p}_${v}_*.upload
}
t-dgit () {
local dgit=${DGIT_TEST-dgit}
+ pwd
: '
{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{'
$dgit --dgit=$dgit --dget:-u --dput:--config=$tmp/dput.cf \
- -dtest-dummy $DGIT_TEST_OPTS ${DGIT_TEST_DEBUG--D} \
- -k39B13D8A "$@"
+ ${dgit_config_debian_alias-"--config-lookup-explode=dgit-distro.debian.alias-canon"} \
+ ${distro+${distro:+-d}}${distro--dtest-dummy} \
+ $DGIT_TEST_OPTS $DGIT_TEST_DEBUG \
+ -k39B13D8A $t_dgit_xopts "$@"
: '}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
'
}
t-diff-nogit () {
- diff --exclude=.git -ruN $*
+ diff --exclude=.git --exclude=.pc -ruN $*
+}
+
+t-files-notexist () {
+ local f
+ for f in "$@"; do
+ if [ -e $f ]; then
+ fail "$f exists!"
+ fi
+ done
}
t-cloned-fetched-good () {
@@ -126,11 +438,11 @@ t-cloned-fetched-good () {
t-refs-same \
refs/heads/dgit/sid \
refs/remotes/dgit/dgit/sid
- t-refs-notexist dgit/unstable remotes/dgit/dgit/unstable
+ t-refs-notexist refs/dgit/unstable refs/remotes/dgit/dgit/unstable
}
t-output () {
- printf "%s\n" "$1" >$tmp/t.want
+ printf "%s${1:+\n}" "$1" >$tmp/t.want
shift
"$@" >$tmp/t.got
diff $tmp/t.want $tmp/t.got
@@ -140,38 +452,120 @@ t-clean-on-branch () {
t-output "## $1" git status -b --porcelain
}
+t-setup-done () {
+ local savevars=$1
+ local savedirs=$2
+ local importeval=$3
+
+ local import=IMPORT.${0##*/}
+ exec 4>$tmp/$import.new
+
+ local vn
+ for vn in $savevars; do
+ perl >&4 -I. -MDebian::Dgit -e '
+ printf "%s=%s\n", $ARGV[0], shellquote $ARGV[1]
+ ' $vn "$(eval "printf '%s\n' \"\$$vn\"")"
+ done
+
+ (set -e; cd $tmp; tar cf $import.tar $savedirs)
+
+ printf >&4 "\n%s\n" "$importeval"
+
+ mv -f $tmp/$import.new $tmp/$import
+}
+
+t-setup-import () {
+ local setupname=$1
+
+ local setupsrc
+ local lock
+ if [ "x$tmpbase" = x ]; then
+ # ADTTMP was set on entry to tests/lib, so we
+ # are not sharing tmp area between tests
+ setupsrc="$tmp"
+ lock="$tmp/.dummy.lock"
+ else
+ setupsrc="$tmpbase/$setupname"
+ lock="$setupsrc.lock"
+ fi
+
+ local simport="$setupsrc/IMPORT.$setupname"
+
+ if ! [ -e "$simport" ]; then
+ with-lock-ex -w "$lock" \
+ xargs -0 -a $tmp/.save-env \
+ bash -xec '
+ cd "$1"; shift
+ setupname="$1"; shift
+ simport="$1"; shift
+ if [ -e "$simport" ]; then exit 0; fi
+ env - "$@" \
+ "tests/setup/$setupname"
+ ' x "$root" "$setupname" "$simport"
+ fi
+
+ if [ "x$setupsrc" != "x$tmp" ]; then
+ (set -e; cd $tmp; tar xf "$simport.tar")
+ fi
+
+ . "$simport"
+}
+
+t-git-get-ref-exact () {
+ local ref=$1
+ # does not dereference, unlike t-git-get-ref
+ case "$ref" in
+ refs/*) ;;
+ *) fail "t-git-get-ref-exact bad $ref" ;;
+ esac
+ git for-each-ref --format='%(objectname)' "[r]efs/${ref#refs/}"
+}
+
t-git-get-ref () {
local ref=$1
- git show-ref -d $1 | perl -ne '
+ case "$ref" in
+ refs/*) ;;
+ *) fail "t-git-get-ref bad $ref" ;;
+ esac
+ (git show-ref -d $1 || test $? = 1) | perl -ne '
$x = $1 if m#^(\w+) \Q'$1'\E(?:\^\{\})?$#;
END { print "$x\n" if length $x; }
'
}
+t-ref-same-exact () {
+ local name="$1"
+ local val; val=`t-git-get-ref-exact $name`
+ t-ref-same-val "$name" $val
+}
+
t-ref-same () {
local name="$1"
- local val=`t-git-get-ref $name`
+ local val; val=`t-git-get-ref $name`
t-ref-same-val "$name" $val
}
t-ref-head () {
- local val=`git rev-parse HEAD`
+ local val; val=`git rev-parse HEAD`
t-ref-same-val HEAD $val
}
t-ref-same-val () {
local name="$1"
local val=$2
- case "$t_ref_val" in
- '') ;;
+ case "${t_ref_val-unset}" in
+ unset) ;;
"$val") ;;
- *) fail "ref varies: $name: $val != $t_ref_val" ;;
+ *) fail "ref varies: ($name)\
+ ${val:-nothing} != ${t_ref_val:-nothing} (${t_ref_names[*]})" ;;
esac
t_ref_val="$val"
+ t_ref_names+=("$name")
}
t-refs-same-start () {
- t_ref_val=''
+ unset t_ref_val
+ t_ref_names=()
}
t-refs-same () {
@@ -184,7 +578,7 @@ t-refs-same () {
t-refs-notexist () {
local val
for g in $*; do
- val=`t-git-get-ref $g >$tmp/t.refx`
+ val=`t-git-get-ref $g`
if [ "x$val" != x ]; then
fail "ref $g unexpectedly exists ($val)"
fi
@@ -192,29 +586,188 @@ t-refs-notexist () {
}
t-v-tag () {
- echo refs/tags/debian/${v//\~/_}
+ echo refs/tags/$tagpfx/${v//\~/_}
+}
+
+t-format-ref () {
+ git log -n1 --pretty=format:"$1" "$2"
+}
+
+t-sametree-parent () {
+ local ref=$1
+ local parent
+ local ctree; ctree=$(t-format-ref '%T' "$ref")
+ while :; do
+ local psame=''
+ for parent in $(t-format-ref '%P' "$ref"); do
+ local ptree; ptree=$(t-format-ref '%T' "$parent")
+ if [ "x$ptree" = "x$ctree" ]; then
+ psame+=" $parent"
+ fi
+ done
+ case "$psame" in ""|" * *") break ;; esac
+ ref="${psame# }"
+ done
+ echo "$ref"
+}
+
+t-check-pushed-master () {
+ local master; master=`t-git-get-ref refs/heads/master`
+ if [ x$master = x$t_ref_val ]; then return; fi
+ if [ x$master = x ]; then fail "failed to push master"; fi
+ # didn't update master, it must be not FF
+ local mbase; mbase=`t-git-merge-base $master $t_ref_val`
+ if [ x$mbase = x$master ]; then fail "failed to ff master"; fi
}
t-pushed-good () {
local branch=$1
+ local suite=${2:-sid}
+ t-refs-same \
+ refs/heads/$branch
+ t-pushed-good-core
+}
+
+t-pushed-good-core () {
t-ref-dsc-dgit
t-refs-same \
- refs/heads/$branch \
`t-v-tag` \
- refs/remotes/dgit/dgit/sid
+ refs/remotes/dgit/dgit/$suite
t-refs-notexist \
refs/heads/dgit/unstable \
refs/remotes/dgit/dgit/unstable
- (set -e; cd $tmp/git/$p.git
+ (set -e; cd $dgitrepo
t-refs-same \
- refs/dgit/sid \
+ refs/dgit/$suite \
`t-v-tag`
+ ${t_check_pushed_master:- : NOT-DRS-NO-CHECK-PUSHED-MASTER}
t-refs-notexist \
refs/dgit/unstable
)
git verify-tag `t-v-tag`
}
+t-splitbrain-pushed-good--unpack () {
+ cd $tmp
+ rm -rf t-unpack
+ mkdir t-unpack
+ cd t-unpack
+ ln -s $tmp/mirror/pool/main/*.orig*.tar* .
+ ln -s $tmp/incoming/*.orig*.tar* . ||:
+ ln -s $incoming_dsc .
+ ln -s ${incoming_dsc/.dsc/.debian.tar}* .
+ dpkg-source "$@" -x *.dsc
+ cd */.
+ git init
+ git fetch ../../$p "refs/tags/*:refs/tags/*"
+}
+
+t-splitbrain-pushed-good--checkprep () {
+ git add -Af .
+ git rm --cached -r --ignore-unmatch .pc
+}
+
+t-splitbrain-pushed-good--checkdiff () {
+ local tag=$1
+ t-splitbrain-pushed-good--checkprep
+ t-output "" git diff --stat --cached $tag
+}
+
+t-splitbrain-pushed-good-start () {
+ dep14tag=refs/tags/test-dummy/${v//\~/_}
+ dgittag=$(t-v-tag)
+ t-output "" git status --porcelain
+ t-ref-head
+ t-refs-same $dep14tag
+ (set -e; cd $dgitrepo; t-refs-same $dep14tag)
+ git merge-base --is-ancestor $dep14tag $dgittag
+
+ t-refs-same-start
+ t-ref-same refs/heads/split.p
+ case "$(t-git-get-ref refs/heads/split.b)" in
+ "$t_ref_val") ;;
+ "$(git rev-parse refs/heads/split.p^0)") ;;
+ "$(git rev-parse refs/heads/split.p^1)") ;;
+ *) fail 'bad b/p' ;;
+ esac
+ t-pushed-good-core
+
+ t-incoming-dsc
+
+ t-splitbrain-pushed-good--unpack
+ t-splitbrain-pushed-good--checkdiff $dgittag
+}
+t-splitbrain-pushed-good-end-made-dep14 () {
+ t-splitbrain-pushed-good--checkdiff $dep14tag
+ cd $tmp/$p
+}
+
+t-splitbrain-rm-gitignore-patch () {
+ perl -i -pe '
+ next unless $_ eq "auto-gitignore\n";
+ die if $counter++;
+ chomp;
+ rename "debian/patches/$_", "../t-auto-gitignore" or die $!;
+ $_ = "";
+ ' debian/patches/series
+}
+
+t-gbp-pushed-good () {
+ local suite=${1:-sid}
+ t-splitbrain-pushed-good-start
+
+ # Right, now we want to check that the maintainer tree and
+ # the dgit tree differ in exactly the ways we expect. We
+ # achieve this by trying to reconstruct the maintainer tree
+ # from the dgit tree.
+
+ # So, unpack it withut the patches applied
+ t-splitbrain-pushed-good--unpack --skip-patches
+
+ # dgit might have added a .gitignore patch, which we need to
+ # drop and remove
+ t-splitbrain-rm-gitignore-patch
+
+ # Now the result should differ only in non-debian/ .gitignores
+ t-splitbrain-pushed-good--checkprep
+ git diff --cached --name-only $dep14tag >../changed
+ perl -ne '
+ next if !m#^debian/# && m#(^|/)\.gitignore#;
+ die "$_ mismatch";
+ ' <../changed
+
+ # If we actually apply the gitignore patch by hand, it
+ # should be perfect:
+ if [ -f ../t-auto-gitignore ]; then
+ patch --backup-if-mismatch -p1 -u <../t-auto-gitignore
+ fi
+
+ t-splitbrain-pushed-good-end-made-dep14
+}
+
+t-unapplied-pushed-good () {
+ t-splitbrain-pushed-good-start
+ t-splitbrain-pushed-good--unpack --skip-patches
+ t-splitbrain-pushed-good-end-made-dep14
+}
+
+t-dpm-pushed-good () {
+ t-splitbrain-pushed-good-start
+ t-splitbrain-pushed-good--unpack
+ t-splitbrain-rm-gitignore-patch
+ t-splitbrain-pushed-good-end-made-dep14
+}
+
+t-commit-build-push-expect-log () {
+ local msg=$1
+ local mpat=$2
+ t-commit "$msg"
+ t-dgit build
+ LC_MESSAGES=C \
+ t-dgit push --new 2>&1 |tee $tmp/push.log
+ t-grep-mpat "$mpat" $tmp/push.log
+}
+
t-822-field () {
local file=$1
local field=$2
@@ -228,18 +781,190 @@ t-822-field () {
' <$file
}
-t-ref-dsc-dgit () {
+t-defdistro () {
+ export DGIT_TEST_DISTRO=''
+ distro=''
+ t-git-config dgit-suite.unstable.distro test-dummy
+}
+
+t-stunt-envvar () {
+ local var=$1
+ local tstunt=$2
+ eval '
+ case "$'$var'" in
+ "$tstunt:"*) ;;
+ *":$tstunt:"*) ;;
+ "") '$var'="$tstunt" ;;
+ *) '$var'="$tstunt:$'$var'" ;;
+ esac
+ export '$var'
+ '
+}
+
+t-tstunt--save-real () {
+ local f="$1"
+ case "$f" in
+ */*) return ;;
+ esac
+
+ local rc
+ local real
+ set +e
+ real=$(
+ p=":$PATH:"
+ p="${p/:"$tmp/tstunt":/:}"
+ p="${p%:}"
+ p="${p#:}"
+ PATH="$p"
+ type -p "$f"
+ )
+ rc=$?
+ set -e
+
+ case $rc in
+ 1) return ;;
+ 0) ;;
+ *) fail "did not find $f on PATH $PATH" ;;
+ esac
+
+ local varname=${f//[^_0-9a-zA-Z]/_}
+ varname=DGIT_TEST_REAL_${varname^^}
+
+ eval "
+ : \${$varname:=\$real}
+ export $varname
+ "
+}
+
+t-tstunt () {
+ local tstunt=$tmp/tstunt
+ t-stunt-envvar PATH $tstunt
+ t-stunt-envvar PERLLIB $tstunt
+ local f
+ for f in "$@"; do
+ t-tstunt--save-real $f
+ f="./$f"
+ local d="$tstunt/${f%/*}"
+ mkdir -p $d
+ ln -sf "$troot/tstunt/$f" "$d"/.
+ done
+}
+
+t-tstunt-parsechangelog () {
+ t-tstunt dpkg-parsechangelog Dpkg/Changelog/Parse.pm
+}
+
+t-tstunt-lintian () {
+ t-tstunt lintian
+}
+
+t-tstunt-debuild () {
+ t-tstunt debuild
+}
+
+t-incoming-dsc () {
local dsc=${p}_${v}.dsc
- local val=`t-822-field $tmp/incoming/$dsc Dgit`
- perl -e '$_=shift @ARGV; die "$dsc Dgit $_ ?" unless m/^\w+\b/;' "$val"
- t-ref-same-val $dsc "$val"
+ incoming_dsc=$tmp/incoming/$dsc
+}
+
+t-ref-dsc-dgit () {
+ t-incoming-dsc
+ local val; val=`t-822-field $incoming_dsc Dgit`
+ val=$( perl -e '
+ $_=shift @ARGV;
+ die "Dgit $_ ?" unless m/^\w+\b/;
+ print $&,"\n" or die $!;
+ ' "$val")
+ t-ref-same-val $incoming_dsc "$val"
}
t-apply-diff () {
local v1=$1
local v2=$2
- (cd $troot/pkg-srcs; debdiff ${p}_${v1}.dsc ${p}_${v2}.dsc) \
- | patch -p1 -u
+ (cd $troot/pkg-srcs;
+ debdiff ${p}_${v1}.dsc ${p}_${v2}.dsc || test $? = 1) \
+ | patch -p1 -u
+}
+
+t-gbp-unapplied-pq2qc () {
+ # does `gbp pq export'
+ # commits the resulting debian/patches on qc/BRANCH
+ # leaves us on qc/BRANCH (eg "qc/quilt-tip"))
+ # qc/BRANCH is not fast-forwarding
+
+ gbp pq export
+
+ branch=`git symbolic-ref HEAD`
+ branch=${branch#refs/heads/}
+
+ case "$branch" in
+ */*) fail "unexpected branch $branch" ;;
+ esac
+
+ git branch -f qc/$branch
+ git checkout qc/$branch
+ git add debian/patches
+ git commit -m 'Commit patch queue'
+}
+
+t-git-pseudo-merge () {
+ # like git merge -s ours
+ if [ ! "$git_pseuomerge_opts" ]; then
+ if git merge --help \
+ | grep -q allow-unrelated-histories; then
+ git_pseuomerge_opts='--allow-unrelated-histories'
+ fi
+ git_pseuomerge_opts+=' -s ours'
+ fi
+ git merge $git_pseuomerge_opts "$@"
+}
+
+t-gbp-example-prep-no-ff () {
+ t-tstunt-parsechangelog
+ t-archive example 1.0-1
+ t-git-none
+ t-worktree 1.0
+
+ cd example
+
+ t-dgit fetch
+
+ git checkout -b patch-queue/quilt-tip-2 patch-queue/quilt-tip
+ gbp pq rebase
+
+ echo '/* some comment */' >>src.c
+ git add src.c
+ git commit -m 'Add a comment to an upstream file'
+
+ t-gbp-unapplied-pq2qc
+
+ t-commit 'some updates' 1.0-2
+}
+
+t-gbp-example-prep () {
+ t-gbp-example-prep-no-ff
+
+ t-git-pseudo-merge \
+ -m 'Pseudo-merge to make descendant of archive' \
+ remotes/dgit/dgit/sid
+}
+
+t-make-badcommit () {
+ badcommit=$(
+ git cat-file commit HEAD | \
+ perl -pe 's/^committer /commiter /' | \
+ git hash-object -w -t commit --stdin
+ )
+ t-expect-fsck-fail $badcommit
+}
+
+t-commit () {
+ local msg=$1
+ v=${2:-${majorv:-1}.$revision}
+ dch --force-distribution -v$v --distribution ${3:-unstable} "$1"
+ git add debian/changelog
+ debcommit
+ revision=$(( ${revision-0} + 1 ))
}
t-git-config () {
@@ -248,18 +973,126 @@ t-git-config () {
t-drs () {
export DGIT_TEST_TROOT=$troot
- export DGIT_TEST_TMP=$tmp
t-git-config dgit-distro.test-dummy.git-url "ext::$troot/drs-git-ext %S "
t-git-config dgit-distro.test-dummy.git-check true
t-git-config dgit-distro.test-dummy.git-create true
- cp $root/tests/gnupg/{dd.gpg,dm.gpg,dm.txt} $tmp/.
- cp $root/tests/suites $tmp/.
+ t-git-config dgit-distro.test-dummy.dgit-tag-format new,old,maint
+ cp $troot/gnupg/{dd.gpg,dm.gpg,dm.txt} $tmp/.
+ cp $troot/suites $tmp/.
+ cp $troot/suites $tmp/suites-master
+
+ export t_check_pushed_master=t-check-pushed-master
+
+ drs_dispatch=$tmp/distro=test-dummy
+ mkdir $drs_dispatch
+
+ if [ "x$DGIT_TEST_INTREE" != x ]; then
+ ln -sf "$DGIT_TEST_INTREE" $drs_dispatch/dgit-live
+ fi
+
+ ln -sf $tmp/git $drs_dispatch/repos
+ ln -sf $tmp/suites $tmp/suites-master $tmp/dm.txt $drs_dispatch/
+ mkdir -p $drs_dispatch/keyrings
+ ln -sf $tmp/dd.gpg $drs_dispatch/keyrings/debian-keyring.gpg
+ ln -sf $tmp/dm.gpg $drs_dispatch/keyrings/debian-maintainers.gpg
+ ln -sf /bin/true $drs_dispatch/policy-hook
+}
+
+t-newtag () {
+ export tagpfx=archive/test-dummy
+ t-git-config dgit-distro.test-dummy.dgit-tag-format new,maint
+}
+t-oldtag () {
+ export tagpfx=test-dummy
+ t-git-config dgit-distro.test-dummy.dgit-tag-format old
}
-t-drs-test () {
+t-dsd () {
t-drs
+ t-git-config dgit-distro.test-dummy.ssh "$troot/dsd-ssh"
+ t-git-config dgit-distro.test-dummy.git-check ssh-cmd
+ t-git-config dgit-distro.test-dummy.git-create true
+ t-git-config dgit-distro.test-dummy.git-url \
+ "ext::$troot/dsd-ssh X %S /dgit/test-dummy/repos"
+
+ t-git-config dgit-distro.test-dummy.diverts.drs /drs
+ t-git-config dgit-distro.test-dummy/drs.ssh "$troot/ssh"
+ t-git-config dgit-distro.test-dummy/drs.git-url $tmp/git
+ t-git-config dgit-distro.test-dummy/drs.git-check ssh-cmd
+ t-git-config dgit-distro.test-dummy/drs.git-create ssh-cmd
+
+ echo 'no-such-package* drs' >$drs_dispatch/diverts
+}
+
+t-policy-admin () {
+ : '(((((((((((((((((((((((((((((((((((((((('
+ ${DGIT_INFRA_PFX}dgit-repos-admin-debian --repos $tmp/git "$@"
+ : '))))))))))))))))))))))))))))))))))))))))'
+}
+
+t-policy-nonexist () {
+ ln -sf no-such-file-or-directory $drs_dispatch/policy-hook
+}
+
+t-make-hook-link () {
+ local hook=$1 # in infra/
+ local linkpath=$2
+ hook=${DGIT_INFRA_PFX}$hook
+ case $hook in
+ */*) ;;
+ *) hook=`type -P $hook` ;;
+ esac
+ ln -sf "$hook" $linkpath
+}
+
+t-policy () {
+ local policyhook=$1
+ t-make-hook-link $policyhook $drs_dispatch/policy-hook
+}
+
+t-debpolicy () {
+ t-dsd
+ t-policy dgit-repos-policy-debian
+
+ mkdir $tmp/git
+ t-policy-admin create-db
+}
+
+t-policy-periodic () {
+ : '(((((((((((((((((((((((((((((((((((((((('
+ ${DGIT_REPOS_SERVER_TEST-dgit-repos-server} \
+ test-dummy $drs_dispatch '' --cron
+ : '))))))))))))))))))))))))))))))))))))))))'
+}
+
+t-restrict () {
+ local restriction=$1
+ (cd $root; t-restriction-$restriction >&2)
+}
+
+t-dependencies () {
+ : "Hopefully installed: $*"
+}
+
+t-chain-test () {
+ local ct=$1
+ local d=${0%/*}
cd $root
export DGIT_TEST_TESTNAME="$testname"
+ export DGIT_TEST_TMPBASE="$tmpbase"
export ADTTMP=$tmp
- exec "${0///drs-//}" "$@"
+ exec "$d/$ct"
+}
+
+t-alt-test () {
+ local t=${0##*/}
+ t-${t%%-*}
+ t-chain-test "${t#*-}"
}
+
+t-git-config dgit.default.old-dsc-distro test-dummy
+
+case "$0" in
+*/gnupg) ;;
+*) t-setup-import gnupg ;;
+esac
diff --git a/tests/lib-build-modes b/tests/lib-build-modes
new file mode 100644
index 0000000..ee2975d
--- /dev/null
+++ b/tests/lib-build-modes
@@ -0,0 +1,235 @@
+
+bm-prep-ownpackage-branches () {
+ cat <<'END' >$tmp/stunt-git
+#!/bin/sh -e
+case "$*" in
+*clean*) echo >&2 "BUILD-MODES PROGRAM git $*" ;;
+esac
+exec git "$@"
+END
+ chmod +x $tmp/stunt-git
+
+ bm_branches="$1"
+}
+
+bm-prep () {
+ t-tstunt-parsechangelog
+
+ t-prep-newpackage example 1.0
+
+ cd $p
+
+ git checkout -b bad-build-deps indep-arch
+ perl -pe 's/Build-Depends.*/$&, x-dgit-no-such-package/' \
+ -i debian/control
+ git commit -a -m bad-build-deps
+
+ bm-prep-ownpackage-branches 'indep-arch bad-build-deps'
+
+ if zgrep 'dpkg-buildpackage: Make dependency checks fatal for -S' \
+ /usr/share/doc/dpkg-dev/changelog.gz; then
+ dpkgbuildpackage_deps_for_clean=true
+ else
+ dpkgbuildpackage_deps_for_clean=false
+ fi
+
+ cleanmodes_default="git none dpkg-source dpkg-source-d"
+ cleanmodes_all="$cleanmodes_default git-ff check"
+ cleanmodes="$cleanmodes_default"
+}
+
+bm-gbp-example-acts () {
+ t-gbp-example-prep
+
+ git checkout -b for-build-modes qc/quilt-tip-2
+ # build-modes cannot cope with branches containing /
+
+ bm-prep-ownpackage-branches for-build-modes
+
+ cleanmodes='git dpkg-source'
+
+ for act in "$@"; do
+ bm-guess-e-source-e-targets "$act"
+ real_act="--quilt=gbp $act"
+ case "$act" in
+ sbuild*) bm_quirk_after_act=bm-quirk-sbuild-after-act ;;
+ gbp-*) real_act="$real_act --git-ignore-branch" ;;
+ *) bm_quirk_after_act='' ;;
+ esac
+ bm-act-iterate
+ done
+}
+
+bm-guess-e-source-e-targets () {
+ local some_act=$1
+ case "$some_act" in
+ sbuild*" --no-arch-all"*)
+ e_source=true; e_targets='build-arch binary-arch' ;;
+ build-source)
+ e_source=true; e_targets='' ;;
+ *" -b") e_source=false; e_targets='build binary' ;;
+ *" -B") e_source=false; e_targets='build-arch binary-arch' ;;
+ *" -A") e_source=false; e_targets='build-indep binary-indep' ;;
+ *" -S") e_source=true; e_targets=' ' ;;
+ *" -F") e_source=true; e_targets='build binary' ;;
+ *" -G") e_source=true; e_targets='build-arch binary-arch' ;;
+ *" -g") e_source=true; e_targets='build-indep binary-indep' ;;
+ *) e_source=true; e_targets='build binary' ;;
+ esac
+}
+
+bm-quirk-sbuild-after-act () {
+ # sbuild likes to run the package clean target in the chroot,
+ # which isn't necessary in our case. We don't disable it in
+ # dgit because we want to do what sbuild does, in case there
+ # are packages which don't build unless their clean target was
+ # run. We know it must be running it in the chroot because we
+ # provide sbuild with the dsc, not the tree, so we simply
+ # ignore all executions of the clean target by schroot.
+ local arch; arch=$(dpkg-architecture -qDEB_BUILD_ARCH)
+ local sblog=../example_${v}_$arch.build
+ if [ -e $sblog ]; then
+ sed '
+ s/^EXAMPLE RULES TARGET clean/HOOK SUPPRESSED &/;
+ ' <$sblog >>$bmlog
+ else
+ echo "SBUILD LOG FILE ($sblog) MISSING"
+ fi
+}
+
+bm-report-test () {
+ local desc=$1; shift
+ if "$@"; then
+ echo >&4 "$desc EXISTS"
+ else
+ echo >&4 "$desc MISSING"
+ fi
+}
+
+bm-build-deps-ok () {
+ case "$branch" in
+ *bad-build-deps*) return 1 ;;
+ *) return 0 ;;
+ esac
+}
+
+bm-compute-expected () {
+ require_fail=unexpect # or required
+ tolerate_fail=unexpect # or tolerate
+
+ exec 4>$bmexp
+ echo >&4 "$heading"
+
+ case $cleanmode in
+ git) echo >&4 'BUILD-MODES PROGRAM git clean -xdf' ;;
+ git-ff) echo >&4 'BUILD-MODES PROGRAM git clean -xdff' ;;
+ check) echo >&4 'BUILD-MODES PROGRAM git clean -xdn' ;;
+ dpkg-source-d) echo >&4 "EXAMPLE RULES TARGET clean" ;;
+ dpkg-source) bm-build-deps-ok || tolerate_fail=tolerate
+ echo >&4 "EXAMPLE RULES TARGET clean"
+ ;;
+ none) ;;
+ *) fail "t-compute-expected-run $cleanmode ??" ;;
+ esac
+
+ if [ "x$e_targets" != x ]; then
+ # e_targets can be " " to mean `/may/ fail due to b-d'
+ bm-build-deps-ok || tolerate_fail=tolerate
+ fi
+
+ for t in $e_targets; do
+ bm-build-deps-ok || require_fail=required
+ echo >&4 "EXAMPLE RULES TARGET $t"
+ done
+
+ bm-report-test "SOURCE FILE" $e_source
+ bm-report-test "SOURCE IN CHANGES" $e_source
+ bm-report-test "DEBS IN CHANGES" expr "$e_targets" : '.*binary.*'
+
+ exec 4>&-
+}
+
+bm-run-one () {
+ local args="$DGIT_TEST_BM_BASEARGS --clean=$cleanmode $real_act"
+
+ bmid="$act,$cleanmode,$branch"
+ bmid=${bmid// /_}
+
+ rm -f ../${p}_{v}_*.changes
+
+ heading="===== [$bmid] dgit $args ====="
+
+ bmlog=$tmp/run.$bmid.output
+ bmexp=$tmp/run.$bmid.expected
+ bmgot=$tmp/run.$bmid.results
+
+ bm-compute-expected
+
+ git checkout $branch
+ git clean -xdf # since we might not do any actual cleaning
+
+ dsc="../example_$v.dsc"
+ rm -f $dsc
+
+ set +o pipefail
+ t-dgit --rm-old-changes --git=$tmp/stunt-git $args 2>&1 | tee $bmlog
+ local ps="${PIPESTATUS[*]}"
+ set -o pipefail
+
+ $bm_quirk_after_act
+
+ exec 4>$bmgot
+ echo >&4 "$heading"
+
+ case $ps in
+ "0 0") actual_status=success ;;
+ *" 0") actual_status=failure; echo >&4 "OPERATION FAILED"; ;;
+ *) fail "tee failed" ;;
+ esac
+
+ case "$require_fail-$tolerate_fail-$actual_status" in
+ required-********-failure) echo >>$bmexp "REQUIRED FAILURE" ;;
+ ********-tolerate-failure) echo >>$bmexp "TOLERATED FAILURE" ;;
+ unexpect-********-success) ;;
+ *) fail "RF=$require_fail TF=$tolerate_fail AS=$actual_status" ;;
+ esac
+
+ egrep >&4 '^EXAMPLE RULES TARGET|^BUILD-MODES' $bmlog || [ $? = 1 ]
+
+ bm-report-test "SOURCE FILE" [ -e $dsc ]
+
+ if [ $actual_status = success ]; then
+ local changes; changes=$(echo ../example_${v}_*.changes)
+ case "$changes" in
+ *' '*) fail "build generated ambiguous .changes: $changes" ;;
+ esac
+
+ perl -ne 'print if m/^files:/i ... m/^\S/' \
+ <$changes >$changes.files
+
+ bm-report-test "SOURCE IN CHANGES" grep '\.dsc$' $changes.files
+ bm-report-test "DEBS IN CHANGES" grep '\.deb$' $changes.files
+ fi
+
+ exec 4>&-
+
+ $bm_quirk_before_diff
+
+ [ $actual_status = failure ] || diff -U10 $bmexp $bmgot
+}
+
+bm-act-iterate () {
+ for cleanmode in $cleanmodes; do
+ for branch in $bm_branches; do
+ bm-run-one
+ done
+ done
+ : bm-act-iterate done.
+}
+
+bm-alwayssplit () {
+ local t=${0##*/}
+ DGIT_TEST_BM_BASEARGS+=' --always-split-source-build'
+ export DGIT_TEST_BM_BASEARGS
+ t-chain-test "${t%%-asplit}"
+}
diff --git a/tests/lib-core b/tests/lib-core
new file mode 100644
index 0000000..6cdffeb
--- /dev/null
+++ b/tests/lib-core
@@ -0,0 +1,38 @@
+#
+
+fail () {
+ echo >&2 "failed: $*"
+ exit 1
+}
+
+t-set-intree () {
+ if [ "x$DGIT_TEST_INTREE" = x ]; then return; fi
+ : ${DGIT_TEST:=$DGIT_TEST_INTREE/dgit}
+ : ${DGIT_BADCOMMIT_FIXUP:=$DGIT_TEST_INTREE/dgit-badcommit-fixup}
+ : ${DGIT_REPOS_SERVER_TEST:=$DGIT_TEST_INTREE/infra/dgit-repos-server}
+ : ${DGIT_SSH_DISPATCH_TEST:=$DGIT_TEST_INTREE/infra/dgit-ssh-dispatch}
+ : ${DGIT_INFRA_PFX:=$DGIT_TEST_INTREE${DGIT_TEST_INTREE:+/infra/}}
+ export DGIT_TEST DGIT_BADCOMMIT_FIXUP
+ export DGIT_REPOS_SERVER_TEST DGIT_SSH_DISPATCH_TEST
+ export PERLLIB="$DGIT_TEST_INTREE${PERLLIB:+:}${PERLLIB}"
+}
+
+t-set-using-tmp () {
+ export HOME=$tmp
+ export DGIT_TEST_DUMMY_DIR=$tmp
+ export DGIT_TEST_TMP=$tmp
+ export GNUPGHOME=$tmp/nonexistent
+ git config --global user.email 'dgit-test@debian.example.net'
+ git config --global user.name 'dgit test git user'
+}
+
+t-filter-out-git-hyphen-dir () {
+ local pathent; pathent=$(type -p git-rev-parse ||:)
+ case "$pathent" in '') return ;; esac
+ pathent=${pathent%/*}
+ local path=":$PATH:"
+ path="${path//:$pathent:/}"
+ path="${path#:}"
+ path="${path%:}"
+ PATH="$path"
+}
diff --git a/tests/lib-import-chk b/tests/lib-import-chk
new file mode 100644
index 0000000..88984c1
--- /dev/null
+++ b/tests/lib-import-chk
@@ -0,0 +1,97 @@
+
+t-import-chk-authorship () {
+ perl -ne 'print $1,"\n" if m/^ -- (\S.*\>) /' debian/changelog \
+ | sort -u \
+ > $tmp/authorship.changelog
+ ${import_chk_changelog_massage:-:} $tmp/authorship.changelog
+ git log --pretty=format:'%an <%ae>%n%cn <%ce>' \
+ | sort -u \
+ > $tmp/authorship.commits
+ diff $tmp/authorship.{changelog,commits}
+}
+
+t-import-chk1 () {
+ p=$1
+ v=$2
+
+ t-archive $p $v
+}
+t-import-chk2() {
+ t-git-none
+ rm -rf $p
+ t-dgit --no-rm-on-error clone $p
+
+ # And now we make an update using the same orig tarball, and
+ # check that the orig import is stable.
+
+ cd $p
+
+ t-import-chk-authorship
+
+ git branch first-import
+
+ m='Commit for import check'
+ echo "$m" >>import-check
+
+ v=${v%-*}-99
+ dch -v $v -D unstable -m "$m"
+
+ git add import-check debian/changelog
+ git commit -m "$m"
+
+ t-dgit -wgf quilt-fixup
+ t-dgit -wgf build-source
+
+ # The resulting .dsc does not have a Dgit line (because dgit push
+ # puts that in). So we just shove it in the archive now
+
+ ln ../${p}_${v}.* $tmp/mirror/pool/main/
+ t-archive-query
+
+ t-dgit fetch
+
+ git branch first-2nd-import remotes/dgit/dgit/sid
+
+ t-git-next-date
+
+ git update-ref refs/remotes/dgit/dgit/sid first-import
+
+ t-dgit fetch
+
+ t-refs-same-start
+ t-ref-same refs/remotes/dgit/dgit/sid
+ t-ref-same refs/heads/first-2nd-import
+
+ for orig in ../${p}_${v%-*}.orig*.tar.*; do
+ tar -atf $orig | LC_ALL=C sort >../files.o
+ pfx=$(perl <../files.o -ne '
+ while (<>) {
+ m#^([^/]+/)# or exit 0;
+ $x //= $1;
+ $x eq $1 or exit 0;
+ }
+ print "$x\n";
+ ')
+ perl -i~ -pe '
+ s#^\Q'"$pfx"'\E##;
+ $_="" if m/^$/ || m#/$# || m#^\.git/#;
+ ' ../files.o
+ orig=${orig#../}
+ pat="^Import ${orig//./\\.}\$"
+ t-refs-same-start
+ for start in first-import first-2nd-import; do
+ git log --pretty='tformat:%H' --grep "$pat" $start \
+ >../t.imp
+ test $(wc -l <../t.imp) = 1
+ imp=$(cat ../t.imp)
+ t-ref-same-val "$orig $start" "$imp"
+ done
+ git ls-tree -r --name-only "$t_ref_val:" \
+ | sort >../files.g
+ diff ../files.{o,g}
+ done
+ cd ..
+}
+
+t-import-chk() { t-import-chk1 "$@"; t-import-chk2; }
+
diff --git a/tests/lib-mirror b/tests/lib-mirror
new file mode 100644
index 0000000..25f0f90
--- /dev/null
+++ b/tests/lib-mirror
@@ -0,0 +1,42 @@
+
+t-mirror-setup () {
+ # p must be set already
+
+ reposmirror=$tmp/git-mirror
+ pmirror=$reposmirror/$p.git
+ queuedir=$tmp/git/_mirror-queue
+
+ mkdir $reposmirror
+
+ mirror_hook=$drs_dispatch/mirror-hook
+ t-make-hook-link dgit-mirror-rsync $mirror_hook
+
+ >$drs_dispatch/mirror-settings
+ t-mirror-set remoterepos="$reposmirror"
+
+ t-mirror-hook setup
+}
+
+t-mirror-set () {
+ echo >>$drs_dispatch/mirror-settings "$1"
+}
+
+t-mirror-hook () {
+ : '(((((((((((((((((((((((((((((((((((((((('
+ "$mirror_hook" "$drs_dispatch" "$@"
+ : '))))))))))))))))))))))))))))))))))))))))'
+}
+
+t-check-mirrored () {
+ t-reporefs master
+ t-reporefs mirror $pmirror
+ diff $tmp/show-refs.{master,mirror}
+ cat $queuedir/$p.log ||:
+ t-files-notexist $queuedir/$p.{n,a,lock,err}
+}
+
+t-check-not-mirrored () {
+ # uses previous t-reporefs-master
+ t-reporefs mirror $pmirror
+ diff $tmp/show-refs.{master,mirror}
+}
diff --git a/tests/lib-orig-include-exclude b/tests/lib-orig-include-exclude
new file mode 100644
index 0000000..104cf0b
--- /dev/null
+++ b/tests/lib-orig-include-exclude
@@ -0,0 +1,67 @@
+# designed to be .'d
+
+t-tstunt-parsechangelog
+
+t-archive example 1.0-1
+t-git-none
+
+t-dgit clone $p
+
+origs='orig orig-docs'
+usvsns='1.0 1.1'
+
+for o in $origs; do
+ cp ${p}_{1.0,1.1}.${o}.tar.gz
+done
+
+mkdir -p "$tmp/aq/file_in_archive/%"
+
+cd $p
+
+test-push-1 () {
+ v=$1
+ ch=$2
+ suite=$3
+
+ t-commit $v $v $suite
+ t-dgit $ch build
+}
+
+test-push-2 () {
+ $test_push_2_hook
+ t-dgit $ch "$@" push
+}
+
+test-push-1 1.0-2 --ch:-sa
+
+grep orig ../${p}_${v}_*.changes
+
+test-push-2
+
+origs_findls () {
+ find $tmp/mirror -name '*orig*' -ls \
+ | perl -pe 's/^(\s*\d+\s+\d+\s+\S+\s)\s*\d+(\s)/$1$2/'
+}
+
+# check that dgit stripped out the orig update
+origs_findls >../before
+
+t-archive-process-incoming sid
+
+origs_findls >../after
+diff -u ../before ../after
+
+test-push-1 1.1-1.2 --ch:-sd
+
+test-push-2
+
+t-archive-process-incoming sid
+
+cd ..
+mkdir get
+cd get
+
+t-dgit clone $p
+# ^ checks that all the origs are there, ie that dgit added the origs
+
+cd ..
diff --git a/tests/lib-reprepro b/tests/lib-reprepro
new file mode 100644
index 0000000..0a688f6
--- /dev/null
+++ b/tests/lib-reprepro
@@ -0,0 +1,97 @@
+# -*- bash -*-
+
+t-reprepro () {
+
+ t_archive_method=reprepro
+
+ t-reprepro-cfg
+}
+
+t-reprepro-cfg () {
+ local rrinst=$1
+ local rrdistro=${2:-test-dummy}
+
+ local etcapt=$tmp/${rrinst}etc-apt
+ local mir=$tmp/${rrinst}mirror
+
+ t-git-config dgit-distro.$rrdistro.archive-query aptget:
+ t-git-config dgit-distro.$rrdistro.mirror file://$mir/
+
+ mkdir $etcapt
+ cat >$etcapt/conf <<END
+Dir::Etc "$etcapt";
+END
+ export APT_CONFIG=$etcapt/conf
+ gpg --export Hannibal >han.pgp
+ fakeroot apt-key add <han.pgp
+ mkdir $etcapt/apt.conf.d
+}
+
+t-archive-none-reprepro () { # hook called by t-archive-none
+ t-reprepro-setup
+ t-reprepro-regen
+ local rrinst=
+}
+t-archive-query-reprepro () { # hook called by t-archive-query
+ local suite=$1
+ local p=$2
+ local v=$3
+ local dscf=$4
+ local rrinst=
+ t-reprepro-includedsc $suite $tmp/mirror/pool/$dscf "$rrinst"
+}
+
+t-reprepro-setup () {
+ local rrinst=$1
+
+ local mir=$tmp/${rrinst}mirror
+ local rrc=$mir/conf
+ mkdir -p $rrc
+ mkdir -p $mir/pool/main
+
+ exec 3>$rrc/distributions
+
+ local arch; arch=`dpkg --print-architecture`
+
+ for suitespec in $suitespecs; do
+ local suite=${suitespec%%:*}
+ local sname=${suitespec#*:}
+
+ mkdir -p $mir/dists
+ if [ $sname != $suite ]; then
+ rm -f $mir/dists/$sname
+ ln -s $suite $mir/dists/$sname
+ fi
+
+ cat >&3 <<END
+Suite: $sname
+Codename: $suite
+Components: main
+Architectures: source binary-$arch
+SignWith: Hannibal
+
+END
+ done
+}
+
+t-reprepro-includedsc () {
+ local suite=$1
+ local dscf=$2
+ local rrinst=$3
+ t-reprepro--run includedsc $suite $dscf
+}
+
+t-reprepro--run () {
+ # caller is supposed to have set rrinst
+ local mir=$tmp/${rrinst}mirror
+ reprepro \
+ --outdir $mir \
+ --basedir $mir \
+ "$@"
+}
+
+t-reprepro-regen () {
+ local rrinst=$1
+
+ t-reprepro--run export
+}
diff --git a/tests/lib-restricts b/tests/lib-restricts
new file mode 100644
index 0000000..bffe13a
--- /dev/null
+++ b/tests/lib-restricts
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+t-restriction-x-dgit-intree-only () {
+ if [ "x$DGIT_TEST_INTREE" != x ]; then return 0; fi
+ echo 'running installed package version'
+ return 1
+}
+
+t-restriction-x-dgit-git-only () {
+ if test -d .git; then return 0; fi
+ echo 'not running out of git clone'
+ return 1
+}
+
+t-restriction-x-dgit-schroot-build () {
+ schroot -l -c build 2>&1 >/dev/null || return 1
+}
+
+t-restriction-x-dgit-unfinished () {
+ echo 'unfinished test, or unfinished feature'
+ return 1
+}
diff --git a/tests/pkg-srcs/example_1.0-1+absurd.debian.tar.xz b/tests/pkg-srcs/example_1.0-1+absurd.debian.tar.xz
new file mode 100644
index 0000000..9a2dd12
--- /dev/null
+++ b/tests/pkg-srcs/example_1.0-1+absurd.debian.tar.xz
Binary files differ
diff --git a/tests/pkg-srcs/example_1.0-1+absurd.dsc b/tests/pkg-srcs/example_1.0-1+absurd.dsc
new file mode 100644
index 0000000..1ab743d
--- /dev/null
+++ b/tests/pkg-srcs/example_1.0-1+absurd.dsc
@@ -0,0 +1,22 @@
+Format: 3.0 (quilt)
+Source: example
+Binary: example
+Architecture: all
+Version: 1.0-1+absurd
+Maintainer: Ian Jackson <ijackson@chiark.greenend.org.uk>
+Standards-Version: 3.9.4.0
+Build-Depends: debhelper (>= 8)
+Package-List:
+ example deb devel extra arch=all
+Checksums-Sha1:
+ 2bc730f941db49de57e9678fb0b07bd95507bb44 236 example_1.0.orig-docs.tar.gz
+ 4bff9170ce9b10cb59937195c5ae2c73719fe150 373 example_1.0.orig.tar.gz
+ dafb6f0db0580179ff246dba1dc2892246e84a2c 1416 example_1.0-1+absurd.debian.tar.xz
+Checksums-Sha256:
+ ad9671f6b25cdd9f0573f803f702448a45a45183db1d79701aa760bccbeed29c 236 example_1.0.orig-docs.tar.gz
+ a3ef7c951152f3ec754f96fd483457aa88ba06df3084e6f1cc7c25b669567c17 373 example_1.0.orig.tar.gz
+ 4003c34398894e46823bb3fda69f4351dbd5649e321259cde266a135f0428c51 1416 example_1.0-1+absurd.debian.tar.xz
+Files:
+ cb0cb5487b1e5bcb82547396b4fe93e5 236 example_1.0.orig-docs.tar.gz
+ 599f47808a7754c66aea3cda1b3208d6 373 example_1.0.orig.tar.gz
+ 0e88c1ed094f09ee7bf57607132d55ee 1416 example_1.0-1+absurd.debian.tar.xz
diff --git a/tests/pkg-srcs/example_1.0-1.100.debian.tar.xz b/tests/pkg-srcs/example_1.0-1.100.debian.tar.xz
new file mode 100644
index 0000000..ea8ec34
--- /dev/null
+++ b/tests/pkg-srcs/example_1.0-1.100.debian.tar.xz
Binary files differ
diff --git a/tests/pkg-srcs/example_1.0-1.100.dsc b/tests/pkg-srcs/example_1.0-1.100.dsc
new file mode 100644
index 0000000..5b075b5
--- /dev/null
+++ b/tests/pkg-srcs/example_1.0-1.100.dsc
@@ -0,0 +1,22 @@
+Format: 3.0 (quilt)
+Source: example
+Binary: example
+Architecture: all
+Version: 1.0-1.100
+Maintainer: Ian Jackson <ijackson@chiark.greenend.org.uk>
+Standards-Version: 3.9.4.0
+Build-Depends: debhelper (>= 8)
+Package-List:
+ example deb devel extra arch=all
+Checksums-Sha1:
+ 2bc730f941db49de57e9678fb0b07bd95507bb44 236 example_1.0.orig-docs.tar.gz
+ 4bff9170ce9b10cb59937195c5ae2c73719fe150 373 example_1.0.orig.tar.gz
+ 86c31eba5e08c1765f8e557b97e59d7e1fd9c208 2108 example_1.0-1.100.debian.tar.xz
+Checksums-Sha256:
+ ad9671f6b25cdd9f0573f803f702448a45a45183db1d79701aa760bccbeed29c 236 example_1.0.orig-docs.tar.gz
+ a3ef7c951152f3ec754f96fd483457aa88ba06df3084e6f1cc7c25b669567c17 373 example_1.0.orig.tar.gz
+ 163f1a753f0ea382148df8d9553240d503781badf03c600946f1400534da1349 2108 example_1.0-1.100.debian.tar.xz
+Files:
+ cb0cb5487b1e5bcb82547396b4fe93e5 236 example_1.0.orig-docs.tar.gz
+ 599f47808a7754c66aea3cda1b3208d6 373 example_1.0.orig.tar.gz
+ 4b7f5d286eff2608107c77c96584a01a 2108 example_1.0-1.100.debian.tar.xz
diff --git a/tests/pkg-srcs/example_1.0-1.debian.tar.xz b/tests/pkg-srcs/example_1.0-1.debian.tar.xz
new file mode 100644
index 0000000..84ca563
--- /dev/null
+++ b/tests/pkg-srcs/example_1.0-1.debian.tar.xz
Binary files differ
diff --git a/tests/pkg-srcs/example_1.0-1.dsc b/tests/pkg-srcs/example_1.0-1.dsc
new file mode 100644
index 0000000..bb65f6e
--- /dev/null
+++ b/tests/pkg-srcs/example_1.0-1.dsc
@@ -0,0 +1,22 @@
+Format: 3.0 (quilt)
+Source: example
+Binary: example
+Architecture: all
+Version: 1.0-1
+Maintainer: Ian Jackson <ijackson@chiark.greenend.org.uk>
+Standards-Version: 3.9.4.0
+Build-Depends: debhelper (>= 8)
+Package-List:
+ example deb devel extra arch=all
+Checksums-Sha1:
+ 2bc730f941db49de57e9678fb0b07bd95507bb44 236 example_1.0.orig-docs.tar.gz
+ 4bff9170ce9b10cb59937195c5ae2c73719fe150 373 example_1.0.orig.tar.gz
+ f2398be1e588e10d11b20ee9bc5ca0eb16e4c158 1304 example_1.0-1.debian.tar.xz
+Checksums-Sha256:
+ ad9671f6b25cdd9f0573f803f702448a45a45183db1d79701aa760bccbeed29c 236 example_1.0.orig-docs.tar.gz
+ a3ef7c951152f3ec754f96fd483457aa88ba06df3084e6f1cc7c25b669567c17 373 example_1.0.orig.tar.gz
+ fd97c0fb879bfa8084f24a0d0f808a56beb533f17d92c808dc293ff297007925 1304 example_1.0-1.debian.tar.xz
+Files:
+ cb0cb5487b1e5bcb82547396b4fe93e5 236 example_1.0.orig-docs.tar.gz
+ 599f47808a7754c66aea3cda1b3208d6 373 example_1.0.orig.tar.gz
+ fd7840d249ee3dba5bdc3dcde7217bbe 1304 example_1.0-1.debian.tar.xz
diff --git a/tests/pkg-srcs/example_1.0.orig-docs.tar.gz b/tests/pkg-srcs/example_1.0.orig-docs.tar.gz
new file mode 100644
index 0000000..f8427aa
--- /dev/null
+++ b/tests/pkg-srcs/example_1.0.orig-docs.tar.gz
Binary files differ
diff --git a/tests/pkg-srcs/example_1.0.orig.tar.gz b/tests/pkg-srcs/example_1.0.orig.tar.gz
new file mode 100644
index 0000000..e5d2cf3
--- /dev/null
+++ b/tests/pkg-srcs/example_1.0.orig.tar.gz
Binary files differ
diff --git a/tests/pkg-srcs/sunxi-tools_1.2-2.~~dgittest.debian.tar.gz b/tests/pkg-srcs/sunxi-tools_1.2-2.~~dgittest.debian.tar.gz
new file mode 100644
index 0000000..c376961
--- /dev/null
+++ b/tests/pkg-srcs/sunxi-tools_1.2-2.~~dgittest.debian.tar.gz
Binary files differ
diff --git a/tests/pkg-srcs/sunxi-tools_1.2-2.~~dgittest.dsc b/tests/pkg-srcs/sunxi-tools_1.2-2.~~dgittest.dsc
new file mode 100644
index 0000000..e0161cd
--- /dev/null
+++ b/tests/pkg-srcs/sunxi-tools_1.2-2.~~dgittest.dsc
@@ -0,0 +1,22 @@
+Format: 3.0 (quilt)
+Source: sunxi-tools
+Binary: sunxi-tools
+Architecture: any
+Version: 1.2-2.~~dgittest
+Maintainer: Ian Campbell <ijc@hellion.org.uk>
+Homepage: http://linux-sunxi.org/Sunxi-tools
+Standards-Version: 3.9.5
+Vcs-Browser: http://git.debian.org/?p=collab-maint/sunxi-tools.git
+Vcs-Git: git://git.debian.org/collab-maint/sunxi-tools.git
+Build-Depends: debhelper (>= 9), pkg-config, libusb-1.0-0-dev, u-boot-tools
+Package-List:
+ sunxi-tools deb utils optional
+Checksums-Sha1:
+ 2457216dbbf5552c413753f7211f7be3db6aff54 35076 sunxi-tools_1.2.orig.tar.gz
+ 6f30698cd897b350a4f92b2b5dded69adca6f82e 5182 sunxi-tools_1.2-2.~~dgittest.debian.tar.gz
+Checksums-Sha256:
+ 03a63203ff79389e728d88ad705e546aa6362a6d08b9901392acb8639998ef95 35076 sunxi-tools_1.2.orig.tar.gz
+ 0a513f3254d245b59aaffbeb5c43159a6461617c1f6f3c6824646c4259cda406 5182 sunxi-tools_1.2-2.~~dgittest.debian.tar.gz
+Files:
+ dbc55f60559f9db497559176c3c753dd 35076 sunxi-tools_1.2.orig.tar.gz
+ a6ec0eb0d897b0121dc978fc00db2ea6 5182 sunxi-tools_1.2-2.~~dgittest.debian.tar.gz
diff --git a/tests/pkg-srcs/sunxi-tools_1.2.orig.tar.gz b/tests/pkg-srcs/sunxi-tools_1.2.orig.tar.gz
new file mode 100644
index 0000000..fe397fa
--- /dev/null
+++ b/tests/pkg-srcs/sunxi-tools_1.2.orig.tar.gz
Binary files differ
diff --git a/tests/run-all b/tests/run-all
index e2e2a6f..3877c76 100755
--- a/tests/run-all
+++ b/tests/run-all
@@ -1,13 +1,21 @@
-#!/bin/sh
+#!/bin/bash
set -e
# convenience script for running the tests outside adt-run
-if [ $# = 0 ]; then
- set `run-parts --list tests/tests`
+# usage: tests/using-intree tests/run-all
+
+set -o pipefail
+
+set +e
+jcpus=`perl -MSys::CPU -we 'printf "-j%d\n", 1.34 * Sys::CPU::cpu_count()'`
+set -e
+
+if [ $# != 0 ]; then
+ set TESTSCRIPTS="$*"
fi
-for f in $@; do
- echo ==================================================
- echo $f
- $f
- echo ==================================================
-done
-echo "ALL PASSED"
+
+mkdir -p tests/tmp
+
+(
+ set -x
+ exec make $jcpus -k -f tests/Makefile "$@"
+) 2>&1 |tee tests/tmp/run-all.log
diff --git a/tests/setup/examplegit b/tests/setup/examplegit
new file mode 100755
index 0000000..112e27a
--- /dev/null
+++ b/tests/setup/examplegit
@@ -0,0 +1,53 @@
+#!/bin/bash
+set -e
+. tests/lib
+
+suitespecs+=' stable testing'
+
+t-tstunt-parsechangelog
+
+t-prep-newpackage example 1.0
+
+cd $p
+
+revision=1
+
+push-to () {
+ t-refs-same-start
+ t-ref-head
+ t-dgit build
+ t-dgit push --new $2
+ t-pushed-good $1 $2
+ t-archive-process-incoming $2
+}
+
+echo ancestor >which
+git add which
+t-commit Ancestor '' stable
+push-to master stable
+
+git checkout -b stable
+
+echo stable >which
+git add which
+t-commit Stable '' stable
+push-to stable stable
+
+git checkout master
+
+majorv=2
+revision=0
+
+echo sid >which
+git add which
+t-commit Sid
+push-to master sid
+
+echo sid-again >>which
+git add which
+t-commit Sid
+push-to master sid
+
+t-setup-done 'p v suitespecs majorv revision' "aq git mirror $p"
+
+t-ok
diff --git a/tests/setup/gnupg b/tests/setup/gnupg
new file mode 100755
index 0000000..43a5c96
--- /dev/null
+++ b/tests/setup/gnupg
@@ -0,0 +1,30 @@
+#!/bin/bash
+set -e
+. tests/lib
+
+mkdir -p $tmp/gnupg
+cp $troot/gnupg/* $tmp/gnupg
+chmod go-rw $tmp/gnupg/*
+
+export GNUPGHOME=$tmp/gnupg
+
+cat >$tmp/gnupg/gpg-agent.conf <<END
+log-file $tmp/gnupg/AGENT.log
+END
+#debug-all
+
+setup='
+ : ${DGIT_TEST_REAL_GPG_AGENT:=$(type -p gpg-agent)}
+ export DGIT_TEST_REAL_GPG_AGENT=$(type -p gpg-agent)
+ export DGIT_STUNT_AGENT=$troot/tstunt/gpg-agent
+ export GNUPGHOME
+ t-tstunt gpg
+'
+
+eval "$setup"
+
+gpg --list-secret
+
+t-setup-done 'GNUPGHOME' 'gnupg' "$setup"
+
+t-ok
diff --git a/tests/tartree-edit b/tests/tartree-edit
index 2e0c017..40bd6e9 100755
--- a/tests/tartree-edit
+++ b/tests/tartree-edit
@@ -2,10 +2,74 @@
set -e
fail () { echo >&2 "$0: $*"; exit 1; }
+play=.git/tartree-edit-work
+
+git_manip_play () {
+ local wd; wd=$(pwd)
+ case "$wd" in
+ *.edit) fail "bad idea to run gitfetchinfo into a .edit tree!" ;;
+ esac
+ rm -rf $play
+ mkdir $play
+}
+
+gitfetchdiff_list () {
+ git for-each-ref --format '%(refname) %(objectname)' \
+ refs/remotes/"$1" \
+ | sed 's/^refs\/remotes\/[^\/]*\///' \
+ | sort >"$play/$2"
+}
+
+gitfetchdiff () {
+ local how="$1"
+ local a="$2"
+ local b="$3"
+ git_manip_play
+
+ rrab=refs/remotes/"$a+$b"
+
+ ulf=\
+"delete refs/remotes/$a/%l
+delete refs/remotes/$b/%l
+"
+ case "$how" in
+ diff)
+ git for-each-ref --format 'delete %(refname)' $rrab \
+ | git update-ref --stdin
+ ;;
+ merge)
+ ulf=\
+"create $rrab/%l
+$ulf"
+ ;;
+ *)
+ fail "internal error bad how ($how)"
+ ;;
+ esac
+
+ gitfetchdiff_list "$a" a
+ gitfetchdiff_list "$b" b
+
+ diff --old-line-format='' --new-line-format='' \
+ --unchanged-line-format="$ulf" \
+ $play/a $play/b >$play/updates \
+ || test $? = 1
+
+ git update-ref --stdin <$play/updates
+ exit 0
+}
+
case "$#.$1" in
2.edit|2.done) mode="$1"; arg="$2" ;;
-2.-*) fail "no options understood" ;;
-*) fail "usage: tartree-edit edit|done DIRECTORY" ;;
+3.gitfetchinfo) mode="$1"; arg="$2"; remote="$3" ;;
+3.gitfetchinfo-diff) gitfetchdiff diff "$2" "$3" ;;
+3.gitfetchinfo-merge) gitfetchdiff merge "$2" "$3" ;;
+?.-*) fail "no options understood" ;;
+*) fail "usage:
+ tartree-edit edit|done DIRECTORY|TARBALL
+ tartree-edit gitfetchinfo DIRECTORY|TARBALL REMOTE
+ tartree-edit gitfetchinfo-merge REMOTE-A REMOTE-B" ;;
+ # we don't document gitfetchinfo-diff because it's rather poor
esac
case "$arg" in
@@ -39,6 +103,83 @@ tryat_edit () {
fi
}
+gitfetchinfo_perhaps_commit () {
+ local m="$1"
+ set +e
+ git diff --cached --quiet --exit-code HEAD
+ local rc=$?
+ set -e
+ case "$rc" in
+ 0) return ;;
+ 1) git commit --allow-empty --author='tartree-edit <>' -m "$m" ;;
+ *) fail "git diff failed ($rc)" ;;
+ esac
+}
+
+tryat_gitfetchinfo () {
+ git_manip_play
+
+ if test -d "$b.edit"; then
+ cp -a "$b.edit"/. "$play"/.
+ else
+ exec 3<"$b.tar"
+ tar -C $play -f - <&3 -x
+ exec 3<&-
+ fi
+
+ local innerwd; innerwd="$(echo $play/*)"
+
+ git for-each-ref --format='%(refname)' refs/remotes >$play/l
+ perl -w -ne '
+ our %remerge;
+ use strict;
+ chomp;
+ next unless m#^refs/remotes/([^/]+)/#;
+ my $old = $_;
+ my $ab = $1;
+ my $rhs = $'\'';
+ my @ab = split /\+/, $ab;
+ next unless @ab == 2;
+ next unless (grep { $_ eq "'"$remote"'" } @ab) == 1;
+ $remerge{"@ab"} = 1;
+ print "update refs/remotes/$_/$rhs $old\n" or die $! foreach @ab;
+ print "delete $old\n" or die $!;
+ END {
+ open REMERGE, ">&3" or die $!;
+ print REMERGE "$_\n" or die $! foreach sort keys %remerge;
+ close REMERGE or die $!;
+ }
+ ' <$play/l >$play/unmerge 3>$play/remerge
+ git update-ref --stdin <$play/unmerge
+
+ git remote remove "$remote" 2>/dev/null ||:
+ git remote add "$remote" $innerwd
+ git fetch --no-tags -p "$remote" \
+ +"HEAD:refs/remotes/$remote/TT-HEAD"
+ cd $innerwd
+ GIT_AUTHOR_DATE=$(git log -n1 --pretty=format:'%ai')
+ GIT_COMMITTER_DATE=$GIT_AUTHOR_DATE
+ export GIT_COMMITTER_DATE GIT_AUTHOR_DATE
+ git checkout -b WORKTREE
+ gitfetchinfo_perhaps_commit 'UNCOMMITTED INDEX'
+ git add -Af .
+ gitfetchinfo_perhaps_commit 'UNCOMMITTED WORKING TREE'
+ cd ../../..
+ git fetch --no-tags "$remote" --refmap \
+ +"refs/*:refs/remotes/$remote/*" \
+ +"refs/*:refs/remotes/$remote/*"
+
+ exec 3<$play/remerge
+ # $play will be destroyed by what follows, but we have
+ # an fd open onto remerge, so this will work
+ while read <&3 a b; do
+ echo "Updating gitfetchinfo-merge $a $b"
+ "$0" gitfetchinfo-merge $a $b
+ done
+
+ exit 0
+}
+
tryat_done () {
local b="$1"
if test -d "$b.edit"; then
diff --git a/tests/tests/absurd-gitapply b/tests/tests/absurd-gitapply
new file mode 100755
index 0000000..62eb190
--- /dev/null
+++ b/tests/tests/absurd-gitapply
@@ -0,0 +1,17 @@
+#!/bin/bash
+set -e
+. tests/lib
+t-tstunt-parsechangelog
+
+t-archive example 1.0-1+absurd
+t-git-none
+
+t-expect-fail 'gbp pq import failed' \
+t-dgit --force-import-gitapply-no-absurd clone $p
+
+t-dgit clone $p
+
+cd $p
+grep moo moo
+
+t-ok
diff --git a/tests/tests/badcommit-rewrite b/tests/tests/badcommit-rewrite
new file mode 100755
index 0000000..b7fc701
--- /dev/null
+++ b/tests/tests/badcommit-rewrite
@@ -0,0 +1,47 @@
+#!/bin/bash
+set -e
+. tests/lib
+
+t-setup-import examplegit
+t-tstunt-parsechangelog
+
+cd example
+
+suite=stable
+
+t-commit 'No changes, just send to stable' '' stable
+
+t-make-badcommit
+git reset --hard $badcommit
+
+t-dgit -wgf build
+t-dgit push --overwrite=1.2 stable
+t-archive-process-incoming stable
+
+rstable=refs/remotes/dgit/dgit/stable
+
+t-dgit fetch stable
+t-has-parent-or-is $rstable $badcommit
+
+fixup=${DGIT_BADCOMMIT_FIXUP-dgit-badcommit-fixup}
+
+cd $tmp/git/$p.git
+$fixup --real
+
+cd $tmp/$p
+git symbolic-ref HEAD >../sym.before
+git rev-parse HEAD >../ref.before
+
+$fixup --real
+
+git symbolic-ref HEAD >../sym.after
+git rev-parse HEAD >../ref.after
+diff ../sym.before ../sym.after
+set +e; diff ../ref.before ../ref.after; rc=$?; set -e; test $rc = 1
+
+t-dgit fetch stable
+
+t-expect-fail "child $rstable lacks parent $badcommit" \
+t-has-parent-or-is $rstable $badcommit
+
+t-ok
diff --git a/tests/tests/build-modes b/tests/tests/build-modes
new file mode 100755
index 0000000..c476ec8
--- /dev/null
+++ b/tests/tests/build-modes
@@ -0,0 +1,35 @@
+#!/bin/bash
+set -e
+. tests/lib
+. $troot/lib-build-modes
+
+bm-prep
+
+for act in \
+ 'build' \
+ 'build -S' \
+ 'build -b' \
+ 'build -B' \
+ 'build -A' \
+ 'build -F' \
+ 'build -g' \
+ 'build -G' \
+ build-source \
+; do
+ bm-guess-e-source-e-targets "$act"
+
+ case $act in
+ build-source)
+ cleanmodes="$cleanmodes_all"
+ ;;
+ *)
+ cleanmodes="$cleanmodes_default"
+ ;;
+ esac
+
+ real_act="$act"
+
+ bm-act-iterate
+done
+
+t-ok
diff --git a/tests/tests/build-modes-asplit b/tests/tests/build-modes-asplit
new file mode 100755
index 0000000..fa3bf8a
--- /dev/null
+++ b/tests/tests/build-modes-asplit
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -e
+. tests/lib
+. $troot/lib-build-modes
+bm-alwayssplit
diff --git a/tests/tests/build-modes-gbp b/tests/tests/build-modes-gbp
new file mode 100755
index 0000000..50a6288
--- /dev/null
+++ b/tests/tests/build-modes-gbp
@@ -0,0 +1,39 @@
+#!/bin/bash
+set -e
+. tests/lib
+. $troot/lib-build-modes
+
+t-dependencies git-buildpackage
+
+quirk-clean-fixup () {
+ case $cleanmode in
+ dpkg-source*)
+ # git-buildpackage runs the clean target twice somehow
+ perl -i.unfixed -ne '
+ print unless
+ $_ eq $last &&
+ $_ eq "EXAMPLE RULES TARGET clean\n";
+ $last = $_;
+ ' $bmgot
+ ;;
+ esac
+}
+bm_quirk_before_diff=quirk-clean-fixup
+
+bm-prep
+
+for act in \
+ 'gbp-build -S' \
+ 'gbp-build -b' \
+ 'gbp-build -B' \
+ 'gbp-build -A' \
+ 'gbp-build -F' \
+ 'gbp-build -g' \
+ 'gbp-build -G' \
+; do
+ bm-guess-e-source-e-targets "$act"
+ real_act="$act --git-ignore-branch"
+ bm-act-iterate
+done
+
+t-ok
diff --git a/tests/tests/build-modes-gbp-asplit b/tests/tests/build-modes-gbp-asplit
new file mode 100755
index 0000000..fa3bf8a
--- /dev/null
+++ b/tests/tests/build-modes-gbp-asplit
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -e
+. tests/lib
+. $troot/lib-build-modes
+bm-alwayssplit
diff --git a/tests/tests/build-modes-sbuild b/tests/tests/build-modes-sbuild
new file mode 100755
index 0000000..19dcc8a
--- /dev/null
+++ b/tests/tests/build-modes-sbuild
@@ -0,0 +1,18 @@
+#!/bin/bash
+set -e
+. tests/lib
+. $troot/lib-build-modes
+
+t-dependencies sbuild
+t-restrict x-dgit-schroot-build
+
+bm_quirk_after_act=bm-quirk-sbuild-after-act
+bm-prep
+
+act="sbuild -c build --no-arch-all"
+real_act="$act"
+
+bm-guess-e-source-e-targets "$act"
+bm-act-iterate
+
+t-ok
diff --git a/tests/tests/clone-clogsigpipe b/tests/tests/clone-clogsigpipe
new file mode 100755
index 0000000..4465cf3
--- /dev/null
+++ b/