use Data::Dumper;
use Getopt::Long qw(:config posix_default gnu_compat bundling);
use Dpkg::Version;
+use File::FnMatch qw(:fnmatch);
-our ($opt_force);
+our ($opt_force, $opt_noop_ok);
+
+our $us = qw(git-debrebase);
sub badusage ($) {
my ($m) = @_;
die "bad usage: $m\n";
}
-sub cfg ($) {
- my ($k) = @_;
+sub cfg ($;$) {
+ my ($k, $optional) = @_;
$/ = "\0";
my @cmd = qw(git config -z);
push @cmd, qw(--get-all) if wantarray;
push @cmd, $k;
- my $out = cmdoutput @cmd;
+ my $out = cmdoutput_errok @cmd;
+ if (!defined $out) {
+ fail "missing required git config $k" unless $optional;
+ return ();
+ }
return split /\0/, $out;
}
sub get_commit ($) {
my ($objid) = @_;
- my $data = git_cat_file $objid, 'commit';
+ my $data = (git_cat_file $objid, 'commit');
$data =~ m/(?<=\n)\n/ or die "$objid ($data) ?";
return ($`,$');
}
my @cmd = (@git, qw(diff-tree -z --no-renames));
push @cmd, @$opts;
push @cmd, "$_:" foreach $x, $y;
- push @cmd, @$limits;
+ push @cmd, '--', @$limits;
my $diffs = cmdoutput @cmd;
foreach (split /\0/, $diffs) { $fn->(); }
};
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';
+ 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 {
return cmdoutput @cmd;
}
+our @fproblem_force_opts;
+our $fproblems_forced;
+our $fproblems_tripped;
+sub fproblem ($$) {
+ my ($tag,$msg) = @_;
+ if (grep { $_ eq $tag } @fproblem_force_opts) {
+ $fproblems_forced++;
+ print STDERR "git-debrebase: safety catch overridden (-f$tag): $msg\n";
+ } else {
+ $fproblems_tripped++;
+ print STDERR "git-debrebase: safety catch tripped (-f$tag): $msg\n";
+ }
+}
+
+sub fproblems_maybe_bail () {
+ if ($fproblems_forced) {
+ printf STDERR
+ "%s: safety catch trips: %d overriden by individual -f options\n",
+ $us, $fproblems_forced;
+ }
+ if ($fproblems_tripped) {
+ if ($opt_force) {
+ printf STDERR
+ "%s: safety catch trips: %d overriden by global --force\n",
+ $us, $fproblems_tripped;
+ } else {
+ fail sprintf
+ "%s: safety catch trips: %d blockers (you could -f<tag>, or --force)",
+ $us, $fproblems_tripped;
+ }
+ }
+}
+sub any_fproblems () {
+ return $fproblems_forced || $fproblems_tripped;
+}
+
# classify returns an info hash like this
# CommitId => $objid
# Hdr => # commit headers, including 1 final newline
return $r;
};
+ my $claims_to_be_breakwater =
+ $r->{Msg} =~ m{^\[git-debrebase breakwater.*\]$}m;
+
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));
}
my @identical = grep { !$_->{Differs} } @p;
- if (@p == 2 && @identical == 1) {
+ 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)
+ ) {
my @overwritten = grep { $_->{Differs} } @p;
confess "internal error $objid ?" unless @overwritten==1;
return $classify->(qw(Pseudomerge),
my $cl;
my $xmsg = sub {
- my ($appendinfo) = @_;
+ my ($prose, $info) = @_;
my $ms = $cl->{Msg};
chomp $ms;
- $ms .= "\n\n[git-debrebase $appendinfo]\n";
+ $info //= '';
+ $ms .= "\n\n[git-debrebase$info: $prose]\n";
return (Msg => $ms);
};
my $rewrite_from_here = sub {
push @brw_cl, {
%$cl,
SpecialMethod => 'DgitImportUpstreamUpdate',
- $xmsg->("convert dgit import: upstream changes")
+ $xmsg->("convert dgit import: upstream changes",
+ " breakwater")
};
}
$prline->(" Import");
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) = @_;
sub update_head_checkout ($$$) {
my ($old, $new, $mrest) = @_;
- my $symref = git_get_symref();
- runcmd @git, qw(checkout), $new, qw(.);
update_head $old, $new, $mrest;
+ runcmd @git, qw(reset --hard);
}
sub update_head_postlaunder ($$$) {
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 record_ffq_prev () {
+ # => ('status', "message")
+ # 'status' may be
+ # written 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
+ 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 $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;
+
+ my $check = sub {
+ my ($lrref, $desc) = @_;
+ 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 defined $lrval;
+
+ 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) {
+ $checked{$lrref} = -1;
+ fproblem 'behind', "you are behind $lrref, divergence risk";
+ } else {
+ $checked{$lrref} = -1;
+ fproblem 'diverged', "you have diverged from $lrref";
+ }
+ };
+
+ my $merge = cfg "branch.$branch.merge",1;
+ if (defined $merge && $merge =~ m{^refs/heads/}) {
+ my $rhs = $';
+ my $check_remote = sub {
+ my ($remote, $desc) = (@_);
+ return unless defined $remote;
+ $check->("refs/remotes/$remote/$rhs", $desc);
+ };
+ $check_remote->((cfg "branch.$branch.remote",1),
+ 'remote fetch/merge branch');
+ $check_remote->((cfg "branch.$branch.pushRemote",1) //
+ (cfg "branch.$branch.pushDefault",1),
+ 'remote push branch');
+ }
+ if ($branch =~ m{^dgit/}) {
+ $check->("remotes/dgit/$branch", 'remote dgit branch');
+ } elsif ($branch =~ m{^master$}) {
+ $check->("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);
+}
+
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
my $old_upstream = parsecommit
$old_upstream_update_cl->{OrigParents}[0]{CommitId};
- my $problems = 0;
- my $problem = sub {
- my ($msg) = @_;
- $problems++;
- print STDERR "preflight check failed: $msg\n";
- };
-
$piece->('', Old => $old_upstream->{CommitId});
if ($old_upstream->{Msg} =~ m{^\[git-debrebase }m) {
if ($old_upstream->{Msg} =~
- m{^\[git-debrebase new-upstream combine \.((?: $extra_orig_namepart_re)+)\]}
+ m{^\[git-debrebase upstream-combine \.((?: $extra_orig_namepart_re)+)\:.*\]$}m
) {
my @oldpieces = ('', split / /, $1);
my $parentix = -1 + scalar @{ $old_upstream->{Parents} };
$piece->($n, Old => $old_upstream->{CommitId}.'^'.$parentix);
}
} else {
- $problem->("previous upstream $old_upstream->{CommitId} is from".
- " git-debrebase but not a \`new-upstream combine' commit");
+ fproblem 'upstream-confusing',
+ "previous upstream $old_upstream->{CommitId} is from".
+ " git-debrebase but not an \`upstream-combine' commit";
}
}
foreach my $pc (values %pieces) {
if (!$pc->{Old}) {
- $problem->("introducing upstream piece \`$pc->{Name}'");
+ fproblem 'upstream-new-piece',
+ "introducing upstream piece \`$pc->{Name}'";
} elsif (!$pc->{New}) {
- $problem->("dropping upstream piece \`$pc->{Name}'");
+ fproblem 'upstream-rm-piece',
+ "dropping upstream piece \`$pc->{Name}'";
} elsif (!is_fast_fwd $pc->{Old}, $pc->{New}) {
- $problem->("not fast forward: $pc->{Name} $pc->{Old}..$pc->{New}");
+ fproblem 'upstream-not-ff',
+ "not fast forward: $pc->{Name} $pc->{Old}..$pc->{New}";
}
}
printdebug "%pieces = ", (dd \%pieces), "\n";
printdebug "\@newpieces = ", (dd \@newpieces), "\n";
- if ($problems) {
- if ($opt_force) {
- printf STDERR
- "preflight check failures (%d) overriden by --force\n",
- $problems;
- } else {
- fail sprintf
- "preflight check failures (%d) (you could --force)",
- $problems;
- }
- }
+ fproblems_maybe_bail();
my $new_bw;
in_workarea sub {
my @upstream_merge_parents;
- if (!$problems) {
+ if (!any_fproblems()) {
push @upstream_merge_parents, $old_upstream->{CommitId};
}
# need to make the upstream subtree merge commit
$new_upstream = make_commit \@upstream_merge_parents,
[ "Combine upstreams for $new_upstream_version",
- ("[git-debrebase new-upstream combine . ".
- (join " ", map { $_->{Name} } @newpieces[1..$#newpieces]).
- "]"),
+ ("[git-debrebase upstream-combine . ".
+ (join " ", map { $_->{Name} } @newpieces[1..$#newpieces]).
+ ": new upstream]"),
];
}
# index now contains the breakwater merge contents
$new_bw = make_commit [ $old_bw, $new_upstream ],
[ "Update to upstream $new_upstream_version",
- "[git-debrebase new-upstream breakwater $new_upstream_version]",
+ "[git-debrebase breakwater: new upstream $new_upstream_version, merge]",
];
# Now we have to add a changelog stanza so the Debian version
# 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 changelog $new_upstream_version]",
+ "[git-debrebase: new upstream $new_upstream_version, changelog]",
];
};
# 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();
+ if ($status eq 'exists' && $opt_noop_ok) {
+ print "Previous head already recorded\n" or die $!;
+ } elsif ($status eq 'written') {
+ } else {
+ fail "Could not preserve: $msg";
+ }
+}
+
+sub cmd_gbp2debrebase () {
+ badusage "needs 1 optional argument, the upstream" 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) {
+ fproblem '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) {
+ fproblem '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') {
+ fproblem 'upstream-has-debian',
+ "upstream ($upstream) contains debian/ directory";
+ }
+
+ fproblems_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 import: drop patch queue',
+ 'Delete debian/patches, as part of converting to git-debrebase format.',
+ '[git-debrebase: gbp2debrebase, drop patches]'
+ ];
+ # make the breakwater pseudomerge
+ # the tree is already exactly right
+ $work = make_commit [$work, $upstream], [
+ 'git-debrebase import: declare upstream',
+ 'First breakwater merge.',
+ '[git-debrebase breakwater: 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, 'gbp2debrebase';
+}
+
sub cmd_downstream_rebase_launder_v0 () {
badusage "needs 1 argument, the baseline" unless @ARGV==1;
my ($base) = @ARGV;
}
GetOptions("D+" => \$debuglevel,
+ 'noop-ok', => \$opt_noop_ok,
+ 'f=s' => \@fproblem_force_opts,
'force!') or die badusage "bad options\n";
initdebug('git-debrebase ');
enabledebug if $debuglevel;