Skip to content

Commit a3d051a

Browse files
committed
Merge branch 'dl/rebase-i-keep-base' into jch
"git rebase --keep-base <upstream>" tries to find the original base of the topic being rebased and rebase on top of that same base, which is useful when running the "git rebase -i" (and its limited variant "git rebase -x"). The command also has learned to fast-forward in more cases where it can instead of replaying to recreate identical commits. * dl/rebase-i-keep-base: rebase: teach rebase --keep-base rebase: fast-forward --fork-point in more cases rebase: fast-forward --onto in more cases rebase: refactor can_fast_forward into goto tower t3432: test rebase fast-forward behavior t3431: add rebase --fork-point tests
2 parents 6be054f + 8809649 commit a3d051a

File tree

8 files changed

+290
-32
lines changed

8 files changed

+290
-32
lines changed

Documentation/git-rebase.txt

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ git-rebase - Reapply commits on top of another base tip
88
SYNOPSIS
99
--------
1010
[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>]]
1313
'git rebase' [-i | --interactive] [<options>] [--exec <cmd>] [--onto <newbase>]
1414
--root [<branch>]
1515
'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
217217
merge base of A and B if there is exactly one merge base. You can
218218
leave out at most one of A and B, in which case it defaults to HEAD.
219219

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+
220238
<upstream>::
221239
Upstream branch to compare against. May be any valid commit,
222240
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.
369387
+
370388
If either <upstream> or --root is given on the command line, then the
371389
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.
372394

373395
--ignore-whitespace::
374396
--whitespace=<option>::
@@ -545,6 +567,8 @@ In addition, the following pairs of options are incompatible:
545567
* --preserve-merges and --rebase-merges
546568
* --rebase-merges and --strategy
547569
* --rebase-merges and --strategy-option
570+
* --keep-base and --onto
571+
* --keep-base and --root
548572

549573
BEHAVIORAL DIFFERENCES
550574
-----------------------
@@ -869,7 +893,7 @@ NOTE: While an "easy case recovery" sometimes appears to be successful
869893
--interactive` will be **resurrected**!
870894

871895
The idea is to manually tell 'git rebase' "where the old 'subsystem'
872-
ended and your 'topic' began", that is, what the old merge-base
896+
ended and your 'topic' began", that is, what the old merge base
873897
between them was. You will have to find a way to name the last commit
874898
of the old 'subsystem', for example:
875899

builtin/rebase.c

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
#include "rebase-interactive.h"
3030

3131
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>]]"),
3434
N_("git rebase [-i] [options] [--exec <cmd>] [--onto <newbase>] "
3535
"--root [<branch>]"),
3636
N_("git rebase --continue | --abort | --skip | --edit-todo"),
@@ -1262,25 +1262,46 @@ static int is_linear_history(struct commit *from, struct commit *to)
12621262
return 1;
12631263
}
12641264

1265-
static int can_fast_forward(struct commit *onto, struct object_id *head_oid,
1266-
struct object_id *merge_base)
1265+
static int can_fast_forward(struct commit *onto, struct commit *upstream,
1266+
struct commit *restrict_revision,
1267+
struct object_id *head_oid, struct object_id *merge_base)
12671268
{
12681269
struct commit *head = lookup_commit(the_repository, head_oid);
1269-
struct commit_list *merge_bases;
1270-
int res;
1270+
struct commit_list *merge_bases = NULL;
1271+
int res = 0;
12711272

12721273
if (!head)
1273-
return 0;
1274+
goto done;
12741275

12751276
merge_bases = get_merge_bases(onto, head);
1276-
if (merge_bases && !merge_bases->next) {
1277-
oidcpy(merge_base, &merge_bases->item->object.oid);
1278-
res = oideq(merge_base, &onto->object.oid);
1279-
} else {
1277+
if (!merge_bases || merge_bases->next) {
12801278
oidcpy(merge_base, &null_oid);
1281-
res = 0;
1279+
goto done;
12821280
}
1281+
1282+
oidcpy(merge_base, &merge_bases->item->object.oid);
1283+
if (!oideq(merge_base, &onto->object.oid))
1284+
goto done;
1285+
1286+
if (restrict_revision && !oideq(&restrict_revision->object.oid, merge_base))
1287+
goto done;
1288+
1289+
if (!upstream)
1290+
goto done;
1291+
12831292
free_commit_list(merge_bases);
1293+
merge_bases = get_merge_bases(upstream, head);
1294+
if (!merge_bases || merge_bases->next)
1295+
goto done;
1296+
1297+
if (!oideq(&onto->object.oid, &merge_bases->item->object.oid))
1298+
goto done;
1299+
1300+
res = 1;
1301+
1302+
done:
1303+
if (merge_bases)
1304+
free_commit_list(merge_bases);
12841305
return res && is_linear_history(onto, head);
12851306
}
12861307

@@ -1378,6 +1399,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
13781399
struct rebase_options options = REBASE_OPTIONS_INIT;
13791400
const char *branch_name;
13801401
int ret, flags, total_argc, in_progress = 0;
1402+
int keep_base = 0;
13811403
int ok_to_skip_pre_rebase = 0;
13821404
struct strbuf msg = STRBUF_INIT;
13831405
struct strbuf revisions = STRBUF_INIT;
@@ -1395,6 +1417,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
13951417
OPT_STRING(0, "onto", &options.onto_name,
13961418
N_("revision"),
13971419
N_("rebase onto given branch instead of upstream")),
1420+
OPT_BOOL(0, "keep-base", &keep_base,
1421+
N_("use the merge-base of upstream and branch as the current base")),
13981422
OPT_BOOL(0, "no-verify", &ok_to_skip_pre_rebase,
13991423
N_("allow pre-rebase hook to run")),
14001424
OPT_NEGBIT('q', "quiet", &options.flags,
@@ -1548,9 +1572,12 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
15481572
usage_with_options(builtin_rebase_usage,
15491573
builtin_rebase_options);
15501574

1551-
if (options.type == REBASE_PRESERVE_MERGES)
1552-
warning(_("git rebase --preserve-merges is deprecated. "
1553-
"Use --rebase-merges instead."));
1575+
if (keep_base) {
1576+
if (options.onto_name)
1577+
die(_("cannot combine '--keep-base' with '--onto'"));
1578+
if (options.root)
1579+
die(_("cannot combine '--keep-base' with '--root'"));
1580+
}
15541581

15551582
if (action != ACTION_NONE && !in_progress)
15561583
die(_("No rebase in progress?"));
@@ -1876,12 +1903,22 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
18761903
}
18771904

18781905
/* Make sure the branch to rebase onto is valid. */
1879-
if (!options.onto_name)
1906+
if (keep_base) {
1907+
strbuf_reset(&buf);
1908+
strbuf_addstr(&buf, options.upstream_name);
1909+
strbuf_addstr(&buf, "...");
1910+
options.onto_name = xstrdup(buf.buf);
1911+
} else if (!options.onto_name)
18801912
options.onto_name = options.upstream_name;
18811913
if (strstr(options.onto_name, "...")) {
1882-
if (get_oid_mb(options.onto_name, &merge_base) < 0)
1883-
die(_("'%s': need exactly one merge base"),
1884-
options.onto_name);
1914+
if (get_oid_mb(options.onto_name, &merge_base) < 0) {
1915+
if (keep_base)
1916+
die(_("'%s': need exactly one merge base with branch"),
1917+
options.upstream_name);
1918+
else
1919+
die(_("'%s': need exactly one merge base"),
1920+
options.onto_name);
1921+
}
18851922
options.onto = lookup_commit_or_die(&merge_base,
18861923
options.onto_name);
18871924
} else {
@@ -2016,13 +2053,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
20162053

20172054
/*
20182055
* Check if we are already based on onto with linear history,
2019-
* but this should be done only when upstream and onto are the same
2020-
* and if this is not an interactive rebase.
2056+
* in which case we could fast-forward without replacing the commits
2057+
* with new commits recreated by replaying their changes. This
2058+
* optimization must not be done if this is an interactive rebase.
20212059
*/
2022-
if (can_fast_forward(options.onto, &options.orig_head, &merge_base) &&
2023-
!is_interactive(&options) && !options.restrict_revision &&
2024-
options.upstream &&
2025-
!oidcmp(&options.upstream->object.oid, &options.onto->object.oid)) {
2060+
if (can_fast_forward(options.onto, options.upstream, options.restrict_revision,
2061+
&options.orig_head, &merge_base) &&
2062+
!is_interactive(&options)) {
20262063
int flag;
20272064

20282065
if (!(options.flags & REBASE_FORCE)) {
@@ -2116,7 +2153,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
21162153
strbuf_addf(&msg, "%s: checkout %s",
21172154
getenv(GIT_REFLOG_ACTION_ENVIRONMENT), options.onto_name);
21182155
if (reset_head(&options.onto->object.oid, "checkout", NULL,
2119-
RESET_HEAD_DETACH | RESET_ORIG_HEAD |
2156+
RESET_HEAD_DETACH | RESET_ORIG_HEAD |
21202157
RESET_HEAD_RUN_POST_CHECKOUT_HOOK,
21212158
NULL, msg.buf))
21222159
die(_("Could not detach HEAD"));

contrib/completion/git-completion.bash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2029,7 +2029,7 @@ _git_rebase ()
20292029
--autosquash --no-autosquash
20302030
--fork-point --no-fork-point
20312031
--autostash --no-autostash
2032-
--verify --no-verify
2032+
--verify --no-verify --keep-base
20332033
--keep-empty --root --force-rebase --no-ff
20342034
--rerere-autoupdate
20352035
--exec

t/t3400-rebase.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh and --show-current-patch' '
295295
echo two >>init.t &&
296296
git commit -a -m two &&
297297
git tag two &&
298-
test_must_fail git rebase --onto init HEAD^ &&
298+
test_must_fail git rebase -f --onto init HEAD^ &&
299299
GIT_TRACE=1 git rebase --show-current-patch >/dev/null 2>stderr &&
300300
grep "show.*$(git rev-parse two)" stderr
301301
)

t/t3404-rebase-interactive.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1063,7 +1063,7 @@ test_expect_success C_LOCALE_OUTPUT 'rebase --edit-todo does not work on non-int
10631063
git reset --hard &&
10641064
git checkout conflict-branch &&
10651065
set_fake_editor &&
1066-
test_must_fail git rebase --onto HEAD~2 HEAD~ &&
1066+
test_must_fail git rebase -f --onto HEAD~2 HEAD~ &&
10671067
test_must_fail git rebase --edit-todo &&
10681068
git rebase --abort
10691069
'

t/t3416-rebase-onto-threedots.sh

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,64 @@ test_expect_success 'rebase -i --onto master...side' '
9999
git checkout side &&
100100
git reset --hard K &&
101101
102+
set_fake_editor &&
102103
test_must_fail git rebase -i --onto master...side J
103104
'
104105

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+
105162
test_done

t/t3431-rebase-fork-point.sh

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/bin/sh
2+
#
3+
# Copyright (c) 2019 Denton Liu
4+
#
5+
6+
test_description='git rebase --fork-point test'
7+
8+
. ./test-lib.sh
9+
10+
# A---B---D---E (master)
11+
# \
12+
# C*---F---G (side)
13+
#
14+
# C was formerly part of master but master was rewound to remove C
15+
#
16+
test_expect_success setup '
17+
test_commit A &&
18+
test_commit B &&
19+
test_commit C &&
20+
git branch -t side &&
21+
git reset --hard HEAD^ &&
22+
test_commit D &&
23+
test_commit E &&
24+
git checkout side &&
25+
test_commit F &&
26+
test_commit G
27+
'
28+
29+
test_rebase () {
30+
expected="$1" &&
31+
shift &&
32+
test_expect_success "git rebase $*" "
33+
git checkout master &&
34+
git reset --hard E &&
35+
git checkout side &&
36+
git reset --hard G &&
37+
git rebase $* &&
38+
test_write_lines $expected >expect &&
39+
git log --pretty=%s >actual &&
40+
test_cmp expect actual
41+
"
42+
}
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

0 commit comments

Comments
 (0)