diff options
-rw-r--r-- | Documentation/git-rebase.txt | 30 | ||||
-rw-r--r-- | builtin/rebase.c | 84 | ||||
-rw-r--r-- | contrib/completion/git-completion.bash | 2 | ||||
-rwxr-xr-x | t/t3400-rebase.sh | 2 | ||||
-rwxr-xr-x | t/t3404-rebase-interactive.sh | 2 | ||||
-rwxr-xr-x | t/t3416-rebase-onto-threedots.sh | 57 | ||||
-rwxr-xr-x | t/t3421-rebase-topology-linear.sh | 29 | ||||
-rwxr-xr-x | t/t3431-rebase-fork-point.sh | 57 | ||||
-rwxr-xr-x | t/t3432-rebase-fast-forward.sh | 125 |
9 files changed, 360 insertions, 28 deletions
diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt index 7ecf766077..639a4179d1 100644 --- a/Documentation/git-rebase.txt +++ b/Documentation/git-rebase.txt @@ -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>:: @@ -543,6 +565,8 @@ In addition, the following pairs of options are incompatible: * --preserve-merges and --interactive * --preserve-merges and --signoff * --preserve-merges and --rebase-merges + * --keep-base and --onto + * --keep-base and --root BEHAVIORAL DIFFERENCES ----------------------- @@ -869,7 +893,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: diff --git a/builtin/rebase.c b/builtin/rebase.c index e8319d5946..f730b15a78 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -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"), @@ -1261,24 +1261,44 @@ static int is_linear_history(struct commit *from, struct commit *to) return 1; } -static int can_fast_forward(struct commit *onto, struct object_id *head_oid, - struct object_id *merge_base) +static int can_fast_forward(struct commit *onto, struct commit *upstream, + struct commit *restrict_revision, + struct object_id *head_oid, struct object_id *merge_base) { struct commit *head = lookup_commit(the_repository, head_oid); - struct commit_list *merge_bases; - int res; + struct commit_list *merge_bases = NULL; + int res = 0; if (!head) - return 0; + goto done; merge_bases = get_merge_bases(onto, head); - if (merge_bases && !merge_bases->next) { - oidcpy(merge_base, &merge_bases->item->object.oid); - res = oideq(merge_base, &onto->object.oid); - } else { + if (!merge_bases || merge_bases->next) { oidcpy(merge_base, &null_oid); - res = 0; + goto done; } + + oidcpy(merge_base, &merge_bases->item->object.oid); + if (!oideq(merge_base, &onto->object.oid)) + goto done; + + if (restrict_revision && !oideq(&restrict_revision->object.oid, merge_base)) + goto done; + + if (!upstream) + goto done; + + free_commit_list(merge_bases); + merge_bases = get_merge_bases(upstream, head); + if (!merge_bases || merge_bases->next) + goto done; + + if (!oideq(&onto->object.oid, &merge_bases->item->object.oid)) + goto done; + + res = 1; + +done: free_commit_list(merge_bases); return res && is_linear_history(onto, head); } @@ -1377,6 +1397,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; @@ -1395,6 +1416,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, @@ -1548,6 +1571,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); @@ -1876,12 +1906,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 { @@ -2016,13 +2056,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) /* * Check if we are already based on onto with linear history, - * but this should be done only when upstream and onto are the same - * and if this is not an interactive rebase. + * in which case we could fast-forward without replacing the commits + * with new commits recreated by replaying their changes. This + * optimization must not be done if this is an interactive rebase. */ - if (can_fast_forward(options.onto, &options.orig_head, &merge_base) && - !is_interactive(&options) && !options.restrict_revision && - options.upstream && - !oidcmp(&options.upstream->object.oid, &options.onto->object.oid)) { + if (can_fast_forward(options.onto, options.upstream, options.restrict_revision, + &options.orig_head, &merge_base) && + !is_interactive(&options)) { int flag; if (!(options.flags & REBASE_FORCE)) { diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index 6f7b4f9607..59cd3e8e90 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -2043,7 +2043,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 diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh index 23469cc789..ab18ac5f28 100755 --- a/t/t3400-rebase.sh +++ b/t/t3400-rebase.sh @@ -295,7 +295,7 @@ test_expect_success 'rebase --am and --show-current-patch' ' echo two >>init.t && git commit -a -m two && git tag two && - test_must_fail git rebase --onto init HEAD^ && + test_must_fail git rebase -f --onto init HEAD^ && GIT_TRACE=1 git rebase --show-current-patch >/dev/null 2>stderr && grep "show.*$(git rev-parse two)" stderr ) diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 04affcc38a..29a35840ed 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -1056,7 +1056,7 @@ test_expect_success C_LOCALE_OUTPUT 'rebase --edit-todo does not work on non-int git reset --hard && git checkout conflict-branch && set_fake_editor && - test_must_fail git rebase --onto HEAD~2 HEAD~ && + test_must_fail git rebase -f --onto HEAD~2 HEAD~ && test_must_fail git rebase --edit-todo && git rebase --abort ' diff --git a/t/t3416-rebase-onto-threedots.sh b/t/t3416-rebase-onto-threedots.sh index ddf2f64853..9c2548423b 100755 --- a/t/t3416-rebase-onto-threedots.sh +++ b/t/t3416-rebase-onto-threedots.sh @@ -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 diff --git a/t/t3421-rebase-topology-linear.sh b/t/t3421-rebase-topology-linear.sh index 7274dca40b..b847064f91 100755 --- a/t/t3421-rebase-topology-linear.sh +++ b/t/t3421-rebase-topology-linear.sh @@ -31,6 +31,16 @@ test_run_rebase success -m test_run_rebase success -i test_have_prereq !REBASE_P || test_run_rebase success -p +test_expect_success 'setup branches and remote tracking' ' + git tag -l >tags && + for tag in $(cat tags) + do + git branch branch-$tag $tag || return 1 + done && + git remote add origin "file://$PWD" && + git fetch origin +' + test_run_rebase () { result=$1 shift @@ -57,6 +67,7 @@ test_run_rebase () { " } test_run_rebase success '' +test_run_rebase success --fork-point test_run_rebase success -m test_run_rebase success -i test_have_prereq !REBASE_P || test_run_rebase failure -p @@ -64,6 +75,23 @@ test_have_prereq !REBASE_P || test_run_rebase failure -p test_run_rebase () { result=$1 shift + test_expect_$result "rebase $* -f rewrites even if remote upstream is an ancestor" " + reset_rebase && + git rebase $* -f branch-b branch-e && + ! test_cmp_rev branch-e origin/branch-e && + test_cmp_rev branch-b HEAD~2 && + test_linear_range 'd e' branch-b.. + " +} +test_run_rebase success '' +test_run_rebase success --fork-point +test_run_rebase success -m +test_run_rebase success -i +test_have_prereq !REBASE_P || test_run_rebase success -p + +test_run_rebase () { + result=$1 + shift test_expect_$result "rebase $* fast-forwards from ancestor of upstream" " reset_rebase && git rebase $* e b && @@ -71,6 +99,7 @@ test_run_rebase () { " } test_run_rebase success '' +test_run_rebase success --fork-point test_run_rebase success -m test_run_rebase success -i test_have_prereq !REBASE_P || test_run_rebase success -p diff --git a/t/t3431-rebase-fork-point.sh b/t/t3431-rebase-fork-point.sh new file mode 100755 index 0000000000..78851b9a2a --- /dev/null +++ b/t/t3431-rebase-fork-point.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# +# Copyright (c) 2019 Denton Liu +# + +test_description='git rebase --fork-point test' + +. ./test-lib.sh + +# A---B---D---E (master) +# \ +# C*---F---G (side) +# +# C was formerly part of master but master was rewound to remove C +# +test_expect_success setup ' + test_commit A && + test_commit B && + test_commit C && + git branch -t side && + git reset --hard HEAD^ && + test_commit D && + test_commit E && + git checkout side && + test_commit F && + test_commit G +' + +test_rebase () { + expected="$1" && + shift && + test_expect_success "git rebase $*" " + git checkout master && + git reset --hard E && + git checkout side && + git reset --hard G && + git rebase $* && + test_write_lines $expected >expect && + git log --pretty=%s >actual && + test_cmp expect actual + " +} + +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 diff --git a/t/t3432-rebase-fast-forward.sh b/t/t3432-rebase-fast-forward.sh new file mode 100755 index 0000000000..034ffc7e76 --- /dev/null +++ b/t/t3432-rebase-fast-forward.sh @@ -0,0 +1,125 @@ +#!/bin/sh +# +# Copyright (c) 2019 Denton Liu +# + +test_description='ensure rebase fast-forwards commits when possible' + +. ./test-lib.sh + +test_expect_success setup ' + test_commit A && + test_commit B && + test_commit C && + test_commit D && + git checkout -t -b side +' + +test_rebase_same_head () { + status_n="$1" && + shift && + what_n="$1" && + shift && + cmp_n="$1" && + shift && + status_f="$1" && + shift && + what_f="$1" && + shift && + cmp_f="$1" && + shift && + test_rebase_same_head_ $status_n $what_n $cmp_n "" "$*" && + test_rebase_same_head_ $status_f $what_f $cmp_f " --no-ff" "$*" +} + +test_rebase_same_head_ () { + status="$1" && + shift && + what="$1" && + shift && + cmp="$1" && + shift && + flag="$1" + shift && + test_expect_$status "git rebase$flag $* with $changes is $what with $cmp HEAD" " + oldhead=\$(git rev-parse HEAD) && + test_when_finished 'git reset --hard \$oldhead' && + git rebase$flag $* >stdout && + if test $what = work + then + # Must check this case first, for 'is up to + # date, rebase forced[...]rewinding head' cases + test_i18ngrep 'rewinding head' stdout + elif test $what = noop + then + test_i18ngrep 'is up to date' stdout && + test_i18ngrep ! 'rebase forced' stdout + elif test $what = noop-force + then + test_i18ngrep 'is up to date, rebase forced' stdout + fi && + newhead=\$(git rev-parse HEAD) && + if test $cmp = same + then + test_cmp_rev \$oldhead \$newhead + elif test $cmp = diff + then + ! test_cmp_rev \$oldhead \$newhead + fi + " +} + +changes='no changes' +test_rebase_same_head success noop same success work same +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 +' + +changes='our changes' +test_rebase_same_head success noop same success work same +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 && + test_commit F && + git checkout side +' + +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 |