diff options
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | Debian/Dgit.pm | 111 | ||||
-rw-r--r-- | Debian/Dgit/ExitStatus.pm | 26 | ||||
-rw-r--r-- | Debian/Dgit/GDR.pm | 26 | ||||
-rw-r--r-- | Makefile | 39 | ||||
-rw-r--r-- | NOTES.git-debrebase | 203 | ||||
-rw-r--r-- | debian/changelog | 31 | ||||
-rw-r--r-- | debian/control | 9 | ||||
-rwxr-xr-x | debian/rules | 30 | ||||
-rw-r--r-- | debian/tests/control | 8 | ||||
-rw-r--r-- | debian/tests/control.in | 2 | ||||
-rwxr-xr-x | dgit | 131 | ||||
-rwxr-xr-x | dgit-badcommit-fixup | 8 | ||||
-rw-r--r-- | dgit.1 | 22 | ||||
-rwxr-xr-x | git-debrebase | 1724 | ||||
-rw-r--r-- | git-debrebase.1.pod | 475 | ||||
-rw-r--r-- | git-debrebase.5.pod | 610 | ||||
-rwxr-xr-x | tests/enumerate-tests | 18 | ||||
-rw-r--r-- | tests/lib | 23 | ||||
-rw-r--r-- | tests/lib-core | 1 | ||||
-rw-r--r-- | tests/lib-gdr | 277 | ||||
-rwxr-xr-x | tests/setup/gdr-convert-gbp | 100 | ||||
-rwxr-xr-x | tests/setup/gdr-convert-gbp-noarchive | 9 | ||||
-rwxr-xr-x | tests/tests/gdr-diverge-nmu | 61 | ||||
-rwxr-xr-x | tests/tests/gdr-diverge-nmu-dgit | 55 | ||||
-rwxr-xr-x | tests/tests/gdr-edits | 40 | ||||
-rwxr-xr-x | tests/tests/gdr-import-dgit | 68 | ||||
-rwxr-xr-x | tests/tests/gdr-newupstream-v0 | 65 | ||||
-rwxr-xr-x | tests/tests/gdr-subcommands | 226 | ||||
-rwxr-xr-x | tests/tests/gdr-viagit | 40 |
30 files changed, 4355 insertions, 86 deletions
@@ -1,6 +1,7 @@ *~ tests/tmp debian/dgit +debian/git-debrebase debian/dgit-infrastructure debian/files debian/*.substvars @@ -13,4 +14,6 @@ dgit-maint-merge.7 dgit-maint-gbp.7 dgit-maint-debrebase.7 dgit-sponsorship.7 +git-debrebase.1 +git-debrebase.5 substituted diff --git a/Debian/Dgit.pm b/Debian/Dgit.pm index c4a61af..960f505 100644 --- a/Debian/Dgit.pm +++ b/Debian/Dgit.pm @@ -44,28 +44,33 @@ BEGIN { server_branch server_ref stat_exists link_ltarget hashfile - fail ensuredir must_getcwd executable_on_path + fail failmsg ensuredir must_getcwd executable_on_path waitstatusmsg failedcmd_waitstatus failedcmd_report_cmd failedcmd runcmd cmdoutput cmdoutput_errok git_rev_parse git_cat_file - git_get_ref git_for_each_ref + git_get_ref git_get_symref git_for_each_ref git_for_each_tag_referring is_fast_fwd + git_check_unmodified $package_re $component_re $deliberately_re $distro_re $versiontag_re $series_filename_re + $extra_orig_namepart_re + $git_null_obj $branchprefix + $ffq_refprefix $gdrlast_refprefix initdebug enabledebug enabledebuglevel printdebug debugcmd $debugprefix *debuglevel *DEBUG shellquote printcmd messagequote $negate_harmful_gitattrs changedir git_slurp_config_src + gdr_ffq_prev_branchinfo playtree_setup); # implicitly uses $main::us %EXPORT_TAGS = ( policyflags => [qw(NOFFCHECK FRESHREPO NOCOMMITCHECK)], playground => [qw(record_maindir $maindir $local_git_cfg $maindir_gitdir $maindir_gitcommon - fresh_playground $playground + fresh_playground ensure_a_playground)]); @EXPORT_OK = ( @{ $EXPORT_TAGS{policyflags} }, @{ $EXPORT_TAGS{playground} } ); @@ -80,6 +85,10 @@ our $distro_re = $component_re; our $versiontag_re = qr{[-+.\%_0-9a-zA-Z/]+}; our $branchprefix = 'dgit'; our $series_filename_re = qr{(?:^|\.)series(?!\n)$}s; +our $extra_orig_namepart_re = qr{[-0-9a-z]+}; +our $git_null_obj = '0' x 40; +our $ffq_refprefix = 'ffq-prev'; +our $gdrlast_refprefix = 'debrebase-last'; # policy hook exit status bits # see dgit-repos-server head comment for documentation @@ -108,7 +117,7 @@ sub forkcheck_mainprocess () { sub setup_sigwarn () { forkcheck_setup(); $SIG{__WARN__} = sub { - die $_[0] if forkcheck_mainprocess; + confess $_[0] if forkcheck_mainprocess; }; } @@ -213,12 +222,16 @@ sub _us () { $::us // ($0 =~ m#[^/]*$#, $&); } -sub fail { +sub failmsg { my $s = "@_\n"; $s =~ s/\n\n$/\n/; my $prefix = _us().": "; $s =~ s/^/$prefix/gm; - die $s; + return $s; +} + +sub fail { + die failmsg @_; } sub ensuredir ($) { @@ -348,11 +361,21 @@ sub git_rev_parse ($) { return cmdoutput qw(git rev-parse), "$_[0]~0"; } -sub git_cat_file ($) { - my ($objname) = @_; +sub git_cat_file ($;$) { + my ($objname, $etype) = @_; # => ($type, $data) or ('missing', undef) # in scalar context, just the data + # if $etype defined, dies unless type is $etype or in @$etype our ($gcf_pid, $gcf_i, $gcf_o); + my $chk = sub { + my ($gtype, $data) = @_; + if ($etype) { + $etype = [$etype] unless ref $etype; + confess "$objname expected @$etype but is $gtype" + unless grep { $gtype eq $_ } @$etype; + } + return ($gtype, $data); + }; if (!$gcf_pid) { my @cmd = qw(git cat-file --batch); debugcmd "GCF|", @cmd; @@ -362,13 +385,26 @@ sub git_cat_file ($) { print $gcf_i $objname, "\n" or die $!; my $x = <$gcf_o>; printdebug "GCF<| ", $x; - if ($x =~ m/ (missing)$/) { return ($1, undef); } + if ($x =~ m/ (missing)$/) { return $chk->($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); + return $chk->($type, $data); +} + +sub git_get_symref (;$) { + my ($symref) = @_; $symref //= 'HEAD'; + # => undef if not a symref, otherwise refs/... + my @cmd = (qw(git symbolic-ref -q HEAD)); + my $branch = cmdoutput_errok @cmd; + if (!defined $branch) { + $?==256 or failedcmd @cmd; + } else { + chomp $branch; + } + return $branch; } sub git_for_each_ref ($$;$) { @@ -418,6 +454,25 @@ sub git_for_each_tag_referring ($$) { }); } +sub git_check_unmodified () { + foreach my $cached (qw(0 1)) { + my @cmd = qw(git diff --quiet); + push @cmd, qw(--cached) if $cached; + push @cmd, qw(HEAD); + debugcmd "+",@cmd; + $!=0; $?=-1; system @cmd; + return if !$?; + if ($?==256) { + fail + $cached + ? "git index contains changes (does not match HEAD)" + : "working tree is dirty (does not match HEAD)"; + } else { + failedcmd @cmd; + } + } +} + sub is_fast_fwd ($$) { my ($ancestor,$child) = @_; my @cmd = (qw(git merge-base), $ancestor, $child); @@ -460,12 +515,28 @@ sub git_slurp_config_src ($) { return $r; } +sub gdr_ffq_prev_branchinfo ($) { + my ($symref) = @_; + # => ('status', "message", [$symref, $ffq_prev, $gdrlast]) + # 'status' may be + # branch message is undef + # weird-symref } no $symref, + # notbranch } no $ffq_prev + return ('detached', 'detached HEAD') unless defined $symref; + return ('weird-symref', 'HEAD symref is not to refs/') + unless $symref =~ m{^refs/}; + my $ffq_prev = "refs/$ffq_refprefix/$'"; + my $gdrlast = "refs/$gdrlast_refprefix/$'"; + printdebug "ffq_prev_branchinfo branch current $symref\n"; + return ('branch', undef, $symref, $ffq_prev, $gdrlast); +} + # ========== playground handling ========== # terminology: # # $maindir user's git working tree -# $playground area in .git/ where we can make files, unpack, etc. etc. +# playground area in .git/ where we can make files, unpack, etc. etc. # playtree git working tree sharing object store with the user's # inside playground, or identical to it # @@ -485,28 +556,26 @@ sub git_slurp_config_src ($) { # # fresh_playground SUBDIR_PATH_COMPONENTS # e.g fresh_playground 'dgit/unpack' ('.git/' is implied) -# default SUBDIR_PATH_COMPONENTS is $playground_subdir +# default SUBDIR_PATH_COMPONENTS is playground_subdir # calls record_maindir # sets up a new playground (destroying any old one) -# assigns to $playground and returns the same pathname +# returns playground pathname # caller may call multiple times with different subdir paths -# createing different playgrounds; but $playground global can -# refer only to one, obv. +# createing different playgrounds # # ensure_a_playground SUBDIR_PATH_COMPONENTS # like fresh_playground except: # merely ensures the directory exists; does not delete an existing one -# never sets global $playground # # then can use # -# changedir $playground +# changedir playground # changedir $maindir # # playtree_setup $local_git_cfg -# # ^ call in some (perhaps trivial) subdir of $playground +# # ^ call in some (perhaps trivial) subdir of playground # -# rmtree $playground +# rmtree playground # ----- maindir ----- @@ -538,8 +607,6 @@ sub record_maindir () { # ----- playgrounds ----- -our $playground; - sub ensure_a_playground_parent ($) { my ($spc) = @_; record_maindir(); @@ -562,7 +629,7 @@ sub fresh_playground ($) { $spc = ensure_a_playground_parent $spc; rmtree $spc; mkdir $spc or fail "failed to mkdir the playground $spc: $!"; - return $playground = $spc; + return $spc; } # ----- playtrees ----- diff --git a/Debian/Dgit/ExitStatus.pm b/Debian/Dgit/ExitStatus.pm new file mode 100644 index 0000000..b69d42d --- /dev/null +++ b/Debian/Dgit/ExitStatus.pm @@ -0,0 +1,26 @@ +# -*- perl -*- + +package Debian::Dgit::ExitStatus; + +# To use this, at the top (before use strict, even): +# +# END { $? = $Debian::Dgit::ExitStatus::desired // -1; }; +# use Debian::Dgit::ExitStatus; +# +# and then replace every call to `exit' with `finish'. +# Add a `finish 0' to the end of the program. + +BEGIN { + use Exporter; + @ISA = qw(Exporter); + @EXPORT = qw(finish $desired); +} + +our $desired; + +sub finish ($) { + $desired = $_[0] // 0; + exit $desired; +} + +1; diff --git a/Debian/Dgit/GDR.pm b/Debian/Dgit/GDR.pm new file mode 100644 index 0000000..ca7e621 --- /dev/null +++ b/Debian/Dgit/GDR.pm @@ -0,0 +1,26 @@ +# -*- perl -*- + +package Debian::Dgit::GDR; + +use strict; +use warnings; + +# Scripts and programs which are going to `use Debian::Dgit' but which +# live in git-debrebase (ie are installed with install-gdr) +# should `use Debian::Dgit::GDR' first. All this module does is +# adjust @INC so that the script gets the version of the script from +# the git-debrebase package (which is installed in a different +# location and may be a different version). + +# To use this with ExitStatus, put at the top (before use strict, even): +# +# END { $? = $Debian::Dgit::ExitStatus::desired // -1; }; +# use Debian::Dgit::GDR; +# use Debian::Dgit::ExitStatus; +# +# and then replace every call to `exit' with `finish'. +# Add a `finish 0' to the end of the program. + +# unshift @INC, q{/usr/share/dgit/gdr/perl5}; ###substituted### + +1; @@ -27,6 +27,7 @@ bindir=$(prefix)/bin mandir=$(prefix)/share/man perldir=$(prefix)/share/perl5 man1dir=$(mandir)/man1 +man5dir=$(mandir)/man5 man7dir=$(mandir)/man7 infraexamplesdir=$(prefix)/share/doc/dgit-infrastructure/examples txtdocdir=$(prefix)/share/doc/dgit @@ -43,9 +44,17 @@ MAN7PAGES=dgit.7 \ dgit-sponsorship.7 TXTDOCS=README.dsc-import -PERLMODULES=Debian/Dgit.pm +PERLMODULES=Debian/Dgit.pm Debian/Dgit/ExitStatus.pm ABSURDITIES=git +GDR_PROGRAMS=git-debrebase +GDR_PERLMODULES= \ + Debian/Dgit.pm \ + Debian/Dgit/GDR.pm \ + Debian/Dgit/ExitStatus.pm +GDR_MAN1PAGES=git-debrebase.1 +GDR_MAN5PAGES=git-debrebase.5 + INFRA_PROGRAMS=dgit-repos-server dgit-ssh-dispatch \ dgit-repos-policy-debian dgit-repos-admin-debian \ dgit-repos-policy-trusting dgit-mirror-rsync @@ -55,7 +64,10 @@ INFRA_PERLMODULES= \ Debian/Dgit/Infra.pm \ Debian/Dgit/Policy/Debian.pm -all: $(MAN7PAGES) $(addprefix substituted/,$(PROGRAMS)) +MANPAGES=$(MAN1PAGES) $(MAN5PAGES) $(MAN7PAGES) \ + $(GDR_MAN1PAGES) $(GDR_MAN5PAGES) + +all: $(MANPAGES) $(addprefix substituted/,$(PROGRAMS)) substituted/%: % mkdir -p substituted @@ -76,10 +88,19 @@ install: installdirs all installdirs: $(INSTALL_DIR) $(DESTDIR)$(bindir) \ - $(DESTDIR)$(man1dir) $(DESTDIR)$(man7dir) \ + $(DESTDIR)$(man1dir) $(DESTDIR)$(man5dir) \ + $(DESTDIR)$(man7dir) \ $(DESTDIR)$(txtdocdir) $(DESTDIR)$(absurddir) \ $(addprefix $(DESTDIR)$(perldir)/, $(dir $(PERLMODULES))) +install-gdr: installdirs-gdr + $(INSTALL_PROGRAM) $(GDR_PROGRAMS) $(DESTDIR)$(bindir) + $(INSTALL_DATA) $(GDR_MAN1PAGES) $(DESTDIR)$(man1dir) + $(INSTALL_DATA) $(GDR_MAN5PAGES) $(DESTDIR)$(man5dir) + set -e; for m in $(GDR_PERLMODULES); do \ + $(INSTALL_DATA) $$m $(DESTDIR)$(perldir)/$${m%/*}; \ + done + install-infra: installdirs-infra $(INSTALL_PROGRAM) $(addprefix infra/, $(INFRA_PROGRAMS)) \ $(DESTDIR)$(bindir) @@ -89,6 +110,11 @@ install-infra: installdirs-infra $(INSTALL_DATA) $$m $(DESTDIR)$(perldir)/$${m%/*}; \ done +installdirs-gdr: + $(INSTALL_DIR) $(DESTDIR)$(bindir) \ + $(DESTDIR)$(man1dir) $(DESTDIR)$(man5dir) \ + $(addprefix $(DESTDIR)$(perldir)/, $(dir $(GDR_PERLMODULES))) + installdirs-infra: $(INSTALL_DIR) $(DESTDIR)$(bindir) $(DESTDIR)$(infraexamplesdir) \ $(addprefix $(DESTDIR)$(perldir)/, $(dir $(INFRA_PERLMODULES))) @@ -97,7 +123,7 @@ check installcheck: clean distclean mostlyclean maintainer-clean: rm -rf tests/tmp substituted - set -e; for m in $(MAN7PAGES); do \ + set -e; for m in $(MANPAGES); do \ test -e $$m.pod && rm -f $$m; \ done @@ -106,5 +132,10 @@ clean distclean mostlyclean maintainer-clean: --name=$(subst .7,,$@) \ $^ $@ +git-debrebase.%: git-debrebase.%.pod + pod2man --section=$* --date="Debian Project" --center="git-debrebase" \ + --name=$(subst .$*,,$@) \ + $^ $@ + %.view: % man -l $* diff --git a/NOTES.git-debrebase b/NOTES.git-debrebase new file mode 100644 index 0000000..f32cf87 --- /dev/null +++ b/NOTES.git-debrebase @@ -0,0 +1,203 @@ +TODO + tutorial + dgit-maint-debrebase(7) + someone should set branch.<name>.mergeOptions to include --ff-only ? + + arrange for dgit to automatically stitch on push + dgit push usually needs to (re)make a pseudomerge. The "first" + git-debrebase stripped out the previous pseudomerge and could + remembeed the old HEAD. But the user has to manually stitch it. + To fix this, do we need a new push hook for dgit ? + + + +workflow + + git-debrebase blah [implies start] strips pseudomerge(s) + + commit / git-debrebase / etc. + + dgit --damp-run push + hook: call git-debrebase prep-push dgit push does not update remote + or something, must add patches at least + + commit / git-debrebase / etc. strips patches + + dgit push + hook: call git-debrebase prep-push dgit push DOES update remote + + commit / git-debrebase / etc. strips last pm, but arranges + that remade pm will incorporate it + + +# problems / outstanding questions: +# +# * dgit push with a `3.0 (quilt)' package means doing quilt +# fixup. Usually this involves recommitting the whole patch +# series, one at a time, with dpkg-source --commit. This is +# terribly terribly slow. (Maybe this should be fixed in dgit.) +# +# * Workflow is currently clumsy. Lots of spurious runes to type. +# There's not even a guide. +# +# * new-upstream-v0 has a terrible UI for multiple upstream pieces. +# You end up with giant runic command lines. Does this matter / +# One consequence of the lack of richness it can need -f in +# fairly sensible situations. +# +# * There should be a good convention for the version number, +# and unfinalised or not changelog, after new-upstream. +# +# * Handing of multi-orig dgit new-upstream .dsc imports is known to +# be broken. They may be not recognised, improperly converted, or +# their conversion may be unrecognised. +# +# * We need to develop a plausible model that works for derivatives, +# who probably want to maintain their stack on top of Debian's. +# downstream-rebase-launder-v0 may be a starting point? +# maybe the hypothetical git-ffqrebase is part of it too. + + +# undocumented usages: +# +# git-debrebase [<options>] downstream-rebase-launder-v0 # experimental + + +======================================== + +Theory for ffq-prev + + refs/ffq-prev/REF relates to refs/REF + +When we strip a pm, we need to maybe record it (or something) as the +new start point. + +When we do a thing + + with no recorded ffq-prev + + ffq-prev is our current tip + + obviously it is safe to say we will overwrite this + we do check whether there are not-included changes in the remotes + because if the new ffq-prev is not ff from the remotes + the later pushes will fail + + this model tends to keep ad-hoc commits made on our + tip branch before we did rebase start, in the + `interchange view' and also in the rebase stack. + + also we can explicitly preserve with + git-debrebase stitch + + It is always safe to rewind ffq-prev: all + that does is overwrite _less_ stuff. + + in any case putative ffq-prev must be ff from remote. + Otherwise when we push it will not be ff, even though we have + made pseudomerge to overwrite ffq-prev. So if we spot + this, report an error. see above + + with a recorded ffq-prev + + we may need to advance ffq-prev, to allow us to generate + future pseudomerges that will be pushable + + advancing ffq-prev is dangerous, since it might + effectively cancel the commits that will-ovewrite is advanced + over. + + ??? advance it to merge-base( current remote, current tip ) + if possible (see above), - ie to current remote, subject + to the condition that that is an ancestor of current tip + + currently this is not implemented + + better maybe to detect divergence ? but it is rather late + by then! + +We check we are ff from remotes before recording new ffq-prev + + ---------- now follows much the same info in different words ---------- + +Re git-debrebase [--noop-ok] stitch + + we will teach dgit to do + git-debrebase stitch + or some such ? + +following parts are not implemented and maybe aren't the +best subcommand names etc. + +3. git-debrebase push + + like git push only does stitch first + ??? command line parsing! + +4. git-debrebase release + + stiches, finalises changelog, signs tags, pushes everything + for the future, when there is some automatic builder + +======================================== + +import from gbp + +what about dgit view branch ? +ideally, would make pseudomerge over dgit view +would need to check that dgit view is actually dgit view of + ond of our ancestors +failing that first push will need --overwrite +that is what is currently implemented + +======================================== + +how to handle divergence and merges (if not detected soon enough) + +same problem + if merge, look at branches before merge + generate new combined branch + pseudomerge to overwrite merge + +current avaiable strategies: + + maybe launder foreign branch + + if foreign branch is nmuish, can rebase it onto ours + + could merge breakwaters (use analyse to find them) + merge breakwaters (assuming same upstream) + manually construct new patch queue by inspection of + the other two patch queues + + instead of manually constructing patch queue, could use + gbp pq export and git merge the patch queues + (ie work with interdiffs) + + if upstreams are different and one is ahead + simply treat that as "ours" and + do the work to import changes from the other + + if upstreams have diverged, can + resolve somehow to make new upstream + do new-upstream on each branch separately + now reduced to previously "solved" problem + + in future, auto patch queue merge algorithm + determine next patch to apply + there are three versions o..O, l..L, r..R + we have already constructed m (previous patch or merged breakwater) + try using vector calculus in the implied cube and compute + multiple ways to check consistency ? + +======================================== + +For downstreams of Debian, sketch of git-ffqrebase + +# git-ffqrebase start [BASE] +# # records previous HEAD so it can be overwritten +# # records base for future git-ffqrebase +# git-ffqrebase set-base BASE +# git-ffqrebase <git-rebase options> +# git-ffqrebase finish +# git-ffqrebase status [BRANCH] diff --git a/debian/changelog b/debian/changelog index eb11dc4..f5a4025 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,9 +1,36 @@ -dgit (4.4~) unstable; urgency=low +dgit (5.0~) unstable; urgency=low - * + Major new utility: + * git-debrebase, a new git workflow tool. + * dgit will now, when appropriate, check if it should call + git-debrebase. + + dgit bugfixes: + * Fix the exit status of programs in dgit.deb, to avoid the Perl + misfeature which sometimes copies $! to the exit status. + * When checking that the tree is clean, check the git index too. + * In quilt_fixup_multipatch, work around git checkout paths + not deleting files. (Hypothetical bug AFAIAA.) + * Respect --quilt=nofix even if single-debian-patch. + + dgit minor fixes: + * "confess" when we die due to a warning, rather than symply dieing. + + Internal changes: + * Move $playground global to dgit. + * Break git_get_symref and $extra_orig_namepart_re out into Dgit.pm. + * Changes to support git-debrebase. -- +dgit (4.4) unstable; urgency=high + + Test suite bugfix: + * Use full key hash rather than short keyid. Closes:#896653. + [ report: Paul Gevers; fix: Chris Lamb ] + + -- Ian Jackson <ijackson@chiark.greenend.org.uk> Mon, 23 Apr 2018 13:18:51 +0100 + dgit (4.3) unstable; urgency=high Documentation improvements: diff --git a/debian/control b/debian/control index 4405e14..224e52a 100644 --- a/debian/control +++ b/debian/control @@ -26,6 +26,15 @@ Description: git interoperability with the Debian archive . dgit clone and dgit fetch construct git commits from uploads. +Package: git-debrebase +Depends: perl, git-core, libdpkg-perl, libfile-fnmatch-perl + ${misc:Depends} +Recommends: dgit, git-buildpackage +Architecture: all +Description: rebasing git workflow tool for Debian packaging + git-debrebase is a tool for representing in git, and manpulating, + Debian packages based on upstream source code. + Package: dgit-infrastructure Depends: ${misc:Depends}, perl, git-core, gpgv, chiark-utils-bin, libjson-perl, libdigest-sha-perl, libdbd-sqlite3-perl, sqlite3, diff --git a/debian/rules b/debian/rules index 9249f88..baff8f8 100755 --- a/debian/rules +++ b/debian/rules @@ -31,25 +31,33 @@ override_dh_gencontrol: 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: +override_dh_auto_install: specpkg_install_gdr specpkg_install_infra 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. + +specpkg_install_gdr: p=git-debrebase +specpkg_install_gdr: pm=GDR + +specpkg_install_infra: p=dgit-infrastructure +specpkg_install_infra: pm=Infra + +specpkg_install_%: tok=$* +specpkg_install_%: specperl=/usr/share/dgit/$(tok)/perl5 +specpkg_install_%: + make install-$(tok) prefix=/usr DESTDIR=debian/$(p) perldir=$(specperl) +# # Most of the Perl modules in this package live in +# # $(specperl). The exception is Debian::Dgit::Infra, which +# # lives in $(globalperl) and adds $(specperl) to @INC. set -ex; \ - base=debian/dgit-infrastructure; \ - mod=Debian/Dgit/Infra.pm; \ - src=$${base}$(infraperl)/$${mod}; \ + base=debian/$(p); \ + mod=Debian/Dgit/$(pm).pm; \ + src=$${base}$(specperl)/$${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)}};' \ + -e 'die unless s{q\{\S+\}}{q{$(specperl)}};' \ $$dst debian/tests/control: tests/enumerate-tests debian/tests/control.in diff --git a/debian/tests/control b/debian/tests/control index f3d20f1..dcc40a7 100644 --- a/debian/tests/control +++ b/debian/tests/control @@ -16,6 +16,14 @@ Tests-Directory: tests/tests Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, chiark-utils-bin Restrictions: x-dgit-intree-only x-dgit-git-only +Tests: gdr-diverge-nmu gdr-diverge-nmu-dgit gdr-edits gdr-import-dgit gdr-subcommands +Tests-Directory: tests/tests +Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, chiark-utils-bin, git-debrebase, git-buildpackage, faketime + +Tests: gdr-newupstream-v0 gdr-viagit +Tests-Directory: tests/tests +Depends: chiark-utils-bin, git-debrebase, git-buildpackage, faketime + Tests: gitattributes Tests-Directory: tests/tests Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, chiark-utils-bin, bsdgames, man-db, git-man diff --git a/debian/tests/control.in b/debian/tests/control.in index 960d3ef..b558a25 100644 --- a/debian/tests/control.in +++ b/debian/tests/control.in @@ -1,2 +1,2 @@ Tests-Directory: tests/tests -Depends: dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, chiark-utils-bin +Depends: @@ -18,6 +18,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +END { $? = $Debian::Dgit::ExitStatus::desired // -1; }; +use Debian::Dgit::ExitStatus; + use strict; use Debian::Dgit qw(:DEFAULT :playground); @@ -95,7 +98,7 @@ our %format_ok = map { $_=>1 } ("1.0","3.0 (native)","3.0 (quilt)"); our $suite_re = '[-+.0-9a-z]+'; our $cleanmode_re = 'dpkg-source(?:-d)?|git|git-ff|check|none'; -our $orig_f_comp_re = 'orig(?:-[-0-9a-z]+)?'; +our $orig_f_comp_re = qr{orig(?:-$extra_orig_namepart_re)?}; our $orig_f_sig_re = '\\.(?:asc|gpg|pgp)'; our $orig_f_tail_re = "$orig_f_comp_re\\.tar(?:\\.\\w+)?(?:$orig_f_sig_re)?"; @@ -114,6 +117,7 @@ our (@gpg) = qw(gpg); our (@sbuild) = qw(sbuild); our (@ssh) = 'ssh'; our (@dgit) = qw(dgit); +our (@git_debrebase) = qw(git-debrebase); our (@aptget) = qw(apt-get); our (@aptcache) = qw(apt-cache); our (@dpkgbuildpackage) = (qw(dpkg-buildpackage), @dpkg_source_ignores); @@ -133,6 +137,7 @@ our %opts_opt_map = ('dget' => \@dget, # accept for compatibility 'ssh' => \@ssh, 'dgit' => \@dgit, 'git' => \@git, + 'git-debrebase' => \@git_debrebase, 'apt-get' => \@aptget, 'apt-cache' => \@aptcache, 'dpkg-source' => \@dpkgsource, @@ -153,6 +158,7 @@ sub parseopts_late_defaults(); sub setup_gitattrs(;$); sub check_gitattrs($$); +our $playground; our $keyid; autoflush STDOUT 1; @@ -235,7 +241,7 @@ END { } }; -sub badcfg { print STDERR "$us: invalid configuration: @_\n"; exit 12; } +sub badcfg { print STDERR "$us: invalid configuration: @_\n"; finish 12; } sub forceable_fail ($$) { my ($forceoptsl, $msg) = @_; @@ -253,7 +259,7 @@ sub forceing ($) { sub no_such_package () { print STDERR "$us: package $package does not exist in suite $isuite\n"; - exit 4; + finish 4; } sub deliberately ($) { @@ -286,6 +292,32 @@ sub dgit_privdir () { our $dgit_privdir_made //= ensure_a_playground 'dgit'; } +sub branch_gdr_info ($$) { + my ($symref, $head) = @_; + my ($status, $msg, $current, $ffq_prev, $gdrlast) = + gdr_ffq_prev_branchinfo($symref); + return () unless $status eq 'branch'; + $ffq_prev = git_get_ref $ffq_prev; + $gdrlast = git_get_ref $gdrlast; + $gdrlast &&= is_fast_fwd $gdrlast, $head; + return ($ffq_prev, $gdrlast); +} + +sub branch_is_gdr ($$) { + my ($symref, $head) = @_; + my ($ffq_prev, $gdrlast) = branch_gdr_info($symref, $head); + return 0 unless $ffq_prev || $gdrlast; + return 1; +} + +sub branch_is_gdr_unstitched_ff ($$$) { + my ($symref, $head, $ancestor) = @_; + my ($ffq_prev, $gdrlast) = branch_gdr_info($symref, $head); + return 0 unless $ffq_prev; + return 0 unless is_fast_fwd $ancestor, $ffq_prev; + return 1; +} + #---------- remote protocol support, common ---------- # remote push initiator/responder protocol: @@ -558,7 +590,7 @@ END sub badusage { print STDERR "$us: @_\n", $helpmsg or die $!; - exit 8; + finish 8; } sub nextarg { @@ -571,7 +603,7 @@ sub pre_help () { } sub cmd_help () { print $helpmsg or die $!; - exit 0; + finish 0; } our $td = $ENV{DGIT_TEST_DUMMY_DIR} || "DGIT_TEST_DUMMY_DIR-unset"; @@ -1683,7 +1715,7 @@ our ($dsc_distro, $dsc_hint_tag, $dsc_hint_url); sub prep_ud () { dgit_privdir(); # ensures that $dgit_privdir_made is based on $maindir - fresh_playground 'dgit/unpack'; + $playground = fresh_playground 'dgit/unpack'; } sub mktree_in_ud_here () { @@ -3507,7 +3539,7 @@ sub fork_for_multisuite ($) { sub { @end = (); fetch(); - exit 0; + finish 0; }); # xxx collecte the ref here @@ -3691,15 +3723,7 @@ sub check_not_dirty () { return if $ignoredirty; - my @cmd = (@git, qw(diff --quiet HEAD)); - debugcmd "+",@cmd; - $!=0; $?=-1; system @cmd; - return if !$?; - if ($?==256) { - fail "working tree is dirty (does not match HEAD)"; - } else { - failedcmd @cmd; - } + git_check_unmodified(); } sub commit_admin ($) { @@ -3708,6 +3732,15 @@ sub commit_admin ($) { runcmd_ordryrun_local @git, qw(commit -m), $m; } +sub quiltify_nofix_bail ($$) { + my ($headinfo, $xinfo) = @_; + if ($quilt_mode eq 'nofix') { + fail "quilt fixup required but quilt mode is \`nofix'\n". + "HEAD commit".$headinfo." differs from tree implied by ". + " debian/patches".$xinfo; + } +} + sub commit_quilty_patch () { my $output = cmdoutput @git, qw(status --porcelain); my %adds; @@ -3722,6 +3755,7 @@ sub commit_quilty_patch () { progress "nothing quilty to commit, ok."; return; } + quiltify_nofix_bail "", " (wanted to commit patch update)"; my @adds = map { s/[][*?\\]/\\$&/g; $_; } sort keys %adds; runcmd_ordryrun_local @git, qw(add -f), @adds; commit_admin <<END @@ -3882,6 +3916,8 @@ sub pseudomerge_make_commit ($$$$ $$) { : !length $overwrite_version ? " --overwrite" : " --overwrite=".$overwrite_version; + # Contributing parent is the first parent - that makes + # git rev-list --first-parent DTRT. my $pmf = dgit_privdir()."/pseudomerge"; open MC, ">", $pmf or die "$pmf $!"; print MC <<END or die $!; @@ -4210,7 +4246,14 @@ END my $format = getfield $dsc, 'Format'; printdebug "format $format\n"; + my $symref = git_get_symref(); my $actualhead = git_rev_parse('HEAD'); + + if (branch_is_gdr_unstitched_ff($symref, $actualhead, $archive_hash)) { + runcmd_ordryrun_local @git_debrebase, 'stitch'; + $actualhead = git_rev_parse('HEAD'); + } + my $dgithead = $actualhead; my $maintviewhead = undef; @@ -4510,13 +4553,8 @@ sub cmd_clone { } sub branchsuite () { - 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) { + my $branch = git_get_symref(); + if (defined $branch && $branch =~ m#$lbranch_re#o) { return $1; } else { return undef; @@ -4547,7 +4585,7 @@ sub cmd_fetch { parseopts(); fetchpullargs(); my $multi_fetched = fork_for_multisuite(sub { }); - exit 0 if $multi_fetched; + finish 0 if $multi_fetched; fetch(); } @@ -4756,7 +4794,7 @@ sub i_resp_complete { i_cleanup(); printdebug "all done\n"; - exit 0; + finish 0; } sub i_resp_file ($) { @@ -5206,11 +5244,7 @@ sub quiltify ($$$$) { 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)"; - } + quiltify_nofix_bail " $c->{Commit}", " (tree object $oldtiptree)"; if ($quilt_mode eq 'smash') { printdebug " search quitting smash\n"; last; @@ -5424,6 +5458,32 @@ END my $clogp = parsechangelog(); my $headref = git_rev_parse('HEAD'); + my $symref = git_get_symref(); + + if ($quilt_mode eq 'linear' + && !$fopts->{'single-debian-patch'} + && branch_is_gdr($symref, $headref)) { + # This is much faster. It also makes patches that gdr + # likes better for future updates without laundering. + # + # However, it can fail in some casses where we would + # succeed: if there are existing patches, which correspond + # to a prefix of the branch, but are not in gbp/gdr + # format, gdr will fail (exiting status 7), but we might + # be able to figure out where to start linearising. That + # will be slower so hopefully there's not much to do. + my @cmd = (@git_debrebase, + qw(--noop-ok -funclean-mixed -funclean-ordering + make-patches --quiet-would-amend)); + # We tolerate soe snags that gdr wouldn't, by default. + if (act_local()) { + $!=0; $?=-1; + failedcmd @cmd if system @cmd and $?!=7; + } else { + dryrun_report @cmd; + } + $headref = git_rev_parse('HEAD'); + } prep_ud(); changedir $playground; @@ -5587,7 +5647,7 @@ sub quilt_check_splitbrain_cache ($$) { if (!stat "$maindir_gitcommon/logs/refs/$splitbraincache") { $! == ENOENT or die $!; printdebug ">(no reflog)\n"; - exit 0; + finish 0; } exec @cmd; die $!; } @@ -5713,6 +5773,7 @@ sub quilt_fixup_multipatch ($$$) { rmtree '.pc'; + rmtree 'debian'; # git checkout commitish paths does not delete! runcmd @git, qw(checkout -f), $headref, qw(-- debian); my $unapplied=git_add_write_tree(); printdebug "fake orig tree object $unapplied\n"; @@ -5839,7 +5900,7 @@ sub quilt_fixup_editor () { } I2->error and die $!; close O or die $1; - exit 0; + finish 0; } sub maybe_apply_patches_dirtily () { @@ -6529,7 +6590,7 @@ sub cmd_setup_new_tree { sub cmd_version { print "dgit version $our_version\n" or die $!; - exit 0; + finish 0; } our (%valopts_long, %valopts_short); @@ -6881,7 +6942,7 @@ print STDERR "DAMP RUN - WILL MAKE LOCAL (UNSIGNED) CHANGES\n" if $dryrun_level == 1; if (!@ARGV) { print STDERR $helpmsg or die $!; - exit 8; + finish 8; } $cmd = $subcommand = shift @ARGV; $cmd =~ y/-/_/; @@ -6895,3 +6956,5 @@ git_slurp_config(); my $fn = ${*::}{"cmd_$cmd"}; $fn or badusage "unknown operation $cmd"; $fn->(); + +finish 0; diff --git a/dgit-badcommit-fixup b/dgit-badcommit-fixup index 3995ceb..3e4a718 100755 --- a/dgit-badcommit-fixup +++ b/dgit-badcommit-fixup @@ -19,6 +19,8 @@ # # 4. Run the mirror script to push changes, if necessary. +END { $? = $Debian::Dgit::ExitStatus::desired // -1; }; +use Debian::Dgit::ExitStatus; use strict; @@ -283,7 +285,7 @@ filter_updates(); if (!@updates) { print Dumper(\%count), "all is well - nothing to do\n"; - exit 0; + finish 0; } #print Dumper(\@updates); @@ -325,5 +327,7 @@ if ($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; + finish 2; } + +finish 0; @@ -185,14 +185,19 @@ 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. + +With \fB-C\fR, performs a dgit push, additionally ensuring that no +binary packages are uploaded. + +When used on a git-debrebase branch, +dgit calls git-debrebase +to prepare the branch +for source package upload and push. .TP \fBdgit push-source\fR [\fIsuite\fP] Without \fB-C\fR, builds a source package and dgit pushes it. Saying \fBdgit push-source\fR is like saying "update the source code in the archive to match my git HEAD, and let the autobuilders do the rest." - -With \fB-C\fR, performs a dgit push, additionally ensuring that no -binary packages are uploaded. .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. @@ -286,6 +291,15 @@ 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. + +When used with a git-debrebase branch, +dgit will ask git-debrebase to prepare patches. +However, +dgit can make patches in some situations where git-debrebase fails, +so dgit quilt-fixup can be useful in its own right. +To always use dgit's own patch generator +instead of git-debrebase make-patches, +pass --git-debrebase=true to dgit. .TP \fBdgit import-dsc\fR [\fIsub-options\fR] \fI../path/to/.dsc\fR [\fB+\fR|\fB..\fR]branch Import a Debian-format source package, @@ -825,6 +839,7 @@ Specifies a single additional option to pass to .BR sbuild , .BR ssh , .BR dgit , +.BR git-debrebase , .BR apt-get , .BR apt-cache , .BR gbp-pq , @@ -872,6 +887,7 @@ Specifies alternative programs to use instead of .BR gpg , .BR ssh , .BR dgit , +.BR git-debrebase , .BR apt-get , .BR apt-cache , .BR git , diff --git a/git-debrebase b/git-debrebase new file mode 100755 index 0000000..216d7df --- /dev/null +++ b/git-debrebase @@ -0,0 +1,1724 @@ +#!/usr/bin/perl -w +# git-debrebase +# Script helping make fast-forwarding histories while still rebasing +# upstream deltas when working on Debian packaging +# +# Copyright (C)2017,2018 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/>. + +END { $? = $Debian::Dgit::ExitStatus::desired // -1; }; +use Debian::Dgit::GDR; +use Debian::Dgit::ExitStatus; + +use strict; + +use Debian::Dgit qw(:DEFAULT :playground); +setup_sigwarn(); + +use Memoize; +use Carp; +use POSIX; +use Data::Dumper; +use Getopt::Long qw(:config posix_default gnu_compat bundling); +use Dpkg::Version; +use File::FnMatch qw(:fnmatch); + +our ($opt_force, $opt_noop_ok, @opt_anchors); +our ($opt_defaultcmd_interactive); + +our $us = qw(git-debrebase); + +sub badusage ($) { + my ($m) = @_; + print STDERR "bad usage: $m\n"; + finish 8; +} + +sub cfg ($;$) { + my ($k, $optional) = @_; + local $/ = "\0"; + my @cmd = qw(git config -z); + push @cmd, qw(--get-all) if wantarray; + push @cmd, $k; + my $out = cmdoutput_errok @cmd; + if (!defined $out) { + fail "missing required git config $k" unless $optional; + return (); + } + my @l = split /\0/, $out; + return wantarray ? @l : $l[0]; +} + +memoize('cfg'); + +sub dd ($) { + my ($v) = @_; + my $dd = new Data::Dumper [ $v ]; + Terse $dd 1; Indent $dd 0; Useqq $dd 1; + return Dump $dd; +} + +sub get_commit ($) { + my ($objid) = @_; + my $data = (git_cat_file $objid, 'commit'); + $data =~ m/(?<=\n)\n/ or die "$objid ($data) ?"; + return ($`,$'); +} + +sub D_UPS () { 0x02; } # upstream files +sub D_PAT_ADD () { 0x04; } # debian/patches/ extra patches at end +sub D_PAT_OTH () { 0x08; } # debian/patches other changes +sub D_DEB_CLOG () { 0x10; } # debian/ (not patches/ or changelog) +sub D_DEB_OTH () { 0x20; } # debian/changelog +sub DS_DEB () { D_DEB_CLOG | D_DEB_OTH; } # debian/ (not patches/) + +our $playprefix = 'debrebase'; +our $rd; +our $workarea; + +our @git = qw(git); + +sub in_workarea ($) { + my ($sub) = @_; + changedir $workarea; + my $r = eval { $sub->(); }; + { local $@; changedir $maindir; } + die $@ if $@; +} + +sub fresh_workarea () { + $workarea = fresh_playground "$playprefix/work"; + in_workarea sub { playtree_setup }; +} + +our $snags_forced = 0; +our $snags_tripped = 0; +our $snags_summarised = 0; +our @deferred_updates; +our @deferred_update_messages; + +sub all_snags_summarised () { + $snags_forced + $snags_tripped == $snags_summarised; +} +sub run_deferred_updates ($) { + my ($mrest) = @_; + + confess 'dangerous internal error' unless all_snags_summarised(); + + my @upd_cmd = (@git, qw(update-ref --stdin -m), "debrebase: $mrest"); + debugcmd '>|', @upd_cmd; + open U, "|-", @upd_cmd or die $!; + foreach (@deferred_updates) { + printdebug ">= ", $_, "\n"; + print U $_, "\n" or die $!; + } + printdebug ">\$\n"; + close U or failedcmd @upd_cmd; + + print $_, "\n" foreach @deferred_update_messages; + + @deferred_updates = (); + @deferred_update_messages = (); +} + +sub get_differs ($$) { + my ($x,$y) = @_; + # This resembles quiltify_trees_differ, in dgit, a bit. + # But we don't care about modes, or dpkg-source-unrepresentable + # changes, and we don't need the plethora of different modes. + # Conversely we need to distinguish different kinds of changes to + # debian/ and debian/patches/. + + my $differs = 0; + + my $rundiff = sub { + my ($opts, $limits, $fn) = @_; + my @cmd = (@git, qw(diff-tree -z --no-renames)); + push @cmd, @$opts; + push @cmd, "$_:" foreach $x, $y; + push @cmd, '--', @$limits; + my $diffs = cmdoutput @cmd; + foreach (split /\0/, $diffs) { $fn->(); } + }; + + $rundiff->([qw(--name-only)], [], sub { + $differs |= $_ eq 'debian' ? DS_DEB : D_UPS; + }); + + if ($differs & DS_DEB) { + $differs &= ~DS_DEB; + $rundiff->([qw(--name-only -r)], [qw(debian)], sub { + $differs |= + m{^debian/patches/} ? D_PAT_OTH : + $_ eq 'debian/changelog' ? D_DEB_CLOG : + D_DEB_OTH; + }); + die "mysterious debian changes $x..$y" + unless $differs & (D_PAT_OTH|DS_DEB); + } + + if ($differs & D_PAT_OTH) { + my $mode; + $differs &= ~D_PAT_OTH; + my $pat_oth = sub { + $differs |= D_PAT_OTH; + no warnings qw(exiting); last; + }; + $rundiff->([qw(--name-status -r)], [qw(debian/patches/)], sub { + no warnings qw(exiting); + if (!defined $mode) { + $mode = $_; next; + } + die unless s{^debian/patches/}{}; + my $ok; + if ($mode eq 'A' && !m/\.series$/s) { + $ok = 1; + } elsif ($mode eq 'M' && $_ eq 'series') { + my $x_s = (git_cat_file "$x:debian/patches/series", 'blob'); + my $y_s = (git_cat_file "$y:debian/patches/series", 'blob'); + chomp $x_s; $x_s .= "\n"; + $ok = $x_s eq substr($y_s, 0, length $x_s); + } else { + # nope + } + $mode = undef; + $differs |= $ok ? D_PAT_ADD : D_PAT_OTH; + }); + die "mysterious debian/patches changes $x..$y" + unless $differs & (D_PAT_ADD|D_PAT_OTH); + } + + printdebug sprintf "get_differs %s, %s = %#x\n", $x, $y, $differs; + + return $differs; +} + +sub commit_pr_info ($) { + my ($r) = @_; + return Data::Dumper->dump([$r], [qw(commit)]); +} + +sub calculate_committer_authline () { + my $c = cmdoutput @git, qw(commit-tree --no-gpg-sign -m), + 'DUMMY COMMIT (git-debrebase)', "HEAD:"; + my ($h,$m) = get_commit $c; + $h =~ m/^committer .*$/m or confess "($h) ?"; + return $&; +} + +sub rm_subdir_cached ($) { + my ($subdir) = @_; + runcmd @git, qw(rm --quiet -rf --cached --ignore-unmatch), $subdir; +} + +sub read_tree_subdir ($$) { + my ($subdir, $new_tree_object) = @_; + rm_subdir_cached $subdir; + runcmd @git, qw(read-tree), "--prefix=$subdir/", $new_tree_object; +} + +sub make_commit ($$) { + my ($parents, $message_paras) = @_; + my $tree = cmdoutput @git, qw(write-tree); + my @cmd = (@git, qw(commit-tree), $tree); + push @cmd, qw(-p), $_ foreach @$parents; + push @cmd, qw(-m), $_ foreach @$message_paras; + return cmdoutput @cmd; +} + +our @snag_force_opts; +sub snag ($$;@) { + my ($tag,$msg) = @_; # ignores extra args, for benefit of keycommits + if (grep { $_ eq $tag } @snag_force_opts) { + $snags_forced++; + print STDERR "git-debrebase: snag ignored (-f$tag): $msg\n"; + } else { + $snags_tripped++; + print STDERR "git-debrebase: snag detected (-f$tag): $msg\n"; + } +} + +# Important: all mainline code must call snags_maybe_bail after +# any point where snag might be called, but before making changes +# (eg before any call to run_deferred_updates). snags_maybe_bail +# may be called more than once if necessary (but this is not ideal +# because then the messages about number of snags may be confusing). +sub snags_maybe_bail () { + return if all_snags_summarised(); + if ($snags_forced) { + printf STDERR + "%s: snags: %d overriden by individual -f options\n", + $us, $snags_forced; + } + if ($snags_tripped) { + if ($opt_force) { + printf STDERR + "%s: snags: %d overriden by global --force\n", + $us, $snags_tripped; + } else { + fail sprintf + "%s: snags: %d blockers (you could -f<tag>, or --force)", + $us, $snags_tripped; + } + } + $snags_summarised = $snags_forced + $snags_tripped; +} +sub any_snags () { + return $snags_forced || $snags_tripped; +} + +# classify returns an info hash like this +# CommitId => $objid +# Hdr => # commit headers, including 1 final newline +# Msg => # commit message (so one newline is dropped) +# Tree => $treeobjid +# Type => (see below) +# Parents = [ { +# Ix => $index # ie 0, 1, 2, ... +# CommitId +# Differs => return value from get_differs +# IsOrigin +# IsDggitImport => 'orig' 'tarball' 'unpatched' 'package' (as from dgit) +# } ...] +# NewMsg => # commit message, but with any [dgit import ...] edited +# # to say "[was: ...]" +# +# Types: +# Packaging +# Changelog +# Upstream +# AddPatches +# Mixed +# +# Pseudomerge +# has additional entres in classification result +# Overwritten = [ subset of Parents ] +# Contributor = $the_remaining_Parent +# +# DgitImportUnpatched +# has additional entry in classification result +# OrigParents = [ subset of Parents ] +# +# Anchor +# has additional entry in classification result +# OrigParents = [ subset of Parents ] # singleton list +# +# TreatAsAnchor +# +# BreakwaterStart +# +# Unknown +# has additional entry in classification result +# Why => "prose" + +sub parsecommit ($;$) { + my ($objid, $p_ref) = @_; + # => hash with CommitId Hdr Msg Tree Parents + # Parents entries have only Ix CommitId + # $p_ref, if provided, must be [] and is used as a base for Parents + + $p_ref //= []; + die if @$p_ref; + + my ($h,$m) = get_commit $objid; + + my ($t) = $h =~ m/^tree (\w+)$/m or die $objid; + my (@ph) = $h =~ m/^parent (\w+)$/mg; + + my $r = { + CommitId => $objid, + Hdr => $h, + Msg => $m, + Tree => $t, + Parents => $p_ref, + }; + + foreach my $ph (@ph) { + push @$p_ref, { + Ix => scalar @$p_ref, + CommitId => $ph, + }; + } + + return $r; +} + +sub classify ($) { + my ($objid) = @_; + + my @p; + my $r = parsecommit($objid, \@p); + my $t = $r->{Tree}; + + foreach my $p (@p) { + $p->{Differs} = (get_differs $p->{CommitId}, $t), + } + + printdebug "classify $objid \$t=$t \@p", + (map { sprintf " %s/%#x", $_->{CommitId}, $_->{Differs} } @p), + "\n"; + + my $classify = sub { + my ($type, @rest) = @_; + $r = { %$r, Type => $type, @rest }; + if ($debuglevel) { + printdebug " = $type ".(dd $r)."\n"; + } + return $r; + }; + my $unknown = sub { + my ($why) = @_; + $r = { %$r, Type => qw(Unknown), Why => $why }; + printdebug " ** Unknown\n"; + return $r; + }; + + if (grep { $_ eq $objid } @opt_anchors) { + return $classify->('TreatAsAnchor'); + } + + my @identical = grep { !$_->{Differs} } @p; + my ($stype, $series) = git_cat_file "$t:debian/patches/series"; + my $haspatches = $stype ne 'missing' && $series =~ m/^\s*[^#\n\t ]/m; + + if ($r->{Msg} =~ m{^\[git-debrebase anchor.*\]$}m) { + # multi-orig upstreams are represented with an anchor merge + # from a single upstream commit which combines the orig tarballs + + # Every anchor tagged this way must be a merge. + # We are relying on the + # [git-debrebase anchor: ...] + # commit message annotation in "declare" anchor merges (which + # do not have any upstream changes), to distinguish those + # anchor merges from ordinary pseudomerges (which we might + # just try to strip). + # + # However, the user is going to be doing git-rebase a lot. We + # really don't want them to rewrite an anchor commit. + # git-rebase trips up on merges, so that is a useful safety + # catch. + # + # BreakwaterStart commits are also anchors in the terminology + # of git-debrebase(5), but they are untagged (and always + # manually generated). + # + # We cannot not tolerate any tagged linear commit (ie, + # BreakwaterStart commits tagged `[anchor:') because such a + # thing could result from an erroneous linearising raw git + # rebase of a merge anchor. That would represent a corruption + # of the branch. and we want to detect and reject the results + # of such corruption before it makes it out anywhere. If we + # reject it here then we avoid making the pseudomerge which + # would be needed to push it. + + my $badanchor = sub { $unknown->("git-debrebase \`anchor' but @_"); }; + @p == 2 or return $badanchor->("has other than two parents"); + $haspatches and return $badanchor->("contains debian/patches"); + + # How to decide about l/r ordering of anchors ? git + # --topo-order prefers to expand 2nd parent first. There's + # already an easy rune to look for debian/ history anyway (git log + # debian/) so debian breakwater branch should be 1st parent; that + # way also there's also an easy rune to look for the upstream + # patches (--topo-order). + + # Also this makes --first-parent be slightly more likely to + # be useful - it makes it provide a linearised breakwater history. + + # Of course one can say somthing like + # gitk -- ':/' ':!/debian' + # to get _just_ the commits touching upstream files, and by + # the TREESAME logic in git-rev-list this will leave the + # breakwater into upstream at the first anchor. But that + # doesn't report debian/ changes at all. + + # Other observations about gitk: by default, gitk seems to + # produce output in a different order to git-rev-list. I + # can't seem to find this documented anywhere. gitk + # --date-order DTRT. But, gitk always seems to put the + # parents from left to right, in order, so it's easy to see + # which way round a pseudomerge is. + + $p[0]{IsOrigin} and $badanchor->("is an origin commit"); + $p[1]{Differs} & ~DS_DEB and + $badanchor->("upstream files differ from left parent"); + $p[0]{Differs} & ~D_UPS and + $badanchor->("debian/ differs from right parent"); + + return $classify->(qw(Anchor), + OrigParents => [ $p[1] ]); + } + + if (@p == 1) { + my $d = $r->{Parents}[0]{Differs}; + if ($d == D_PAT_ADD) { + return $classify->(qw(AddPatches)); + } elsif ($d & (D_PAT_ADD|D_PAT_OTH)) { + return $unknown->("edits debian/patches"); + } elsif ($d & DS_DEB and !($d & ~DS_DEB)) { + my ($ty,$dummy) = git_cat_file "$p[0]{CommitId}:debian"; + if ($ty eq 'tree') { + if ($d == D_DEB_CLOG) { + return $classify->(qw(Changelog)); + } else { + return $classify->(qw(Packaging)); + } + } elsif ($ty eq 'missing') { + return $classify->(qw(BreakwaterStart)); + } else { + return $unknown->("parent's debian is not a directory"); + } + } elsif ($d == D_UPS) { + return $classify->(qw(Upstream)); + } elsif ($d & DS_DEB and $d & D_UPS and !($d & ~(DS_DEB|D_UPS))) { + return $classify->(qw(Mixed)); + } elsif ($d == 0) { + return $unknown->("no changes"); + } else { + confess "internal error $objid ?"; + } + } + if (!@p) { + return $unknown->("origin commit"); + } + + if (@p == 2 && @identical == 1) { + my @overwritten = grep { $_->{Differs} } @p; + confess "internal error $objid ?" unless @overwritten==1; + return $classify->(qw(Pseudomerge), + Overwritten => [ $overwritten[0] ], + Contributor => $identical[0]); + } + if (@p == 2 && @identical == 2) { + my $get_t = sub { + my ($ph,$pm) = get_commit $_[0]{CommitId}; + $ph =~ m/^committer .* (\d+) [-+]\d+$/m or die "$_->{CommitId} ?"; + $1; + }; + my @bytime = @p; + my $order = $get_t->($bytime[0]) <=> $get_t->($bytime[1]); + if ($order > 0) { # newer first + } elsif ($order < 0) { + @bytime = reverse @bytime; + } else { + # same age, default to order made by -s ours + # that is, commit was made by someone who preferred L + } + return $classify->(qw(Pseudomerge), + SubType => qw(Ambiguous), + Contributor => $bytime[0], + Overwritten => [ $bytime[1] ]); + } + foreach my $p (@p) { + my ($p_h, $p_m) = get_commit $p->{CommitId}; + $p->{IsOrigin} = $p_h !~ m/^parent \w+$/m; + ($p->{IsDgitImport},) = $p_m =~ m/^\[dgit import ([0-9a-z]+) .*\]$/m; + } + my @orig_ps = grep { ($_->{IsDgitImport}//'X') eq 'orig' } @p; + my $m2 = $r->{Msg}; + if (!(grep { !$_->{IsOrigin} } @p) and + (@orig_ps >= @p - 1) and + $m2 =~ s{^\[(dgit import unpatched .*)\]$}{[was: $1]}m) { + $r->{NewMsg} = $m2; + return $classify->(qw(DgitImportUnpatched), + OrigParents => \@orig_ps); + } + + return $unknown->("complex merge"); +} + +sub keycommits ($;$$$$) { + my ($head, $furniture, $unclean, $trouble, $fatal) = @_; + # => ($anchor, $breakwater) + + # $unclean->("unclean-$tagsfx", $msg, $cl) + # $furniture->("unclean-$tagsfx", $msg, $cl) + # $dgitimport->("unclean-$tagsfx", $msg, $cl)) + # is callled for each situation or commit that + # wouldn't be found in a laundered branch + # $furniture is for furniture commits such as might be found on an + # interchange branch (pseudomerge, d/patches, changelog) + # $trouble is for things whnich prevent the return of + # anchor and breakwater information; if that is ignored, + # then keycommits returns (undef, undef) instead. + # $fatal is for unprocessable commits, and should normally cause + # a failure. If ignored, agaion, (undef, undef) is returned. + # + # If a callback is undef, fail is called instead. + # If a callback is defined but false, the situation is ignored. + # Callbacks may say: + # no warnings qw(exiting); last; + # if the answer is no longer wanted. + + my ($anchor, $breakwater); + my $clogonly; + my $cl; + $fatal //= sub { fail $_[2]; }; + my $x = sub { + my ($cb, $tagsfx, $why) = @_; + my $m = "branch needs laundering (run git-debrebase): $why"; + fail $m unless defined $cb; + return unless $cb; + $cb->("unclean-$tagsfx", $why, $cl); + }; + for (;;) { + $cl = classify $head; + my $ty = $cl->{Type}; + if ($ty eq 'Packaging') { + $breakwater //= $clogonly; + $breakwater //= $head; + } elsif ($ty eq 'Changelog') { + # this is going to count as the tip of the breakwater + # only if it has no upstream stuff before it + $clogonly //= $head; + } elsif ($ty eq 'Anchor' or + $ty eq 'TreatAsAnchor' or + $ty eq 'BreakwaterStart') { + $anchor = $head; + $breakwater //= $clogonly; + $breakwater //= $head; + last; + } elsif ($ty eq 'Upstream') { + $x->($unclean, 'ordering', + "packaging change ($breakwater) follows upstream change (eg $head)") + if defined $breakwater; + $clogonly = undef; + $breakwater = undef; + } elsif ($ty eq 'Mixed') { + $x->($unclean, 'mixed', + "found mixed upstream/packaging commit ($head)"); + $clogonly = undef; + $breakwater = undef; + } elsif ($ty eq 'Pseudomerge' or + $ty eq 'AddPatches') { + $x->($furniture, (lc $ty), + "found interchange bureaucracy commit ($ty, $head)"); + } elsif ($ty eq 'DgitImportUnpatched') { + $x->($trouble, 'dgitimport', + "found dgit dsc import ($head)"); + return (undef,undef); + } else { + $x->($fatal, 'unprocessable', + "found unprocessable commit, cannot cope: $head; $cl->{Why}" + ); + return (undef,undef); + } + $head = $cl->{Parents}[0]{CommitId}; + } + return ($anchor, $breakwater); +} + +sub walk ($;$$); +sub walk ($;$$) { + my ($input, + $nogenerate,$report) = @_; + # => ($tip, $breakwater_tip, $last_anchor) + # (or nothing, if $nogenerate) + + printdebug "*** WALK $input ".($nogenerate//0)." ".($report//'-')."\n"; + + # go through commits backwards + # we generate two lists of commits to apply: + # breakwater branch and upstream patches + my (@brw_cl, @upp_cl, @processed); + my %found; + my $upp_limit; + my @pseudomerges; + + my $cl; + my $xmsg = sub { + my ($prose, $info) = @_; + my $ms = $cl->{Msg}; + chomp $ms; + $info //= ''; + $ms .= "\n\n[git-debrebase$info: $prose]\n"; + return (Msg => $ms); + }; + my $rewrite_from_here = sub { + my ($cl) = @_; + my $sp_cl = { SpecialMethod => 'StartRewrite' }; + push @$cl, $sp_cl; + push @processed, $sp_cl; + }; + my $cur = $input; + + my $prdelim = ""; + my $prprdelim = sub { print $report $prdelim if $report; $prdelim=""; }; + + my $prline = sub { + return unless $report; + print $report $prdelim, @_; + $prdelim = "\n"; + }; + + my $bomb = sub { # usage: return $bomb->(); + print $report " Unprocessable" if $report; + print $report " ($cl->{Why})" if $report && defined $cl->{Why}; + $prprdelim->(); + if ($nogenerate) { + return (undef,undef); + } + die "commit $cur: Cannot cope with this commit (d.". + (join ' ', map { sprintf "%#x", $_->{Differs} } + @{ $cl->{Parents} }). + (defined $cl->{Why} ? "; $cl->{Why}": ''). + ")"; + }; + + my $build; + my $breakwater; + + my $build_start = sub { + my ($msg, $parent) = @_; + $prline->(" $msg"); + $build = $parent; + no warnings qw(exiting); last; + }; + + my $last_anchor; + + for (;;) { + $cl = classify $cur; + my $ty = $cl->{Type}; + my $st = $cl->{SubType}; + $prline->("$cl->{CommitId} $cl->{Type}"); + $found{$ty. ( defined($st) ? "-$st" : '' )}++; + push @processed, $cl; + my $p0 = @{ $cl->{Parents} }==1 ? $cl->{Parents}[0]{CommitId} : undef; + if ($ty eq 'AddPatches') { + $cur = $p0; + $rewrite_from_here->(\@upp_cl); + next; + } elsif ($ty eq 'Packaging' or $ty eq 'Changelog') { + push @brw_cl, $cl; + $cur = $p0; + next; + } elsif ($ty eq 'BreakwaterStart') { + $last_anchor = $cur; + $build_start->('FirstPackaging', $cur); + } elsif ($ty eq 'Upstream') { + push @upp_cl, $cl; + $cur = $p0; + next; + } elsif ($ty eq 'Mixed') { + my $queue = sub { + my ($q, $wh) = @_; + my $cls = { %$cl, $xmsg->("split mixed commit: $wh part") }; + push @$q, $cls; + }; + $queue->(\@brw_cl, "debian"); + $queue->(\@upp_cl, "upstream"); + $rewrite_from_here->(\@brw_cl); + $cur = $p0; + next; + } elsif ($ty eq 'Pseudomerge') { + my $contrib = $cl->{Contributor}{CommitId}; + print $report " Contributor=$contrib" if $report; + push @pseudomerges, $cl; + $rewrite_from_here->(\@upp_cl); + $cur = $contrib; + next; + } elsif ($ty eq 'Anchor' or $ty eq 'TreatAsAnchor') { + $last_anchor = $cur; + $build_start->("Anchor", $cur); + } elsif ($ty eq 'DgitImportUnpatched') { + my $pm = $pseudomerges[-1]; + if (defined $pm) { + # To an extent, this is heuristic. Imports don't have + # a useful history of the debian/ branch. We assume + # that the first pseudomerge after an import has a + # useful history of debian/, and ignore the histories + # from later pseudomerges. Often the first pseudomerge + # will be the dgit import of the upload to the actual + # suite intended by the non-dgit NMUer, and later + # pseudomerges may represent in-archive copies. + my $ovwrs = $pm->{Overwritten}; + printf $report " PM=%s \@Overwr:%d", + $pm->{CommitId}, (scalar @$ovwrs) + if $report; + if (@$ovwrs != 1) { + printdebug "*** WALK BOMB DgitImportUnpatched\n"; + return $bomb->(); + } + my $ovwr = $ovwrs->[0]{CommitId}; + printf $report " Overwr=%s", $ovwr if $report; + # This import has a tree which is just like a + # breakwater tree, but it has the wrong history. It + # ought to have the previous breakwater (which the + # pseudomerge overwrote) as an ancestor. That will + # make the history of the debian/ files correct. As + # for the upstream version: either it's the same as + # was ovewritten (ie, same as the previous + # breakwater), in which case that history is precisely + # right; or, otherwise, it was a non-gitish upload of a + # new upstream version. We can tell these apart by + # looking at the tree of the supposed upstream. + push @brw_cl, { + %$cl, + SpecialMethod => 'DgitImportDebianUpdate', + $xmsg->("convert dgit import: debian changes") + }, { + %$cl, + SpecialMethod => 'DgitImportUpstreamUpdate', + $xmsg->("convert dgit import: upstream update", + " anchor") + }; + $prline->(" Import"); + $rewrite_from_here->(\@brw_cl); + $upp_limit //= $#upp_cl; # further, deeper, patches discarded + $cur = $ovwr; + next; + } else { + # Everything is from this import. This kind of import + # is already in valid breakwater format, with the + # patches as commits. + printf $report " NoPM" if $report; + # last thing we processed will have been the first patch, + # if there is one; which is fine, so no need to rewrite + # on account of this import + $build_start->("ImportOrigin", $cur); + } + die "$ty ?"; + } else { + printdebug "*** WALK BOMB unrecognised\n"; + return $bomb->(); + } + } + $prprdelim->(); + + printdebug "*** WALK prep done cur=$cur". + " brw $#brw_cl upp $#upp_cl proc $#processed pm $#pseudomerges\n"; + + return if $nogenerate; + + # Now we build it back up again + + fresh_workarea(); + + my $rewriting = 0; + + my $read_tree_debian = sub { + my ($treeish) = @_; + read_tree_subdir 'debian', "$treeish:debian"; + rm_subdir_cached 'debian/patches'; + }; + my $read_tree_upstream = sub { + my ($treeish) = @_; + runcmd @git, qw(read-tree), $treeish; + $read_tree_debian->($build); + }; + + $#upp_cl = $upp_limit if defined $upp_limit; + + my $committer_authline = calculate_committer_authline(); + + printdebug "WALK REBUILD $build ".(scalar @processed)."\n"; + + confess "internal error" unless $build eq (pop @processed)->{CommitId}; + + in_workarea sub { + mkdir $rd or $!==EEXIST or die $!; + my $current_method; + runcmd @git, qw(read-tree), $build; + foreach my $cl (qw(Debian), (reverse @brw_cl), + { SpecialMethod => 'RecordBreakwaterTip' }, + qw(Upstream), (reverse @upp_cl)) { + if (!ref $cl) { + $current_method = $cl; + next; + } + my $method = $cl->{SpecialMethod} // $current_method; + my @parents = ($build); + my $cltree = $cl->{CommitId}; + printdebug "WALK BUILD ".($cltree//'undef'). + " $method (rewriting=$rewriting)\n"; + if ($method eq 'Debian') { + $read_tree_debian->($cltree); + } elsif ($method eq 'Upstream') { + $read_tree_upstream->($cltree); + } elsif ($method eq 'StartRewrite') { + $rewriting = 1; + next; + } elsif ($method eq 'RecordBreakwaterTip') { + $breakwater = $build; + next; + } elsif ($method eq 'DgitImportDebianUpdate') { + $read_tree_debian->($cltree); + } elsif ($method eq 'DgitImportUpstreamUpdate') { + confess unless $rewriting; + my $differs = (get_differs $build, $cltree); + next unless $differs & D_UPS; + $read_tree_upstream->($cltree); + push @parents, map { $_->{CommitId} } @{ $cl->{OrigParents} }; + } else { + confess "$method ?"; + } + if (!$rewriting) { + my $procd = (pop @processed) // 'UNDEF'; + if ($cl ne $procd) { + $rewriting = 1; + printdebug "WALK REWRITING NOW cl=$cl procd=$procd\n"; + } + } + my $newtree = cmdoutput @git, qw(write-tree); + my $ch = $cl->{Hdr}; + $ch =~ s{^tree .*}{tree $newtree}m or confess "$ch ?"; + $ch =~ s{^parent .*\n}{}mg; + $ch =~ s{(?=^author)}{ + join '', map { "parent $_\n" } @parents + }me or confess "$ch ?"; + if ($rewriting) { + $ch =~ s{^committer .*$}{$committer_authline}m + or confess "$ch ?"; + } + my $cf = "$rd/m$rewriting"; + open CD, ">", $cf or die $!; + print CD $ch, "\n", $cl->{Msg} or die $!; + close CD or die $!; + my @cmd = (@git, qw(hash-object)); + push @cmd, qw(-w) if $rewriting; + push @cmd, qw(-t commit), $cf; + my $newcommit = cmdoutput @cmd; + confess "$ch ?" unless $rewriting or $newcommit eq $cl->{CommitId}; + $build = $newcommit; + if (grep { $method eq $_ } qw(DgitImportUpstreamUpdate)) { + $last_anchor = $cur; + } + } + }; + + my $final_check = get_differs $build, $input; + die sprintf "internal error %#x %s %s", $final_check, $build, $input + if $final_check & ~D_PAT_ADD; + + my @r = ($build, $breakwater, $last_anchor); + printdebug "*** WALK RETURN @r\n"; + return @r +} + +sub get_head () { + git_check_unmodified(); + return git_rev_parse qw(HEAD); +} + +sub update_head ($$$) { + my ($old, $new, $mrest) = @_; + push @deferred_updates, "update HEAD $new $old"; + run_deferred_updates $mrest; +} + +sub update_head_checkout ($$$) { + my ($old, $new, $mrest) = @_; + update_head $old, $new, $mrest; + runcmd @git, qw(reset --hard); +} + +sub update_head_postlaunder ($$$) { + my ($old, $tip, $reflogmsg) = @_; + return if $tip eq $old; + print "git-debrebase: laundered (head was $old)\n"; + update_head $old, $tip, $reflogmsg; + # no tree changes except debian/patches + runcmd @git, qw(rm --quiet --ignore-unmatch -rf debian/patches); +} + +sub do_launder_head ($) { + my ($reflogmsg) = @_; + my $old = get_head(); + record_ffq_auto(); + my ($tip,$breakwater) = walk $old; + snags_maybe_bail(); + update_head_postlaunder $old, $tip, $reflogmsg; + return ($tip,$breakwater); +} + +sub cmd_launder_v0 () { + badusage "no arguments to launder-v0 allowed" if @ARGV; + my $old = get_head(); + my ($tip,$breakwater,$last_anchor) = walk $old; + update_head_postlaunder $old, $tip, 'launder'; + printf "# breakwater tip\n%s\n", $breakwater; + printf "# working tip\n%s\n", $tip; + printf "# last anchor\n%s\n", $last_anchor; +} + +sub defaultcmd_rebase () { + push @ARGV, @{ $opt_defaultcmd_interactive // [] }; + my ($tip,$breakwater) = do_launder_head 'launder for rebase'; + runcmd @git, qw(rebase), @ARGV, $breakwater if @ARGV; +} + +sub cmd_analyse () { + die if ($ARGV[0]//'') =~ m/^-/; + badusage "too many arguments to analyse" if @ARGV>1; + my ($old) = @ARGV; + if (defined $old) { + $old = git_rev_parse $old; + } else { + $old = git_rev_parse 'HEAD'; + } + my ($dummy,$breakwater) = walk $old, 1,*STDOUT; + STDOUT->error and die $!; +} + +sub ffq_prev_branchinfo () { + my $current = git_get_symref(); + return gdr_ffq_prev_branchinfo($current); +} + +sub record_ffq_prev_deferred () { + # => ('status', "message") + # 'status' may be + # deferred message is undef + # exists + # detached + # weird-symref + # notbranch + # if not ff from some branch we should be ff from, is an snag + # if "deferred", will have added something about that to + # @deferred_update_messages, and also maybe printed (already) + # some messages about ff checks + my ($status, $message, $current, $ffq_prev, $gdrlast) + = ffq_prev_branchinfo(); + return ($status, $message) unless $status eq 'branch'; + + my $currentval = get_head(); + + my $exists = git_get_ref $ffq_prev; + return ('exists',"$ffq_prev already exists") if $exists; + + return ('not-branch', 'HEAD symref is not to refs/heads/') + unless $current =~ m{^refs/heads/}; + my $branch = $'; + + my @check_specs = split /\;/, (cfg "branch.$branch.ffq-ffrefs",1) // '*'; + my %checked; + + printdebug "ffq check_specs @check_specs\n"; + + my $check = sub { + my ($lrref, $desc) = @_; + printdebug "ffq might check $lrref ($desc)\n"; + my $invert; + for my $chk (@check_specs) { + my $glob = $chk; + $invert = $glob =~ s{^[!^]}{}; + last if fnmatch $glob, $lrref; + } + return if $invert; + my $lrval = git_get_ref $lrref; + return unless length $lrval; + + if (is_fast_fwd $lrval, $currentval) { + print "OK, you are ahead of $lrref\n" or die $!; + $checked{$lrref} = 1; + } elsif (is_fast_fwd $currentval, $lrval) { + $checked{$lrref} = -1; + snag 'behind', "you are behind $lrref, divergence risk"; + } else { + $checked{$lrref} = -1; + snag 'diverged', "you have diverged from $lrref"; + } + }; + + my $merge = cfg "branch.$branch.merge",1; + if (defined $merge and $merge =~ m{^refs/heads/}) { + my $rhs = $'; + printdebug "ffq merge $rhs\n"; + my $check_remote = sub { + my ($remote, $desc) = @_; + printdebug "ffq check_remote ".($remote//'undef')." $desc\n"; + return unless defined $remote; + $check->("refs/remotes/$remote/$rhs", $desc); + }; + $check_remote->((scalar cfg "branch.$branch.remote",1), + 'remote fetch/merge branch'); + $check_remote->((scalar cfg "branch.$branch.pushRemote",1) // + (scalar cfg "branch.$branch.pushDefault",1), + 'remote push branch'); + } + if ($branch =~ m{^dgit/}) { + $check->("refs/remotes/dgit/$branch", 'remote dgit branch'); + } elsif ($branch =~ m{^master$}) { + $check->("refs/remotes/dgit/dgit/sid", 'remote dgit branch for sid'); + } + + snags_maybe_bail(); + + push @deferred_updates, "update $ffq_prev $currentval $git_null_obj"; + push @deferred_updates, "delete $gdrlast"; + push @deferred_update_messages, "Recorded current head for preservation"; + return ('deferred', undef); +} + +sub record_ffq_auto () { + my ($status, $message) = record_ffq_prev_deferred(); + if ($status eq 'deferred' || $status eq 'exists') { + } else { + snag $status, "could not record ffq-prev: $message"; + snags_maybe_bail(); + } +} + +sub ffq_prev_info () { + # => ($ffq_prev, $gdrlast, $ffq_prev_commitish) + my ($status, $message, $current, $ffq_prev, $gdrlast) + = ffq_prev_branchinfo(); + if ($status ne 'branch') { + snag $status, "could not check ffq-prev: $message"; + snags_maybe_bail(); + } + my $ffq_prev_commitish = $ffq_prev && git_get_ref $ffq_prev; + return ($ffq_prev, $gdrlast, $ffq_prev_commitish); +} + +sub stitch ($$$$$) { + my ($old_head, $ffq_prev, $gdrlast, $ffq_prev_commitish, $prose) = @_; + + push @deferred_updates, "delete $ffq_prev $ffq_prev_commitish"; + + if (is_fast_fwd $old_head, $ffq_prev_commitish) { + my $differs = get_differs $old_head, $ffq_prev_commitish; + unless ($differs & ~D_PAT_ADD) { + # ffq-prev is ahead of us, and the only tree changes it has + # are possibly addition of things in debian/patches/. + # Just wind forwards rather than making a pointless pseudomerge. + push @deferred_updates, + "update $gdrlast $ffq_prev_commitish $git_null_obj"; + update_head_checkout $old_head, $ffq_prev_commitish, + "stitch (fast forward)"; + return; + } + } + fresh_workarea(); + # We make pseudomerges with L as the contributing parent. + # This makes git rev-list --first-parent work properly. + my $new_head = make_commit [ $old_head, $ffq_prev ], [ + 'Declare fast forward / record previous work', + "[git-debrebase pseudomerge: $prose]", + ]; + push @deferred_updates, "update $gdrlast $new_head $git_null_obj"; + update_head $old_head, $new_head, "stitch: $prose"; +} + +sub do_stitch ($;$) { + my ($prose, $unclean) = @_; + + my ($ffq_prev, $gdrlast, $ffq_prev_commitish) = ffq_prev_info(); + if (!$ffq_prev_commitish) { + fail "No ffq-prev to stitch." unless $opt_noop_ok; + return; + } + my $dangling_head = get_head(); + + keycommits $dangling_head, $unclean,$unclean,$unclean; + snags_maybe_bail(); + + stitch($dangling_head, $ffq_prev, $gdrlast, $ffq_prev_commitish, $prose); +} + +sub cmd_new_upstream_v0 () { + # automatically and unconditionally launders before rebasing + # if rebase --abort is used, laundering has still been done + + my %pieces; + + badusage "need NEW-VERSION [UPS-COMMITTISH]" unless @ARGV >= 1; + + # parse args - low commitment + my $new_version = (new Dpkg::Version scalar(shift @ARGV), check => 1); + my $new_upstream_version = $new_version->version(); + + my $new_upstream = shift @ARGV; + if (!defined $new_upstream) { + my @tried; + # todo: at some point maybe use git-deborig to do this + foreach my $tagpfx ('', 'v', 'upstream/') { + my $tag = $tagpfx.(dep14_version_mangle $new_upstream_version); + $new_upstream = git_get_ref "refs/tags/$tag"; + last if length $new_upstream; + push @tried, $tag; + } + if (!length $new_upstream) { + fail "Could not determine appropriate upstream commitish.\n". + " (Tried these tags: @tried)\n". + " Check version, and specify upstream commitish explicitly."; + } + } + $new_upstream = git_rev_parse $new_upstream; + + record_ffq_auto(); + + my $piece = sub { + my ($n, @x) = @_; # may be '' + my $pc = $pieces{$n} //= { + Name => $n, + Desc => ($n ? "upstream piece \`$n'" : "upstream (main piece"), + }; + while (my $k = shift @x) { $pc->{$k} = shift @x; } + $pc; + }; + + my @newpieces; + my $newpiece = sub { + my ($n, @x) = @_; # may be '' + my $pc = $piece->($n, @x, NewIx => (scalar @newpieces)); + push @newpieces, $pc; + }; + + $newpiece->('', + OldIx => 0, + New => $new_upstream, + ); + while (@ARGV && $ARGV[0] !~ m{^-}) { + my $n = shift @ARGV; + + badusage "for each EXTRA-UPS-NAME need EXTRA-UPS-COMMITISH" + unless @ARGV && $ARGV[0] !~ m{^-}; + + my $c = git_rev_parse shift @ARGV; + die unless $n =~ m/^$extra_orig_namepart_re$/; + $newpiece->($n, New => $c); + } + + # now we need to investigate the branch this generates the + # laundered version but we don't switch to it yet + my $old_head = get_head(); + my ($old_laundered_tip,$old_bw,$old_anchor) = walk $old_head; + + my $old_bw_cl = classify $old_bw; + my $old_anchor_cl = classify $old_anchor; + my $old_upstream; + if (!$old_anchor_cl->{OrigParents}) { + snag 'anchor-treated', + 'old anchor is recognised due to --anchor, cannot check upstream'; + } else { + $old_upstream = parsecommit + $old_anchor_cl->{OrigParents}[0]{CommitId}; + $piece->('', Old => $old_upstream->{CommitId}); + } + + if ($old_upstream && $old_upstream->{Msg} =~ m{^\[git-debrebase }m) { + if ($old_upstream->{Msg} =~ + m{^\[git-debrebase upstream-combine (\.(?: $extra_orig_namepart_re)+)\:.*\]$}m + ) { + my @oldpieces = (split / /, $1); + my $old_n_parents = scalar @{ $old_upstream->{Parents} }; + if (@oldpieces != $old_n_parents) { + snag 'upstream-confusing', sprintf + "previous upstream combine %s". + " mentions %d pieces (each implying one orig commit)". + " but has %d parents", + $old_upstream->{CommitId}, + (scalar @oldpieces), + $old_n_parents; + } elsif ($oldpieces[0] ne '.') { + snag 'upstream-confusing', sprintf + "previous upstream combine %s". + " first piece is not \`.'", + $oldpieces[0]; + } else { + $oldpieces[0] = ''; + foreach my $i (0..$#oldpieces) { + my $n = $oldpieces[$i]; + $piece->($n, Old => $old_upstream->{CommitId}.'^'.($i+1)); + } + } + } else { + snag 'upstream-confusing', + "previous upstream $old_upstream->{CommitId} is from". + " git-debrebase but not an \`upstream-combine' commit"; + } + } + + foreach my $pc (values %pieces) { + if (!$old_upstream) { + # we have complained already + } elsif (!$pc->{Old}) { + snag 'upstream-new-piece', + "introducing upstream piece \`$pc->{Name}'"; + } elsif (!$pc->{New}) { + snag 'upstream-rm-piece', + "dropping upstream piece \`$pc->{Name}'"; + } elsif (!is_fast_fwd $pc->{Old}, $pc->{New}) { + snag 'upstream-not-ff', + "not fast forward: $pc->{Name} $pc->{Old}..$pc->{New}"; + } + } + + printdebug "%pieces = ", (dd \%pieces), "\n"; + printdebug "\@newpieces = ", (dd \@newpieces), "\n"; + + snags_maybe_bail(); + + my $new_bw; + + fresh_workarea(); + in_workarea sub { + my @upstream_merge_parents; + + if (!any_snags()) { + push @upstream_merge_parents, $old_upstream->{CommitId}; + } + + foreach my $pc (@newpieces) { # always has '' first + if ($pc->{Name}) { + read_tree_subdir $pc->{Name}, $pc->{New}; + } else { + runcmd @git, qw(read-tree), $pc->{New}; + } + push @upstream_merge_parents, $pc->{New}; + } + + # index now contains the new upstream + + if (@newpieces > 1) { + # need to make the upstream subtree merge commit + $new_upstream = make_commit \@upstream_merge_parents, + [ "Combine upstreams for $new_upstream_version", + ("[git-debrebase upstream-combine . ". + (join " ", map { $_->{Name} } @newpieces[1..$#newpieces]). + ": new upstream]"), + ]; + } + + # $new_upstream is either the single upstream commit, or the + # combined commit we just made. Either way it will be the + # "upstream" parent of the anchor merge. + + read_tree_subdir 'debian', "$old_bw:debian"; + + # index now contains the anchor merge contents + $new_bw = make_commit [ $old_bw, $new_upstream ], + [ "Update to upstream $new_upstream_version", + "[git-debrebase anchor: new upstream $new_upstream_version, merge]", + ]; + + my $clogsignoff = cmdoutput qw(git show), + '--pretty=format:%an <%ae> %aD', + $new_bw; + + # Now we have to add a changelog stanza so the Debian version + # is right. + die if unlink "debian"; + die $! unless $!==ENOENT or $!==ENOTEMPTY; + unlink "debian/changelog" or $!==ENOENT or die $!; + mkdir "debian" or die $!; + open CN, ">", "debian/changelog" or die $!; + my $oldclog = git_cat_file ":debian/changelog"; + $oldclog =~ m/^($package_re) \(\S+\) / or + fail "cannot parse old changelog to get package name"; + my $p = $1; + print CN <<END, $oldclog or die $!; +$p ($new_version) UNRELEASED; urgency=medium + + * Update to new upstream version $new_upstream_version. + + -- $clogsignoff + +END + close CN or die $!; + runcmd @git, qw(update-index --add --replace), 'debian/changelog'; + + # Now we have the final new breakwater branch in the index + $new_bw = make_commit [ $new_bw ], + [ "Update changelog for new upstream $new_upstream_version", + "[git-debrebase: new upstream $new_upstream_version, changelog]", + ]; + }; + + # we have constructed the new breakwater. we now need to commit to + # the laundering output, because git-rebase can't easily be made + # to make a replay list which is based on some other branch + + update_head_postlaunder $old_head, $old_laundered_tip, + 'launder for new upstream'; + + my @cmd = (@git, qw(rebase --onto), $new_bw, $old_bw, @ARGV); + runcmd @cmd; + # now it's for the user to sort out +} + +sub cmd_record_ffq_prev () { + badusage "no arguments allowed" if @ARGV; + my ($status, $msg) = record_ffq_prev_deferred(); + if ($status eq 'exists' && $opt_noop_ok) { + print "Previous head already recorded\n" or die $!; + } elsif ($status eq 'deferred') { + run_deferred_updates 'record-ffq-prev'; + } else { + fail "Could not preserve: $msg"; + } +} + +sub cmd_anchor () { + badusage "no arguments allowed" if @ARGV; + my ($anchor, $bw) = keycommits +(git_rev_parse 'HEAD'), 0,0; + print "$bw\n" or die $!; +} + +sub cmd_breakwater () { + badusage "no arguments allowed" if @ARGV; + my ($anchor, $bw) = keycommits +(git_rev_parse 'HEAD'), 0,0; + print "$bw\n" or die $!; +} + +sub cmd_status () { + badusage "no arguments allowed" if @ARGV; + + # todo: gdr status should print divergence info + # todo: gdr status should print upstream component(s) info + # todo: gdr should leave/maintain some refs with this kind of info ? + + my $oldest = [ 0 ]; + my $newest; + my $note = sub { + my ($badness, $ourmsg, $snagname, $kcmsg, $cl) = @_; + if ($oldest->[0] < $badness) { + $oldest = $newest = undef; + } + $oldest = \@_; # we're walking backwards + $newest //= \@_; + }; + my ($anchor, $bw) = keycommits +(git_rev_parse 'HEAD'), + sub { $note->(1, 'branch contains furniture (not laundered)', @_); }, + sub { $note->(2, 'branch is unlaundered', @_); }, + sub { $note->(3, 'branch needs laundering', @_); }, + sub { $note->(4, 'branch not in git-debrebase form', @_); }; + + my $prcommitinfo = sub { + my ($cid) = @_; + flush STDOUT or die $!; + runcmd @git, qw(--no-pager log -n1), + '--pretty=format: %h %s%n', + $cid; + }; + + print "current branch contents, in git-debrebase terms:\n"; + if (!$oldest->[0]) { + print " branch is laundered\n"; + } else { + print " $oldest->[1]\n"; + my $printed = ''; + foreach my $info ($oldest, $newest) { + my $cid = $info->[4]{CommitId}; + next if $cid eq $printed; + $printed = $cid; + print " $info->[3]\n"; + $prcommitinfo->($cid); + } + } + + my $prab = sub { + my ($cid, $what) = @_; + if (!defined $cid) { + print " $what is not well-defined\n"; + } else { + print " $what\n"; + $prcommitinfo->($cid); + } + }; + print "key git-debrebase commits:\n"; + $prab->($anchor, 'anchor'); + $prab->($bw, 'breakwater'); + + my ($ffqstatus, $ffq_msg, $current, $ffq_prev, $gdrlast) = + ffq_prev_branchinfo(); + + print "branch and ref status, in git-debrebase terms:\n"; + if ($ffq_msg) { + print " $ffq_msg\n"; + } else { + $ffq_prev = git_get_ref $ffq_prev; + $gdrlast = git_get_ref $gdrlast; + if ($ffq_prev) { + print " unstitched; previous tip was:\n"; + $prcommitinfo->($ffq_prev); + } elsif (!$gdrlast) { + print " stitched? (no record of git-debrebase work)\n"; + } elsif (is_fast_fwd $gdrlast, 'HEAD') { + print " stitched\n"; + } else { + print " not git-debrebase (diverged since last stitch)\n" + } + } +} + +sub cmd_stitch () { + my $prose = 'stitch'; + GetOptions('prose=s', \$prose) or die badusage("bad options to stitch"); + badusage "no arguments allowed" if @ARGV; + do_stitch $prose, 0; +} +sub cmd_prepush () { cmd_stitch(); } + +sub cmd_quick () { + badusage "no arguments allowed" if @ARGV; + do_launder_head 'launder for git-debrebase quick'; + do_stitch 'quick'; +} + +sub cmd_conclude () { + my ($ffq_prev, $gdrlast, $ffq_prev_commitish) = ffq_prev_info(); + if (!$ffq_prev_commitish) { + fail "No ongoing git-debrebase session." unless $opt_noop_ok; + return; + } + my $dangling_head = get_head(); + + badusage "no arguments allowed" if @ARGV; + do_launder_head 'launder for git-debrebase quick'; + do_stitch 'quick'; +} + +sub make_patches_staged ($) { + my ($head) = @_; + # Produces the patches that would result from $head if it were + # laundered. + my ($secret_head, $secret_bw, $last_anchor) = walk $head; + fresh_workarea(); + in_workarea sub { + runcmd @git, qw(checkout -q -b bw), $secret_bw; + runcmd @git, qw(checkout -q -b patch-queue/bw), $secret_head; + runcmd qw(gbp pq export); + runcmd @git, qw(add debian/patches); + }; +} + +sub make_patches ($) { + my ($head) = @_; + keycommits $head, 0, \&snag; + make_patches_staged $head; + my $out; + in_workarea sub { + my $ptree = cmdoutput @git, qw(write-tree --prefix=debian/patches/); + runcmd @git, qw(read-tree), $head; + read_tree_subdir 'debian/patches', $ptree; + $out = make_commit [$head], [ + 'Commit patch queue (exported by git-debrebase)', + '[git-debrebase: export and commit patches]', + ]; + }; + return $out; +} + +sub cmd_make_patches () { + my $opt_quiet_would_amend; + GetOptions('quiet-would-amend!', \$opt_quiet_would_amend) + or die badusage("bad options to make-patches"); + badusage "no arguments allowed" if @ARGV; + my $old_head = get_head(); + my $new = make_patches $old_head; + my $d = get_differs $old_head, $new; + if ($d == 0) { + fail "No (more) patches to export." unless $opt_noop_ok; + return; + } elsif ($d == D_PAT_ADD) { + snags_maybe_bail(); + update_head_checkout $old_head, $new, 'make-patches'; + } else { + print STDERR failmsg + "Patch export produced patch amendments". + " (abandoned output commit $new).". + " Try laundering first." + unless $opt_quiet_would_amend; + finish 7; + } +} + +sub cmd_convert_from_gbp () { + badusage "needs 1 optional argument, the upstream git rev" + unless @ARGV<=1; + my ($upstream_spec) = @ARGV; + $upstream_spec //= 'refs/heads/upstream'; + my $upstream = git_rev_parse $upstream_spec; + my $old_head = get_head(); + + my $upsdiff = get_differs $upstream, $old_head; + if ($upsdiff & D_UPS) { + runcmd @git, qw(--no-pager diff), + $upstream, $old_head, + qw( -- :!/debian :/); + fail "upstream ($upstream_spec) and HEAD are not identical in upstream files"; + } + + if (!is_fast_fwd $upstream, $old_head) { + snag 'upstream-not-ancestor', + "upstream ($upstream) is not an ancestor of HEAD"; + } else { + my $wrong = cmdoutput + (@git, qw(rev-list --ancestry-path), "$upstream..HEAD", + qw(-- :/ :!/debian)); + if (length $wrong) { + snag 'unexpected-upstream-changes', + "history between upstream ($upstream) and HEAD contains direct changes to upstream files - are you sure this is a gbp (patches-unapplied) branch?"; + print STDERR "list expected changes with: git log --stat --ancestry-path $upstream_spec..HEAD -- :/ ':!/debian'\n"; + } + } + + if ((git_cat_file "$upstream:debian")[0] ne 'missing') { + snag 'upstream-has-debian', + "upstream ($upstream) contains debian/ directory"; + } + + snags_maybe_bail(); + + my $work; + + fresh_workarea(); + in_workarea sub { + runcmd @git, qw(checkout -q -b gdr-internal), $old_head; + # make a branch out of the patch queue - we'll want this in a mo + runcmd qw(gbp pq import); + # strip the patches out + runcmd @git, qw(checkout -q gdr-internal~0); + rm_subdir_cached 'debian/patches'; + $work = make_commit ['HEAD'], [ + 'git-debrebase convert-from-gbp: drop patches from tree', + 'Delete debian/patches, as part of converting to git-debrebase format.', + '[git-debrebase convert-from-gbp: drop patches from tree]' + ]; + # make the anchor merge + # the tree is already exactly right + $work = make_commit [$work, $upstream], [ + 'git-debrebase import: declare upstream', + 'First breakwater merge.', + '[git-debrebase anchor: declare upstream]' + ]; + + # rebase the patch queue onto the new breakwater + runcmd @git, qw(reset --quiet --hard patch-queue/gdr-internal); + runcmd @git, qw(rebase --quiet --onto), $work, qw(gdr-internal); + $work = git_rev_parse 'HEAD'; + }; + + update_head_checkout $old_head, $work, 'convert-from-gbp'; +} + +sub cmd_convert_to_gbp () { + badusage "no arguments allowed" if @ARGV; + my $head = get_head(); + my (undef, undef, undef, $ffq, $gdrlast) = ffq_prev_branchinfo(); + keycommits $head, 0; + my $out; + make_patches_staged $head; + in_workarea sub { + $out = make_commit ['HEAD'], [ + 'Commit patch queue (converted from git-debrebase format)', + '[git-debrebase convert-to-gbp: commit patches]', + ]; + }; + if (defined $ffq) { + push @deferred_updates, "delete $ffq"; + push @deferred_updates, "delete $gdrlast"; + } + snags_maybe_bail(); + update_head_checkout $head, $out, "convert to gbp (v0)"; + print <<END or die $!; +git-debrebase: converted to git-buildpackage branch format +git-debrebase: WARNING: do not now run "git-debrebase" any more +git-debrebase: WARNING: doing so would drop all upstream patches! +END +} + +sub cmd_downstream_rebase_launder_v0 () { + badusage "needs 1 argument, the baseline" unless @ARGV==1; + my ($base) = @ARGV; + $base = git_rev_parse $base; + my $old_head = get_head(); + my $current = $old_head; + my $topmost_keep; + for (;;) { + if ($current eq $base) { + $topmost_keep //= $current; + print " $current BASE stop\n"; + last; + } + my $cl = classify $current; + print " $current $cl->{Type}"; + my $keep = 0; + my $p0 = $cl->{Parents}[0]{CommitId}; + my $next; + if ($cl->{Type} eq 'Pseudomerge') { + print " ^".($cl->{Contributor}{Ix}+1); + $next = $cl->{Contributor}{CommitId}; + } elsif ($cl->{Type} eq 'AddPatches' or + $cl->{Type} eq 'Changelog') { + print " strip"; + $next = $p0; + } else { + print " keep"; + $next = $p0; + $keep = 1; + } + print "\n"; + if ($keep) { + $topmost_keep //= $current; + } else { + die "to-be stripped changes not on top of the branch\n" + if $topmost_keep; + } + $current = $next; + } + if ($topmost_keep eq $old_head) { + print "unchanged\n"; + } else { + print "updating to $topmost_keep\n"; + update_head_checkout + $old_head, $topmost_keep, + 'downstream-rebase-launder-v0'; + } +} + +GetOptions("D+" => \$debuglevel, + 'noop-ok', => \$opt_noop_ok, + 'f=s' => \@snag_force_opts, + 'anchor=s' => \@opt_anchors, + 'force!', + '-i:s' => sub { + my ($opt,$val) = @_; + badusage "git-debrebase: no cuddling to -i for git-rebase" + if length $val; + die if $opt_defaultcmd_interactive; # should not happen + $opt_defaultcmd_interactive = [ qw(-i) ]; + # This access to @ARGV is excessive familiarity with + # Getopt::Long, but there isn't another sensible + # approach. '-i=s{0,}' does not work with bundling. + push @$opt_defaultcmd_interactive, @ARGV; + @ARGV=(); + }) or die badusage "bad options\n"; +initdebug('git-debrebase '); +enabledebug if $debuglevel; + +my $toplevel = cmdoutput @git, qw(rev-parse --show-toplevel); +chdir $toplevel or die "chdir $toplevel: $!"; + +$rd = fresh_playground "$playprefix/misc"; + +@opt_anchors = map { git_rev_parse $_ } @opt_anchors; + +if (!@ARGV || $opt_defaultcmd_interactive || $ARGV[0] =~ m{^-}) { + defaultcmd_rebase(); +} else { + my $cmd = shift @ARGV; + my $cmdfn = $cmd; + $cmdfn =~ y/-/_/; + $cmdfn = ${*::}{"cmd_$cmdfn"}; + + $cmdfn or badusage "unknown git-debrebase sub-operation $cmd"; + $cmdfn->(); +} + +finish 0; diff --git a/git-debrebase.1.pod b/git-debrebase.1.pod new file mode 100644 index 0000000..6a98ed2 --- /dev/null +++ b/git-debrebase.1.pod @@ -0,0 +1,475 @@ +=head1 NAME + +git-debrebase - delta queue rebase tool for Debian packaging + +=head1 SYNOPSYS + + git-debrebase [<options...>] [-- <git-rebase options...>] + git-debrebase [<options...>] <operation> [<operation options...> + +=head1 INTRODUCTION + +git-debrebase is a tool for representing in git, +and manpulating, +Debian packages based on upstream source code. + +This is the command line reference. +Please read the tutorial +L<dgit-maint-debrebase(5)>. +For background, theory of operation, +and definitions see L<git-debrebase(5)>. + +You should read this manpage in conjunction with +L<git-debrebase(5)/TERMINOLOGY>, +which defines many important terms used here. + +=head1 PRINCIPAL OPERATIONS + +=over + +=item git-debrebase [-- <git-rebase options...>] + +=item git-debrebase [-i <further git-rebase options...>] + +Unstitches and launders the branch. +(See L</UNSTITCHING AND LAUNDERING> below.) + +Then, if any git-rebase options were supplied, +edits the Debian delta queue, +using git-rebase, by running + + git rebase <git-rebase options> <breakwater-tip> + +Do not pass a base branch argument: +git-debrebase will supply that. +Do not use --onto, or --fork-point. +Useful git-rebase options include -i and --autosquash. + +If git-rebase stops for any reason, +you may git-rebase --abort, --continue, or --skip, as usual. +If you abort the git-rebase, +the branch will still have been laundered, +but everything in the rebase will be undone. + +The options for git-rebase must either start with C<-i>, +or be prececded by C<-->, +to distinguish them from options for git-debrebase. + +=item git-debrebase status + +Analyise the current branch, +both in terms of its conents, +and the refs which are relevant to git-debrebase, +and print a human-readable summary. + +Please do not attempt to parse the output; +it may be reformatted or reorganised in the future. +Instead, +use one of the L<UNDERLYING AND SUPPLEMENTARY OPERATIONS> +described below. + +=item git-debrebase conclude + +Finishes a git-debrebase session, +tidying up the branch and making it fast forward again. + +Specifically: if the branch is unstitched, +launders and restitches it, +making a new pseudomerge. +Otherwise, it is an error, +unless --noop-ok. + +=item git-debrebase quick + +Unconditionally launders and restitches the branch, +consuming any ffq-prev +and making a new pseudomerge. + +If the branch is already laundered and stitched, does nothing. + +=item git-debrebase prepush [--prose=<for commit message>] + +=item git-debrebase stitch [--prose=<for commit message>] + +Stitches the branch, +consuming ffq-prev. +This is a good command to run before pushing to a git server. + +If there is no ffq-prev, it is an error, unless --noop-ok. + +You should consider using B<conclude> instead, +because that launders the branch too. + +=item git-debrebase new-upstream-v0 <new-version> [<upstream-details>...] + +Rebases the delta queue +onto a new upstream version. In detail: + +Firstly, checks that the proposed rebase seems to make sense: +It is a snag unless the new upstream(s) +are fast forward from the previous upstream(s) +as found in the current breakwater anchor. +And, in the case of a multi-piece upstream +(a multi-component upstream, in dpkg-source terminology), +if the pieces are not in the same order, with the same names. + +If all seems well, unstitches and launders the branch. + +Then, +generates +(in a private working area) +a new anchor merge commit, +on top of the breakwater tip, +and on top of that a commit to +update the version number in debian/changelog. + +Finally, +starts a git-rebase +of the delta queue onto these new commits. + +That git-rebase may complete successfully, +or it may require your assistance, +just like a normal git-rebase. + +If you git-rebase --abort, +the whole new upstream operation is aborted, +except for the laundering. + +The <upstream-details> are, optionally, in order: + +=over + +=item <upstream-commit-ish> + +The new upstream branch (or commit-ish). +The default is to look for one of these tags, in this order: +U vU upstream/U; +where U is the new upstream version. +(This is the same algorithm as L<git-deborig(1)>.) + +It is a snag if the upstream contains a debian/ directory; +if forced to proceed, +git-debrebase will disregard the upstream's debian/ and +take (only) the packaging from the current breakwater. + +=item <piece-name> <piece-upstream-commit-ish> + +Specifies that this is a multi-piece upstream. +May be repeated. + +When such a pair is specified, +git-debrebase will first combine the pieces of the upstream +together, +and then use the result as the combined new upstream. + +For each <piece-name>, +the tree of the <piece-upstream-commit-ish> +becomes the subdirectory <piece-name> +in the combined new upstream +(supplanting any subdirectory that might be there in +the main upstream branch). + +<piece-name> has a restricted syntax: +it may contain only ASCII alphanumerics and hyphens. + +The combined upstream is itself recorded as a commit, +with each of the upstream pieces' commits as parents. +The combined commit contains an annotation +to allow a future git-debrebase new upstream operation +to make the coherency checks described above. + +=item <git-rebase options> + +These will be passed to git rebase. + +If the upstream rebase is troublesome, -i may be helpful. +As with plain git-debrebase, +do not specify a base, or --onto, or --fork-point. + +=back + +If you are planning to generate a .dsc, +you will also need to have, or generate, +actual orig tarball(s), +which must be identical to the rev-spec(s) +passed to git-debrebase. +git-debrebase does not concern itself with source packages +so neither helps with this, nor checks it. +L<git-deborig(1)>, +L<git-archive(1)>, L<dgit(1)> and +L<gbp-import-orig(1)> may be able to help. + +This subcommand has -v0 in its name because we are not yet sure +that its command line syntax is optimal. +We may want to introduce an incompatible replacement syntax +under the name C<new-upstream>. + +=item git-debrebase make-patches [--quiet-would-amend] + +Generate patches in debian/patches/ +representing the changes made to upstream files. + +It is not normally necessary to run this command explicitly. +When uploading to Debian, +dgit and git-debrebase +will cooperate to regenerate patches as necessary. +When working with pure git remotes, +the patches are not needed. + +Normally git-debrebase make-patches will +require a laundered branch. +(A laundered branch does not contain any patches.) +But if there are already some patches made by +git-debrebase make-patches, +and all that has happened is that more +changes to upstream files have been committed, +running it again can add the missing patches. + +If the patches implied by the current branch +are not a simple superset of those already in debian/patches, +make-patches will fail with exit status 7, +and an error message. +(The message can be suppress with --quiet-would-amend.) +If the problem is simply that +the existing patches were not made by git-debrebase, +using dgit quilt-fixup instead should succeed. + +=item git-debrebase convert-from-gbp [<upstream-commit-ish>] + +Cnnverts a gbp patches-unapplied branch +(not a gbp pq patch queue branch) +into a git-debrebase interchange branch. + +This is done by generating a new anchor merge, +converting the quilt patches as a delta queue, +and dropping the patches from the tree. + +The upstream commit-ish should correspond to +the gbp upstream branch, if there is one. +It is a snag if it is not an ancestor of HEAD, +or if the history between the upstream and HEAD +contains commits which make changes to upstream files. + +It is also a snag if the specified upstream +has a debian/ subdirectory. +This check exists to detect certain likely user errors, +but if this situation is true and expected, +forcing it is fine. + +The result is a well-formed git-debrebase interchange branch. +The result is also fast-forward from the gbp branch. + +Note that it is dangerous not to know whether you are +dealing with a gbp patches-unappled branch containing quilt patches, +or a git-debrebase interchange branch. +At worst, +using the wrong tool for the branch format might result in +a dropped patch queue! + +=back + +=head1 UNDERLYING AND SUPPLEMENTARY OPERATIONS + +=over + +=item git-debrebase breakwater + +Prints the breakwater tip commitid. +If your HEAD branch is not fully laundered, +prints the tip of the so-far-laundered breakwater. + +=item git-debrebase anchor + +Prints the breakwater anchor commitid. + +=item git-debrebase analyse + +Walks the history of the current branch, +most recent commit first, +back until the most recent anchor, +printing the commit object id, +and commit type and info +(ie the semantics in the git-debrebase model) +for each commit. + +=item git-debrebase record-ffq-prev + +Establishes the current branch's ffq-prev, +as discussed in L</UNSTITCHING AND LAUNDERING>, +but does not launder the branch or move HEAD. + +It is an error if the ffq-prev could not be recorded. +It is also an error if an ffq-prev has already been recorded, +unless --noop-ok. + +=item git-debrebase launder-v0 + +Launders the branch without recording anything in ffq-prev. +Then prints some information about the current branch. +Do not use this operation; +it will be withdrawn soon. + +=item git-debrebase convert-to-gbp + +Converts a laundered branch into a +gbp patches-unapplied branch containing quilt patches. +The result is not fast forward from the interchange branch, +and any ffq-prev is deleted. + +This is provided mostly for the test suite +and for unusual situations. +It should only be used with a care and +with a proper understanding of the underlying theory. + +Be sure to not accidentally treat the result as +a git-debrebase branch, +or you will drop all the patches! + +=back + +=head1 OPTIONS + +This section documents the general options +to git-debrebase +(ie, the ones which immediately follow +git-debrebase +or +git debrebase +on the command line). +Individual operations may have their own options which are +docuented under each operation. + +=over + +=item -f<snag-id> + +Turns snag(s) with id <snag-id> into warnings. + +Some troublesome things which git-debrebase encounters +are B<snag>s. +(The specific instances are discussed +in the text for the relvant operation.) + +When a snag is detected, +a message is printed to stderr containing the snag id +(in the form C<-f<snag-idE<gt>>), +along with some prose. + +If snags are detected, git-debrebase does not continue, +unless the relevant -f<snag-id> is specified, +or --force is specified. + +=item --force + +Turns all snags into warnings. +See the -f<snag-id> option. + +Do not invoke git-debrebase --force in scripts and aliases; +instead, specify the particular -f<snag-id> for expected snags. + +=item --noop-ok + +Suppresses the error in +some situations where git-debrebase does nothing, +because there is nothing to do. + +The specific instances are discussed +in the text for the relvant operation. + +=item --anchor=<commit-ish> + +Treats <commit-ish> as an anchor. +This overrides the usual logic which automatically classifies +commits as anchors, pseudomerges, delta queue commits, etc. + +It also disables some coherency checks +which depend on metadata extracted from its commit message, +so +it is a snag if <commit-ish> is the anchor +for the previous upstream version in +git-debrebase new-upstream operations. + +=item -D + +Requests (more) debugging. May be repeated. + +=back + +=head1 UNSTITCHING AND LAUNDERING + +Several operations unstitch and launder the branch first. +In detail this means: + +=head2 Establish the current branch's ffq-prev + +If ffq-prev is not yet recorded, +git-debrebase checks that the current branch is ahead of relevant +remote tracking branches. +The relevant branches depend on +the current branch (and its +git configuration) +and are as follows: + +=over + +=item + +The branch that git would merge from +(remote.<branch>.merge, remote.<branch>.remote); + +=item + +The branch git would push to, if different +(remote.<branch>.pushRemote etc.); + +=item + +For local dgit suite branches, +the corresponding tracking remote; + +=item + +If you are on C<master>, +remotes/dgit/dgit/sid. + +=back + +The apparently relevant ref names to check are filtered through +branch.<branch>.ffq-ffrefs, +which is a semicolon-separated list of glob patterns, +each optionally preceded by !; first match wins. + +In each case it is a snag if +the local HEAD is behind the checked remote, +or if local HEAD has diverged from it. +All the checks are done locally using the remote tracking refs: +git-debrebase does not fetch anything from anywhere. + +If these checks pass, +or are forced, +git-debrebse then records the current tip as ffq-prev. + +=head2 Examine the branch + +git-debrebase +analyses the current HEAD's history to find the anchor +in its breakwater, +and the most recent breakwater tip. + +=head2 Rewrite the commits into laundered form + +Mixed debian+upstream commits are split into two commits each. +Delta queue (upstream files) commits bubble to the top. +Pseudomerges, +and quilt patch additions, +are dropped. + +This rewrite will always succeed, by construction. +The result is the laundered branch. + +=head1 SEE ALSO + +git-debrebase(1), +dgit-maint-rebase(7), +dgit(1), +gitglossary(7) diff --git a/git-debrebase.5.pod b/git-debrebase.5.pod new file mode 100644 index 0000000..5cfa376 --- /dev/null +++ b/git-debrebase.5.pod @@ -0,0 +1,610 @@ +=head1 NAME + +git-debrebase - git data model for Debian packaging + +=head1 INTRODUCTION + +git-debrebase is a tool for representing in git, +and manpulating, +Debian packages based on upstream source code. + +The Debian packaging +has a fast forwarding history. +The delta queue (changes to upstream files) is represented +as a series of individual git commits, +which can worked on with rebase, +and also shared. + +git-debrebase is designed to work well with dgit. +git-debrebase can also be used in workflows without source packages, +for example to work on Debian-format packages outside or alongside Debian. + +git-debrebase +itself is not very suitable for use by Debian derivatives, +to work on packages inherited from Debian, +because it assumes that you want to throw away any packaging +provided by your upstream. +However, of git-debrebase in Debian does not make anything harder for +derivatives, and it can make some things easier. + +=head1 TERMINOLOGY + +=over + +=item Pseudomerge + +A merge which does not actually merge the trees; +instead, it is constructed by taking the tree +from one of the parents +(ignoring the contents of the other parents). +These are used to make a rewritten history fast forward +from a previous tip, +so that it can be pushed and pulled normally. +Manual construction of pseudomerges can be done with +C<git merge -s ours> +but is not normally needed when using git-debrebase. + +=item Packaging files + +Files in the source tree within B<debian/>, +excluding anything in B<debian/patches/>. + +=item Upstream + +The version of the package without Debian's packaging. +Typically provided by the actual upstream project, +and sometimes tracked by Debian contributors in a branch C<upstream>. + +Upstream contains upstream files, +but some upstreams also contain packaging files in B<debian/>. +Any such non-upstream files found in upstream +are thrown away by git-debrebase +each time a new upstream version is incorporated. + +=item Upstream files + +Files in the source tree outside B<debian/>. +These may include unmodified source from upstream, +but also files which have been modified or created for Debian. + +=item Delta queue + +Debian's changes to upstream files: +a series of git commits. + +=item Quilt patches + +Files in B<debian/patches/> generated for the benefit of +dpkg-source's 3.0 (quilt) .dsc source package format. +Not used, often deleted, and regenerated when needed +(such as when uploading to Debian), +by git-debrebase. + +=item Interchange branch; breakwater; stitched; laundered + +See L</BRANCHES AND BRANCH STATES - OVERVIEW>. + +=item Anchor; Packaging + +See L</BRANCH CONTENTS - DETAILED SPECIFICATION>. + +=item ffq-prev; debrebase-last + +See L</STITCHING, PSEUDO-MERGES, FFQ RECORD>. + +=back + +=head1 DIAGRAM + + ------/--A!----/--B3!--%--/--> interchange view + / / / with debian/ directory + % % % entire delta queue applied + / / / 3.0 (quilt) has debian/patches + / / 3* "master" on Debian git servers + / / / + 2* 2* 2 + / / / + 1 1 1 breakwater branch, merging baseline + / / / unmodified upstream code + ---@-----@--A----@--B--C plus debian/ (but no debian/patches) + / / / no ref refers to this: we + --#-----#-------#-----> upstream reconstruct its identity by + inspecting interchange branch + Key: + + 1,2,3 commits touching upstream files only + A,B,C commits touching debian/ only + B3 mixed commit (eg made by an NMUer) + # upstream releases + + -@- anchor merge, takes contents of debian/ from the + / previous `breakwater' commit and rest from upstream + + -/- pseudomerge; contents are identical to + / parent lower on diagram. + + % dgit- or git-debrebase- generated commit of debian/patches. + `3.0 (quilt)' only; generally dropped by git-debrebase. + + * Maintainer's HEAD was here while they were editing, + before they said they were done, at which point their + tools made -/- (and maybe %) to convert to + the fast-forwarding interchange branch. + + ! NMUer's HEAD was here when they said `dgit push'. + Rebase branch launderer turns each ! into an + equivalent *. + +=head1 BRANCHES AND BRANCH STATES - OVERVIEW + +git-debrebase has one primary branch, +the B<interchange branch>. +This branch is found on Debian contributor's workstations +(typically, a maintainer would call it B<master>), +in the Debian dgit git server as the suite branch (B<dgit/dgit/sid>) +and on other git servers which support Debian work +(eg B<master> on salsa). + +The interchange branch is fast-forwarding +(by virtue of pseudomerges, where necessary). + +It is possible to have multiple different interchange branches +for the same package, +stored as different local and remote git branches. +However, divergence should be avoided where possible - +see L</OTHER MERGES>. + +A suitable interchange branch can be used directly with dgit. +In this case each dgit archive suite branch is a separate +interchange branch. + +Within the ancestry of the interchange branch, +there is another important, implicit branch, the +B<breakwater>. +The breakwater contains unmodified upstream source, +but with Debian's packaging superimposed +(replacing any C<debian/> directory that may be in +the upstream commits). +The breakwater does not contain any representation of +the delta queue (not even debian/patches). +The part of the breakwater processed by git-debrebase +is the part since the most reecent B<anchor>, +which is usually a special merge generated by git-debrebase. + +When working, locally, +the user's branch can be in a rebasing state, +known as B<unstitched>. +While a branch is unstitched, +it is not in interchange format. +The previous interchange branch tip +tip is recorded, +so that the previous history +and the user's work +can later be +stitched into the fast-forwarding interchange form. + +An unstitched branch may be in +B<laundered> +state, +which means it has a more particular special form +convenient for manipulating the delta queue. + +=head1 BRANCH CONTENTS - DETAILED SPECIFICATION + +It is most convenient to describe the +B<breakwater> +branch first. +A breakwater is B<fast-forwarding>, +but is not usually named by a ref. +It contains B<in this order> (ancestors first): + +=over + +=item Anchor + +An B<anchor> commit, +which is usually a special two-parent merge: + +The first parent +contains the most recent version, at that point, +of the Debian packaging (in debian/); +it also often contains upstream files, +but they are to be ignored. +Often the first parent is a previous breakwater tip. + +The second parent +is an upstream source commit. +It may sometimes contain a debian/ subdirectory, +but if so that is to be ignored. +The second parent's upstream files +are identical to the anchor's. +Anchor merges always contain +C<[git-debrebase anchor: ...]> +as a line in the commit message. + +Alternatively, +an anchor may be a single-parent commit which introduces +the C<debian/> directory and makes no other changes: +ie, the start of Debian packaging. + +=item Packaging + +Zero or more single-parent commits +containing only packaging changes. +(And no quilt patch changes.) + +=back + +The +B<laundered> +branch state is B<rebasing>. +A laundered branch is based on a breakwater +but also contains, additionally, +B<after> the breakwater, +a representation of the delta queue: + +=over + +=item Delta queue commits + +Zero or more single-parent commits +contaioning only changes to upstream files. + +=back + +The merely +B<unstitched> +(ie, unstitched but unlaundered) +branch state is also B<rebasing>. +It has the same contents as the laundered state, +except that it may contain, +additionally, +in B<in any order but after the breakwater>: + +=over + +=item Linear commits to the source + +Further commit(s) containing changes to +to upstream files +and/or +to packaging, +possibly mixed within a single commit. +(But not quilt patch changes.) + +=item Quilt patch addition for `3.0 (quilt)' + +Commit(s) which add patches to B<debian/patches/>, +and add those patches to the end of B<series>. + +These are only necessary when working with +packages in C<.dsc 3.0 (quilt)> format. +For git-debrebase they are purely an output; +they are deleted when branches are laundered. +git-debrebase takes care to make a proper patch +series out of the delta queue, +so that any resulting source packages are nice. + +=back + +Finally, an +B<interchange> +branch is B<fast forwarding>. +It has the same contents as an +unlaundered branch state, +but may (and usually will) additionally contain +(in some order, +possibly intermixed with the extra commits +which may be found on an unstitched unlaundered branch): + +=over + +=item Pseudomerge to make fast forward + +A pseudomerge making the branch fast forward from +previous history. +The contributing parent is itself in interchange format. +Normally the overwritten parent is +a previous tip of an interchange branch, +but this is not necessary as the overwritten +parent is not examined. + +If the two parents have identical trees, +the one with the later commit date +(or, if the commit dates are the same, +the first parent) +is treated as +the contributing parent. + +=item dgit dsc import pseudomerge + +Debian .dsc source package import(s) +made by dgit +(during dgit fetch of a package most recently +uploaded to Debian without dgit, +or during dgit import-dsc). + +git-debrebase requires that +each such import is in the fast-forwarding +format produced by dgit: +a two-parent pseudomerge, +whose contributing parent is in the +non-fast-forwarding +dgit dsc import format (not described further here), +and whose overwritten parent is +the previous interchange tip +(eg, the previous tip of the dgit suite branch). + +=back + +=head1 STITCHING, PSEUDO-MERGES, FFQ RECORD + +Whenever the branch C<refs/B> is unstitched, +the previous head is recorded in the git ref C<refs/ffq-prev/B>. + +Unstiched branches are not fast forward from the published +interchange branches [1]. +So before a branch can be pushed, +the right pseudomerge must be reestablished. +This is the stitch operation, +which consumes the ffq-prev ref. + +When the user has an unstitched branch, +they may rewrite it freely, +from the breakwater tip onwards. +Such a git rebase is the default operation for git-debrebase. +Rebases should not go back before the breakwater tip, +and certainly not before the most recent anchor. + +Unstitched branches must not be pushed to interchange branch refs +(by the use of C<git push -f> or equivalent). +It is OK to share an unstitched branch +in similar circumstances and with similar warnings +to sharing any other rebasing git branch. + +[1] Strictly, for a package +which has never had a Debian delta queue, +the interchange and breakwater branches may be identical, +in which case the unstitched branch is fast forward +from the interchange branch and no pseudomerge is needed. + +When ffq-prev is not present, +C<refs/debrebase-last/B> records some ancestor of refs/B, +(usually, the result of last stitch). +This can be used to quickly determine whether refs/B +is being maintained in git-debrebase form. + +=head1 OTHER MERGES + +Note that the representation described here does not permit +general merges on any of the relevant branches. +For this reason the tools will try to help the user +avoid divergence of the interchange branch. + +See dgit-maint-rebase(7) XXX TBD +for a discussio of what kinds of behaviours +should be be avoided +because +they might generate such merges. + +Automatic resolution of divergent interchange branches +(or laundering of merges on the interchange branch) +is thought to be possible, +but there is no tooling for this yet: + +Nonlinear (merging) history in the interchange branch is awkward +because it (obviously) does not preserve +the linearity of the delta queue. +Easy merging of divergent delta queues is a research problem. + +Nonlinear (merging) history in the breakwater branch is +in principle tolerable, +but each of the parents would have to be, in turn, +a breakwater, +and difficult qeustions arise if they don't have the same anchor. + +We use the commit message annotation to +distinguish the special anchor merges from other general merges, +so we can at least detect unsupported merges. + +=head1 LEGAL OPERATIONS + +The following basic operations follows from this model +(refer to the diagram above): + +=over + +=item Append linear commits + +No matter the branch state, +it is always fine to simply git commit +(or cherry-pick etc.) +commits containing upstream file changes, packaging changes, +or both. + +(This may make the branch unlaundered.) + +=item Launder branch + +Record the previous head in ffq-prev, +if we were stitched before +(and delete debrebase-last). + +Reorganise the current branch so that the packaging +changes come first, +followed by the delta queue, +turning C<-@-A-1-2-B3> into C<...@-A-B-1-2-3>. + +Drop pseudomerges and any quilt patch additions. + +=item Interactive rebase + +With a laundered branch, +one can do an interactive git rebase of the delta queue. + +=item New upstream rebase + +Start rebasing onto a new upstream version, +turning C<...#..@-A-B-1-2-3> into C<(...#..@-A-B-, ...#'-)@'-1-2>. + +This has to be a wrapper around git-rebase, +which prepares @' and then tries to rebase 1 2 onto @'. +If the user asks for an interactive rebase, +@' doesn't appear in the commit list, since +@' is the newbase of the rebase (see git-rebase(1)). + +Note that the construction of @' cannot fail +because @' simply copies debian/ from B and and everything else from #'. +(Rebasing A and B is undesirable. +We want the debian/ files to be non-rebasing +so that git log shows the packaging history.) + +=item Stitch + +Make a pseudomerge, +whose contributing parent to is the unstitched branch +and +whose overwritten parent is ffq-prev, +consuming ffq-prev in the process +(and writing debrebase-last instead). +Ideally the contributing parent would be a laundered branch, +or perhaps a laundered branch with a quilt patch addition commit. + +=item Commit quilt patches + +To generate a tree which can be represented as a +3.0 (quilt) .dsc source packages, +the delta queue must be reified inside the git tree +in B<debian/patches/>. +These patch files can be stripped out and/or regenerated as needed. + +=back + +=head1 COMMIT MESSAGE ANNOTATIONS + +git-debrebase makes annotations +in the messages of commits it generates. + +The general form is + + [git-debrebase[ COMMIT-TYPE [ ARGS...]]: PROSE, MORE PROSE] + +git-debrebase treats anything after the colon as a comment, +paying no attention to PROSE. + +The full set of annotations is: + [git-debrebase: split mixed commit, debian part] + [git-debrebase: split mixed commit, upstream-part] + [git-debrebase: convert dgit import, debian changes] + [git-debrebase anchor: convert dgit import, upstream changes] + + [git-debrebase upstream-combine . PIECE[ PIECE...]: new upstream] + [git-debrebase anchor: new upstream NEW-UPSTREAM-VERSION, merge] + [git-debrebase: new upstream NEW-UPSTREAM-VERSION, changelog] + [git-debrebase: export and commit patches] + + [git-debrebase convert-from-gbp: drop patches] + [git-debrebase anchor: declare upstream] + [git-debrebase pseudomerge: stitch] + + [git-debrebase convert-to-gbp: commit patches] + +Only anchor merges have the C<[git-debrebase anchor: ...]> tag. +Single-parent anchors are not generated by git-debrebase, +and when made manually should not be tagged. + +The C<split mixed commit> and C<convert dgit import> +tags are added to the pre-existing commit message, +when git-debrebase rewrites the commit. + +=head1 APPENDIX - DGIT IMPORT HANDLING + +The dgit .dsc import format is not documented or specified +(so some of the following terms are not defined anywhere). +The dgit import format it is defined by the implementation in dgit, +of which git-debrebase has special knowledge. + +Consider a non-dgit NMU followed by a dgit NMU: + + interchange --/--B3!--%--//----D*--> + / / + % 4 + / 3 + / 2 + / 1 + 2 &_ + / /| \ + 1 0 00 =XBC% + / + / + --@--A breakwater + / + --#--------> upstream + + + Supplementary key: + + =XBC% dgit tarball import of .debian.tar.gz containing + Debian packaging including changes B C and quilt patches + 0 dgit tarball import of upstream tarball + 00 dgit tarball import of supplementary upstream piece + &_ dgit import nearly-breakwater-anchor + // dgit fetch / import-dsc pseudomerge to make fast forward + + &' git-debrebase converted import (upstream files only) + C' git-debrebase converted packaging change import + + * ** before and after HEAD + +We want to transform this into: + +=over + +=item I. No new upstream version + + (0 + 00 eq #) + --/--B3!--%--//-----D*-------------/--> + / / / + % 4 4** + / 3 3 + / 2 2 + / 1 1 + 2 &_ / + / /| \ / + 1 0 00 =XBC% / + / / + / / + --@--A-----B---------------------C'---D + / + --#-----------------------------------------> + +=item II. New upstream + + (0 + 00 neq #) + + --/--B3!--%--//-----D*-------------/--> + / / / + % 4 4** + / 3 3 + / 2 2 + / 1 1 + 2 &_ / + / /| \ / + 1 0 00 =XBC% / + / / + / / + --@--A-----B-----------------@---C'---D + / / + --#--------------------- - - / - - ---------> + / + &' + /| + 0 00 + +=back + +=head1 SEE ALSO + +git-debrebase(1), +dgit-maint-rebase(7), +dgit(1) diff --git a/tests/enumerate-tests b/tests/enumerate-tests index 2c00f97..5a4d235 100755 --- a/tests/enumerate-tests +++ b/tests/enumerate-tests @@ -42,15 +42,29 @@ finish- () { test-begin-gencontrol () { restrictions='' - dependencies='' + dependencies='dgit, dgit-infrastructure, devscripts, debhelper (>=8), fakeroot, build-essential, chiark-utils-bin' } restriction-gencontrol () { restrictions+=" $r" } +gencontrol-add-deps () { + for dep in "$@"; do + dependencies+="${dependencies:+, }$dep" + done +} + dependencies-gencontrol () { - dependencies+=", $deps" + for dep in "$deps"; do + case "$dep" in + NO-DGIT) dependencies='chiark-utils-bin' ;; + GDR) gencontrol-add-deps \ + git-debrebase git-buildpackage faketime + ;; + *) gencontrol-add-deps "$dep" ;; + esac + done } test-done-gencontrol () { @@ -31,8 +31,8 @@ export DGIT_TEST_DEBUG : ${DGIT_TEST_DISTRO+ ${distro=${DGIT_TEST_DISTRO}}} -export GIT_COMMITTER_DATE='1440253867 +0100' -export GIT_AUTHOR_DATE='1440253867 +0100' +export GIT_COMMITTER_DATE='1515000000 +0100' +export GIT_AUTHOR_DATE='1515000000 +0100' root=`pwd` troot=$root/tests @@ -189,6 +189,13 @@ t-git-none () { (set -e; cd $tmp/git; tar xf $troot/git-template.tar) } +t-salsa-add-remote () { + local d=$tmp/salsa/$p + mkdir -p $d + (set -e; cd $d; git init --bare) + git remote add ${1-origin} $d +} + t-git-merge-base () { git merge-base $1 $2 || test $? = 1 } @@ -415,9 +422,10 @@ t-dgit () { {{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{' $dgit --dgit=$dgit --dget:-u --dput:--config=$tmp/dput.cf \ ${dgit_config_debian_alias-"--config-lookup-explode=dgit-distro.debian.alias-canon"} \ + ${DGIT_GITDEBREBASE_TEST+--git-debrebase=}${DGIT_GITDEBREBASE_TEST} \ ${distro+${distro:+-d}}${distro--dtest-dummy} \ $DGIT_TEST_OPTS $DGIT_TEST_DEBUG \ - -k39B13D8A $t_dgit_xopts "$@" + -kBCD22CD83243B79D3DFAC33EA3DBCBC039B13D8A $t_dgit_xopts "$@" : '}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} ' } @@ -472,12 +480,12 @@ t-setup-done () { local savedirs=$2 local importeval=$3 - local import=IMPORT.${0##*/} + local import=IMPORT.${DGIT_TEST_TESTNAME-${0##*/}} exec 4>$tmp/$import.new local vn for vn in $savevars; do - perl >&4 -I. -MDebian::Dgit -e ' + perl >&4 -"I$root" -MDebian::Dgit -e ' printf "%s=%s\n", $ARGV[0], shellquote $ARGV[1] ' $vn "$(eval "printf '%s\n' \"\$$vn\"")" done @@ -997,6 +1005,11 @@ t-commit () { revision=$(( ${revision-0} + 1 )) } +t-dch-commit () { + faketime @"${GIT_AUTHOR_DATE% *}" dch "$@" + git commit -m "dch $*" debian/changelog +} + t-git-config () { git config --global "$@" } diff --git a/tests/lib-core b/tests/lib-core index d65a1ff..c3a04cb 100644 --- a/tests/lib-core +++ b/tests/lib-core @@ -12,6 +12,7 @@ t-set-intree () { : ${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/}} + : ${DGIT_GITDEBREBASE_TEST:=$DGIT_TEST_INTREE/git-debrebase} export DGIT_TEST DGIT_BADCOMMIT_FIXUP export DGIT_REPOS_SERVER_TEST DGIT_SSH_DISPATCH_TEST export PERLLIB="$DGIT_TEST_INTREE${PERLLIB:+:}${PERLLIB}" diff --git a/tests/lib-gdr b/tests/lib-gdr new file mode 100644 index 0000000..9eb7537 --- /dev/null +++ b/tests/lib-gdr @@ -0,0 +1,277 @@ +# + +: ${GDR_TEST_DEBUG=-D} +export GDR_TEST_DEBUG + +t-git-debrebase () { + local gdr=${DGIT_GITDEBREBASE_TEST-git-debrebase} + : ' +{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{' + $gdr $GDR_TEST_OPTS $GDR_TEST_DEBUG $t_gdr_xopts "$@" + : '}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}} +' +} + +t-gdr-good () { + local state=$1 + local beforetag=$2 # say HEAD to skip this check + # state should be one of + # laundered + # stitched + # pushed + + git diff --quiet ${beforetag-t.before} -- ':.' ':!debian/patches' + + local etypes bwtip + + LC_MESSAGES=C t-git-debrebase status >../status.check + case $state in + laundered) + egrep '^ *branch is laundered' ../status.check + ;; + stitched|pushed) + egrep \ + '^ *branch contains furniture|^ *branch is unlaundered|^ *branch needs laundering' ../status.check + egrep '^ stitched$' ../status.check + ;; + esac + + # etypes is either a type, + # or PseudoMerge-<more etypes> + # or AddPatches-<more etypes> + + case $state in + laundered) + etypes=Upstream + bwtip=Y:`t-git-debrebase breakwater` + ;; + stitched) etypes=Pseudomerge-Upstream ;; + pushed) etypes=AddPatches-Pseudomerge-Upstream ;; + pushed-interop) etypes=Pseudomerge-AddPatchesInterop-Upstream ;; + esac + + t-git-debrebase analyse >../anal.check + expect=`git rev-parse HEAD` + exec <../anal.check + local cid ctype info nparents + while read cid ctype info; do + : ===== $cid $ctype $info ===== + test $cid = $expect + local cetype=${etypes%%-*} + if [ "x$ctype" = "x$cetype" ]; then cetype=SAME; fi + local parents="`git log -n1 --pretty=format:%P $cid`" + expect="$parents" + enparents=1 + : "$ctype/$cetype" "$parents" + + case "$ctype/$cetype" in + Pseudomerge/SAME) ;; + Packaging/SAME) ;; + Packaging/Upstream) ;; + AddPatches/SAME) ;; + AddPatches/AddPatchesInterop) ;; + Changelog/Packaging) ;; + Changelog/Upstream) ;; + Upstream/SAME) ;; + Anchor/Upstream) ;; + Anchor/Packaging) ;; + *) + fail "etypes=$etypes ctype=$ctype cetype=$cetype $cid" + ;; + esac + + case "$ctype/$etypes" in + Packaging/Upstream|\ + Changelog/Upstream) + if [ "x$bwtip" != x ]; then + test "$bwtip" = "Y:$cid" + bwtip='' + fi + esac + + case "$cetype" in + AddPatchesInterop) + git log -n1 --pretty=format:%B \ + | grep '^\[git-debrebase[ :]' + ;; + esac + + case "$ctype" in + Pseudomerge) + expect=${info#Contributor=} + expect=${expect%% *} + enparents=2 + git diff --quiet $expect..$cid + etypes=${etypes#*-} + + : 'reject pointless pseudomerges' + local overwritten=${parents/$expect/} + overwritten=${overwritten// /} + t-git-debrebase analyse $overwritten >../anal.overwr + local ocid otype oinfo + read <../anal.overwr ocid otype oinfo + case "$otype" in + Pseudomerge) test "x$info" != "x$oinfo" ;; + esac + ;; + Packaging) + git diff --quiet $expect..$cid -- ':.' ':!debian' + git diff --quiet $expect..$cid -- ':debian/patches' + etypes=Packaging + ;; + AddPatches) + git diff --quiet $expect..$cid -- \ + ':.' ':!debian/patches' + etypes=${etypes#*-} + ;; + Changelog) + git diff --quiet $expect..$cid -- \ + ':.' ':!debian/changelog' + etypes=Packaging + ;; + Upstream/SAME) + git diff --quiet $expect..$cid -- ':debian' + ;; + Anchor) + break + ;; + esac + + local cnparents=`printf "%s" "$parents" | wc -w` + test $cnparents = $enparents + + local cndparents=` + for f in $parents; do echo $f; done | sort -u | wc -w + ` + test $cndparents = $cnparents + + case "$parents" in + *"$expect"*) ;; + *) fail 'unexpected parent' ;; + esac + + done +} + +t-some-changes () { + local token=$1 + + t-git-next-date + + echo >>debian/zorkmid "// debian $token" + git add debian/zorkmid + git commit -m "DEBIAN add zorkmid ($token)" + + echo >>src.c "// upstream $token" + git commit -a -m "UPSTREAM edit src.c ($token)" + + for f in debian/zorkmid src.c; do + echo "// both! $token" >>$f + git add $f + done + git commit -m "MIXED add both ($token)" + + t-git-next-date +} + +t-make-new-upstream-tarball () { + local uv=$1 + git checkout make-upstream + # leaves ust set to filename of orig tarball + echo "upstream $uv" >>docs/README + git commit -a -m "upstream $uv tarball" + ust=example_$uv.orig.tar.gz + git archive -o ../$ust --prefix=example-2.0/ make-upstream +} + +t-nmu-upload-1 () { + # usage: + # v=<full version> + # nmu-upload-1 <nmubranch> + # gbp pq import or perhaps other similar things + # nmu-upload-2 + # maybe make some dgit-covertible commits + # nmu-upload-3 + + t-git-next-date + nmubranch=$1 + git checkout -f -b $nmubranch + t-git-debrebase + t-git-debrebase convert-to-gbp + t-git-next-date + # now we are on a gbp patched-unapplied branch +} + + +t-nmu-upload-2 () { + t-git-next-date + t-dch-commit -v $v -m "nmu $nmubranch $v" +} + +t-nmu-upload-3 () { + t-dch-commit -r sid + + t-dgit -wgf build-source + + cd .. + c=${p}_${v}_source.changes + debsign -kBCD22CD83243B79D3DFAC33EA3DBCBC039B13D8A $c + dput -c $tmp/dput.cf test-dummy $c + + t-archive-process-incoming sid + t-git-next-date + cd $p + git checkout master +} + +t-nmu-commit-an-upstream-change () { + echo >>newsrc.c "// more upstream" + git add newsrc.c + git commit -m 'UPSTREAM NMU' +} + +t-maintainer-commit-some-changes () { + t-dch-commit -v$v -m "start $v" + + t-some-changes "maintainer $v" + t-git-debrebase + t-git-debrebase stitch + + git branch did.maintainer + + t-git-next-date +} + +t-nmu-causes-ff-fail () { + t-dgit fetch + + t-expect-fail E:'Not.*fast-forward' \ + git merge --ff-only dgit/dgit/sid + + t-expect-fail E:'-fdiverged.*refs/remotes/dgit/dgit/sid' \ + t-git-debrebase +} + +t-nmu-reconciled-good () { + local nmutree=$1 + + : 'check that what we have is what is expected' + + git checkout -b compare.nmu origin/master~0 + git checkout $nmutree . + git rm -rf debian/patches + git commit -m 'rm patches nmu' + + git checkout -b compare.maintainer origin/master~0 + git checkout did.maintainer . + git rm -rf --ignore-unmatch debian/patches + git commit --allow-empty -m 'rm patches maintainer' + + git merge compare.nmu + git diff --quiet master + + : 'check that dgit still likes it' + + git checkout master + t-dgit -wgf quilt-fixup +} diff --git a/tests/setup/gdr-convert-gbp b/tests/setup/gdr-convert-gbp new file mode 100755 index 0000000..0b525c8 --- /dev/null +++ b/tests/setup/gdr-convert-gbp @@ -0,0 +1,100 @@ +#!/bin/bash +set -e +. tests/lib +. $troot/lib-gdr + +t-dependencies GDR + +t-tstunt-parsechangelog + +not-gdr-processable () { + t-git-debrebase analyse | grep 'Unknown Unprocessable' +} + +p=example +t-worktree 1.1 + +cd example + +: 'fake up some kind of upstream' +git checkout -b upstream quilt-tip +rm -rf debian +mkdir debian +echo junk >debian/rules +git add debian +git commit -m "an upstream retcon ($0)" + +: 'fake up that our quilt-tip was descended from upstream' +git checkout quilt-tip +git merge --no-edit -s ours upstream + +: 'fake up that our quilt-tip had the patch queue in it' +git checkout patch-queue/quilt-tip +gbp pq export +git add debian/patches +git commit -m "patch queue update ($0)" + +not-gdr-processable + +: 'fake up an upstream 2.0' +git branch make-upstream upstream +t-make-new-upstream-tarball 2.0 + +: 'make branch names more conventional' +git branch -D master +git branch -m quilt-tip master + +for b in \ + quilt-tip-2 \ + gitish-only \ + quilt-tip-1.1 \ + patch-queue/quilt-tip \ + indep-arch \ +; do + git branch -D $b +done + +: 'see what gbp import-orig does' +git checkout master +gbp import-orig --upstream-version=2.0 ../$ust + +not-gdr-processable + +t-dch-commit -v 2.0-1 -m 'new upstream (did gbp import-orig)' +t-dch-commit -r sid + +$ifarchive t-archive-none $p +$ifarchive t-git-none +$ifarchive t-dgit -wgf --gbp push-source --new + +t-salsa-add-remote +git push --set-upstream origin master + +# OK now this looks like something more normal. +# We have: +# maintainer (gbp) view dgit view +# master +# debian/2.0-1 archive/debian/2.0-1 +# remotes/origin/master remotes/dgit/dgit/sid + +t-git-debrebase -fupstream-has-debian convert-from-gbp + +v=2.0-2 +t-dch-commit -v $v -m 'switch to git-debrebase, no other changes' +t-dch-commit -r sid + +$ifarchive t-dgit -wgf push-source --new --overwrite +git push + +cd .. + +$ifarchive t-archive-process-incoming sid + +t-setup-done '' "$(echo $p*) salsa $($ifarchive echo git mirror aq)" ' + . $troot/lib-gdr + t-tstunt-parsechangelog + p=example + t-git-next-date +' + +t-ok diff --git a/tests/setup/gdr-convert-gbp-noarchive b/tests/setup/gdr-convert-gbp-noarchive new file mode 100755 index 0000000..dfeea3b --- /dev/null +++ b/tests/setup/gdr-convert-gbp-noarchive @@ -0,0 +1,9 @@ +#!/bin/bash +set -e +. tests/lib +. $troot/lib-gdr + +t-dependencies GDR + +export ifarchive=: +t-chain-test gdr-convert-gbp diff --git a/tests/tests/gdr-diverge-nmu b/tests/tests/gdr-diverge-nmu new file mode 100755 index 0000000..15bf901 --- /dev/null +++ b/tests/tests/gdr-diverge-nmu @@ -0,0 +1,61 @@ +#!/bin/bash +set -e +. tests/lib + +t-dependencies GDR + +t-setup-import gdr-convert-gbp + +cd $p + +t-dgit setup-mergechangelogs + +: 'maintainer' + +v=2.0-3 +t-maintainer-commit-some-changes + +t-git-next-date + +: 'non-dgit upload (but we prepare it with dgit anyway)' + +t-git-next-date +git checkout origin/master + +v=2.0-2+nmu1 +t-nmu-upload-1 nmu +gbp pq import +t-nmu-upload-2 +t-nmu-commit-an-upstream-change +t-nmu-upload-3 + +: 'ad hocery' + +t-git-next-date +git checkout master +t-nmu-causes-ff-fail + +git cherry-pick 'dgit/dgit/sid^{/UPSTREAM NMU}' + +t-expect-fail 'Automatic merge failed; fix conflicts' \ +git merge --squash -m 'Incorporate NMU' dgit/dgit/sid + +git rm -rf debian/patches +git commit -m 'Incorporate NMU' + +git merge -s ours -m 'Declare incorporate NMU' dgit/dgit/sid + +: 'right, how are we' + +t-git-next-date + +t-git-debrebase +t-gdr-good laundered + +t-git-debrebase stitch +t-gdr-good stitched + + +t-nmu-reconciled-good patch-queue/nmu + +t-ok diff --git a/tests/tests/gdr-diverge-nmu-dgit b/tests/tests/gdr-diverge-nmu-dgit new file mode 100755 index 0000000..4b5907a --- /dev/null +++ b/tests/tests/gdr-diverge-nmu-dgit @@ -0,0 +1,55 @@ +#!/bin/bash +set -e +. tests/lib + +t-dependencies GDR + +t-setup-import gdr-convert-gbp + +cd $p + +t-dgit setup-mergechangelogs + +: 'maintainer' + +git checkout master + +v=2.0-3 +t-maintainer-commit-some-changes + +t-git-next-date + +: 'nmu' + +git checkout -b nmu origin/master~0 + +t-git-next-date + +v=2.0-2+nmu1 +t-nmu-commit-an-upstream-change +t-dch-commit -v$v -m finalise +t-dch-commit -r sid + +t-dgit -wgf push-source + +t-archive-process-incoming sid + +: 'rebase nmu onto our branch' + +t-git-next-date +git checkout master +t-nmu-causes-ff-fail + +git checkout dgit/dgit/sid # detach + +t-expect-fail 'E:CONFLICT.*Commit Debian 3\.0 \(quilt\) metadata' \ +git rebase master +git rebase --skip + +git push . HEAD:master +git checkout master + + +t-nmu-reconciled-good nmu + +t-ok diff --git a/tests/tests/gdr-edits b/tests/tests/gdr-edits new file mode 100755 index 0000000..6c77184 --- /dev/null +++ b/tests/tests/gdr-edits @@ -0,0 +1,40 @@ +#!/bin/bash +set -e +. tests/lib + +t-dependencies GDR + +t-setup-import gdr-convert-gbp + +cd $p + +v=2.0-3 +t-dch-commit -v $v -m testing + +t-git-debrebase analyse |tee ../anal.1 +cat ../anal.1 + +t-some-changes edits + +t-dch-commit -r sid + +git tag t.before + +t-git-debrebase +t-gdr-good laundered + +t-dgit push-source +t-gdr-good pushed-interop + +git branch before-noop + +t-git-next-date +t-git-debrebase +t-git-debrebase stitch +t-gdr-good pushed-interop + +t-refs-same-start +t-ref-same refs/heads/before-noop +t-ref-head + +t-ok diff --git a/tests/tests/gdr-import-dgit b/tests/tests/gdr-import-dgit new file mode 100755 index 0000000..19918d8 --- /dev/null +++ b/tests/tests/gdr-import-dgit @@ -0,0 +1,68 @@ +#!/bin/bash +set -e +. tests/lib + +t-dependencies GDR + +t-setup-import gdr-convert-gbp + +cd $p + +: 'non-dgit upload (but we prepare it with dgit anyway)' + +v=2.0-2+nmu1 +t-nmu-upload-1 nmu +gbp pq import +t-nmu-upload-2 +t-some-changes $numbranch +t-nmu-upload-3 + +: 'done the nmu, switching back to the maintainer hat' + +nmu-fold () { + t-git-next-date + t-dgit fetch + t-git-next-date + git merge --ff-only dgit/dgit/sid + + git diff --exit-code patch-queue/$nmubranch + + git branch unlaundered.$nmubranch + + t-git-debrebase + t-gdr-good laundered + + t-git-debrebase stitch + t-gdr-good stitched +} + +nmu-fold + +v=2.0-3 +t-dch-commit -v $v -m "incorporate nmu" +t-dch-commit -r sid +t-dgit -wgf push-source + +: 'now test a new upstream' + +t-make-new-upstream-tarball 2.1 + +git checkout master +v=2.1-0+nmu1 +t-nmu-upload-1 nmu2 + +gbp import-orig --upstream-version=2.1 --debian-branch=nmu2 ../$ust +t-dch-commit -v $v -m "new upstream $v" +gbp pq import + +#t-dgit -wgf build-source + +t-nmu-upload-2 +t-some-changes $numbranch +t-nmu-upload-3 + +: 'done the nmu, back to the maintainer' + +nmu-fold + +t-ok diff --git a/tests/tests/gdr-newupstream-v0 b/tests/tests/gdr-newupstream-v0 new file mode 100755 index 0000000..e866edc --- /dev/null +++ b/tests/tests/gdr-newupstream-v0 @@ -0,0 +1,65 @@ +#!/bin/bash +set -e +. tests/lib + +t-dependencies NO-DGIT GDR + +t-setup-import gdr-convert-gbp-noarchive + +cd $p + +: 'upstream hat' + +new-upstream () { + uv=$1 + t-git-next-date + git checkout make-upstream + git reset --hard upstream + t-make-new-upstream-tarball $uv + git push . make-upstream:upstream + git checkout master + t-git-next-date +} + +new-upstream 2.1 + +: 'maintainer hat' + +git branch startpoint +v=2.1-1 + +git checkout master + +t-expect-fail F:'Could not determine appropriate upstream commitish' \ +t-git-debrebase new-upstream-v0 $v + +git tag v2.1 upstream + +t-git-debrebase new-upstream-v0 $v +t-gdr-good laundered + +t-git-debrebase stitch +t-gdr-good stitched + +git branch ordinary + +: 'with --anchor' + +git reset --hard startpoint + +t-git-debrebase analyse >../anal.anch +anchor=$(perl <../anal.anch -ne ' + next unless m/^(\w+) Anchor\s/; + print $1,"\n"; + exit; +') + +t-git-debrebase --anchor=$anchor -fanchor-treated new-upstream-v0 $v upstream +t-gdr-good laundered + +t-git-debrebase stitch +t-gdr-good stitched + +git diff --quiet ordinary + +t-ok diff --git a/tests/tests/gdr-subcommands b/tests/tests/gdr-subcommands new file mode 100755 index 0000000..e59fc07 --- /dev/null +++ b/tests/tests/gdr-subcommands @@ -0,0 +1,226 @@ +#!/bin/bash +set -e +. tests/lib + +t-dependencies GDR + +t-setup-import gdr-convert-gbp + +cd $p + +t-dgit setup-mergechangelogs + +mix-it () { + t-git-next-date + + local m=$(git symbolic-ref HEAD) + t-some-changes "subcommands $m 1" + + # we want patches mde by dgit, not gdr, for our test cases + t-dgit --git-debrebase=true -wgf quilt-fixup + t-git-next-date + + t-some-changes "subcommands $m 2" + t-git-next-date +} + +git checkout -b stitched-laundered master +mix-it +t-git-debrebase quick +t-gdr-good stitched HEAD + +git checkout -b stitched-mixed master +mix-it + +git checkout -b unstitched-laundered master +mix-it +t-git-debrebase +t-gdr-good laundered + +git checkout -b unstitched-mixed master +t-git-debrebase +mix-it + +t-git-next-date + +git show-ref + +subcmd () { + local subcmd=$1 + shift + for startbranch in {stitched,unstitched}-{laundered,mixed}; do + work="work-$subcmd-$startbranch" + + : "---------- $subcmd $startbranch ----------" + + git for-each-ref "**/$startbranch"{,/**} \ + --format='create %(refname) %(objectname)' \ + | sed "s/$startbranch/$work/" \ + | git update-ref --stdin + + git checkout $work + checkletters=$1; shift + + before=before-$work + git branch $before + + local xopts='' + + case "$checkletters" in + XX*) + fail "$checkletters" # for debugging + ;; + esac + + case "$checkletters" in + X*) + t-expect-fail E:'snags: [0-9]* blockers' \ + t-git-debrebase $xopts $subcmd + xopts+=' --force' + next_checkletter + ;; + esac + + case "$checkletters" in + N*) + t-expect-fail E:. \ + t-git-debrebase $xopts $subcmd + xopts+=' --noop-ok' + next_checkletter + ;; + esac + + case "$checkletters" in + [EF]:*) + t-expect-fail "$checkletters" \ + t-git-debrebase $xopts $subcmd + continue + ;; + *) + t-git-debrebase $xopts $subcmd + ;; + esac + + peel=peel-$subcmd-$startbranch + git checkout -b $peel + t-clean-on-branch $peel + + : "---------- $subcmd $startbranch $checkletters ----------" + + while [ "x$checkletters" != x ]; do + : "---- $subcmd $startbranch ...$checkletters ----" + make_check "$checkletters" + checkletters="${checkletters#?}" + done + done + +} + +next_checkletter () { + checkletters="${checkletters#?}" +} + +make_check () { + case "$1" in + [Nn]*) + t-refs-same-start + t-refs-same refs/heads/$before refs/heads/$work + ;; + U*) + t-refs-same-start + t-refs-same refs/heads/$before refs/ffq-prev/heads/$work + make_check u + ;; + u*) + t-git-get-ref refs/ffq-prev/heads/$work + t-refs-notexist refs/debrebase-last/heads/$work + ;; + V*) + t-refs-same-start + t-refs-same refs/ffq-prev/heads/$work \ + refs/ffq-prev/heads/$startbranch + t-refs-notexist refs/debrebase-last/heads/$work + ;; + s*) + t-refs-notexist refs/ffq-prev/heads/$work + t-refs-same-start + t-refs-same refs/debrebase-last/heads/$work \ + refs/debrebase-last/heads/$startbranch + t-has-ancestor HEAD refs/debrebase-last/heads/$work + ;; + S*) + t-refs-notexist refs/ffq-prev/heads/$work + t-refs-same-start refs/debrebase-last/heads/$work + t-ref-head + git diff --quiet HEAD^1 + git diff HEAD^2 | grep $startbranch + git reset --hard HEAD^1 + ;; + P*) + t-dgit -wgf --quilt=nofix quilt-fixup + git diff HEAD~ debian/patches | egrep . + git diff --quiet HEAD~ -- ':.' ':!debian/patches' + git reset --hard HEAD~ + ;; + l*) + git diff --quiet HEAD refs/heads/$before -- ':.' ':!debian/patches' + t-gdr-good laundered + ;; + t*) + git diff --quiet HEAD refs/heads/$before + ;; + f*) + t-has-ancestor HEAD refs/heads/$before + ;; + *) + fail "$1" + ;; + esac +} + +Ec="F:No ongoing git-debrebase session" +Ep="F:Patch export produced patch amendments" + +# input state: +# stitched? st'd st'd unst'd unst'd +# laundered? laund'd mixed laund'd mixed +# +# "mixed" means an out of order branch +# containing mixed commits and patch additions, +# but which needs even more patches +# +subcmd '' Ult Ull Vlt Vl +subcmd stitch Ns Nu Sltf Stf +subcmd prepush Ns Nu Sltf Stf +subcmd quick ns Sl Sltf Sl +subcmd conclude "$Ec" "$Ec" Sltf Sl +subcmd make-patches sPft "$Ep" uPft "$Ep" +#subcmd dgit-upload-hook Psft "$Ep" SPft "$Ep" +# +# result codes, each one is a check: +# E:$pat } this is an error (must come first) +# F:$pat } arg is passed to expect-fail +# +# X should fail due to snags, but succeed when forced +# XX crash out of script for manual debugging +# +# N this is a noop, error unless --noop-ok +# n this is a silent noop +# both of these imply tf; but, specify also one of u s +# +# should normally specify one of these: +# U just unstiched: ffq-prev is exactly previous HEAD; implies u +# u result is unstitched +# V ffq-prev remains unchanged; implies also u +# s result is stitched, debrebase-last exists and is unchanged +# S result is stitch just made, remaining letters apply to result~ +# +# P result is add-patches, remaining letters apply to result~ +# +# should normally specify one or both of these: +# l result is laundered, tree is same as before minus d/patches +# t tree is exactly same as before +# +# f result is ff from previous HEAD + +t-ok diff --git a/tests/tests/gdr-viagit b/tests/tests/gdr-viagit new file mode 100755 index 0000000..644d2d4 --- /dev/null +++ b/tests/tests/gdr-viagit @@ -0,0 +1,40 @@ +#!/bin/bash +set -e +. tests/lib + +t-dependencies NO-DGIT GDR + +t-setup-import gdr-convert-gbp-noarchive + +: 'set up so t-git-debrebase runs gdr via git' + +case "$DGIT_GITDEBREBASE_TEST" in +''|git-debrebase) ;; +*) + t-tstunt + st=$tmp/tstunt/git-debrebase + export DGIT_GITDEBREBASE_TEST_REAL="$DGIT_GITDEBREBASE_TEST" + cat <<'END' >$st +#!/bin/sh +set -x +exec "$DGIT_GITDEBREBASE_TEST_REAL" "$@" +END + chmod +x $st + ;; +esac + +DGIT_GITDEBREBASE_TEST='git debrebase' + +: 'do a simple test' + +cd $p + +t-some-changes + +t-git-debrebase +t-gdr-good laundered + +t-git-debrebase stitch --prose=wombat +t-gdr-good stitched + +t-ok |