#
# 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>] gbp2debrebase-v0 \
-# <upstream>
+# git-debrebase [<options>] convert-from-gbp [<upstream-git-rev>]
+# git-debrebase [<options>] convert-to-gbp
# problems / outstanding questions:
#
die "bad usage: $m\n";
}
-sub cfg ($) {
- my ($k) = @_;
- $/ = "\0";
+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 @cmd;
- return split /\0/, $out;
+ 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');
# 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),
- Overwritten => $overwritten[0],
+ Overwritten => [ $overwritten[0] ],
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";
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;
$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;
$rewrite_from_here->();
$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) {
# 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, (scalar @$ovwrs)
+ printf $report " PM=%s \@Overwr:%d",
+ $pm->{CommitId}, (scalar @$ovwrs)
if $report;
if (@$ovwrs != 1) {
printdebug "*** WALK BOMB DgitImportUnpatched\n";
%$cl,
SpecialMethod => 'DgitImportDebianUpdate',
$xmsg->("convert dgit import: debian changes")
+ }, {
+ %$cl,
+ SpecialMethod => 'DgitImportUpstreamUpdate',
+ $xmsg->("convert dgit import: upstream update",
+ " anchor")
};
- my $differs = (get_differs $ovwr, $cl->{Tree});
- printf $report " Differs=%#x", $differs if $report;
- if ($differs & D_UPS) {
- printf $report " D_UPS" if $report;
- # This will also trigger if a non-dgit git-based NMU
- # deleted .gitignore (which is a thing that some of
- # the existing git tools do if the user doesn't
- # somehow tell them not to). Ah well.
- push @brw_cl, {
- %$cl,
- SpecialMethod => 'DgitImportUpstreamUpdate',
- $xmsg->("convert dgit import: upstream changes",
- " breakwater")
- };
- }
$prline->(" Import");
$rewrite_from_here->();
$upp_limit //= $#upp_cl; # further, deeper, patches discarded
- die 'BUG $upp_limit is not used anywhere?';
$cur = $ovwr;
next;
} else {
runcmd @git, qw(read-tree), $treeish;
$read_tree_debian->($build);
};
+
+ $#upp_cl = $upp_limit if defined $upp_limit;
my $committer_authline = calculate_committer_authline();
next;
} elsif ($method eq 'DgitImportDebianUpdate') {
$read_tree_debian->($cltree);
- rm_subdir_cached qw(debian/patches);
} 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 {
my $newtree = cmdoutput @git, qw(write-tree);
my $ch = $cl->{Hdr};
$ch =~ s{^tree .*}{tree $newtree}m or confess "$ch ?";
- $ch =~ s{^parent .*\n}{}m;
+ $ch =~ s{^parent .*\n}{}mg;
$ch =~ s{(?=^author)}{
join '', map { "parent $_\n" } @parents
}me or confess "$ch ?";
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 get_head () { return git_rev_parse qw(HEAD); }
+sub get_head () {
+ git_check_unmodified();
+ return git_rev_parse qw(HEAD);
+}
sub update_head ($$$) {
my ($old, $new, $mrest) = @_;
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') {
+ } else {
+ fproblem $status, "could not record ffq-prev: $message";
+ fproblems_maybe_bail();
+ }
my ($tip,$breakwater) = walk $old;
update_head_postlaunder $old, $tip, 'launder for rebase';
- @ARGV = qw(-i) unless @ARGV; # make configurable
runcmd @git, qw(rebase), @ARGV, $breakwater;
}
if (defined $old) {
$old = git_rev_parse $old;
} else {
- $old = get_head();
+ $old = git_rev_parse 'HEAD';
}
my ($dummy,$breakwater) = walk $old, 1,*STDOUT;
STDOUT->error and die $!;
}
+sub ffq_prev_branchinfo () {
+ # => ('status', "message", [$current, $ffq_prev])
+ # 'status' may be
+ # branch message is undef
+ # weird-symref } no $current,
+ # notbranch } no $ffq_prev
+ my $current = git_get_symref();
+ return ('detached', 'detached HEAD') unless defined $current;
+ 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 () {
# => ('status', "message")
# 'status' may be
# 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
- my $current = git_get_symref();
- return ('detached', 'detached HEAD') unless defined $current;
- return ('weird-symref', 'HEAD symref is not to refs/')
- unless $current =~ m{^refs/};
- my $ffq_prev = "refs/$ffq_refprefix/$'";
+ my ($status, $message, $current, $ffq_prev) = 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 defined $exists;
+ 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") // '*';
+ 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";
- if (defined $merge && $merge =~ m{^refs/heads/}) {
+ 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) = (@_);
+ 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"),
+ $check_remote->((scalar cfg "branch.$branch.remote",1),
'remote fetch/merge branch');
- $check_remote->((cfg "branch.$branch.pushRemote") //
- (cfg "branch.$branch.pushDefault"),
+ $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->("refs/remotes/dgit/dgit/sid", 'remote dgit branch for sid');
}
- fproblems_maybe_fail();
+ 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 $!;
}
sub cmd_new_upstream_v0 () {
- # tree should be clean and this is not checked
# automatically and unconditionally launders before rebasing
# if rebase --abort is used, laundering has still been done
# $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_gbp2debrebase () {
- badusage "needs 1 optional argument, the upstream" unless @ARGV<=1;
+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");
+ badusage "no arguments allowed" if @ARGV;
+ my ($status, $message, $current, $ffq_prev) = ffq_prev_branchinfo();
+ if ($status ne 'branch') {
+ fproblem $status, "could not check ffq-prev: $message";
+ fproblems_maybe_bail();
+ }
+ my $prev = $ffq_prev && git_get_ref $ffq_prev;
+ if (!$prev) {
+ fail "No ffq-prev to stitch." unless $opt_noop_ok;
+ }
+ fresh_workarea();
+ my $old_head = get_head();
+ 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;
+}
+
+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;
runcmd @git, qw(checkout -q gdr-internal~0);
rm_subdir_cached 'debian/patches';
$work = make_commit ['HEAD'], [
- 'git-debrebase import: drop patch queue',
+ 'git-debrebase convert-from-gbp: drop patches from tree',
'Delete debian/patches, as part of converting to git-debrebase format.',
- '[git-debrebase: gbp2debrebase, drop patches]'
+ '[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
runcmd @git, qw(reset --quiet --hard patch-queue/gdr-internal);
runcmd @git, qw(rebase --quiet --onto), $work, qw(gdr-internal);
- $work = get_head();
+ $work = git_rev_parse 'HEAD';
};
- update_head_checkout $old_head, $work, 'gbp2debrebase';
+ 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) {
+ runcmd @git, qw(update-ref -m),
+ "debrebase: converting corresponding main branch to gbp format",
+ $ffq, $git_null_obj;
+ }
+ 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 () {