chiark / gitweb /
git-debrebase: stitch better
[dgit.git] / git-debrebase
index 6655b2d200dde46f1c997866abe1713389daa51f..a853c0639c1cf1f0daad4f891e0c3838c1912b87 100755 (executable)
@@ -29,6 +29,7 @@
 #    git-debrebase [<options> --] [<git-rebase options...>]
 #    git-debrebase [<options>] analyse
 #    git-debrebase [<options>] launder         # prints breakwater tip etc.
+#    git-debrebase [<options>] stitch [--prose=<for commit message>]
 #    git-debrebase [<options>] downstream-rebase-launder-v0  # experimental
 #
 #    git-debrebase [<options>] gbp2debrebase-v0 \
@@ -87,6 +88,7 @@ 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, $opt_noop_ok);
 
@@ -97,13 +99,17 @@ sub badusage ($) {
     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;
 }
 
@@ -790,7 +796,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) = @_;
@@ -824,9 +833,14 @@ sub cmd_launder () {
 
 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;
 }
 
@@ -837,14 +851,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
 
@@ -1022,6 +1126,49 @@ 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_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_gbp2debrebase () {
     badusage "needs 1 optional argument, the upstream" unless @ARGV<=1;
     my ($upstream_spec) = @ARGV;
@@ -1084,7 +1231,7 @@ sub cmd_gbp2debrebase () {
        # 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';