Skip to content

Commit 640f9cd

Browse files
committed
Merge branch 'dl/rebase-i-keep-base'
"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 tests: test linear branch topology 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 for --no-ff's interaction with fast-forward t3432: distinguish "noop-same" v.s. "work-same" in "same head" tests t3432: test rebase fast-forward behavior t3431: add rebase --fork-point tests
2 parents 026428c + 414d924 commit 640f9cd

9 files changed

+360
-28
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>::
@@ -543,6 +565,8 @@ In addition, the following pairs of options are incompatible:
543565
* --preserve-merges and --interactive
544566
* --preserve-merges and --signoff
545567
* --preserve-merges and --rebase-merges
568+
* --keep-base and --onto
569+
* --keep-base and --root
546570

547571
BEHAVIORAL DIFFERENCES
548572
-----------------------
@@ -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: 62 additions & 22 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"),
@@ -1261,24 +1261,44 @@ static int is_linear_history(struct commit *from, struct commit *to)
12611261
return 1;
12621262
}
12631263

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

12711272
if (!head)
1272-
return 0;
1273+
goto done;
12731274

12741275
merge_bases = get_merge_bases(onto, head);
1275-
if (merge_bases && !merge_bases->next) {
1276-
oidcpy(merge_base, &merge_bases->item->object.oid);
1277-
res = oideq(merge_base, &onto->object.oid);
1278-
} else {
1276+
if (!merge_bases || merge_bases->next) {
12791277
oidcpy(merge_base, &null_oid);
1280-
res = 0;
1278+
goto done;
12811279
}
1280+
1281+
oidcpy(merge_base, &merge_bases->item->object.oid);
1282+
if (!oideq(merge_base, &onto->object.oid))
1283+
goto done;
1284+
1285+
if (restrict_revision && !oideq(&restrict_revision->object.oid, merge_base))
1286+
goto done;
1287+
1288+
if (!upstream)
1289+
goto done;
1290+
1291+
free_commit_list(merge_bases);
1292+
merge_bases = get_merge_bases(upstream, head);
1293+
if (!merge_bases || merge_bases->next)
1294+
goto done;
1295+
1296+
if (!oideq(&onto->object.oid, &merge_bases->item->object.oid))
1297+
goto done;
1298+
1299+
res = 1;
1300+
1301+
done:
12821302
free_commit_list(merge_bases);
12831303
return res && is_linear_history(onto, head);
12841304
}
@@ -1377,6 +1397,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
13771397
struct rebase_options options = REBASE_OPTIONS_INIT;
13781398
const char *branch_name;
13791399
int ret, flags, total_argc, in_progress = 0;
1400+
int keep_base = 0;
13801401
int ok_to_skip_pre_rebase = 0;
13811402
struct strbuf msg = STRBUF_INIT;
13821403
struct strbuf revisions = STRBUF_INIT;
@@ -1395,6 +1416,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
13951416
OPT_STRING(0, "onto", &options.onto_name,
13961417
N_("revision"),
13971418
N_("rebase onto given branch instead of upstream")),
1419+
OPT_BOOL(0, "keep-base", &keep_base,
1420+
N_("use the merge-base of upstream and branch as the current base")),
13981421
OPT_BOOL(0, "no-verify", &ok_to_skip_pre_rebase,
13991422
N_("allow pre-rebase hook to run")),
14001423
OPT_NEGBIT('q', "quiet", &options.flags,
@@ -1548,6 +1571,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
15481571
warning(_("git rebase --preserve-merges is deprecated. "
15491572
"Use --rebase-merges instead."));
15501573

1574+
if (keep_base) {
1575+
if (options.onto_name)
1576+
die(_("cannot combine '--keep-base' with '--onto'"));
1577+
if (options.root)
1578+
die(_("cannot combine '--keep-base' with '--root'"));
1579+
}
1580+
15511581
if (action != ACTION_NONE && !in_progress)
15521582
die(_("No rebase in progress?"));
15531583
setenv(GIT_REFLOG_ACTION_ENVIRONMENT, "rebase", 0);
@@ -1876,12 +1906,22 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
18761906
}
18771907

18781908
/* Make sure the branch to rebase onto is valid. */
1879-
if (!options.onto_name)
1909+
if (keep_base) {
1910+
strbuf_reset(&buf);
1911+
strbuf_addstr(&buf, options.upstream_name);
1912+
strbuf_addstr(&buf, "...");
1913+
options.onto_name = xstrdup(buf.buf);
1914+
} else if (!options.onto_name)
18801915
options.onto_name = options.upstream_name;
18811916
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);
1917+
if (get_oid_mb(options.onto_name, &merge_base) < 0) {
1918+
if (keep_base)
1919+
die(_("'%s': need exactly one merge base with branch"),
1920+
options.upstream_name);
1921+
else
1922+
die(_("'%s': need exactly one merge base"),
1923+
options.onto_name);
1924+
}
18851925
options.onto = lookup_commit_or_die(&merge_base,
18861926
options.onto_name);
18871927
} else {
@@ -2016,13 +2056,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
20162056

20172057
/*
20182058
* 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.
2059+
* in which case we could fast-forward without replacing the commits
2060+
* with new commits recreated by replaying their changes. This
2061+
* optimization must not be done if this is an interactive rebase.
20212062
*/
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)) {
2063+
if (can_fast_forward(options.onto, options.upstream, options.restrict_revision,
2064+
&options.orig_head, &merge_base) &&
2065+
!is_interactive(&options)) {
20262066
int flag;
20272067

20282068
if (!(options.flags & REBASE_FORCE)) {

contrib/completion/git-completion.bash

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2043,7 +2043,7 @@ _git_rebase ()
20432043
--autosquash --no-autosquash
20442044
--fork-point --no-fork-point
20452045
--autostash --no-autostash
2046-
--verify --no-verify
2046+
--verify --no-verify --keep-base
20472047
--keep-empty --root --force-rebase --no-ff
20482048
--rerere-autoupdate
20492049
--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 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
@@ -1056,7 +1056,7 @@ test_expect_success C_LOCALE_OUTPUT 'rebase --edit-todo does not work on non-int
10561056
git reset --hard &&
10571057
git checkout conflict-branch &&
10581058
set_fake_editor &&
1059-
test_must_fail git rebase --onto HEAD~2 HEAD~ &&
1059+
test_must_fail git rebase -f --onto HEAD~2 HEAD~ &&
10601060
test_must_fail git rebase --edit-todo &&
10611061
git rebase --abort
10621062
'

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/t3421-rebase-topology-linear.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ test_run_rebase success -m
3131
test_run_rebase success -i
3232
test_have_prereq !REBASE_P || test_run_rebase success -p
3333

34+
test_expect_success 'setup branches and remote tracking' '
35+
git tag -l >tags &&
36+
for tag in $(cat tags)
37+
do
38+
git branch branch-$tag $tag || return 1
39+
done &&
40+
git remote add origin "file://$PWD" &&
41+
git fetch origin
42+
'
43+
3444
test_run_rebase () {
3545
result=$1
3646
shift
@@ -57,10 +67,28 @@ test_run_rebase () {
5767
"
5868
}
5969
test_run_rebase success ''
70+
test_run_rebase success --fork-point
6071
test_run_rebase success -m
6172
test_run_rebase success -i
6273
test_have_prereq !REBASE_P || test_run_rebase failure -p
6374

75+
test_run_rebase () {
76+
result=$1
77+
shift
78+
test_expect_$result "rebase $* -f rewrites even if remote upstream is an ancestor" "
79+
reset_rebase &&
80+
git rebase $* -f branch-b branch-e &&
81+
! test_cmp_rev branch-e origin/branch-e &&
82+
test_cmp_rev branch-b HEAD~2 &&
83+
test_linear_range 'd e' branch-b..
84+
"
85+
}
86+
test_run_rebase success ''
87+
test_run_rebase success --fork-point
88+
test_run_rebase success -m
89+
test_run_rebase success -i
90+
test_have_prereq !REBASE_P || test_run_rebase success -p
91+
6492
test_run_rebase () {
6593
result=$1
6694
shift
@@ -71,6 +99,7 @@ test_run_rebase () {
7199
"
72100
}
73101
test_run_rebase success ''
102+
test_run_rebase success --fork-point
74103
test_run_rebase success -m
75104
test_run_rebase success -i
76105
test_have_prereq !REBASE_P || test_run_rebase success -p

0 commit comments

Comments
 (0)