X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?p=dgit.git;a=blobdiff_plain;f=git-debrebase;h=592dd292f4fe7ecdfa77a5b53a5b1dbbc50fa1e1;hp=88118aa42c6decdc923647bc7dcd241a03fbec40;hb=a865af47e6e6dc6160812fa0cdfbcb4d57f38723;hpb=850b9cacb62d6009cb503cbec266174fe92a90a7 diff --git a/git-debrebase b/git-debrebase index 88118aa4..592dd292 100755 --- a/git-debrebase +++ b/git-debrebase @@ -87,21 +87,28 @@ 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); +our ($opt_force, $opt_noop_ok); + +our $us = qw(git-debrebase); sub badusage ($) { my ($m) = @_; 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; + my $out = cmdoutput_errok @cmd; + if (!defined $out) { + fail "missing required git config $k" unless $optional; + return (); + } return split /\0/, $out; } @@ -252,25 +259,41 @@ sub make_commit ($$) { return cmdoutput @cmd; } -our $fproblems; -sub fproblem ($) { - my ($msg) = @_; - $fproblems++; - print STDERR "git-debrebase: safety catch tripped: $msg\n"; +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) { + 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 - "safety catch trips (%d) overriden by --force\n", - $fproblems; + "%s: safety catch trips: %d overriden by global --force\n", + $us, $fproblems_tripped; } else { fail sprintf - "safety catch trips (%d) (you could --force)", - $fproblems; + "%s: safety catch trips: %d blockers (you could -f, or --force)", + $us, $fproblems_tripped; } } } +sub any_fproblems () { + return $fproblems_forced || $fproblems_tripped; +} # classify returns an info hash like this # CommitId => $objid @@ -371,7 +394,13 @@ sub classify ($) { 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)); @@ -405,7 +434,10 @@ sub classify ($) { } 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), @@ -492,10 +524,11 @@ sub walk ($;$$) { 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 { @@ -628,7 +661,8 @@ sub walk ($;$$) { push @brw_cl, { %$cl, SpecialMethod => 'DgitImportUpstreamUpdate', - $xmsg->("convert dgit import: upstream changes") + $xmsg->("convert dgit import: upstream changes", + " breakwater") }; } $prline->(" Import"); @@ -761,7 +795,10 @@ sub walk ($;$$) { 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) = @_; @@ -797,7 +834,6 @@ sub defaultcmd_rebase () { my $old = get_head(); 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; } @@ -808,14 +844,104 @@ sub cmd_analyse () { 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/$'"; + return ('branch', undef, $current, $ffq_prev); +} + +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 ($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 $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 @@ -876,7 +1002,7 @@ sub cmd_new_upstream_v0 () { if ($old_upstream->{Msg} =~ m{^\[git-debrebase }m) { if ($old_upstream->{Msg} =~ - m{^\[git-debrebase (?:\w*-)?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} }; @@ -885,18 +1011,22 @@ sub cmd_new_upstream_v0 () { $piece->($n, Old => $old_upstream->{CommitId}.'^'.$parentix); } } else { - fproblem "previous upstream $old_upstream->{CommitId} is from". - " git-debrebase but not an \`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}) { - fproblem "introducing upstream piece \`$pc->{Name}'"; + fproblem 'upstream-new-piece', + "introducing upstream piece \`$pc->{Name}'"; } elsif (!$pc->{New}) { - fproblem "dropping upstream piece \`$pc->{Name}'"; + fproblem 'upstream-rm-piece', + "dropping upstream piece \`$pc->{Name}'"; } elsif (!is_fast_fwd $pc->{Old}, $pc->{New}) { - fproblem "not fast forward: $pc->{Name} $pc->{Old}..$pc->{New}"; + fproblem 'upstream-not-ff', + "not fast forward: $pc->{Name} $pc->{Old}..$pc->{New}"; } } @@ -911,7 +1041,7 @@ sub cmd_new_upstream_v0 () { in_workarea sub { my @upstream_merge_parents; - if (!$fproblems) { + if (!any_fproblems()) { push @upstream_merge_parents, $old_upstream->{CommitId}; } @@ -930,9 +1060,9 @@ sub cmd_new_upstream_v0 () { # 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]"), ]; } @@ -945,7 +1075,7 @@ sub cmd_new_upstream_v0 () { # 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 @@ -973,7 +1103,7 @@ END # 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]", ]; }; @@ -989,6 +1119,17 @@ END # 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; @@ -1005,19 +1146,22 @@ sub cmd_gbp2debrebase () { } if (!is_fast_fwd $upstream, $old_head) { - fproblem "upstream ($upstream) is not an ancestor of 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 "history between upstream ($upstream) and HEAD contains direct changes to upstream files - are you sure this is a gbp (patches-unapplied) branch?"; + 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 ($upstream) contains debian/ directory"; + fproblem 'upstream-has-debian', + "upstream ($upstream) contains debian/ directory"; } fproblems_maybe_bail(); @@ -1035,20 +1179,20 @@ sub cmd_gbp2debrebase () { $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]' + '[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 declare-upstream breakwater]' + '[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 = get_head(); + $work = git_rev_parse 'HEAD'; }; update_head_checkout $old_head, $work, 'gbp2debrebase'; @@ -1104,6 +1248,8 @@ sub cmd_downstream_rebase_launder_v0 () { } 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;