#
# git-debrebase [<options> --] [<git-rebase options...>]
# git-debrebase [<options>] analyse
-# git-debrebase [<options>] launder # prints breakwater tip etc.
+# git-debrebase [<options>] breakwater # prints breakwater tip only
# git-debrebase [<options>] stitch [--prose=<for commit message>]
+# git-debrebase [<options>] launder-v0 # prints breakwater tip etc.
# git-debrebase [<options>] downstream-rebase-launder-v0 # experimental
#
-# git-debrebase [<options>] convert-from-gbp <upstream-git-rev>
+# git-debrebase [<options>] convert-from-gbp [<upstream-git-rev>]
+# git-debrebase [<options>] convert-to-gbp
# problems / outstanding questions:
#
fail "missing required git config $k" unless $optional;
return ();
}
- return split /\0/, $out;
+ my @l = split /\0/, $out;
+ return wantarray ? @l : $l[0];
}
memoize('cfg');
in_workarea sub { playtree_setup };
}
+our @deferred_updates;
+our @deferred_update_messages;
+
+sub run_deferred_updates ($) {
+ my ($mrest) = @_;
+
+ 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.
# Upstream
# AddPatches
# Mixed
-# Unknown
#
# Pseudomerge
# has additional entres in classification result
# has additional entry in classification result
# OrigParents = [ subset of Parents ]
#
-# BreakwaterUpstreamMerge
+# Anchor
# has additional entry in classification result
# OrigParents = [ subset of Parents ] # singleton list
+#
+# BreakwaterStart
+#
+# Unknown
+# has additional entry in classification result
+# Why => "prose"
sub parsecommit ($;$) {
my ($objid, $p_ref) = @_;
};
my $unknown = sub {
my ($why) = @_;
- $r = { %$r, Type => qw(Unknown) };
+ $r = { %$r, Type => qw(Unknown), Why => $why };
printdebug " ** Unknown\n";
return $r;
};
- my $claims_to_be_breakwater =
- $r->{Msg} =~ m{^\[git-debrebase breakwater.*\]$}m;
+ 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
+
+ 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).
+
+ $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) {
- if ($claims_to_be_breakwater) {
- return $unknown->("single-parent git-debrebase breakwater \`merge'");
- }
my $d = $r->{Parents}[0]{Differs};
if ($d == D_PAT_ADD) {
return $classify->(qw(AddPatches));
return $unknown->("origin commit");
}
- my @identical = grep { !$_->{Differs} } @p;
- if (@p == 2 && @identical == 1 && !$claims_to_be_breakwater
- # breakwater merges can look like pseudomerges, if they are
- # "declare" commits (ie, there are no upstream changes)
- ) {
+ if (@p == 2 && @identical == 1) {
my @overwritten = grep { $_->{Differs} } @p;
confess "internal error $objid ?" unless @overwritten==1;
return $classify->(qw(Pseudomerge),
Contributor => $identical[0]);
}
if (@p == 2 && @identical == 2) {
- my @bytime = nsort_by {
- my ($ph,$pm) = get_commit $_->{CommitId};
+ my $get_t = sub {
+ my ($ph,$pm) = get_commit $_[0]{CommitId};
$ph =~ m/^committer .* (\d+) [-+]\d+$/m or die "$_->{CommitId} ?";
$1;
- } @p;
+ };
+ 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),
- Overwritten => [ $bytime[0] ],
- Contributor => $bytime[1]);
+ Contributor => $bytime[0],
+ Overwritten => [ $bytime[1] ]);
}
foreach my $p (@p) {
my ($p_h, $p_m) = get_commit $p->{CommitId};
OrigParents => \@orig_ps);
}
- my ($stype, $series) = git_cat_file "$t:debian/patches/series";
- my $haspatches = $stype ne 'missing' && $series =~ m/^\s*[^#\n\t ]/m;
+ return $unknown->("complex merge");
+}
- # How to decide about l/r ordering of breakwater merges ? 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).
-
- # The above tells us which way *we* will generate them. But we
- # might encounter ad-hoc breakwater merges generated manually,
- # which might be the other way around. In principle, in some odd
- # situations, a breakwater merge might have two identical parents.
- # In that case we guess which way round it is (ie, which parent
- # has the upstream history). The order of the 2-iteration loop
- # controls which guess we make.
-
- foreach my $prevbrw (qw(0 1)) {
- if (@p == 2 &&
- !$haspatches &&
- !$p[$prevbrw]{IsOrigin} && # breakwater never starts with an origin
- !($p[!$prevbrw]{Differs} & ~DS_DEB) && # no non-debian changess
- !($p[$prevbrw]{Differs} & ~D_UPS)) { # no non-upstream changes
- return $classify->(qw(BreakwaterUpstreamMerge),
- OrigParents => [ $p[!$prevbrw] ]);
+sub breakwater_of ($) {
+ my ($head) = @_; # must be laundered
+ my $breakwater;
+ my $unclean = sub {
+ my ($why) = @_;
+ fail "branch needs laundering (run git-debrebase): $why";
+ };
+ for (;;) {
+ my $cl = classify $head;
+ my $ty = $cl->{Type};
+ if ($ty eq 'Packaging' or
+ $ty eq 'Changelog') {
+ $breakwater //= $head;
+ } elsif ($ty eq 'Anchor' or
+ $ty eq 'BreakwaterStart') {
+ $breakwater //= $head;
+ last;
+ } elsif ($ty eq 'Upstream') {
+ $unclean->("packaging change ($breakwater)".
+ " follows upstream change (eg $head)")
+ if defined $breakwater;
+ } elsif ($ty eq 'Mixed') {
+ $unclean->('found mixed upstream/packaging commit ($head)');
+ } elsif ($ty eq 'Pseudomerge' or
+ $ty eq 'AddPatches') {
+ $unclean->("found interchange conversion commit ($ty, $head)");
+ } elsif ($ty eq 'DgitImportUnpatched') {
+ $unclean->("found dgit dsc import ($head)");
+ } else {
+ fail "found unprocessable commit, cannot cope: $head; $cl->{Why}";
}
+ $head = $cl->{Parents}[0]{CommitId};
}
-
- # multi-orig upstreams are represented with a breakwater merge
- # from a single upstream commit which combines the orig tarballs
-
- return $unknown->("complex merge");
+ return $breakwater;
}
sub walk ($;$$);
sub walk ($;$$) {
my ($input,
$nogenerate,$report) = @_;
- # => ($tip, $breakwater_tip, $last_upstream_merge_in_breakwater)
+ # => ($tip, $breakwater_tip, $last_anchor)
# (or nothing, if $nogenerate)
printdebug "*** WALK $input ".($nogenerate//0)." ".($report//'-')."\n";
return (Msg => $ms);
};
my $rewrite_from_here = sub {
+ my ($cl) = @_;
my $sp_cl = { SpecialMethod => 'StartRewrite' };
- push @brw_cl, $sp_cl;
+ push @$cl, $sp_cl;
push @processed, $sp_cl;
};
my $cur = $input;
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} }). ")";
+ @{ $cl->{Parents} }).
+ (defined $cl->{Why} ? "; $cl->{Why}": '').
+ ")";
};
my $build;
no warnings qw(exiting); last;
};
- my $last_upstream_update;
+ my $last_anchor;
for (;;) {
$cl = classify $cur;
my $p0 = @{ $cl->{Parents} }==1 ? $cl->{Parents}[0]{CommitId} : undef;
if ($ty eq 'AddPatches') {
$cur = $p0;
- $rewrite_from_here->();
+ $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_upstream_update = $cur;
+ $last_anchor = $cur;
$build_start->('FirstPackaging', $cur);
} elsif ($ty eq 'Upstream') {
push @upp_cl, $cl;
};
$queue->(\@brw_cl, "debian");
$queue->(\@upp_cl, "upstream");
- $rewrite_from_here->();
+ $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->();
+ $rewrite_from_here->(\@upp_cl);
$cur = $contrib;
next;
- } elsif ($ty eq 'BreakwaterUpstreamMerge') {
- $last_upstream_update = $cur;
- $build_start->("PreviousBreakwater", $cur);
+ } elsif ($ty eq 'Anchor') {
+ $last_anchor = $cur;
+ $build_start->("Anchor", $cur);
} elsif ($ty eq 'DgitImportUnpatched') {
my $pm = $pseudomerges[-1];
if (defined $pm) {
%$cl,
SpecialMethod => 'DgitImportUpstreamUpdate',
$xmsg->("convert dgit import: upstream update",
- " breakwater")
+ " anchor")
};
$prline->(" Import");
- $rewrite_from_here->();
+ $rewrite_from_here->(\@brw_cl);
$upp_limit //= $#upp_cl; # further, deeper, patches discarded
$cur = $ovwr;
next;
confess "$ch ?" unless $rewriting or $newcommit eq $cl->{CommitId};
$build = $newcommit;
if (grep { $method eq $_ } qw(DgitImportUpstreamUpdate)) {
- $last_upstream_update = $cur;
+ $last_anchor = $cur;
}
}
};
die sprintf "internal error %#x %s %s", $final_check, $build, $input
if $final_check & ~D_PAT_ADD;
- my @r = ($build, $breakwater, $last_upstream_update);
+ my @r = ($build, $breakwater, $last_anchor);
printdebug "*** WALK RETURN @r\n";
return @r
}
sub update_head ($$$) {
my ($old, $new, $mrest) = @_;
- runcmd @git, qw(update-ref -m), "debrebase: $mrest", 'HEAD', $new, $old;
+ push @deferred_updates, "update HEAD $new $old";
+ run_deferred_updates $mrest;
}
sub update_head_checkout ($$$) {
runcmd @git, qw(rm --quiet --ignore-unmatch -rf debian/patches);
}
-sub cmd_launder () {
- badusage "no arguments to launder allowed" if @ARGV;
+sub cmd_launder_v0 () {
+ badusage "no arguments to launder-v0 allowed" if @ARGV;
my $old = get_head();
- my ($tip,$breakwater,$last_upstream_merge) = walk $old;
+ 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 upstream merge\n%s\n", $last_upstream_merge;
+ printf "# last anchor\n%s\n", $last_anchor;
}
sub defaultcmd_rebase () {
my $old = get_head();
- my ($status, $message) = record_ffq_prev();
- if ($status eq 'written' || $status eq 'exists') {
+ my ($status, $message) = record_ffq_prev_deferred();
+ if ($status eq 'deferred' || $status eq 'exists') {
} else {
fproblem $status, "could not record ffq-prev: $message";
fproblems_maybe_bail();
return ('weird-symref', 'HEAD symref is not to refs/')
unless $current =~ m{^refs/};
my $ffq_prev = "refs/$ffq_refprefix/$'";
+ printdebug "ffq_prev_branchinfo branch current $current\n";
return ('branch', undef, $current, $ffq_prev);
}
-sub record_ffq_prev () {
+sub record_ffq_prev_deferred () {
# => ('status', "message")
# 'status' may be
- # written message is undef
+ # deferred message is undef
# exists
# detached
# weird-symref
# notbranch
# if not ff from some branch we should be ff from, is an fproblem
- # if "written", will have printed something about that to stdout,
- # and also some messages about ff checks
+ # 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) = ffq_prev_branchinfo();
return ($status, $message) unless $status eq '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{^[^!]}{};
+ $invert = $glob =~ s{^[!^]}{};
last if fnmatch $glob, $lrref;
}
return if $invert;
if (is_fast_fwd $lrval, $currentval) {
print "OK, you are ahead of $lrref\n" or die $!;
$checked{$lrref} = 1;
- } if (is_fast_fwd $currentval, $lrval) {
+ } elsif (is_fast_fwd $currentval, $lrval) {
$checked{$lrref} = -1;
fproblem 'behind', "you are behind $lrref, divergence risk";
} else {
};
my $merge = cfg "branch.$branch.merge",1;
- if (defined $merge && $merge =~ m{^refs/heads/}) {
+ if (defined $merge and $merge =~ m{^refs/heads/}) {
my $rhs = $';
+ printdebug "ffq merge $rhs\n";
my $check_remote = sub {
- my ($remote, $desc) = (@_);
+ my ($remote, $desc) = @_;
+ printdebug "ffq check_remote ".($remote//'undef')." $desc\n";
return unless defined $remote;
$check->("refs/remotes/$remote/$rhs", $desc);
};
- $check_remote->((cfg "branch.$branch.remote",1),
+ $check_remote->((scalar cfg "branch.$branch.remote",1),
'remote fetch/merge branch');
- $check_remote->((cfg "branch.$branch.pushRemote",1) //
- (cfg "branch.$branch.pushDefault",1),
+ $check_remote->((scalar cfg "branch.$branch.pushRemote",1) //
+ (scalar cfg "branch.$branch.pushDefault",1),
'remote push branch');
}
if ($branch =~ m{^dgit/}) {
- $check->("remotes/dgit/$branch", 'remote dgit branch');
+ $check->("refs/remotes/dgit/$branch", 'remote dgit branch');
} elsif ($branch =~ m{^master$}) {
- $check->("remotes/dgit/dgit/sid", 'remote dgit branch for sid');
+ $check->("refs/remotes/dgit/dgit/sid", 'remote dgit branch for sid');
}
fproblems_maybe_bail();
- runcmd @git, qw(update-ref -m), "record current head for preservation",
- $ffq_prev, $currentval, $git_null_obj;
- print "Recorded current head for preservation\n" or die $!;
- return ('written', undef);
+
+ push @deferred_updates, "update $ffq_prev $currentval $git_null_obj";
+ push @deferred_update_messages, "Recorded current head for preservation";
+ return ('deferred', undef);
}
sub cmd_new_upstream_v0 () {
# $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 breakwater special merge.
+ # "upstream" parent of the anchor merge.
read_tree_subdir 'debian', "$old_bw:debian";
- # index now contains the breakwater merge contents
+ # index now contains the anchor merge contents
$new_bw = make_commit [ $old_bw, $new_upstream ],
[ "Update to upstream $new_upstream_version",
- "[git-debrebase breakwater: new upstream $new_upstream_version, merge]",
+ "[git-debrebase anchor: new upstream $new_upstream_version, merge]",
];
# Now we have to add a changelog stanza so the Debian version
sub cmd_record_ffq_prev () {
badusage "no arguments allowed" if @ARGV;
- my ($status, $msg) = record_ffq_prev();
+ 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 'written') {
+ } elsif ($status eq 'deferred') {
+ run_deferred_updates 'record-ffq-prev';
} else {
fail "Could not preserve: $msg";
}
}
+sub cmd_breakwater () {
+ badusage "no arguments allowed" if @ARGV;
+ my $bw = breakwater_of git_rev_parse 'HEAD';
+ print "$bw\n" or die $!;
+}
+
sub cmd_stitch () {
my $prose = '';
GetOptions('prose=s', \$prose) or die badusage("bad options to stitch");
if (!$prev) {
fail "No ffq-prev to stitch." unless $opt_noop_ok;
}
- fresh_workarea();
+ push @deferred_updates, "delete $ffq_prev $prev";
+
my $old_head = get_head();
+ if (is_fast_fwd $old_head, $prev) {
+ my $differs = get_differs $old_head, $prev;
+ 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.
+ update_head_checkout $old_head, $prev, "stitch (fast forward)";
+ return;
+ }
+ }
+ fresh_workarea();
my $new_head = make_commit [ $old_head, $ffq_prev ], [
'Declare fast forward / record previous work',
"[git-debrebase pseudomerge: stitch$prose]",
];
- my @upd_cmd = (@git, qw(update-ref --stdin));
- debugcmd '>|', @upd_cmd;
- open U, "|-", @upd_cmd or die $!;
- my $u = <<END;
-update HEAD $new_head $old_head
-delete $ffq_prev $prev
-END
- printdebug ">= ", $_, "\n" foreach split /\n/, $u;
- print U $u;
- printdebug ">\$\n";
- close U or failedcmd @upd_cmd;
+ update_head $old_head, $new_head, "stitch";
}
sub cmd_convert_from_gbp () {
'Delete debian/patches, as part of converting to git-debrebase format.',
'[git-debrebase convert-from-gbp: drop patches from tree]'
];
- # make the breakwater pseudomerge
+ # 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 breakwater: declare upstream]'
+ '[git-debrebase anchor: declare upstream]'
];
# rebase the patch queue onto the new breakwater
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 $ffq = (ffq_prev_branchinfo())[3];
+ my $bw = breakwater_of $head;
+ fresh_workarea();
+ my $out;
+ in_workarea sub {
+ runcmd @git, qw(checkout -q -b bw), $bw;
+ runcmd @git, qw(checkout -q -b patch-queue/bw), $head;
+ runcmd qw(gbp pq export);
+ runcmd @git, qw(add debian/patches);
+ $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";
+ }
+ 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;