#!/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 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 . use strict; use Memoize; use Data::Dumper; use Debian::Dgit; sub cfg ($) { my ($k) = @_; $/ = "\0"; my @cmd = qw(git config -z); push @cmd, qw(--get-all) if wantarray; push @cmd, $k; my $out = cmdoutput @cmd; return split /\0/, $out; } memoize('cfg'); # usage # git debrebase launder sub get_commit ($) { my ($objid) = @_; my ($type,$data) = git_cat_file $objid; die unless $type eq 'commit'; $data =~ m/(?<=\n)\n/; return ($`,$'); } sub D_DEB () { return 0x1; } sub D_UPS () { return 0x2; } sub D_PAT_ADD () { return 0x4; } sub D_PAT_OTH () { return 0x8; } sub commit_pr_info ($) { my ($r) = @_; return Data::Dumper->dump([$r], [qw(commit)]); } sub classify ($) { my ($objid) = @_; my ($h,$m) = get_commit $objid; my ($t) = $h =~ m/^tree (\w+)$/m or die $cur; my (@ph) = $h =~ m/^parent (\w+)$/m; my @p; my $r = { CommitId => $objid, Hdr => $hdr, Msg => $m, Tree => $t, Parents => \@p, }; foreach my $ph (@ph) { push @p, { Ix => $#p, CommitId => $ph, Differs => (get_differs $t, $ph), }; } my $classify = sub { my ($type, @rest) = @_; $r = { %r, Type => $type, @rest }; return $r; }; my $unknown = sub { my ($why) = @_; $r = { %r, Type => Unknown }; return $r; } if (@p == 1) { my $d = $r->{Parents}[0]{Differs}; if ($d == D_DPAT_ADD) { return $classify->(qw(AddPatches)); } elsif ($d & (D_DPAT_ADD|D_DPAT_OTH)) { return $unknown->("edits debian/patches"); } elsif ($d == D_DEB) { return $classify->(qw(Packaging)); } elsif ($d == D_UPS) { return $classify->(qw(Upstream)); } elsif ($d == D_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"); } my @identical = grep { !$_->{Differs} } @p; 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 @bytime = nsort_by { my ($ph,$pm) = get_commit $_->{CommitId}; $ph =~ m/^committer .* (\d+) [-+]\d+$/m or die "$_->{CommitId} ?"; $1; } @p; return $classify->(qw(Pseudomerge), SubType => qw(Ambiguous), Overwritten => $bytime[0], Contributor => $bytime[1]); }! foreach my $p (@p) { my ($p_h, $p_m) = get_commit $p; $p->{IsOrigin} = $p_h !~ m/^parent \w+$/m; $p->{IsDgitImport} = $p_m =~ m/^\[dgit import .*\]$/m; $p->{IsDgitImportOrig} = $p_m =~ m/^\[dgit import orig .*\]$/m; } my $m2 = $m; if (!grep { !($_->{IsOrigin} && $_->{isDgitImport}) } @p and $m2 =~ s{^\[(dgit import unpatched .*)\]$}{[was: $1]}m) { xxx check exactly one IsDgitImport that is tarball $r->{NewMsg} = $m2; return $classify->(qw(DgitImportUnpatched)); } my ($stype, $series) = git_cat_file "$t:debian/patches/series"; my $haspatches = $stype ne 'missing' && $series =~ m/^\s*[^#\n\t ]/m; # how to decide about l/r ordering of breakwater merges # git --topo-order prefers to expand 2nd parent first # easy rune to look for debian/ history so that should # be 1st parent. if (@p == 2 && !$haspatches && !$p[0]{IsOrigin} && # breakwater merge never starts with an origin !($p[0]{Differs} & ~D_DEB) && !($p[1]{Differs} & ~D_UPS)) { return $classify->(qw(BreakwaterUpstreamMerge), Upstream => $p[1]); } return $unknown->("complex merge"); } sub launder ($) { my ($cur) = @_; # go through commits backwards # we generate two lists of commits to apply my (@phases, @deb_cl, @ups_cl); my %found; my @psuedomerges; for (;;) { my $cl = classify $cur; my $ty = $cl->{Type}; my $st = $cl->{SubType}; $found{$ty. ( defined($st) ? "-$st" : '' )}++; my $p0 = $cl->{Parents}[0]{CommitId}; if ($ty eq 'AddPatches') { $cur = $p0; next; } elsif ($ty eq 'Packaging') { push @deb_cl, $cur; $cur = $p0; next; } elsif ($ty eq 'Upstream') { push @ups_cl, $cur; $cur = $p0; next; } elsif ($ty eq 'Mixed') { my $queue = sub { my ($q, $wh) = @_; my $ms = $cl->{Msg}; chomp $ms; $ms .= "\n[git-debrebase split mixed commit: $wh part]\n"; my $cls = { $cl, Msg => $ms }; push @$q, $cls; }; $queue->(\@deb_cl, "debian"); $queue->(\@ups_cl, "upstream"); next; } elsif ($ty eq 'Pseudomerge') { push @pseudomerges, $cl; $cur = $ty->{Contributor}; next; } elsif ($ty eq 'DgitImportUnpatched' && @pseudomerges == 1) { # This import has a tree which is just like a breakwater # tree, but it has the wrong history. Its ought to have # the previous breakwater (which dgit ought to have # generated a pseudomerge to overwrite) as an ancestor. # That will make the history of the debian/ files correct. # As for the upstream version: either it's the same upstream # as the previous breakwater, in which case that history is # precisely right. 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. my $previous_breakwater = launder $pseudomerges[0]{Overwritten}; my $differs = get_differs $previous_breakwater, $cl->{Tree}; if ($differs & D_UPS) { $r->{FixupUpstreamMerge} = $ push @deb_cl, { $cl, Type => NonGitNewUpstreamDgitImport, PreviousBreakwaterMerge => push @phases, [ @deb_cl, @ups_cl ]; if (@pseudomerges != 1) { } if ($ARGV[0] eq 'launder') { launder(); } use Data::Dumper; print Dumper(cfg('wombat.foo.bar')); ((git_cat_file "$t:debian/patches/series" my @ return $r; $r->{Type} = ' $r->{Type} = ' return # changes on debian/patches, discard it $cur = $p[0]; next; } if ($d & DPAT) { ($r->{Tree},) = when starting must record original start (for ff) and new rebase basis