rebase: teach rebase --keep-base
authorDenton Liu <liu.denton@gmail.com>
Tue, 27 Aug 2019 05:38:06 +0000 (01:38 -0400)
committerJunio C Hamano <gitster@pobox.com>
Tue, 27 Aug 2019 22:33:40 +0000 (15:33 -0700)
A common scenario is if a user is working on a topic branch and they
wish to make some changes to intermediate commits or autosquash, they
would run something such as

git rebase -i --onto master... master

in order to preserve the merge base. This is useful when contributing a
patch series to the Git mailing list, one often starts on top of the
current 'master'. While developing the patches, 'master' is also
developed further and it is sometimes not the best idea to keep rebasing
on top of 'master', but to keep the base commit as-is.

In addition to this, a user wishing to test individual commits in a
topic branch without changing anything may run

git rebase -x ./test.sh master... master

Since rebasing onto the merge base of the branch and the upstream is
such a common case, introduce the --keep-base option as a shortcut.

This allows us to rewrite the above as

git rebase -i --keep-base master

and

git rebase -x ./test.sh --keep-base master

respectively.

Add tests to ensure --keep-base works correctly in the normal case and
fails when there are multiple merge bases, both in regular and
interactive mode. Also, test to make sure conflicting options cause
rebase to fail. While we're adding test cases, add a missing
set_fake_editor call to 'rebase -i --onto master...side'.

While we're documenting the --keep-base option, change an instance of
"merge-base" to "merge base", which is the consistent spelling.

Helped-by: Eric Sunshine <sunshine@sunshineco.com>
Helped-by: Junio C Hamano <gitster@pobox.com>
Helped-by: Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Helped-by: Johannes Schindelin <Johannes.Schindelin@gmx.de>
Signed-off-by: Denton Liu <liu.denton@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/git-rebase.txt
builtin/rebase.c
contrib/completion/git-completion.bash
t/t3416-rebase-onto-threedots.sh
t/t3431-rebase-fork-point.sh
t/t3432-rebase-fast-forward.sh
index 6156609cf7149ccf5c1f79df2d9807cdbcc609ba..3146c1592deaa9790e143694bc9b5a07bb47d254 100644 (file)
@@ -8,8 +8,8 @@ git-rebase - Reapply commits on top of another base tip
 SYNOPSIS
 --------
 [verse]
-'git rebase' [-i | --interactive] [<options>] [--exec <cmd>] [--onto <newbase>]
-       [<upstream> [<branch>]]
+'git rebase' [-i | --interactive] [<options>] [--exec <cmd>]
+       [--onto <newbase> | --keep-base] [<upstream> [<branch>]]
 'git rebase' [-i | --interactive] [<options>] [--exec <cmd>] [--onto <newbase>]
        --root [<branch>]
 'git rebase' (--continue | --skip | --abort | --quit | --edit-todo | --show-current-patch)
@@ -217,6 +217,24 @@ As a special case, you may use "A\...B" as a shortcut for the
 merge base of A and B if there is exactly one merge base. You can
 leave out at most one of A and B, in which case it defaults to HEAD.
 
+--keep-base::
+       Set the starting point at which to create the new commits to the
+       merge base of <upstream> <branch>. Running
+       'git rebase --keep-base <upstream> <branch>' is equivalent to
+       running 'git rebase --onto <upstream>... <upstream>'.
++
+This option is useful in the case where one is developing a feature on
+top of an upstream branch. While the feature is being worked on, the
+upstream branch may advance and it may not be the best idea to keep
+rebasing on top of the upstream but to keep the base commit as-is.
++
+Although both this option and --fork-point find the merge base between
+<upstream> and <branch>, this option uses the merge base as the _starting
+point_ on which new commits will be created, whereas --fork-point uses
+the merge base to determine the _set of commits_ which will be rebased.
++
+See also INCOMPATIBLE OPTIONS below.
+
 <upstream>::
        Upstream branch to compare against.  May be any valid commit,
        not just an existing branch name. Defaults to the configured
@@ -369,6 +387,10 @@ ends up being empty, the <upstream> will be used as a fallback.
 +
 If either <upstream> or --root is given on the command line, then the
 default is `--no-fork-point`, otherwise the default is `--fork-point`.
++
+If your branch was based on <upstream> but <upstream> was rewound and
+your branch contains commits which were dropped, this option can be used
+with `--keep-base` in order to drop those commits from your branch.
 
 --ignore-whitespace::
 --whitespace=<option>::
@@ -545,6 +567,8 @@ In addition, the following pairs of options are incompatible:
  * --preserve-merges and --rebase-merges
  * --rebase-merges and --strategy
  * --rebase-merges and --strategy-option
+ * --keep-base and --onto
+ * --keep-base and --root
 
 BEHAVIORAL DIFFERENCES
 -----------------------
@@ -870,7 +894,7 @@ NOTE: While an "easy case recovery" sometimes appears to be successful
       --interactive` will be **resurrected**!
 
 The idea is to manually tell 'git rebase' "where the old 'subsystem'
-ended and your 'topic' began", that is, what the old merge-base
+ended and your 'topic' began", that is, what the old merge base
 between them was.  You will have to find a way to name the last commit
 of the old 'subsystem', for example:
 
index 7ef9095e7c32f56e45392afaee5539642681bca6..27d5c4e19d1c16491060aff4a05500917fb28be1 100644 (file)
@@ -29,8 +29,8 @@
 #include "rebase-interactive.h"
 
 static char const * const builtin_rebase_usage[] = {
-       N_("git rebase [-i] [options] [--exec <cmd>] [--onto <newbase>] "
-               "[<upstream>] [<branch>]"),
+       N_("git rebase [-i] [options] [--exec <cmd>] "
+               "[--onto <newbase> | --keep-base] [<upstream> [<branch>]]"),
        N_("git rebase [-i] [options] [--exec <cmd>] [--onto <newbase>] "
                "--root [<branch>]"),
        N_("git rebase --continue | --abort | --skip | --edit-todo"),
@@ -1396,6 +1396,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
        struct rebase_options options = REBASE_OPTIONS_INIT;
        const char *branch_name;
        int ret, flags, total_argc, in_progress = 0;
+       int keep_base = 0;
        int ok_to_skip_pre_rebase = 0;
        struct strbuf msg = STRBUF_INIT;
        struct strbuf revisions = STRBUF_INIT;
@@ -1414,6 +1415,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                OPT_STRING(0, "onto", &options.onto_name,
                           N_("revision"),
                           N_("rebase onto given branch instead of upstream")),
+               OPT_BOOL(0, "keep-base", &keep_base,
+                        N_("use the merge-base of upstream and branch as the current base")),
                OPT_BOOL(0, "no-verify", &ok_to_skip_pre_rebase,
                         N_("allow pre-rebase hook to run")),
                OPT_NEGBIT('q', "quiet", &options.flags,
@@ -1567,6 +1570,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
                warning(_("git rebase --preserve-merges is deprecated. "
                          "Use --rebase-merges instead."));
 
+       if (keep_base) {
+               if (options.onto_name)
+                       die(_("cannot combine '--keep-base' with '--onto'"));
+               if (options.root)
+                       die(_("cannot combine '--keep-base' with '--root'"));
+       }
+
        if (action != ACTION_NONE && !in_progress)
                die(_("No rebase in progress?"));
        setenv(GIT_REFLOG_ACTION_ENVIRONMENT, "rebase", 0);
@@ -1902,12 +1912,22 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
        }
 
        /* Make sure the branch to rebase onto is valid. */
-       if (!options.onto_name)
+       if (keep_base) {
+               strbuf_reset(&buf);
+               strbuf_addstr(&buf, options.upstream_name);
+               strbuf_addstr(&buf, "...");
+               options.onto_name = xstrdup(buf.buf);
+       } else if (!options.onto_name)
                options.onto_name = options.upstream_name;
        if (strstr(options.onto_name, "...")) {
-               if (get_oid_mb(options.onto_name, &merge_base) < 0)
-                       die(_("'%s': need exactly one merge base"),
-                           options.onto_name);
+               if (get_oid_mb(options.onto_name, &merge_base) < 0) {
+                       if (keep_base)
+                               die(_("'%s': need exactly one merge base with branch"),
+                                   options.upstream_name);
+                       else
+                               die(_("'%s': need exactly one merge base"),
+                                   options.onto_name);
+               }
                options.onto = lookup_commit_or_die(&merge_base,
                                                    options.onto_name);
        } else {
index e087c4bf0085add8e968e128db6b667acbc80320..71e7159694376197401f163535d188c8088c8584 100644 (file)
@@ -2030,7 +2030,7 @@ _git_rebase ()
                        --autosquash --no-autosquash
                        --fork-point --no-fork-point
                        --autostash --no-autostash
-                       --verify --no-verify
+                       --verify --no-verify --keep-base
                        --keep-empty --root --force-rebase --no-ff
                        --rerere-autoupdate
                        --exec
index ddf2f648538319df0319c6826f1a64321b3afdb2..9c2548423bcef02e060d0c7047e920f3e6f2485a 100755 (executable)
@@ -99,7 +99,64 @@ test_expect_success 'rebase -i --onto master...side' '
        git checkout side &&
        git reset --hard K &&
 
+       set_fake_editor &&
        test_must_fail git rebase -i --onto master...side J
 '
 
+test_expect_success 'rebase --keep-base --onto incompatible' '
+       test_must_fail git rebase --keep-base --onto master...
+'
+
+test_expect_success 'rebase --keep-base --root incompatible' '
+       test_must_fail git rebase --keep-base --root
+'
+
+test_expect_success 'rebase --keep-base master from topic' '
+       git reset --hard &&
+       git checkout topic &&
+       git reset --hard G &&
+
+       git rebase --keep-base master &&
+       git rev-parse C >base.expect &&
+       git merge-base master HEAD >base.actual &&
+       test_cmp base.expect base.actual &&
+
+       git rev-parse HEAD~2 >actual &&
+       git rev-parse C^0 >expect &&
+       test_cmp expect actual
+'
+
+test_expect_success 'rebase --keep-base master from side' '
+       git reset --hard &&
+       git checkout side &&
+       git reset --hard K &&
+
+       test_must_fail git rebase --keep-base master
+'
+
+test_expect_success 'rebase -i --keep-base master from topic' '
+       git reset --hard &&
+       git checkout topic &&
+       git reset --hard G &&
+
+       set_fake_editor &&
+       EXPECT_COUNT=2 git rebase -i --keep-base master &&
+       git rev-parse C >base.expect &&
+       git merge-base master HEAD >base.actual &&
+       test_cmp base.expect base.actual &&
+
+       git rev-parse HEAD~2 >actual &&
+       git rev-parse C^0 >expect &&
+       test_cmp expect actual
+'
+
+test_expect_success 'rebase -i --keep-base master from side' '
+       git reset --hard &&
+       git checkout side &&
+       git reset --hard K &&
+
+       set_fake_editor &&
+       test_must_fail git rebase -i --keep-base master
+'
+
 test_done
index 2d5c6e641ecf097278a92eb57720735055927bf2..78851b9a2ae84d6b73b3cffc0026082abce322e1 100755 (executable)
@@ -43,11 +43,15 @@ test_rebase () {
 
 test_rebase 'G F E D B A'
 test_rebase 'G F D B A' --onto D
+test_rebase 'G F B A' --keep-base
 test_rebase 'G F C E D B A' --no-fork-point
 test_rebase 'G F C D B A' --no-fork-point --onto D
+test_rebase 'G F C B A' --no-fork-point --keep-base
 test_rebase 'G F E D B A' --fork-point refs/heads/master
 test_rebase 'G F D B A' --fork-point --onto D refs/heads/master
+test_rebase 'G F B A' --fork-point --keep-base refs/heads/master
 test_rebase 'G F C E D B A' refs/heads/master
 test_rebase 'G F C D B A' --onto D refs/heads/master
+test_rebase 'G F C B A' --keep-base refs/heads/master
 
 test_done
index 9ce0750b43700b74c2d8538810d69dae3a404467..034ffc7e76caf16fe1bc9f448eb0c9675d386fc9 100755 (executable)
@@ -75,11 +75,15 @@ test_rebase_same_head success noop same success noop-force same master
 test_rebase_same_head success noop same success noop-force diff --onto B B
 test_rebase_same_head success noop same success noop-force diff --onto B... B
 test_rebase_same_head success noop same success noop-force same --onto master... master
+test_rebase_same_head success noop same success noop-force same --keep-base master
+test_rebase_same_head success noop same success noop-force same --keep-base
 test_rebase_same_head success noop same success noop-force same --no-fork-point
+test_rebase_same_head success noop same success noop-force same --keep-base --no-fork-point
 test_rebase_same_head success noop same success work same --fork-point master
 test_rebase_same_head success noop same success work diff --fork-point --onto B B
 test_rebase_same_head success noop same success work diff --fork-point --onto B... B
 test_rebase_same_head success noop same success work same --fork-point --onto master... master
+test_rebase_same_head success noop same success work same --keep-base --keep-base master
 
 test_expect_success 'add work same to side' '
        test_commit E
@@ -91,11 +95,15 @@ test_rebase_same_head success noop same success noop-force same master
 test_rebase_same_head success noop same success noop-force diff --onto B B
 test_rebase_same_head success noop same success noop-force diff --onto B... B
 test_rebase_same_head success noop same success noop-force same --onto master... master
+test_rebase_same_head success noop same success noop-force same --keep-base master
+test_rebase_same_head success noop same success noop-force same --keep-base
 test_rebase_same_head success noop same success noop-force same --no-fork-point
+test_rebase_same_head success noop same success noop-force same --keep-base --no-fork-point
 test_rebase_same_head success noop same success work same --fork-point master
 test_rebase_same_head success noop same success work diff --fork-point --onto B B
 test_rebase_same_head success noop same success work diff --fork-point --onto B... B
 test_rebase_same_head success noop same success work same --fork-point --onto master... master
+test_rebase_same_head success noop same success work same --fork-point --keep-base master
 
 test_expect_success 'add work same to upstream' '
        git checkout master &&
@@ -107,8 +115,11 @@ changes='our and their changes'
 test_rebase_same_head success noop same success noop-force diff --onto B B
 test_rebase_same_head success noop same success noop-force diff --onto B... B
 test_rebase_same_head success noop same success work diff --onto master... master
+test_rebase_same_head success noop same success work diff --keep-base master
+test_rebase_same_head success noop same success work diff --keep-base
 test_rebase_same_head failure work same success work diff --fork-point --onto B B
 test_rebase_same_head failure work same success work diff --fork-point --onto B... B
 test_rebase_same_head success noop same success work diff --fork-point --onto master... master
+test_rebase_same_head success noop same success work diff --fork-point --keep-base master
 
 test_done