chiark / gitweb /
git-debrebase: break out ffq_prev_branchinfo (nfc)
[dgit.git] / git-debrebase
index 88118aa42c6decdc923647bc7dcd241a03fbec40..592dd292f4fe7ecdfa77a5b53a5b1dbbc50fa1e1 100755 (executable)
@@ -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<tag>, 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;