rebase: teach rebase --keep-base

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>

Denton Liu committed Aug 27, 2019 at 01:38 UTC 414d924beb41b9f39744b5574231856d5ffbcba8
6 files changed +126 -10
Documentation/git-rebase.txt
+27 -3
@@ -8,8 +8,8 @@ git-rebase - Reapply commits on top of another base tip
8 SYNOPSIS
9 --------
10 [verse]
11 -'git rebase' [-i | --interactive] [<options>] [--exec <cmd>] [--onto <newbase>]
12 - [<upstream> [<branch>]]
11 +'git rebase' [-i | --interactive] [<options>] [--exec <cmd>]
12 + [--onto <newbase> | --keep-base] [<upstream> [<branch>]]
13 'git rebase' [-i | --interactive] [<options>] [--exec <cmd>] [--onto <newbase>]
14 --root [<branch>]
15 '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
217 merge base of A and B if there is exactly one merge base. You can
218 leave out at most one of A and B, in which case it defaults to HEAD.
219
220 +--keep-base::
221 + Set the starting point at which to create the new commits to the
222 + merge base of <upstream> <branch>. Running
223 + 'git rebase --keep-base <upstream> <branch>' is equivalent to
224 + running 'git rebase --onto <upstream>... <upstream>'.
225 ++
226 +This option is useful in the case where one is developing a feature on
227 +top of an upstream branch. While the feature is being worked on, the
228 +upstream branch may advance and it may not be the best idea to keep
229 +rebasing on top of the upstream but to keep the base commit as-is.
230 ++
231 +Although both this option and --fork-point find the merge base between
232 +<upstream> and <branch>, this option uses the merge base as the _starting
233 +point_ on which new commits will be created, whereas --fork-point uses
234 +the merge base to determine the _set of commits_ which will be rebased.
235 ++
236 +See also INCOMPATIBLE OPTIONS below.
237 +
238 <upstream>::
239 Upstream branch to compare against. May be any valid commit,
240 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.
387 +
388 If either <upstream> or --root is given on the command line, then the
389 default is `--no-fork-point`, otherwise the default is `--fork-point`.
390 ++
391 +If your branch was based on <upstream> but <upstream> was rewound and
392 +your branch contains commits which were dropped, this option can be used
393 +with `--keep-base` in order to drop those commits from your branch.
394
395 --ignore-whitespace::
396 --whitespace=<option>::
@@ -545,6 +567,8 @@ In addition, the following pairs of options are incompatible:
567 * --preserve-merges and --rebase-merges
568 * --rebase-merges and --strategy
569 * --rebase-merges and --strategy-option
570 + * --keep-base and --onto
571 + * --keep-base and --root
572
573 BEHAVIORAL DIFFERENCES
574 -----------------------
@@ -870,7 +894,7 @@ NOTE: While an "easy case recovery" sometimes appears to be successful
894 --interactive` will be **resurrected**!
895
896 The idea is to manually tell 'git rebase' "where the old 'subsystem'
873 -ended and your 'topic' began", that is, what the old merge-base
897 +ended and your 'topic' began", that is, what the old merge base
898 between them was. You will have to find a way to name the last commit
899 of the old 'subsystem', for example:
900
builtin/rebase.c
+26 -6
@@ -29,8 +29,8 @@
29 #include "rebase-interactive.h"
30
31 static char const * const builtin_rebase_usage[] = {
32 - N_("git rebase [-i] [options] [--exec <cmd>] [--onto <newbase>] "
33 - "[<upstream>] [<branch>]"),
32 + N_("git rebase [-i] [options] [--exec <cmd>] "
33 + "[--onto <newbase> | --keep-base] [<upstream> [<branch>]]"),
34 N_("git rebase [-i] [options] [--exec <cmd>] [--onto <newbase>] "
35 "--root [<branch>]"),
36 N_("git rebase --continue | --abort | --skip | --edit-todo"),
@@ -1396,6 +1396,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
1396 struct rebase_options options = REBASE_OPTIONS_INIT;
1397 const char *branch_name;
1398 int ret, flags, total_argc, in_progress = 0;
1399 + int keep_base = 0;
1400 int ok_to_skip_pre_rebase = 0;
1401 struct strbuf msg = STRBUF_INIT;
1402 struct strbuf revisions = STRBUF_INIT;
@@ -1414,6 +1415,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
1415 OPT_STRING(0, "onto", &options.onto_name,
1416 N_("revision"),
1417 N_("rebase onto given branch instead of upstream")),
1418 + OPT_BOOL(0, "keep-base", &keep_base,
1419 + N_("use the merge-base of upstream and branch as the current base")),
1420 OPT_BOOL(0, "no-verify", &ok_to_skip_pre_rebase,
1421 N_("allow pre-rebase hook to run")),
1422 OPT_NEGBIT('q', "quiet", &options.flags,
@@ -1567,6 +1570,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
1570 warning(_("git rebase --preserve-merges is deprecated. "
1571 "Use --rebase-merges instead."));
1572
1573 + if (keep_base) {
1574 + if (options.onto_name)
1575 + die(_("cannot combine '--keep-base' with '--onto'"));
1576 + if (options.root)
1577 + die(_("cannot combine '--keep-base' with '--root'"));
1578 + }
1579 +
1580 if (action != ACTION_NONE && !in_progress)
1581 die(_("No rebase in progress?"));
1582 setenv(GIT_REFLOG_ACTION_ENVIRONMENT, "rebase", 0);
@@ -1902,12 +1912,22 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
1912 }
1913
1914 /* Make sure the branch to rebase onto is valid. */
1905 - if (!options.onto_name)
1915 + if (keep_base) {
1916 + strbuf_reset(&buf);
1917 + strbuf_addstr(&buf, options.upstream_name);
1918 + strbuf_addstr(&buf, "...");
1919 + options.onto_name = xstrdup(buf.buf);
1920 + } else if (!options.onto_name)
1921 options.onto_name = options.upstream_name;
1922 if (strstr(options.onto_name, "...")) {
1908 - if (get_oid_mb(options.onto_name, &merge_base) < 0)
1909 - die(_("'%s': need exactly one merge base"),
1910 - options.onto_name);
1923 + if (get_oid_mb(options.onto_name, &merge_base) < 0) {
1924 + if (keep_base)
1925 + die(_("'%s': need exactly one merge base with branch"),
1926 + options.upstream_name);
1927 + else
1928 + die(_("'%s': need exactly one merge base"),
1929 + options.onto_name);
1930 + }
1931 options.onto = lookup_commit_or_die(&merge_base,
1932 options.onto_name);
1933 } else {
contrib/completion/git-completion.bash
+1 -1
@@ -2030,7 +2030,7 @@ _git_rebase ()
2030 --autosquash --no-autosquash
2031 --fork-point --no-fork-point
2032 --autostash --no-autostash
2033 - --verify --no-verify
2033 + --verify --no-verify --keep-base
2034 --keep-empty --root --force-rebase --no-ff
2035 --rerere-autoupdate
2036 --exec
t/t3416-rebase-onto-threedots.sh
+57
@@ -99,7 +99,64 @@ test_expect_success 'rebase -i --onto master...side' '
99 git checkout side &&
100 git reset --hard K &&
101
102 + set_fake_editor &&
103 test_must_fail git rebase -i --onto master...side J
104 '
105
106 +test_expect_success 'rebase --keep-base --onto incompatible' '
107 + test_must_fail git rebase --keep-base --onto master...
108 +'
109 +
110 +test_expect_success 'rebase --keep-base --root incompatible' '
111 + test_must_fail git rebase --keep-base --root
112 +'
113 +
114 +test_expect_success 'rebase --keep-base master from topic' '
115 + git reset --hard &&
116 + git checkout topic &&
117 + git reset --hard G &&
118 +
119 + git rebase --keep-base master &&
120 + git rev-parse C >base.expect &&
121 + git merge-base master HEAD >base.actual &&
122 + test_cmp base.expect base.actual &&
123 +
124 + git rev-parse HEAD~2 >actual &&
125 + git rev-parse C^0 >expect &&
126 + test_cmp expect actual
127 +'
128 +
129 +test_expect_success 'rebase --keep-base master from side' '
130 + git reset --hard &&
131 + git checkout side &&
132 + git reset --hard K &&
133 +
134 + test_must_fail git rebase --keep-base master
135 +'
136 +
137 +test_expect_success 'rebase -i --keep-base master from topic' '
138 + git reset --hard &&
139 + git checkout topic &&
140 + git reset --hard G &&
141 +
142 + set_fake_editor &&
143 + EXPECT_COUNT=2 git rebase -i --keep-base master &&
144 + git rev-parse C >base.expect &&
145 + git merge-base master HEAD >base.actual &&
146 + test_cmp base.expect base.actual &&
147 +
148 + git rev-parse HEAD~2 >actual &&
149 + git rev-parse C^0 >expect &&
150 + test_cmp expect actual
151 +'
152 +
153 +test_expect_success 'rebase -i --keep-base master from side' '
154 + git reset --hard &&
155 + git checkout side &&
156 + git reset --hard K &&
157 +
158 + set_fake_editor &&
159 + test_must_fail git rebase -i --keep-base master
160 +'
161 +
162 test_done
t/t3431-rebase-fork-point.sh
+4
@@ -43,11 +43,15 @@ test_rebase () {
43
44 test_rebase 'G F E D B A'
45 test_rebase 'G F D B A' --onto D
46 +test_rebase 'G F B A' --keep-base
47 test_rebase 'G F C E D B A' --no-fork-point
48 test_rebase 'G F C D B A' --no-fork-point --onto D
49 +test_rebase 'G F C B A' --no-fork-point --keep-base
50 test_rebase 'G F E D B A' --fork-point refs/heads/master
51 test_rebase 'G F D B A' --fork-point --onto D refs/heads/master
52 +test_rebase 'G F B A' --fork-point --keep-base refs/heads/master
53 test_rebase 'G F C E D B A' refs/heads/master
54 test_rebase 'G F C D B A' --onto D refs/heads/master
55 +test_rebase 'G F C B A' --keep-base refs/heads/master
56
57 test_done
t/t3432-rebase-fast-forward.sh
+11
@@ -75,11 +75,15 @@ test_rebase_same_head success noop same success noop-force same master
75 test_rebase_same_head success noop same success noop-force diff --onto B B
76 test_rebase_same_head success noop same success noop-force diff --onto B... B
77 test_rebase_same_head success noop same success noop-force same --onto master... master
78 +test_rebase_same_head success noop same success noop-force same --keep-base master
79 +test_rebase_same_head success noop same success noop-force same --keep-base
80 test_rebase_same_head success noop same success noop-force same --no-fork-point
81 +test_rebase_same_head success noop same success noop-force same --keep-base --no-fork-point
82 test_rebase_same_head success noop same success work same --fork-point master
83 test_rebase_same_head success noop same success work diff --fork-point --onto B B
84 test_rebase_same_head success noop same success work diff --fork-point --onto B... B
85 test_rebase_same_head success noop same success work same --fork-point --onto master... master
86 +test_rebase_same_head success noop same success work same --keep-base --keep-base master
87
88 test_expect_success 'add work same to side' '
89 test_commit E
@@ -91,11 +95,15 @@ test_rebase_same_head success noop same success noop-force same master
95 test_rebase_same_head success noop same success noop-force diff --onto B B
96 test_rebase_same_head success noop same success noop-force diff --onto B... B
97 test_rebase_same_head success noop same success noop-force same --onto master... master
98 +test_rebase_same_head success noop same success noop-force same --keep-base master
99 +test_rebase_same_head success noop same success noop-force same --keep-base
100 test_rebase_same_head success noop same success noop-force same --no-fork-point
101 +test_rebase_same_head success noop same success noop-force same --keep-base --no-fork-point
102 test_rebase_same_head success noop same success work same --fork-point master
103 test_rebase_same_head success noop same success work diff --fork-point --onto B B
104 test_rebase_same_head success noop same success work diff --fork-point --onto B... B
105 test_rebase_same_head success noop same success work same --fork-point --onto master... master
106 +test_rebase_same_head success noop same success work same --fork-point --keep-base master
107
108 test_expect_success 'add work same to upstream' '
109 git checkout master &&
@@ -107,8 +115,11 @@ changes='our and their changes'
115 test_rebase_same_head success noop same success noop-force diff --onto B B
116 test_rebase_same_head success noop same success noop-force diff --onto B... B
117 test_rebase_same_head success noop same success work diff --onto master... master
118 +test_rebase_same_head success noop same success work diff --keep-base master
119 +test_rebase_same_head success noop same success work diff --keep-base
120 test_rebase_same_head failure work same success work diff --fork-point --onto B B
121 test_rebase_same_head failure work same success work diff --fork-point --onto B... B
122 test_rebase_same_head success noop same success work diff --fork-point --onto master... master
123 +test_rebase_same_head success noop same success work diff --fork-point --keep-base master
124
125 test_done