rebase -i: introduce --recreate-merges=[no-]rebase-cousins
authorJohannes Schindelin <johannes.schindelin@gmx.de>
Fri, 23 Feb 2018 12:39:54 +0000 (13:39 +0100)
committerJunio C Hamano <gitster@pobox.com>
Fri, 23 Feb 2018 19:30:52 +0000 (11:30 -0800)
This one is a bit tricky to explain, so let's try with a diagram:

C
/ \
A - B - E - F
\ /
D

To illustrate what this new mode is all about, let's consider what
happens upon `git rebase -i --recreate-merges B`, in particular to
the commit `D`. So far, the new branch structure would be:

--- C' --
/ \
A - B ------ E' - F'
\ /
D'

This is not really preserving the branch topology from before! The
reason is that the commit `D` does not have `B` as ancestor, and
therefore it gets rebased onto `B`.

This is unintuitive behavior. Even worse, when recreating branch
structure, most use cases would appear to want cousins *not* to be
rebased onto the new base commit. For example, Git for Windows (the
heaviest user of the Git garden shears, which served as the blueprint
for --recreate-merges) frequently merges branches from `next` early, and
these branches certainly do *not* want to be rebased. In the example
above, the desired outcome would look like this:

--- C' --
/ \
A - B ------ E' - F'
\ /
-- D' --

Let's introduce the term "cousins" for such commits ("D" in the
example), and let's not rebase them by default, introducing the new
"rebase-cousins" mode for use cases where they should be rebased.

Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/git-rebase.txt
builtin/rebase--helper.c
git-rebase--interactive.sh
git-rebase.sh
sequencer.c
sequencer.h
t/t3430-rebase-recreate-merges.sh
index e9da7e26329e89515a6794a6715bb5a9ab0397a1..0e6d020d92493e8033638d6de49fc6d32359d014 100644 (file)
@@ -368,11 +368,16 @@ The commit list format can be changed by setting the configuration option
 rebase.instructionFormat.  A customized instruction format will automatically
 have the long commit hash prepended to the format.
 
 rebase.instructionFormat.  A customized instruction format will automatically
 have the long commit hash prepended to the format.
 
---recreate-merges::
+--recreate-merges[=(rebase-cousins|no-rebase-cousins)]::
        Recreate merge commits instead of flattening the history by replaying
        merges. Merge conflict resolutions or manual amendments to merge
        commits are not recreated automatically, but have to be recreated
        manually.
        Recreate merge commits instead of flattening the history by replaying
        merges. Merge conflict resolutions or manual amendments to merge
        commits are not recreated automatically, but have to be recreated
        manually.
++
+By default, or when `no-rebase-cousins` was specified, commits which do not
+have `<upstream>` as direct ancestor keep their original branch point.
+If the `rebase-cousins` mode is turned on, such commits are rebased onto
+`<upstream>` (or `<onto>`, if specified).
 
 -p::
 --preserve-merges::
 
 -p::
 --preserve-merges::
index a34ab5c06558e8233a6d19da3a13385cbe49b6df..cea99cb32354a6e19223c0a86a9ec319d838b0b2 100644 (file)
@@ -13,7 +13,7 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
 {
        struct replay_opts opts = REPLAY_OPTS_INIT;
        unsigned flags = 0, keep_empty = 0, recreate_merges = 0;
 {
        struct replay_opts opts = REPLAY_OPTS_INIT;
        unsigned flags = 0, keep_empty = 0, recreate_merges = 0;
-       int abbreviate_commands = 0;
+       int abbreviate_commands = 0, rebase_cousins = -1;
        enum {
                CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
                CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS, REARRANGE_SQUASH,
        enum {
                CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
                CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS, REARRANGE_SQUASH,
@@ -23,6 +23,8 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
                OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")),
                OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
                OPT_BOOL(0, "recreate-merges", &recreate_merges, N_("recreate merge commits")),
                OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")),
                OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
                OPT_BOOL(0, "recreate-merges", &recreate_merges, N_("recreate merge commits")),
+               OPT_BOOL(0, "rebase-cousins", &rebase_cousins,
+                        N_("keep original branch points of cousins")),
                OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
                                CONTINUE),
                OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
                OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
                                CONTINUE),
                OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
@@ -57,8 +59,13 @@ int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
        flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
        flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
        flags |= recreate_merges ? TODO_LIST_RECREATE_MERGES : 0;
        flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
        flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
        flags |= recreate_merges ? TODO_LIST_RECREATE_MERGES : 0;
+       flags |= rebase_cousins > 0 ? TODO_LIST_REBASE_COUSINS : 0;
        flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
 
        flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
 
+       if (rebase_cousins >= 0 && !recreate_merges)
+               warning(_("--[no-]rebase-cousins has no effect without "
+                         "--recreate-merges"));
+
        if (command == CONTINUE && argc == 1)
                return !!sequencer_continue(&opts);
        if (command == ABORT && argc == 1)
        if (command == CONTINUE && argc == 1)
                return !!sequencer_continue(&opts);
        if (command == ABORT && argc == 1)
index cfe3a537ac210a96919590e70c022fb263dd1ea2..e199fe1cca569fa03a7afc90c40342580169788c 100644 (file)
@@ -903,6 +903,7 @@ if test t != "$preserve_merges"
 then
        git rebase--helper --make-script ${keep_empty:+--keep-empty} \
                ${recreate_merges:+--recreate-merges} \
 then
        git rebase--helper --make-script ${keep_empty:+--keep-empty} \
                ${recreate_merges:+--recreate-merges} \
+               ${rebase_cousins:+--rebase-cousins} \
                $revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
        die "$(gettext "Could not generate todo list")"
 else
                $revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
        die "$(gettext "Could not generate todo list")"
 else
index d69bc7d0e0d7847a30dbae8c84fbe3451c03e6de..58d778a2da05e3590d618417e2108dc05395321d 100755 (executable)
@@ -17,7 +17,7 @@ q,quiet!           be quiet. implies --no-stat
 autostash          automatically stash/stash pop before and after
 fork-point         use 'merge-base --fork-point' to refine upstream
 onto=!             rebase onto given branch instead of upstream
 autostash          automatically stash/stash pop before and after
 fork-point         use 'merge-base --fork-point' to refine upstream
 onto=!             rebase onto given branch instead of upstream
-recreate-merges!   try to recreate merges instead of skipping them
+recreate-merges?   try to recreate merges instead of skipping them
 p,preserve-merges! try to recreate merges instead of ignoring them
 s,strategy=!       use the given merge strategy
 no-ff!             cherry-pick all commits, even if unchanged
 p,preserve-merges! try to recreate merges instead of ignoring them
 s,strategy=!       use the given merge strategy
 no-ff!             cherry-pick all commits, even if unchanged
@@ -88,6 +88,7 @@ state_dir=
 # One of {'', continue, skip, abort}, as parsed from command line
 action=
 recreate_merges=
 # One of {'', continue, skip, abort}, as parsed from command line
 action=
 recreate_merges=
+rebase_cousins=
 preserve_merges=
 autosquash=
 keep_empty=
 preserve_merges=
 autosquash=
 keep_empty=
@@ -268,6 +269,15 @@ do
                recreate_merges=t
                test -z "$interactive_rebase" && interactive_rebase=implied
                ;;
                recreate_merges=t
                test -z "$interactive_rebase" && interactive_rebase=implied
                ;;
+       --recreate-merges=*)
+               recreate_merges=t
+               case "${1#*=}" in
+               rebase-cousins) rebase_cousins=t;;
+               no-rebase-cousins) rebase_cousins=;;
+               *) die "Unknown mode: $1";;
+               esac
+               test -z "$interactive_rebase" && interactive_rebase=implied
+               ;;
        --preserve-merges)
                preserve_merges=t
                test -z "$interactive_rebase" && interactive_rebase=implied
        --preserve-merges)
                preserve_merges=t
                test -z "$interactive_rebase" && interactive_rebase=implied
index 0bb59e4411528a5cf3dee9b7d3db2c3514cb32c8..d8cc63dbe4da9553ae96b42f244c3c043bf56dc7 100644 (file)
@@ -2937,6 +2937,7 @@ static int make_script_with_merges(struct pretty_print_context *pp,
                                   unsigned flags)
 {
        int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
                                   unsigned flags)
 {
        int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
+       int rebase_cousins = flags & TODO_LIST_REBASE_COUSINS;
        struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
        struct strbuf label = STRBUF_INIT;
        struct commit_list *commits = NULL, **tail = &commits, *iter;
        struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
        struct strbuf label = STRBUF_INIT;
        struct commit_list *commits = NULL, **tail = &commits, *iter;
@@ -3112,6 +3113,9 @@ static int make_script_with_merges(struct pretty_print_context *pp,
                                           &commit->object.oid);
                        if (entry)
                                to = entry->string;
                                           &commit->object.oid);
                        if (entry)
                                to = entry->string;
+                       else if (!rebase_cousins)
+                               to = label_oid(&commit->object.oid, NULL,
+                                              &state);
 
                        if (!to || !strcmp(to, "onto"))
                                fprintf(out, "%s onto\n", cmd_reset);
 
                        if (!to || !strcmp(to, "onto"))
                                fprintf(out, "%s onto\n", cmd_reset);
index 11d1ac925ef75e79f61f90db2920e9111d72ceda..deebc6e325823021a5cf02cf0d05b80bafe07655 100644 (file)
@@ -49,6 +49,12 @@ int sequencer_remove_state(struct replay_opts *opts);
 #define TODO_LIST_SHORTEN_IDS (1U << 1)
 #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
 #define TODO_LIST_RECREATE_MERGES (1U << 3)
 #define TODO_LIST_SHORTEN_IDS (1U << 1)
 #define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
 #define TODO_LIST_RECREATE_MERGES (1U << 3)
+/*
+ * When recreating merges, commits that do have the base commit as ancestor
+ * ("cousins") are *not* rebased onto the new base by default. If those
+ * commits should be rebased onto the new base, this flag needs to be passed.
+ */
+#define TODO_LIST_REBASE_COUSINS (1U << 4)
 int sequencer_make_script(FILE *out, int argc, const char **argv,
                          unsigned flags);
 
 int sequencer_make_script(FILE *out, int argc, const char **argv,
                          unsigned flags);
 
index 2fb74cbbd32aa4ccf408c588c33ab7f39432e0fb..f2f44d2a66352b2145c863b3402aa7414ee6ba79 100755 (executable)
@@ -143,6 +143,29 @@ test_expect_success 'with a branch tip that was cherry-picked already' '
        EOF
 '
 
        EOF
 '
 
+test_expect_success 'do not rebase cousins unless asked for' '
+       write_script copy-editor.sh <<-\EOF &&
+       cp "$1" "$(git rev-parse --git-path ORIGINAL-TODO)"
+       EOF
+
+       test_config sequence.editor \""$PWD"/copy-editor.sh\" &&
+       git checkout -b cousins master &&
+       before="$(git rev-parse --verify HEAD)" &&
+       test_tick &&
+       git rebase -i --recreate-merges HEAD^ &&
+       test_cmp_rev HEAD $before &&
+       test_tick &&
+       git rebase -i --recreate-merges=rebase-cousins HEAD^ &&
+       test_cmp_graph HEAD^.. <<-\EOF
+       *   Merge the topic branch '\''onebranch'\''
+       |\
+       | * D
+       | * G
+       |/
+       o H
+       EOF
+'
+
 test_expect_success 'refs/rewritten/* is worktree-local' '
        git worktree add wt &&
        cat >wt/script-from-scratch <<-\EOF &&
 test_expect_success 'refs/rewritten/* is worktree-local' '
        git worktree add wt &&
        cat >wt/script-from-scratch <<-\EOF &&