rebase: learn --keep-base
cover rebase: learn --keep-base
This series teaches rebase the --keep-base option.
'git rebase --keep-base <upstream>' is equivalent to 'git rebase --onto <upstream>... <upstream>' or
'git rebase --onto $(git merge-base <upstream> HEAD) <upstream>' .
This seems to be a common case that people (including myself!) run into; I was able to find these StackOverflow posts about this use case:
* https://stackoverflow.com/questions/53234798/can-i-rebase-on-a-branchs-fork-point-without-explicitly-specifying-the-parent
* https://stackoverflow.com/questions/41529128/how-do-you-rebase-only-changes-between-two-branches-into-another-branch
* https://stackoverflow.com/a/4207357
Denton Liu (3):
rebase: teach rebase --keep-base
t3416: test rebase --keep-base
git-rebase.txt: document --keep-base option
Documentation/git-rebase.txt | 12 +++++-- builtin/rebase.c | 25 ++++++++++++-- t/t3416-rebase-onto-threedots.sh | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 6 deletions(-)
Signature · 1 line
2.21.0.512.g57bf1b23e1
PATCH 1/3 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 autosquashing, they would run something such as
git rebase -i --onto master... master
in order to preserve the merge base. This prevents unnecessary commit churning.
Alternatively, 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.
--- builtin/rebase.c | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-)
builtin/rebase.c +22 -3
@@ -27,8 +27,8 @@#include "branch.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" ),
@@ -1018,6 +1018,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) };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 ;
@@ -1051,6 +1052,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,
@@ -1217,6 +1220,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)usage_with_options (builtin_rebase_usage, builtin_rebase_options); +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 !=NO_ACTION && !in_progress)die (_ ("No rebase in progress?" ));setenv (GIT_REFLOG_ACTION_ENVIRONMENT ,"rebase" ,0 );
@@ -1541,10 +1551,19 @@ 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 ) +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,
Signature · 1 line
2.21.0.512.g57bf1b23e1
PATCH 2/3 t3416: test rebase --keep-base
In reply to Denton LiuTest rebase --keep-base to ensure it 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 causes rebase to fail.
While we're at it, add a missing set_fake_editor call to 'rebase -i --onto master...side'.
--- t/t3416-rebase-onto-threedots.sh | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+)
t/t3416-rebase-onto-threedots.sh +57 -0
@@ -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
Signature · 1 line
2.21.0.512.g57bf1b23e1
PATCH 3/3 git-rebase.txt: document --keep-base option
In reply to Denton LiuDocument the --keep-base rebase option.
While we're at it, change an instance of "merge-base" to "merge base", which is the consistent spelling.
--- Documentation/git-rebase.txt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-)
Documentation/git-rebase.txt +9 -3
@@ -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,12 @@ 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>'. + <upstream>:: Upstream branch to compare against. May be any valid commit, not just an existing branch name. Defaults to the configured
@@ -863,7 +869,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:
Signature · 1 line
2.21.0.512.g57bf1b23e1
Re: [PATCH 1/3] rebase: teach rebase --keep-base
In reply to Denton LiuOn Sat, Mar 23, 2019 at 11:25 AM Denton Liu <[email protected]> wrote:>
Quoted 13 lines
[...]
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.[...]Signed-off-by Denton Liu <[email protected]>---
builtin/rebase.c +3
-1@@ -1541,10 +1551,19 @@ 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);
builtin/rebase.c int cmd_rebase(int argc, const char **argv, const char *prefix) · line 1564
Quoted 10 lines
builtin/rebase.c +7
-0+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 ) +if (keep_base) +die (_ ("'%s': need exactly one merge base with branch" ), + options.upstream_name); +else
Style: Indent with tabs, not spaces.
Quoted 9 lines · Denton Liu
This series teaches rebase the --keep-base option.
'git rebase --keep-base <upstream>' is equivalent to 'git rebase --onto <upstream>... <upstream>' or
'git rebase --onto $(git merge-base <upstream> HEAD) <upstream>' .This seems to be a common case that people (including myself!) run into; I was able to find these StackOverflow posts about this use case:
Since this is 0/3 I won't complain too loudly, but read the above again while pretending that you didn't know what your initial motivating example was. The last three lines does not explain anything useful to such a reader, and the reader needs to decipher the two sample commands to guess what you wanted to achieve.
Before "teaches rebase the --keep-base option", you must tell what you wanted to do with that new feature to attract readers---convince them your new contribution is worth their time to read over.
If I understand correctly, what "--onto $(git merge-base @{u} HEAD) @{u}" lets you express is:
no matter how much progress the upstream has made while I was away from the keyboard, I want to rebase the current topic on top of the same commit from the upstream, on which I based the current iteration of the topic.
I suspect that such a rebase will become no-op without "-i". Am I mistaken? I am not sure if "--keep-base" is useful without "-i".
But of course, it would be useful with "-i", i.e. when you want to further polish the topic. You need to give <upstream> to the command to let it know where their work stops and your work begins, i.e. letting the command figure out what commits to replay. But in such a workflow, you do not want <upstream> to affect on top of what commit the replayed history is built. And "keep base" would be a very direct way to achieve that.
Re: [PATCH 1/3] rebase: teach rebase --keep-base
In reply to Denton LiuQuoted 19 lines · Denton Liu
A common scenario is if a user is working on a topic branch and they wish to make some changes to intermediate commits or autosquashing, they would run something such as
git rebase -i --onto master... masterin order to preserve the merge base. This prevents unnecessary commit churning.
Alternatively, 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
Quoted 7 lines
git rebase -i --keep-base masterand
git rebase -x ./test.sh --keep-base masterrespectively.
I never use the "feature" myself, but I recall that when "git rebase" is run on a branch appropriately prepared, you do not even have to say <upstream> (iow, you type "git rebase<RET>" and rebase on top of @{upstream}).
Can this new "--keep-base" feature mesh well with it? When the current branch has forked from origin/master, for example, it would be good if
$ git rebase -i --same-base
becomes a usable short-hand for
$ git rebase -i --same-base origin/master
aka
$ git rebase -i --onto $(git merge-base HEAD origin/master) origin/master
Re: [PATCH 1/3] rebase: teach rebase --keep-base
In reply to Denton Liubuiltin/rebase.c int cmd_rebase(int argc, const char **argv, const char *prefix) · line 1568
Quoted 8 lines · Denton Liu
builtin/rebase.c +4
-0if (strstr (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);
If you turn a single statement body into if/else, have {} around the body of the outer if, i.e.
if (get_oid_mb(...) < 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);
}Otherwise -Werror=danglng-else will stop compilation.
Thanks.
Hi Junio,
Quoted 22 lines · Junio C Hamano
Quoted 9 lines · Denton Liu
This series teaches rebase the --keep-base option.
'git rebase --keep-base <upstream>' is equivalent to 'git rebase --onto <upstream>... <upstream>' or
'git rebase --onto $(git merge-base <upstream> HEAD) <upstream>' .This seems to be a common case that people (including myself!) run into; I was able to find these StackOverflow posts about this use case:
Since this is 0/3 I won't complain too loudly, but read the above again while pretending that you didn't know what your initial motivating example was. The last three lines does not explain anything useful to such a reader, and the reader needs to decipher the two sample commands to guess what you wanted to achieve.
Before "teaches rebase the --keep-base option", you must tell what you wanted to do with that new feature to attract readers---convince them your new contribution is worth their time to read over.
Quoted 7 lines
If I understand correctly, what "--onto $(git merge-base @{u} HEAD) @{u}" lets you express is:
no matter how much progress the upstream has made while I was away from the keyboard, I want to rebase the current topic on top of the same commit from the upstream, on which I based the current iteration of the topic.
Thanks for the clarification. I'll try my best to write future cover letters more clearly.
Quoted 3 lines
I suspect that such a rebase will become no-op without "-i". Am I mistaken? I am not sure if "--keep-base" is useful without "-i".
It's useful in the case of "-x", although that is a grey area since "-x" uses interactive machinery internally. Aside from "-x", I can't really think of a situation where we would use "--keep-base" without "-i".
Quoted 11 lines
But of course, it would be useful with "-i", i.e. when you want to further polish the topic. You need to give <upstream> to the command to let it know where their work stops and your work begins, i.e. letting the command figure out what commits to replay. But in such a workflow, you do not want <upstream> to affect on top of what commit the replayed history is built. And "keep base" would be a very direct way to achieve that.
Thanks,
Denton
Re: [PATCH 1/3] rebase: teach rebase --keep-base
In reply to Junio C HamanoHi Junio,
Quoted 38 lines · Junio C Hamano
Quoted 19 lines · Denton Liu
A common scenario is if a user is working on a topic branch and they wish to make some changes to intermediate commits or autosquashing, they would run something such as
git rebase -i --onto master... masterin order to preserve the merge base. This prevents unnecessary commit churning.
Alternatively, 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
Quoted 7 lines
git rebase -i --keep-base masterand
git rebase -x ./test.sh --keep-base masterrespectively.
I never use the "feature" myself, but I recall that when "git rebase" is run on a branch appropriately prepared, you do not even have to say <upstream> (iow, you type "git rebase<RET>" and rebase on top of @{upstream}).
Can this new "--keep-base" feature mesh well with it? When the current branch has forked from origin/master, for example, it would be good if
Quoted 5 lines
$ git rebase -i --same-basebecomes a usable short-hand for
$ git rebase -i --same-base origin/master
By "--same-base", I am assuming you mistyped and meant to write "--keep-base"? If that's the case, I can make it a shorthand.
Thanks,
Denton
Quoted 5 lines
aka
$ git rebase -i --onto $(git merge-base HEAD origin/master) origin/master
Re: [PATCH 1/3] rebase: teach rebase --keep-base
Quoted 41 lines · Denton Liu
Hi Junio,
Quoted 38 lines · Junio C Hamano
Quoted 19 lines · Denton Liu
A common scenario is if a user is working on a topic branch and they wish to make some changes to intermediate commits or autosquashing, they would run something such as
git rebase -i --onto master... masterin order to preserve the merge base. This prevents unnecessary commit churning.
Alternatively, 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
Quoted 7 lines
git rebase -i --keep-base masterand
git rebase -x ./test.sh --keep-base masterrespectively.
I never use the "feature" myself, but I recall that when "git rebase" is run on a branch appropriately prepared, you do not even have to say <upstream> (iow, you type "git rebase<RET>" and rebase on top of @{upstream}).
Can this new "--keep-base" feature mesh well with it? When the current branch has forked from origin/master, for example, it would be good if
Quoted 8 lines
Quoted 5 lines
$ git rebase -i --same-basebecomes a usable short-hand for
$ git rebase -i --same-base origin/masterBy "--same-base", I am assuming you mistyped and meant to write "--keep-base"? If that's the case, I can make it a shorthand.
Sorry, I misunderstood your question. "--keep-base" already has the shorthand case handled by default.
One caveat is that "--fork-point" is automatically implied if no upstream is supplied but this behaviour is the same for "--onto" or when no other options are supplied as well.
I don't use the feature either so I'm not really sure if this behaviour would be the most sane thing to do.
Quoted 9 lines
Thanks,
Denton
Quoted 5 lines
aka
$ git rebase -i --onto $(git merge-base HEAD origin/master) origin/master
Re: [PATCH 1/3] rebase: teach rebase --keep-base
In reply to Denton LiuQuoted 2 lines · Denton Liu
A common scenario is if a user is working on a topic branch and they wish to make some changes to intermediate commits or autosquashing, they
Sorry, small typo here:
s/autosquashing/autosquash/
-Denton
Quoted 100 lines
would run something such as
git rebase -i --onto master... masterin order to preserve the merge base. This prevents unnecessary commit churning.
Alternatively, 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 masterand
git rebase -x ./test.sh --keep-base masterrespectively.
Signed-off-by Denton Liu <[email protected]>--- builtin/rebase.c | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-)builtin/rebase.c +22
-3@@ -27,8 +27,8 @@#include "branch.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" ),@@ -1018,6 +1018,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) };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 ;@@ -1051,6 +1052,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,@@ -1217,6 +1220,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)usage_with_options (builtin_rebase_usage, builtin_rebase_options); +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 !=NO_ACTION && !in_progress)die (_ ("No rebase in progress?" ));setenv (GIT_REFLOG_ACTION_ENVIRONMENT ,"rebase" ,0 );@@ -1541,10 +1551,19 @@ 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 ) +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,Signature · 1 line
2.21.0.512.g57bf1b23e1
Re: [PATCH 1/3] rebase: teach rebase --keep-base
In reply to Denton LiuHi Denton,
Quoted 11 lines · Denton Liu
[...]
This allows us to rewrite the above as
git rebase -i --keep-base masterand
git rebase -x ./test.sh --keep-base masterrespectively.
Just a quick note: this breaks t5407 because that test uses git rebase --keep and expects that abbreviated option to be expanded to --keep-empty, which is now no longer the only possible expansion.
I just submitted a patch series to fix that, and other uses of abbreviated options in the test suite, in
https://public-inbox.org/git/[email protected]/T/#t
Ciao,
Johannes
P.S.: Did you run the test suite before submitting your patches?
Re: [PATCH 1/3] rebase: teach rebase --keep-base
Hi Johannes,
Quoted 16 lines · Johannes Schindelin
Hi Denton,
Quoted 11 lines · Denton Liu
[...]
This allows us to rewrite the above as
git rebase -i --keep-base masterand
git rebase -x ./test.sh --keep-base masterrespectively.
Quoted 7 lines
Just a quick note: this breaks t5407 because that test uses
git rebase --keepand expects that abbreviated option to be expanded to--keep-empty, which is now no longer the only possible expansion.I just submitted a patch series to fix that, and other uses of abbreviated options in the test suite, in
https://public-inbox.org/git/[email protected]/T/#t
Thanks for catching this. I replied with a (tiny) review.
Quoted 5 lines
Ciao,
Johannes
P.S.: Did you run the test suite before submitting your patches?
Usually I'm more diligent about running tests but I wrote this patchset in the back of a car when I was running low on batteries. I only ran the rebase-related tests but I guess that wasn't enough.
My mistake, though. I'll be sure to _always_ run tests in the future.
Thanks,
Denton
Re: [PATCH 1/3] rebase: teach rebase --keep-base
Hi Denton,
Quoted 7 lines · Denton Liu
Quoted 1 line · Johannes Schindelin
P.S.: Did you run the test suite before submitting your patches?Usually I'm more diligent about running tests but I wrote this patchset in the back of a car when I was running low on batteries. I only ran the rebase-related tests but I guess that wasn't enough.
I totally understand that kind of scenario.
Quoted 1 line
My mistake, though. I'll be sure to _always_ run tests in the future.
Please do not feel bad. I did work pretty hard on making the Azure Pipelines support as fast as it is for the very purpose of helping exactly situations like yours: to hand off the testing to the cloud when batteries run low (or when you want to use your laptop for something else than running Git's test suite).
And of course to verify that you don't break things on platforms other than the one you happen to develop on. And to check the documentation. And to run some static analyses.
:-)
Maybe you want to open PRs at https://github.com/git/git or at
https://github.com/gitgitgadget/git to benefit from that kind of automatic testing, benefitting from my work (which would make me feel real good about spending that amount of time on it).
Ciao,
Johannes
Quoted 6 lines · Denton Liu
This series teaches rebase the --keep-base option.
'git rebase --keep-base <upstream>' is equivalent to 'git rebase --onto <upstream>... <upstream>' or
'git rebase --onto $(git merge-base <upstream> HEAD) <upstream>' .
Quoted 6 lines
This seems to be a common case that people (including myself!) run into; I was able to find these StackOverflow posts about this use case:
* https://stackoverflow.com/questions/53234798/can-i-rebase-on-a-branchs-fork-point-without-explicitly-specifying-the-parent
* https://stackoverflow.com/questions/41529128/how-do-you-rebase-only-changes-between-two-branches-into-another-branch
* https://stackoverflow.com/a/4207357
Like with another series of yours I think this would be best squashed into one patch.
Maybe I've misunderstood this but isn't this like --fork-point except with just plain "git merge-base" instead of "git merge-base --fork-point", but then again 2/3 shows multiple base aren't supported, but merge-base supports that.
I'd find something like the "DISCUSSION ON FORK-POINT MODE" in git-merge-base helpful with examples of what we'd pick in the various scenarios, and also if whatever commit this picks was something you could have "git merge-base" spew out, so you could get what rebase would do here from other tooling (which maybe is possible, but I'm confused by the "no multiple bases"...).
Hi Ævar,
Quoted 12 lines · Ævar Arnfjörð Bjarmason
Quoted 9 lines · Denton Liu
This series teaches rebase the --keep-base option.
'git rebase --keep-base <upstream>' is equivalent to 'git rebase --onto <upstream>... <upstream>' or
'git rebase --onto $(git merge-base <upstream> HEAD) <upstream>' .This seems to be a common case that people (including myself!) run into; I was able to find these StackOverflow posts about this use case:
Quoted 6 lines
Quoted 3 lines
* https://stackoverflow.com/questions/53234798/can-i-rebase-on-a-branchs-fork-point-without-explicitly-specifying-the-parent
* https://stackoverflow.com/questions/41529128/how-do-you-rebase-only-changes-between-two-branches-into-another-branch
* https://stackoverflow.com/a/4207357Like with another series of yours I think this would be best squashed into one patch.
Will do.
Quoted 6 lines
Maybe I've misunderstood this but isn't this like --fork-point except with just plain "git merge-base" instead of "git merge-base --fork-point", but then again 2/3 shows multiple base aren't supported, but merge-base supports that.
--fork-point gets used to determine the _set of_ commits which are to be rebased, whereas --keep-base (and --onto) determine the base where that set of commits will be spliced. As a result, these two options cover orthogonal use-cases.
The reason why --keep-base doesn't support multiple bases for the same reason that --onto already disallows multiple bases. If we have multiple bases, how do we determine which one is the "true base" to use? It makes more sense to error out and let the user manually specify it.
Quoted 6 lines
I'd find something like the "DISCUSSION ON FORK-POINT MODE" in git-merge-base helpful with examples of what we'd pick in the various scenarios, and also if whatever commit this picks was something you could have "git merge-base" spew out, so you could get what rebase would do here from other tooling (which maybe is possible, but I'm confused by the "no multiple bases"...).
If I'm understanding you correctly then yes, this could be done with other tooling. See the 0/3 for equivalent commands.
Perhaps I should update the rebase documentation to mention that --fork-point and --keep-base are orthogonal because it's unclear for you, it's probably unclear for other users as well.
Thanks,
Denton
Quoted 31 lines · Denton Liu
Hi Ævar,
Quoted 12 lines · Ævar Arnfjörð Bjarmason
Quoted 9 lines · Denton Liu
This series teaches rebase the --keep-base option.
'git rebase --keep-base <upstream>' is equivalent to 'git rebase --onto <upstream>... <upstream>' or
'git rebase --onto $(git merge-base <upstream> HEAD) <upstream>' .This seems to be a common case that people (including myself!) run into; I was able to find these StackOverflow posts about this use case:
Quoted 6 lines
Quoted 3 lines
* https://stackoverflow.com/questions/53234798/can-i-rebase-on-a-branchs-fork-point-without-explicitly-specifying-the-parent
* https://stackoverflow.com/questions/41529128/how-do-you-rebase-only-changes-between-two-branches-into-another-branch
* https://stackoverflow.com/a/4207357Like with another series of yours I think this would be best squashed into one patch.
Will do.
Quoted 6 lines
Maybe I've misunderstood this but isn't this like --fork-point except with just plain "git merge-base" instead of "git merge-base --fork-point", but then again 2/3 shows multiple base aren't supported, but merge-base supports that.
Quoted 4 lines
--fork-point gets used to determine the
_setof_commits which are to be rebased, whereas --keep-base (and --onto) determine the base where that set of commits will be spliced. As a result, these two options cover orthogonal use-cases.
Right. After playing with this a bit more though --fork-point is mostly there, it it does find the same fork point, as evidenced all your tests (that aren't asserting incompatibility with other options) passing with this:
diff --git a/t/t3416-rebase-onto-threedots.sh b/t/t3416-rebase-onto-threedots.sh
index 9c2548423b..ab2d50e69a 100755
--- a/t/t3416-rebase-onto-threedots.sh
+++ b/t/t3416-rebase-onto-threedots.sh
@@ -116,7 +116,7 @@ test_expect_success 'rebase --keep-base master from topic' '
git checkout topic &&
git reset --hard G &&
- git rebase --keep-base master &&
+ git rebase $(git merge-base --fork-point master HEAD) &&
git rev-parse C >base.expect &&
git merge-base master HEAD >base.actual &&
test_cmp base.expect base.actual &&
@@ -140,7 +140,7 @@ test_expect_success 'rebase -i --keep-base master from topic' '
git reset --hard G &&
set_fake_editor &&
- EXPECT_COUNT=2 git rebase -i --keep-base master &&
+ EXPECT_COUNT=2 git rebase -i $(git merge-base --fork-point master HEAD) &&
git rev-parse C >base.expect &&
git merge-base master HEAD >base.actual &&
test_cmp base.expect base.actual &&I've poked at some of this recently in
https://public-inbox.org/git/[email protected]/ as noted in the feedback there (I haven't gotten around to v2 yet) it's entirely possible that I haven't understood this at all :)
But it seems to me that this patch/implementation conflates two unrelated things.
Once is that we use --fork-point to mean that we're going to find the divergence point with "merge-base --fork-point". This gets you halfway to where you want to be, i.e. AFAICT the --keep-base and --fork-point will always find the same commit for "git rebase" and "git rebase --keep-base". See the "options.restrict_revision = get_fork_point(...)" part of the code.
The other, which you want to disable, is that --fork-point *also* says "OK, once we've found the divergence point, let's then rebase it on the latest upstream. Or in the example above the "master" part of "git merge-base --fork-point master HEAD".
Shouldn't --keep-base just be implemented in terms of skipping *that* part, i.e. we find the fork point using the upstream info, but then don't rebase *on* upstream.
The reason the distinction matters is because with your patch these two act differently:
git rebase --keep-base
git rebase $(git merge-base @{u} HEAD)The latter will skip work ("Current branch master is up to date"), but --keep-base will always re-rebase things. There's some cases where --fork-point does that, which I was trying to address with my linked WIP patch above.
Whereas the thing you actually want to work is:
git rebase -i --keep-base
git rebase -i $(git merge-base @{u} HEAD)I.e. to have both of those allow you to re-arrange/fixup whatever and still rebase on the same divergence point with @{u}, and won't run rebase when there's no work to do unless you give it --force-rebase.
Quoted 3 lines
reason that --onto already disallows multiple bases. If we have multiple bases, how do we determine which one is the "true base" to use? It makes more sense to error out and let the user manually specify it.
Ah, makes sense.
Quoted 17 lines
Quoted 6 lines
I'd find something like the "DISCUSSION ON FORK-POINT MODE" in git-merge-base helpful with examples of what we'd pick in the various scenarios, and also if whatever commit this picks was something you could have "git merge-base" spew out, so you could get what rebase would do here from other tooling (which maybe is possible, but I'm confused by the "no multiple bases"...).
If I'm understanding you correctly then yes, this could be done with other tooling. See the 0/3 for equivalent commands.
Perhaps I should update the rebase documentation to mention that --fork-point and --keep-base are orthogonal because it's unclear for you, it's probably unclear for other users as well.
Thanks,
Denton
Hi Ævar,
Quoted 75 lines · Ævar Arnfjörð Bjarmason
Quoted 31 lines · Denton Liu
Hi Ævar,
Quoted 12 lines · Ævar Arnfjörð Bjarmason
Quoted 9 lines · Denton Liu
This series teaches rebase the --keep-base option.
'git rebase --keep-base <upstream>' is equivalent to 'git rebase --onto <upstream>... <upstream>' or
'git rebase --onto $(git merge-base <upstream> HEAD) <upstream>' .This seems to be a common case that people (including myself!) run into; I was able to find these StackOverflow posts about this use case:
Quoted 6 lines
Quoted 3 lines
* https://stackoverflow.com/questions/53234798/can-i-rebase-on-a-branchs-fork-point-without-explicitly-specifying-the-parent
* https://stackoverflow.com/questions/41529128/how-do-you-rebase-only-changes-between-two-branches-into-another-branch
* https://stackoverflow.com/a/4207357Like with another series of yours I think this would be best squashed into one patch.
Will do.
Quoted 6 lines
Maybe I've misunderstood this but isn't this like --fork-point except with just plain "git merge-base" instead of "git merge-base --fork-point", but then again 2/3 shows multiple base aren't supported, but merge-base supports that.
Quoted 4 lines
--fork-point gets used to determine the
_setof_commits which are to be rebased, whereas --keep-base (and --onto) determine the base where that set of commits will be spliced. As a result, these two options cover orthogonal use-cases.Right. After playing with this a bit more though --fork-point is mostly there, it it does find the same fork point, as evidenced all your tests (that aren't asserting incompatibility with other options) passing with this:
diff --git a/t/t3416-rebase-onto-threedots.sh b/t/t3416-rebase-onto-threedots.sh index 9c2548423b..ab2d50e69a 100755 --- a/t/t3416-rebase-onto-threedots.sh +++ b/t/t3416-rebase-onto-threedots.sh @@ -116,7 +116,7 @@ test_expect_success 'rebase --keep-base master from topic' ' git checkout topic && git reset --hard G && - git rebase --keep-base master && + git rebase $(git merge-base --fork-point master HEAD) && git rev-parse C >base.expect && git merge-base master HEAD >base.actual && test_cmp base.expect base.actual && @@ -140,7 +140,7 @@ test_expect_success 'rebase -i --keep-base master from topic' ' git reset --hard G && set_fake_editor && - EXPECT_COUNT=2 git rebase -i --keep-base master && + EXPECT_COUNT=2 git rebase -i $(git merge-base --fork-point master HEAD) && git rev-parse C >base.expect && git merge-base master HEAD >base.actual && test_cmp base.expect base.actual &&I've poked at some of this recently in
https://public-inbox.org/git/[email protected]/ as noted in the feedback there (I haven't gotten around to v2 yet) it's entirely possible that I haven't understood this at all :)But it seems to me that this patch/implementation conflates two unrelated things.
Quoted 6 lines
Once is that we use --fork-point to mean that we're going to find the divergence point with "merge-base --fork-point". This gets you halfway to where you want to be, i.e. AFAICT the --keep-base and --fork-point will always find the same commit for "git rebase" and "git rebase --keep-base". See the "options.restrict_revision =
get_fork_point(...)" part of the code.
I don't think this is true. The code that --keep-base uses to find the merge base is get_oid_mb, see the relevant snippet
if (strstr(options.onto_name, "...")) {
if (get_oid_mb(options.onto_name, &merge_base) < 0)whereas the --fork-point code uses get_fork_point, as you mentioned above. As a result, they don't necessarily refer to the same commit in the case where upstream is rewound.
Quoted 5 lines
The other, which you want to disable, is that --fork-point *also* says "OK, once we've found the divergence point, let's then rebase it on the latest upstream. Or in the example above the "master" part of "git merge-base --fork-point master HEAD".
Correct, I guess in essence this is what I'm doing.
Quoted 8 lines
Shouldn't --keep-base just be implemented in terms of skipping *that* part, i.e. we find the fork point using the upstream info, but then don't rebase *on* upstream.
The reason the distinction matters is because with your patch these two act differently:
Quoted 7 lines
git rebase --keep-base git rebase $(git merge-base @{u} HEAD)The latter will skip work ("Current branch master is up to date"), but --keep-base will always re-rebase things. There's some cases where --fork-point does that, which I was trying to address with my linked WIP patch above.
I believe this is desired behaviour. Suppose we have this (modified) graph from the git-merge-base docs, where B3 was formerly part of origin/master but it was then rewound:
---o---o---B2--o---o---o---B (origin/master)
\
B3
\
Derived (local master)If we run "git rebase --keep-base", we'll get the following graph:
---o---o---B2--o---o---o---B (origin/master)
\
Derived (local master)which I believe is the desired behaviour (we're abandoning B3 since upstream abandoned it).
I hope I'm understanding you correctly. Please let me know if I've misinterpreted anything you've said or if anything I've said is unclear.
Thanks,
Denton
Quoted 33 lines
Whereas the thing you actually want to work is:
git rebase -i --keep-base git rebase -i $(git merge-base @{u} HEAD)I.e. to have both of those allow you to re-arrange/fixup whatever and still rebase on the same divergence point with @{u}, and won't run rebase when there's no work to do unless you give it --force-rebase.
Quoted 3 lines
reason that --onto already disallows multiple bases. If we have multiple bases, how do we determine which one is the "true base" to use? It makes more sense to error out and let the user manually specify it.
Ah, makes sense.
Quoted 17 lines
Quoted 6 lines
I'd find something like the "DISCUSSION ON FORK-POINT MODE" in git-merge-base helpful with examples of what we'd pick in the various scenarios, and also if whatever commit this picks was something you could have "git merge-base" spew out, so you could get what rebase would do here from other tooling (which maybe is possible, but I'm confused by the "no multiple bases"...).
If I'm understanding you correctly then yes, this could be done with other tooling. See the 0/3 for equivalent commands.
Perhaps I should update the rebase documentation to mention that --fork-point and --keep-base are orthogonal because it's unclear for you, it's probably unclear for other users as well.
Thanks,
Denton
Quoted 135 lines · Denton Liu
Hi Ævar,
Quoted 75 lines · Ævar Arnfjörð Bjarmason
Quoted 31 lines · Denton Liu
Hi Ævar,
Quoted 12 lines · Ævar Arnfjörð Bjarmason
Quoted 9 lines · Denton Liu
This series teaches rebase the --keep-base option.
'git rebase --keep-base <upstream>' is equivalent to 'git rebase --onto <upstream>... <upstream>' or
'git rebase --onto $(git merge-base <upstream> HEAD) <upstream>' .This seems to be a common case that people (including myself!) run into; I was able to find these StackOverflow posts about this use case:
Quoted 6 lines
Quoted 3 lines
* https://stackoverflow.com/questions/53234798/can-i-rebase-on-a-branchs-fork-point-without-explicitly-specifying-the-parent
* https://stackoverflow.com/questions/41529128/how-do-you-rebase-only-changes-between-two-branches-into-another-branch
* https://stackoverflow.com/a/4207357Like with another series of yours I think this would be best squashed into one patch.
Will do.
Quoted 6 lines
Maybe I've misunderstood this but isn't this like --fork-point except with just plain "git merge-base" instead of "git merge-base --fork-point", but then again 2/3 shows multiple base aren't supported, but merge-base supports that.
Quoted 4 lines
--fork-point gets used to determine the
_setof_commits which are to be rebased, whereas --keep-base (and --onto) determine the base where that set of commits will be spliced. As a result, these two options cover orthogonal use-cases.Right. After playing with this a bit more though --fork-point is mostly there, it it does find the same fork point, as evidenced all your tests (that aren't asserting incompatibility with other options) passing with this:
diff --git a/t/t3416-rebase-onto-threedots.sh b/t/t3416-rebase-onto-threedots.sh index 9c2548423b..ab2d50e69a 100755 --- a/t/t3416-rebase-onto-threedots.sh +++ b/t/t3416-rebase-onto-threedots.sh @@ -116,7 +116,7 @@ test_expect_success 'rebase --keep-base master from topic' ' git checkout topic && git reset --hard G && - git rebase --keep-base master && + git rebase $(git merge-base --fork-point master HEAD) && git rev-parse C >base.expect && git merge-base master HEAD >base.actual && test_cmp base.expect base.actual && @@ -140,7 +140,7 @@ test_expect_success 'rebase -i --keep-base master from topic' ' git reset --hard G && set_fake_editor && - EXPECT_COUNT=2 git rebase -i --keep-base master && + EXPECT_COUNT=2 git rebase -i $(git merge-base --fork-point master HEAD) && git rev-parse C >base.expect && git merge-base master HEAD >base.actual && test_cmp base.expect base.actual &&I've poked at some of this recently in
https://public-inbox.org/git/[email protected]/ as noted in the feedback there (I haven't gotten around to v2 yet) it's entirely possible that I haven't understood this at all :)But it seems to me that this patch/implementation conflates two unrelated things.
Quoted 6 lines
Once is that we use --fork-point to mean that we're going to find the divergence point with "merge-base --fork-point". This gets you halfway to where you want to be, i.e. AFAICT the --keep-base and --fork-point will always find the same commit for "git rebase" and "git rebase --keep-base". See the "options.restrict_revision =
get_fork_point(...)" part of the code.I don't think this is true. The code that --keep-base uses to find the merge base is
get_oid_mb, see the relevant snippetif (strstr(options.onto_name, "...")) { if (get_oid_mb(options.onto_name, &merge_base) < 0)whereas the --fork-point code uses
get_fork_point, as you mentioned above. As a result, they don't necessarily refer to the same commit in the case where upstream is rewound.Quoted 5 lines
The other, which you want to disable, is that --fork-point *also* says "OK, once we've found the divergence point, let's then rebase it on the latest upstream. Or in the example above the "master" part of "git merge-base --fork-point master HEAD".
Correct, I guess in essence this is what I'm doing.
Quoted 8 lines
Shouldn't --keep-base just be implemented in terms of skipping *that* part, i.e. we find the fork point using the upstream info, but then don't rebase *on* upstream.
The reason the distinction matters is because with your patch these two act differently:
Quoted 7 lines
git rebase --keep-base git rebase $(git merge-base @{u} HEAD)The latter will skip work ("Current branch master is up to date"), but --keep-base will always re-rebase things. There's some cases where --fork-point does that, which I was trying to address with my linked WIP patch above.
I believe this is desired behaviour. Suppose we have this (modified) graph from the git-merge-base docs, where B3 was formerly part of origin/master but it was then rewound:
---o---o---B2--o---o---o---B (origin/master) \ B3 \ Derived (local master)If we run "git rebase --keep-base", we'll get the following graph:
---o---o---B2--o---o---o---B (origin/master) \ Derived (local master)
Quoted 5 lines
which I believe is the desired behaviour (we're abandoning B3 since upstream abandoned it).
I hope I'm understanding you correctly. Please let me know if I've misinterpreted anything you've said or if anything I've said is unclear.
Yeah. I'm still confused, but mainly because I haven't allocated enough brainpower to try to understand it :)
So yeah, I can believe it's subtly different, would be great to have a v2 whose docs/tests cover those subtleties, right now (as seen in my discussion upthread) the tests that are there can identically use the fork point.
I also wonder if we can holistically think about this UI / how we can expose different things. E.g. for the times I've needed this and have manually dug up the fork point I haven't wanted to handle the case of upstream rewinding, just re-rebase-i on some old base, while still having upstream tracking info, and for rebase to exit early if there's nothing to do (similar to if I feed it the fork point as a rev).
Quoted 33 lines
Quoted 33 lines
Whereas the thing you actually want to work is:
git rebase -i --keep-base git rebase -i $(git merge-base @{u} HEAD)I.e. to have both of those allow you to re-arrange/fixup whatever and still rebase on the same divergence point with @{u}, and won't run rebase when there's no work to do unless you give it --force-rebase.
Quoted 3 lines
reason that --onto already disallows multiple bases. If we have multiple bases, how do we determine which one is the "true base" to use? It makes more sense to error out and let the user manually specify it.
Ah, makes sense.
Quoted 17 lines
Quoted 6 lines
I'd find something like the "DISCUSSION ON FORK-POINT MODE" in git-merge-base helpful with examples of what we'd pick in the various scenarios, and also if whatever commit this picks was something you could have "git merge-base" spew out, so you could get what rebase would do here from other tooling (which maybe is possible, but I'm confused by the "no multiple bases"...).
If I'm understanding you correctly then yes, this could be done with other tooling. See the 0/3 for equivalent commands.
Perhaps I should update the rebase documentation to mention that --fork-point and --keep-base are orthogonal because it's unclear for you, it's probably unclear for other users as well.
Thanks,
Denton
PATCH v2 1/1 rebase: teach rebase --keep-base
In reply to Denton LiuA 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 prevents unnecessary commit churning.
Alternatively, 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.
---
Ævar, I have a feeling that we're still miscommunicating and we don't fully understand each other. I'm putting up v2 to hopefully clear things up a little but I welcome more feedback.
This patch now depends "[PATCH 1/8] tests (rebase): spell out the --keep-empty option" which is the first patch of Johannes's "Do not use abbreviated options in tests" patchset[1]. (Thanks for catching that, Johannes!)
Changes since v1:
* Squashed old set into one patch
* Fixed indentation style and dangling else
* Added more documentation after discussion with Ævar
Documentation/git-rebase.txt | 25 ++++++++++++-- builtin/rebase.c | 32 ++++++++++++++---- t/t3416-rebase-onto-threedots.sh | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 9 deletions(-)
Documentation/git-rebase.txt +22 -3
@@ -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,19 @@ 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>'. ++ +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
@@ -364,6 +377,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>::
@@ -539,6 +556,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 -----------------------
@@ -863,7 +882,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:
builtin/rebase.c +26 -6
@@ -27,8 +27,8 @@#include "branch.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" ),
@@ -1018,6 +1018,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) };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 ;
@@ -1051,6 +1052,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,
@@ -1217,6 +1220,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)usage_with_options (builtin_rebase_usage, builtin_rebase_options); +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 !=NO_ACTION && !in_progress)die (_ ("No rebase in progress?" ));setenv (GIT_REFLOG_ACTION_ENVIRONMENT ,"rebase" ,0 );
@@ -1541,12 +1551,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 {
t/t3416-rebase-onto-threedots.sh +57 -0
64 lines not on this page · raw message
Signature · 1 line
2.21.0.695.gaf8658f249
Re: [PATCH v2] rebase: teach rebase --keep-base
Quoted 42 lines · Denton Liu
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... masterin order to preserve the merge base. This prevents unnecessary commit churning.
Alternatively, 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 masterand
git rebase -x ./test.sh --keep-base masterrespectively.
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_editorcall 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 <[email protected]>Helped-by Junio C Hamano <[email protected]>Signed-off-by Denton Liu <[email protected]>---
Quoted 8 lines
Ævar, I have a feeling that we're still miscommunicating and we don't fully understand each other. I'm putting up v2 to hopefully clear things up a little but I welcome more feedback.
This patch now depends "[PATCH 1/8] tests (rebase): spell out the
--keep-emptyoption" which is the first patch of Johannes's "Do not use abbreviated options in tests" patchset[1]. (Thanks for catching that, Johannes!)
Yeah I'm still confused. Gotta go now, but just some early poking I did.
First, there's now docs saying "starting point" v.s. "fork point", but the tests are still the same, i.e. the ones I can just replace with either of git merge-base [--fork-point] @{u} master.
It would really help if the tests actually stressed the parts where this is different, including argument-less versions. I.e. just "git rebase --keep-base".
Speaking of that, even though you say this is different than "--fork-point" you may or may not have noticed that if "argc < 1" you *still* go through the whole fork_point codepath, which will set "options.restrict_revision" for you. This is part of what I mentioned upthread. I.e. with this on top of this patch all your tests still pass:
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 3347dd8975..e38a5044eb 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -1515,7 +1515,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
NULL);
if (!options.upstream_name)
error_on_missing_default_upstream();
- if (fork_point < 0)
+ if (fork_point < 0 && !keep_base)
fork_point = 1;
} else {
options.upstream_name = argv[0];
@@ -1524,9 +1524,11 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
if (!strcmp(options.upstream_name, "-"))
options.upstream_name = "@{-1}";
}
- options.upstream = peel_committish(options.upstream_name);
- if (!options.upstream)
- die(_("invalid upstream '%s'"), options.upstream_name);
+ if (!keep_base) {
+ options.upstream = peel_committish(options.upstream_name);
+ if (!options.upstream)
+ /* I suppose we need to keep this die(...) somewhere still... */
+ die(_("invalid upstream '%s'"), options.upstream_name);
+ }
options.upstream_arg = options.upstream_name;
} else {
if (!options.onto_name) {
@@ -1564,6 +1566,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
}
options.onto = lookup_commit_or_die(&merge_base,
options.onto_name);
+ if (keep_base)
+ options.upstream = options.onto;
} else {
options.onto = peel_committish(options.onto_name);
if (!options.onto)But now because (and bear with me, I don't really get all this) we are not implicitly setting options.restrict_revision later in the "fork_point > 0" case *and* our "options.upstream" SHA-1 is the base you find with --keep-base instead of the @{u} SHA-1 the can_fast_forward(...) "if" kicks in, so now:
$ git status
On branch master
Your branch and 'origin/master' have diverged,
and have 3 and 37 different commits each, respectively.# Here your version would always re-rebase it $ ~/g/git/git --exec-path=$PWD rebase --keep-base Current branch master is up to date.
But "-i" still works as intended:
$ ~/g/git/git --exec-path=$PWD rebase --keep-base -i
hint: Waiting for your editor to close the file... Waiting for Emacs...
Successfully rebased and updated refs/heads/master.
$ git status
On branch master
Your branch and 'origin/master' have diverged,
and have 3 and 37 different commits each, respectively.I don't know about the *other* use-cases you have in mind, but I can't see a reason for why *that* simple case shouldn't work like that. Why would we redundantly rebase every time in this case, just becase some mode of --onto does it? I think if anything we should teach it the same lazyness, or maybe that breaks stuff (what stuff?).
Peff discussed some of these variables & their interaction in
https://public-inbox.org/git/[email protected]/
I just re-read that, but this whole thing has paged out of my brain in the meantime, and on my quick reading before sending this E-Mail I'm still not sure what all the subtleties involved are.
But one thing's for sure, I think exhaustive testing of all the edge cases involved will make this a lot clearer. I.e.:
* Here upstream has diverged, we rebase (and in the "noop case?)
* Here upstream has diverged, *and* rewound (and in the "noop case?)
* etc. etc.
Quoted 219 lines
Changes since v1:
* Squashed old set into one patch
* Fixed indentation style and dangling else
* Added more documentation after discussion with ÆvarDocumentation/git-rebase.txt | 25 ++++++++++++-- builtin/rebase.c | 32 ++++++++++++++---- t/t3416-rebase-onto-threedots.sh | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 9 deletions(-)Documentation/git-rebase.txt +22
-3@@ -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,19 @@ 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>'. ++ +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@@ -364,6 +377,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>::@@ -539,6 +556,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 -----------------------@@ -863,7 +882,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:builtin/rebase.c +26
-6@@ -27,8 +27,8 @@#include "branch.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" ),@@ -1018,6 +1018,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) };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 ;@@ -1051,6 +1052,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,@@ -1217,6 +1220,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)usage_with_options (builtin_rebase_usage, builtin_rebase_options); +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 !=NO_ACTION && !in_progress)die (_ ("No rebase in progress?" ));setenv (GIT_REFLOG_ACTION_ENVIRONMENT ,"rebase" ,0 );@@ -1541,12 +1551,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 {t/t3416-rebase-onto-threedots.sh +57
-0@@ -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
Re: [PATCH v2] rebase: teach rebase --keep-base
In reply to Denton LiuHi Denton,
Quoted 8 lines · Denton Liu
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... masterin order to preserve the merge base. This prevents unnecessary commit churning.
Maybe an example would clarify what you try to do here? Something like:
Example: When contributing a patch series to the Git mailing list, one often starts on top of the current master. However, while developing the patch series, 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 such a case, the user can call
git rebase -i --onto master... master
as a shortcut to using the merge base between master and the current branch as base commit.
I wonder, however, whether it makes sense to introduce a shorter, sweeter way to do this:
git rebase -i master...
I.e. if we detect that the <upstream> argument is not a valid ref, that it ends with three dots, and that stripping those dots off makes it a valid ref, then we internally convert that to the same as --onto master... master.
What do you think?
Ciao,
Dscho
Quoted 265 lines
Alternatively, 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 masterand
git rebase -x ./test.sh --keep-base masterrespectively.
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_editorcall 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 <[email protected]>Helped-by Junio C Hamano <[email protected]>Signed-off-by Denton Liu <[email protected]>---
Ævar, I have a feeling that we're still miscommunicating and we don't fully understand each other. I'm putting up v2 to hopefully clear things up a little but I welcome more feedback.
This patch now depends "[PATCH 1/8] tests (rebase): spell out the
--keep-emptyoption" which is the first patch of Johannes's "Do not use abbreviated options in tests" patchset[1]. (Thanks for catching that, Johannes!)Changes since v1:
* Squashed old set into one patch
* Fixed indentation style and dangling else
* Added more documentation after discussion with ÆvarDocumentation/git-rebase.txt | 25 ++++++++++++-- builtin/rebase.c | 32 ++++++++++++++---- t/t3416-rebase-onto-threedots.sh | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 9 deletions(-)Documentation/git-rebase.txt +22
-3@@ -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,19 @@ 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>'. ++ +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@@ -364,6 +377,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>::@@ -539,6 +556,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 -----------------------@@ -863,7 +882,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:builtin/rebase.c +26
-6@@ -27,8 +27,8 @@#include "branch.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" ),@@ -1018,6 +1018,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) };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 ;@@ -1051,6 +1052,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,@@ -1217,6 +1220,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)usage_with_options (builtin_rebase_usage, builtin_rebase_options); +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 !=NO_ACTION && !in_progress)die (_ ("No rebase in progress?" ));setenv (GIT_REFLOG_ACTION_ENVIRONMENT ,"rebase" ,0 );@@ -1541,12 +1551,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 {t/t3416-rebase-onto-threedots.sh +57
-1@@ -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 --2.21.0.695.gaf8658f249
Re: [PATCH v2] rebase: teach rebase --keep-base
Hi Johannes,
Quoted 22 lines · Johannes Schindelin
Hi Denton,
Quoted 8 lines · Denton Liu
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... masterin order to preserve the merge base. This prevents unnecessary commit churning.
Maybe an example would clarify what you try to do here? Something like:
Example: When contributing a patch series to the Git mailing list, one often starts on top of the current
master. However, while developing the patch series,masteris also developed further, and it is sometimes not the best idea to keep rebasing on top ofmaster, but to keep the base commit as-is. In such a case, the user can call
Quoted 4 lines
git rebase -i --onto master... master
as a shortcut to using the merge base between
masterand the current branch as base commit.
Will do, I'll include an example in the next reroll (probably in the docs too).
Quoted 10 lines
I wonder, however, whether it makes sense to introduce a shorter, sweeter way to do this:
git rebase -i master...
I.e. if we detect that the
<upstream>argument is not a valid ref, that it ends with three dots, and that stripping those dots off makes it a valid ref, then we internally convert that to the same as--onto master... master.
There's one use-case that this syntax wouldn't cover. Currently, if <upstream> isn't specified, rebase automatically uses 'git merge-base --fork-point @{u} HEAD' as <upstream>. This is even true for an invocation of 'git rebase --keep-base'.
As a result, suppose we have the following graph
o - f - o - U' origin/master
\
U - A master
where U used to be in origin/master (when we forked off) but upstream was rewound to drop it. If we run 'git rebase --keep-base', we'll get the following graph:
o - f - o - U' origin/master \ A master
i.e. we'll drop the commit dropped by upstream.
I'm not entirely sure how useful this is, though, so if we decide to drop support for this use-case, then your proposed syntax will be fine.
Thanks,
Denton
Quoted 271 lines
What do you think?
Ciao,
DschoQuoted 265 lines
Alternatively, 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 masterand
git rebase -x ./test.sh --keep-base masterrespectively.
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_editorcall 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 <[email protected]>Helped-by Junio C Hamano <[email protected]>Signed-off-by Denton Liu <[email protected]>---
Ævar, I have a feeling that we're still miscommunicating and we don't fully understand each other. I'm putting up v2 to hopefully clear things up a little but I welcome more feedback.
This patch now depends "[PATCH 1/8] tests (rebase): spell out the
--keep-emptyoption" which is the first patch of Johannes's "Do not use abbreviated options in tests" patchset[1]. (Thanks for catching that, Johannes!)Changes since v1:
* Squashed old set into one patch
* Fixed indentation style and dangling else
* Added more documentation after discussion with ÆvarDocumentation/git-rebase.txt | 25 ++++++++++++-- builtin/rebase.c | 32 ++++++++++++++---- t/t3416-rebase-onto-threedots.sh | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 9 deletions(-)Documentation/git-rebase.txt +22
-3@@ -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,19 @@ 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>'. ++ +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@@ -364,6 +377,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>::@@ -539,6 +556,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 -----------------------@@ -863,7 +882,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:builtin/rebase.c +26
-6@@ -27,8 +27,8 @@#include "branch.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" ),@@ -1018,6 +1018,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) };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 ;@@ -1051,6 +1052,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,@@ -1217,6 +1220,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)usage_with_options (builtin_rebase_usage, builtin_rebase_options); +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 !=NO_ACTION && !in_progress)die (_ ("No rebase in progress?" ));setenv (GIT_REFLOG_ACTION_ENVIRONMENT ,"rebase" ,0 );@@ -1541,12 +1551,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 {t/t3416-rebase-onto-threedots.sh +57
-1@@ -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 --2.21.0.695.gaf8658f249
Re: [PATCH 1/3] rebase: teach rebase --keep-base
In reply to Denton LiuQuoted 13 lines · Denton Liu
Quoted 13 lines
Quoted 13 lines
I never use the "feature" myself, but I recall that when "git rebase" is run on a branch appropriately prepared, you do not even have to say <upstream> (iow, you type "git rebase<RET>" and rebase on top of @{upstream}).
Can this new "--keep-base" feature mesh well with it? When the current branch has forked from origin/master, for example, it would be good if
$ git rebase -i --same-basebecomes a usable short-hand for
Quoted 7 lines
Quoted 4 lines
Quoted 1 line
$ git rebase -i --same-base origin/masterBy "--same-base", I am assuming you mistyped and meant to write "--keep-base"? If that's the case, I can make it a shorthand.
Sorry, I misunderstood your question. "--keep-base" already has the shorthand case handled by default.
I actually think you understood _my_ question perfectly well, but misremembered what your implementation already supported ;-)
If the new option works well with "the branch knows who its upstream is" feature already, so that the user does not have to type origin/master in the above example without doing anything special on your implementation's side, that is a great news.
Quoted 6 lines · Denton Liu
Quoted 2 lines
I suspect that such a rebase will become no-op without "-i". Am I mistaken? I am not sure if "--keep-base" is useful without "-i".
It's useful in the case of "-x", although that is a grey area since "-x" uses interactive machinery internally. Aside from "-x", I can't really think of a situation where we would use "--keep-base" without "-i".
I consider "-x" is a mere sugar-coat around "-i". What I was getting at was if we need some mention of the fact in the documentation (e.g. "the option is accepted but fairly useless unless you are doing the rebase '-i/-x'").
Re: [PATCH v2] rebase: teach rebase --keep-base
In reply to Johannes SchindelinQuoted 8 lines · Johannes Schindelin
Hi Denton,
Quoted 4 lines · Denton Liu
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
Quoted 6 lines
Quoted 1 line
git rebase -i --onto master... master...
I wonder, however, whether it makes sense to introduce a shorter, sweeter way to do this:git rebase -i master...
I agree that this is very tempting, as it mimicks "git checkout master...". But as Denton responds, it is not quite the same, so...
v3 cover rebase: teach rebase --keep-base
In reply to Denton LiuThanks again for your feedback, Ævar! I think we're both on the same page now. Hopefully I've addressed all of your high-level concerns with this patchset and we can move into a discussion on implementation detail.
This patchset now depends "[PATCH 1/8] tests (rebase): spell out the --keep-empty option" which is the first patch of Johannes's "Do not use abbreviated options in tests" patchset[1]. (Thanks for catching that, Johannes!)
Changes since v1:
* Squashed old set into one patch
* Fixed indentation style and dangling else
* Added more documentation after discussion with Ævar
Changes since v2:
* Add testing for rebase --fork-point behaviour
* Add testing for rebase fast-forward behaviour
* Make rebase --onto fast-forward in more cases
* Update documentation to include use-case
Denton Liu (4):
t3431: add rebase --fork-point tests
t3432: test rebase fast-forward behavior
rebase: fast-forward --onto in more cases
rebase: teach rebase --keep-base
Documentation/git-rebase.txt | 30 +++++++++++-- builtin/rebase.c | 72 +++++++++++++++++++++++--------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3416-rebase-onto-threedots.sh | 57 +++++++++++++++++++++++++ t/t3431-rebase-fork-point.sh | 57 +++++++++++++++++++++++++ t/t3432-rebase-fast-forward.sh | 62 +++++++++++++++++++++++++++ 7 files changed, 258 insertions(+), 24 deletions(-) create mode 100755 t/t3431-rebase-fork-point.sh create mode 100755 t/t3432-rebase-fast-forward.sh
Signature · 1 line
2.21.0.695.gaf8658f249
PATCH v3 1/4 t3431: add rebase --fork-point tests
--- t/t3431-rebase-fork-point.sh | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 t/t3431-rebase-fork-point.sh
t/t3431-rebase-fork-point.sh new file +53 -0
@@ -0,0 +1,53 @@ +#!/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 is side out +# +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 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 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 C E D B A' 'refs/heads/master' +test_rebase 'G F C D B A' '--onto D refs/heads/master' + +test_done
Signature · 1 line
2.21.0.695.gaf8658f249
PATCH v3 2/4 t3432: test rebase fast-forward behavior
In reply to Denton LiuWhen rebase is run on a branch that can be fast-forwarded, this should automatically be done. Create test to ensure this behavior happens.
There is one case that currently does not pass. In the case where a feature and master have diverged, running "git rebase master... master" causes a full rebase to happen even though a fast-forward should happen. Mark this case as failure so we can fix it later.
--- t/t3432-rebase-fast-forward.sh | 59 ++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100755 t/t3432-rebase-fast-forward.sh
t/t3432-rebase-fast-forward.sh new file +59 -0
@@ -0,0 +1,59 @@ +#!/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="$1" && + shift && + test_expect_$status "git rebase $@ with $changes is no-op" " + oldhead=\$(git rev-parse HEAD) && + test_when_finished 'git reset --hard \$oldhead' && + git rebase $@ && + newhead=\$(git rev-parse HEAD) && + test_cmp_rev \$oldhead \$newhead + " +} + +changes='no changes' +test_rebase_same_head success '' +test_rebase_same_head success 'master' +test_rebase_same_head success '--onto B B' +test_rebase_same_head success '--onto B... B' +test_rebase_same_head success '--onto master... master' + +test_expect_success 'add work to side' ' + test_commit E +' + +changes='our changes' +test_rebase_same_head success '' +test_rebase_same_head success 'master' +test_rebase_same_head success '--onto B B' +test_rebase_same_head success '--onto B... B' +test_rebase_same_head success '--onto master... master' + +test_expect_success 'add work to upstream' ' + git checkout master && + test_commit F && + git checkout side +' + +changes='our and their changes' +test_rebase_same_head success '--onto B B' +test_rebase_same_head success '--onto B... B' +test_rebase_same_head failure '--onto master... master' + +test_done
Signature · 1 line
2.21.0.695.gaf8658f249
PATCH v3 3/4 rebase: fast-forward --onto in more cases
In reply to Denton LiuBefore, when we had the following graph,
A---B---C (master) \ D (side)
running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to can_fast_forward so that this case can be detected and a fast-forward will be performed.
While we're at it, remove a trailing whitespace.
--- builtin/rebase.c | 40 +++++++++++++++++++++++----------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 2 +- 4 files changed, 30 insertions(+), 16 deletions(-)
builtin/rebase.c +27 -13
@@ -895,12 +895,12 @@ 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 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 ;
@@ -908,12 +908,29 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid, 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); +if (!oideq (merge_base, &onto->object.oid)) + goto done; }else {oidcpy (merge_base, &null_oid); - res =0 ; + goto done; } + +if (!upstream) + goto done; +free_commit_list (merge_bases); + merge_bases =get_merge_bases (upstream, head); +if (merge_bases && !merge_bases->next) { +if (!oideq (&onto->object.oid, &merge_bases->item->object.oid)) + goto done; + }else + goto done; + + res =1 ; + +done: +if (merge_bases) +free_commit_list (merge_bases);return res &&is_linear_history (onto, head); }
@@ -1682,13 +1699,10 @@ 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. +* but this should be done if this is not 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.orig_head, &merge_base) && + !is_interactive (&options) && !options.restrict_revision) {int flag;if (!(options.flags &REBASE_FORCE )) {
@@ -1782,7 +1796,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)strbuf_addf (&msg,"%s: checkout %s" ,getenv (GIT_REFLOG_ACTION_ENVIRONMENT ), options.onto_name);if (reset_head (&options.onto->object.oid,"checkout" ,NULL , -RESET_HEAD_DETACH |RESET_ORIG_HEAD | +RESET_HEAD_DETACH |RESET_ORIG_HEAD |RESET_HEAD_RUN_POST_CHECKOUT_HOOK ,NULL , msg.buf))die (_ ("Could not detach HEAD" ));
t/t3400-rebase.sh +1 -1
@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh 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 )
t/t3404-rebase-interactive.sh +1 -1
@@ -1066,7 +1066,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 '
t/t3432-rebase-fast-forward.sh +1 -1
@@ -54,6 +54,6 @@ test_expect_success 'add work to upstream' ' changes='our and their changes' test_rebase_same_head success '--onto B B' test_rebase_same_head success '--onto B... B' -test_rebase_same_head failure '--onto master... master' +test_rebase_same_head success '--onto master... master' test_done
Signature · 1 line
2.21.0.695.gaf8658f249
PATCH v3 4/4 rebase: teach rebase --keep-base
In reply to Denton LiuA 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'. However, while developing the patches, 'master' is also developed further and it is sometimes not the bst idea to keep rebasing on top of 'master', but to keep the base commit as-is.
Alternatively, 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.
--- Documentation/git-rebase.txt | 30 +++++++++++++++-- builtin/rebase.c | 32 ++++++++++++++---- t/t3416-rebase-onto-threedots.sh | 57 ++++++++++++++++++++++++++++++++ t/t3431-rebase-fork-point.sh | 4 +++ t/t3432-rebase-fast-forward.sh | 3 ++ 5 files changed, 117 insertions(+), 9 deletions(-)
Documentation/git-rebase.txt +27 -3
@@ -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
@@ -364,6 +382,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>::
@@ -539,6 +561,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 -----------------------
@@ -863,7 +887,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:
builtin/rebase.c +26 -6
@@ -27,8 +27,8 @@#include "branch.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" ),
@@ -1035,6 +1035,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) };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 ;
@@ -1068,6 +1069,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,
@@ -1234,6 +1237,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)usage_with_options (builtin_rebase_usage, builtin_rebase_options); +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 !=NO_ACTION && !in_progress)die (_ ("No rebase in progress?" ));setenv (GIT_REFLOG_ACTION_ENVIRONMENT ,"rebase" ,0 );
@@ -1558,12 +1568,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 {
t/t3416-rebase-onto-threedots.sh +57 -0
64 lines not on this page · raw message
t/t3431-rebase-fork-point.sh +4 -0
@@ -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
t/t3432-rebase-fast-forward.sh +3 -0
@@ -33,6 +33,7 @@ test_rebase_same_head success 'master' test_rebase_same_head success '--onto B B' test_rebase_same_head success '--onto B... B' test_rebase_same_head success '--onto master... master' +test_rebase_same_head success '--keep-base master' test_expect_success 'add work to side' ' test_commit E
@@ -44,6 +45,7 @@ test_rebase_same_head success 'master' test_rebase_same_head success '--onto B B' test_rebase_same_head success '--onto B... B' test_rebase_same_head success '--onto master... master' +test_rebase_same_head success '--keep-base master' test_expect_success 'add work to upstream' ' git checkout master &&
@@ -55,5 +57,6 @@ changes='our and their changes' test_rebase_same_head success '--onto B B' test_rebase_same_head success '--onto B... B' test_rebase_same_head success '--onto master... master' +test_rebase_same_head success '--keep-base master' test_done
Signature · 1 line
2.21.0.695.gaf8658f249
Re: [PATCH v3 3/4] rebase: fast-forward --onto in more cases
In reply to Denton LiuQuoted 6 lines · Denton Liu
Before, when we had the following graph,
A---B---C (master) \ D (side)
Quoted 7 lines
running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to
can_fast_forwardso that this case can be detected and a fast-forward will be performed.
OK. As long as the 'onto' commit is a strict ancestor of the side branch being rebased, the 'upstream' that is used only to determine which commits are on the side branch (essentially those that are not reachable from upstream but that are from the side branch) should not count in the equation to decide if we fast-forward or not. That makes sense.
Quoted 30 lines
--- builtin/rebase.c | 40 +++++++++++++++++++++++----------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 2 +- 4 files changed, 30 insertions(+), 16 deletions(-)builtin/rebase.c +4
-4@@ -895,12 +895,12 @@ 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 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 ;@@ -908,12 +908,29 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid, merge_bases =get_merge_bases (onto, head);
builtin/rebase.c static int can_fast_forward(struct commit *onto, struct object_id *head_oid, · line 916
Quoted 10 lines
builtin/rebase.c +3
-2if (merge_bases && !merge_bases->next) {oidcpy (merge_base, &merge_bases->item->object.oid); - res =oideq (merge_base, &onto->object.oid); +if (!oideq (merge_base, &onto->object.oid)) + goto done; }else {oidcpy (merge_base, &null_oid); - res =0 ; + goto done; }
The above does not change any existing logic, but purely simplifies the code. In your picture in the log message
A---B---C (master)
\
D (side)where "rebase --onto master... master side" is run, "onto" in this function is B, and "head" is D. There is a single merge base B (i.e. merge_bases->next == NULL), so we jump to the label "done:" with res==0.
So why does the remainder of the function need to be changed?
builtin/rebase.c static int can_fast_forward(struct commit *onto, struct object_id *head_oid, · line 927
Quoted 10 lines
builtin/rebase.c +9
-0+if (!upstream) + goto done; +free_commit_list (merge_bases); + merge_bases =get_merge_bases (upstream, head); +if (merge_bases && !merge_bases->next) { +if (!oideq (&onto->object.oid, &merge_bases->item->object.oid)) + goto done; + }else + goto done;
This computes the same between C and D (instead of B and D). Why is this needed?
Stepping back a bit, I understand that your argument we saw in the log message was that we only need to know if the commit we are transplanting the history on (i.e. "onto") already is an ancestor of the history being transplanted (i.e. "onto..head"), and it does not matter what upstream is. Am I mistaken? Why does this function now need to know what 'upstream' is?
Puzzled....
Quoted 4 lines
builtin/rebase.c +0
-0@@ -1682,13 +1699,10 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)/* * Check if we are already based on onto with linear history,
builtin/rebase.c int cmd_rebase(int argc, const char **argv, const char *prefix) · line 1705
Quoted 10 lines
builtin/rebase.c +3
-6-* but this should be done only when upstream and onto are the same -* and if this is not an interactive rebase. +* but this should be done if this is not 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.orig_head, &merge_base) && + !is_interactive (&options) && !options.restrict_revision) {
Ditto. Shouldn't it be the matter of
if (can_fast_forward(options.onto, &options.orig_head, &merge_base) &&
!is_interactive(&options) && !options.restrict_revision) {i.e. the original without paying any attention to the options.upstream?
Quoted 18 lines
t/t3400-rebase.sh +1
-1@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh 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 )t/t3404-rebase-interactive.sh +0
-0
t/t3404-rebase-interactive.sh test_expect_success C_LOCALE_OUTPUT 'rebase --edit-todo does not work on non-int · line 1072
Quoted 8 lines
t/t3404-rebase-interactive.sh +1
-1@@ -1066,7 +1066,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 '
The above two changes to 3400 and 3404 are not explained.
t/t3432-rebase-fast-forward.sh test_expect_success 'add work to upstream' ' · line 57
Quoted 10 lines
t/t3432-rebase-fast-forward.sh +1
-1@@ -54,6 +54,6 @@ test_expect_success 'add work to upstream' ' changes='our and their changes' test_rebase_same_head success '--onto B B' test_rebase_same_head success '--onto B... B' -test_rebase_same_head failure '--onto master... master' +test_rebase_same_head success '--onto master... master'
This shows what the change wanted to achieve, which makes sense to me.
Re: [PATCH v3 3/4] rebase: fast-forward --onto in more cases
Quoted 92 lines · Junio C Hamano
Quoted 6 lines · Denton Liu
Before, when we had the following graph,
A---B---C (master) \ D (side)Quoted 7 lines
running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to
can_fast_forwardso that this case can be detected and a fast-forward will be performed.OK. As long as the 'onto' commit is a strict ancestor of the side branch being rebased, the 'upstream' that is used only to determine which commits are on the side branch (essentially those that are not reachable from upstream but that are from the side branch) should not count in the equation to decide if we fast-forward or not. That makes sense.
Quoted 30 lines
--- builtin/rebase.c | 40 +++++++++++++++++++++++----------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 2 +- 4 files changed, 30 insertions(+), 16 deletions(-)builtin/rebase.c +4
-4@@ -895,12 +895,12 @@ 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 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 ;@@ -908,12 +908,29 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid, merge_bases =get_merge_bases (onto, head);builtin/rebase.c static int can_fast_forward(struct commit *onto, struct object_id *head_oid, · line 916
Quoted 10 lines
builtin/rebase.c +3
-2if (merge_bases && !merge_bases->next) {oidcpy (merge_base, &merge_bases->item->object.oid); - res =oideq (merge_base, &onto->object.oid); +if (!oideq (merge_base, &onto->object.oid)) + goto done; }else {oidcpy (merge_base, &null_oid); - res =0 ; + goto done; }The above does not change any existing logic, but purely simplifies the code. In your picture in the log message
A---B---C (master) \ D (side)where "rebase --onto master... master side" is run, "onto" in this function is B, and "head" is D. There is a single merge base B (i.e.
merge_bases->next== NULL), so we jump to the label "done:" with res==0.So why does the remainder of the function need to be changed?
builtin/rebase.c static int can_fast_forward(struct commit *onto, struct object_id *head_oid, · line 926
Quoted 10 lines
builtin/rebase.c +9
-0+if (!upstream) + goto done; +free_commit_list (merge_bases); + merge_bases =get_merge_bases (upstream, head); +if (merge_bases && !merge_bases->next) { +if (!oideq (&onto->object.oid, &merge_bases->item->object.oid)) + goto done; + }else + goto done;This computes the same between C and D (instead of B and D). Why is this needed?
Quoted 8 lines
Stepping back a bit, I understand that your argument we saw in the log message was that we only need to know if the commit we are transplanting the history on (i.e. "onto") already is an ancestor of the history being transplanted (i.e. "onto..head"), and it does not matter what upstream is. Am I mistaken? Why does this function now need to know what 'upstream' is?
Puzzled....
So I replaced the part for builtin/rebase.c from your patch with the above suggestion (attached) and the result seems to pass the three tests you touched. I am still puzzled.
Thanks.
builtin/rebase.c | 7 ++----- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-)
builtin/rebase.c +2 -5
@@ -1682,13 +1682,10 @@ 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. +* but this should be done if this is not 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)) { + !is_interactive (&options) && !options.restrict_revision) {int flag;if (!(options.flags &REBASE_FORCE )) {
t/t3400-rebase.sh +1 -1
@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh 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 )
t/t3404-rebase-interactive.sh +1 -1
@@ -1066,7 +1066,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 '
t/t3432-rebase-fast-forward.sh +1 -1
@@ -54,6 +54,6 @@ test_expect_success 'add work to upstream' ' changes='our and their changes' test_rebase_same_head success '--onto B B' test_rebase_same_head success '--onto B... B' -test_rebase_same_head failure '--onto master... master' +test_rebase_same_head success '--onto master... master' test_done
Re: [PATCH v3 3/4] rebase: fast-forward --onto in more cases
Hi Junio,
Quoted 69 lines · Junio C Hamano
Quoted 67 lines · Junio C Hamano
Quoted 6 lines · Denton Liu
Before, when we had the following graph,
A---B---C (master) \ D (side)Quoted 7 lines
running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to
can_fast_forwardso that this case can be detected and a fast-forward will be performed.OK. As long as the 'onto' commit is a strict ancestor of the side branch being rebased, the 'upstream' that is used only to determine which commits are on the side branch (essentially those that are not reachable from upstream but that are from the side branch) should not count in the equation to decide if we fast-forward or not. That makes sense.
Quoted 30 lines
--- builtin/rebase.c | 40 +++++++++++++++++++++++----------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 2 +- 4 files changed, 30 insertions(+), 16 deletions(-)builtin/rebase.c +4
-4@@ -895,12 +895,12 @@ 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 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 ;@@ -908,12 +908,29 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid, merge_bases =get_merge_bases (onto, head);builtin/rebase.c static int can_fast_forward(struct commit *onto, struct object_id *head_oid, · line 916
Quoted 10 lines
builtin/rebase.c +3
-2if (merge_bases && !merge_bases->next) {oidcpy (merge_base, &merge_bases->item->object.oid); - res =oideq (merge_base, &onto->object.oid); +if (!oideq (merge_base, &onto->object.oid)) + goto done; }else {oidcpy (merge_base, &null_oid); - res =0 ; + goto done; }The above does not change any existing logic, but purely simplifies the code. In your picture in the log message
Quoted 8 lines
Quoted 8 lines
A---B---C (master) \ D (side)where "rebase --onto master... master side" is run, "onto" in this function is B, and "head" is D. There is a single merge base B (i.e.
merge_bases->next== NULL), so we jump to the label "done:" with res==0.
I don't think this is correct. There is a single merge base so we compare 'merge_base' and 'onto'. They are the same so we don't jump to 'done', instead we keep going.
Quoted 16 lines
Quoted 16 lines
So why does the remainder of the function need to be changed?
builtin/rebase.c static int can_fast_forward(struct commit *onto, struct object_id *head_oid, · line 926
Quoted 10 lines
builtin/rebase.c +9
-0+if (!upstream) + goto done; +free_commit_list (merge_bases); + merge_bases =get_merge_bases (upstream, head); +if (merge_bases && !merge_bases->next) { +if (!oideq (&onto->object.oid, &merge_bases->item->object.oid)) + goto done; + }else + goto done;This computes the same between C and D (instead of B and D). Why is this needed?
Without this logic, t3416 would fail case 2 and 3. It would only see the 'head' and the 'onto' and therefore, it would incorrectly return 1, indicating that it is fast-forwardable.
The graph for t3416 is as follows:
F---G topic / A---B---C---D---E master \ \ / \ x \ / \ H---I---J---K side
And the command that fails is
git rebase --onto master...topic F # called from topic
Without the additional logic, merge base is C and there is only one merge base. Also, head is G. Finally, onto is also C. We enter the outer if becase there is one merge base then we see that onto == merge_base. Thus, the function would falsely conclude that this is fast-forwardable. This gives us ABCFG when we're expecting ABCG.
The additional logic detects if we're not rebasing the entire branch but only part of the branch. In this case, it detects that F is excluded and as a result, we can't fast-forward.
Quoted 7 lines
Quoted 7 lines
Stepping back a bit, I understand that your argument we saw in the log message was that we only need to know if the commit we are transplanting the history on (i.e. "onto") already is an ancestor of the history being transplanted (i.e. "onto..head"), and it does not matter what upstream is. Am I mistaken? Why does this function now need to know what 'upstream' is?
As stated above, the function needs to know what upstream is because it uses it to determine if the set of commits being rebased is equal to master..topic. If it's not that set, then it's not fast-forwardable.
Quoted 2 lines
Quoted 2 lines
Puzzled....
...I guess this is a sign that I need to clarify my commit message. I'll put what I said above into the commit message in my next reroll.
Thanks,
Denton
Quoted 71 lines
So I replaced the part for
builtin/rebase.cfrom your patch with the above suggestion (attached) and the result seems to pass the three tests you touched. I am still puzzled.Thanks.
builtin/rebase.c | 7 ++----- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-)builtin/rebase.c +2
-5@@ -1682,13 +1682,10 @@ 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. +* but this should be done if this is not 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)) { + !is_interactive (&options) && !options.restrict_revision) {int flag;if (!(options.flags &REBASE_FORCE )) {t/t3400-rebase.sh +1
-1@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh 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 )t/t3404-rebase-interactive.sh +1
-1@@ -1066,7 +1066,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 't/t3432-rebase-fast-forward.sh +1
-1@@ -54,6 +54,6 @@ test_expect_success 'add work to upstream' ' changes='our and their changes' test_rebase_same_head success '--onto B B' test_rebase_same_head success '--onto B... B' -test_rebase_same_head failure '--onto master... master' +test_rebase_same_head success '--onto master... master' test_done
Re: [PATCH v3 1/4] t3431: add rebase --fork-point tests
In reply to Denton LiuQuoted 16 lines · Denton Liu
Signed-off-by Denton Liu <[email protected]>--- t/t3431-rebase-fork-point.sh | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 t/t3431-rebase-fork-point.sht/t3431-rebase-fork-point.sh new file +4
-0@@ -0,0 +1,53 @@ +#!/bin/sh +# +# Copyright (c) 2019 Denton Liu +#
t/t3431-rebase-fork-point.sh line 14
Quoted 10 lines
t/t3431-rebase-fork-point.sh new file +10
-0+ +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 is side out
Sorry, small typo. We should probably fix this before it gets merged into next.
This should read "C was formerly part of master but master was rewound to remove C"
Thanks,
Denton
Quoted 42 lines
t/t3431-rebase-fork-point.sh new file +39
-0+# +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 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 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 C E D B A' 'refs/heads/master' +test_rebase 'G F C D B A' '--onto D refs/heads/master' + +test_doneSignature · 1 line
2.21.0.695.gaf8658f249
Re: [PATCH v3 1/4] t3431: add rebase --fork-point tests
In reply to Denton LiuQuoted 46 lines · Denton Liu
Signed-off-by Denton Liu <[email protected]>--- t/t3431-rebase-fork-point.sh | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 t/t3431-rebase-fork-point.sht/t3431-rebase-fork-point.sh new file +34
-0@@ -0,0 +1,53 @@ +#!/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 is side out +# +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 &&
t/t3431-rebase-fork-point.sh line 44
Quoted 10 lines
t/t3431-rebase-fork-point.sh new file +10
-0+ 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' ''
It appears that this last empty argument triggers some bug in Bash v4.2 and older (and on macOS such an old Bash is the default /bin/sh), as it turns that empty argument into something else, which in turn fails the test with:
<...>
++ git rebase $'\177'
fatal: invalid upstream '?'
error: last command exited with $?=128
not ok 2 - git rebase
https://travis-ci.org/git/git/jobs/516070862#L2276
Omitting that empty argument avoids this issue, and the test still checks what it was supposed to.
Quoted 12 lines
t/t3431-rebase-fork-point.sh new file +9
-0+test_rebase 'G F D B A' '--onto D' +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 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 C E D B A' 'refs/heads/master' +test_rebase 'G F C D B A' '--onto D refs/heads/master' + +test_doneSignature · 1 line
2.21.0.695.gaf8658f249
Re: [PATCH v3 1/4] t3431: add rebase --fork-point tests
In reply to Denton LiuHi Denton,
Quoted 33 lines · Denton Liu
t/t3431-rebase-fork-point.sh new file +27
-0@@ -0,0 +1,53 @@ +#!/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 is side out +# +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 +'
t/t3431-rebase-fork-point.sh line 37
Quoted 10 lines
t/t3431-rebase-fork-point.sh new file +10
-0+ +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 $@ &&
I think we need this patch, to make the macOS build happy:
-- snip --
Subject: fixup??? t3431: add rebase --fork-point tests
Try to fix the Mac build, which currently fails thusly:
++ git reset --hard G
HEAD is now at d8775ba G
++ git rebase $'\177'
fatal: invalid upstream '?'
error: last command exited with $?=128
t/t3431-rebase-fork-point.sh +1 -1
@@ -34,7 +34,7 @@ test_rebase() { git reset --hard E && git checkout side && git reset --hard G && - git rebase $@ && + eval git rebase \"$@\" && test_write_lines $expected >expect && git log --pretty=%s >actual && test_cmp expect actual
-- snap --
Ciao,
Dscho
Quoted 20 lines
t/t3431-rebase-fork-point.sh new file +16
-1+ 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 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 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 C E D B A' 'refs/heads/master' +test_rebase 'G F C D B A' '--onto D refs/heads/master' + +test_done --2.21.0.695.gaf8658f249
Re: [PATCH v3 1/4] t3431: add rebase --fork-point tests
Quoted 41 lines · Johannes Schindelin
Hi Denton,
Quoted 37 lines · Denton Liu
t/t3431-rebase-fork-point.sh new file +31
-0@@ -0,0 +1,53 @@ +#!/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 is side out +# +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 &&
Quoted 8 lines
t/t3431-rebase-fork-point.sh line 37
Quoted 6 lines
t/t3431-rebase-fork-point.sh new file +6
-0+ test_expect_success "git rebase $@" " + git checkout master && + git reset --hard E && + git checkout side && + git reset --hard G && + git rebase $@ &&I think we need this patch, to make the macOS build happy:
Thanks for digging into this, both.
Out of curiosity, t3432 is written similarly:
test_rebase_same_head() {
status="$1" &&
shift &&
test_expect_$status "git rebase $@ with $changes is no-op" "
oldhead=\$(git rev-parse HEAD) &&
test_when_finished 'git reset --hard \$oldhead' &&
git rebase $@ &&
newhead=\$(git rev-parse HEAD) &&
test_cmp_rev \$oldhead \$newhead
"
}and is also invoked similarly
test_rebase_same_head success ''
but it doesn't seem to fail. Any ideas on why?
Thanks,
Denton
Quoted 52 lines
-- snip --
Subject: fixup??? t3431: add rebase --fork-point testsTry to fix the Mac build, which currently fails thusly:
++ git reset --hard G
HEAD is now at d8775ba G
++ git rebase $'\177'
fatal: invalid upstream '?'
error: last command exited with $?=128Signed-off-by Johannes Schindelin <[email protected]>t/t3431-rebase-fork-point.sh +1
-2@@ -34,7 +34,7 @@ test_rebase() { git reset --hard E && git checkout side && git reset --hard G && - git rebase $@ && + eval git rebase \"$@\" && test_write_lines $expected >expect && git log --pretty=%s >actual && test_cmp expect actual -- snap --Ciao,
DschoQuoted 20 lines
t/t3431-rebase-fork-point.sh new file +16
-1+ 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 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 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 C E D B A' 'refs/heads/master' +test_rebase 'G F C D B A' '--onto D refs/heads/master' + +test_done --2.21.0.695.gaf8658f249
Re: [PATCH v3 1/4] t3431: add rebase --fork-point tests
Am 05.04.19 um 19:25 schrieb Denton Liu:
Quoted 18 lines · Johannes Schindelin
Quoted 12 lines · Denton Liu
Review
Quoted 9 lines
(quoted diff) +9
-0+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 $@ &&I think we need this patch, to make the macOS build happy:
Thanks for digging into this, both.
Out of curiosity, t3432 is written similarly:
Quoted 11 lines
test_rebase_same_head() {
status="$1" &&
shift &&
test_expect_$status "git rebase $@ with $changes is no-op" "
oldhead=\$(git rev-parse HEAD) &&
test_when_finished 'git reset --hard \$oldhead' &&
git rebase $@ &&
newhead=\$(git rev-parse HEAD) &&
test_cmp_rev \$oldhead \$newhead
"
}Using $@ in these expansions is wrong. You do not want to forward an argument list, but you want to construct a command line. $* is correct here. Then you can remove the single-quotes at the invocation, like so:
test_rebase_same_head success test_rebase_same_head success --onto B B
Function test_rebase() could be done in the same way, but the first argument, expected, still needs quotes at the call site, of course.
-- Hannes
Re: [PATCH v3 1/4] t3431: add rebase --fork-point tests
Hi Hannes & Denton,
Quoted 14 lines · Johannes Sixt
Am 05.04.19 um 19:25 schrieb Denton Liu:
Quoted 13 lines · Johannes Schindelin
Quoted 12 lines · Denton Liu
Review
Quoted 9 lines
(quoted diff) +9
-0+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 $@ &&I think we need this patch, to make the macOS build happy:
Actually, my patch did not even fix the build, I looked at the wrong (succeeding) build, sorry for the noise.
Quoted 4 lines
Quoted 4 lines
Thanks for digging into this, both.
Out of curiosity, t3432 is written similarly:
Quoted 11 lines
Quoted 11 lines
test_rebase_same_head() { status="$1" && shift && test_expect_$status "git rebase $@ with $changes is no-op" " oldhead=\$(git rev-parse HEAD) && test_when_finished 'git reset --hard \$oldhead' && git rebase $@ && newhead=\$(git rev-parse HEAD) && test_cmp_rev \$oldhead \$newhead " }
That is curious, indeed!
Quoted 9 lines
Using $@ in these expansions is wrong. You do not want to forward an argument list, but you want to construct a command line. $* is correct here. Then you can remove the single-quotes at the invocation, like so:
test_rebase_same_head success test_rebase_same_head success --onto B BFunction
test_rebase()could be done in the same way, but the first argument, expected, still needs quotes at the call site, of course.
That's a good idea, let me run with it.
Ciao,
Dscho
Re: [PATCH v3 1/4] t3431: add rebase --fork-point tests
Hi,
Quoted 19 lines · Johannes Schindelin
Quoted 17 lines · Johannes Sixt
Am 05.04.19 um 19:25 schrieb Denton Liu:
Quoted 11 lines · Johannes Schindelin
Quoted 10 lines · Denton Liu
Review
Quoted 9 lines
(quoted diff) +9
-0+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 $@ &&Using $@ in these expansions is wrong. You do not want to forward an argument list, but you want to construct a command line. $* is correct here. Then you can remove the single-quotes at the invocation, like so:
Quoted 7 lines
Quoted 5 lines
test_rebase_same_head success test_rebase_same_head success --onto B BFunction
test_rebase()could be done in the same way, but the first argument, expected, still needs quotes at the call site, of course.That's a good idea, let me run with it.
Indeed, this patch fixes it (see e.g.
https://dev.azure.com/Git-for-Windows/git/_build/results?buildId=34370):
-- snipsnap --
Subject: [PATCH] fixup??? t3431: add rebase --fork-point tests
Try to fix the Mac build, which currently fails thusly:
++ git reset --hard G
HEAD is now at d8775ba G
++ git rebase $'\177'
fatal: invalid upstream '?'
error: last command exited with $?=128
--- t/t3431-rebase-fork-point.sh | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-)
t/t3431-rebase-fork-point.sh +14 -14
@@ -29,29 +29,29 @@ test_expect_success setup ' test_rebase() { expected="$1" && shift && - test_expect_success "git rebase $@" " + test_expect_success "git rebase $*" " git checkout master && git reset --hard E && git checkout side && git reset --hard G && - git rebase $@ && + eval 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_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
--
2.21.0.windows.1.152.g5895f170b6
Re: [PATCH v3 1/4] t3431: add rebase --fork-point tests
Quoted 56 lines · Johannes Schindelin
Hi,
Quoted 19 lines · Johannes Schindelin
Quoted 17 lines · Johannes Sixt
Am 05.04.19 um 19:25 schrieb Denton Liu:
Quoted 11 lines · Johannes Schindelin
Quoted 10 lines · Denton Liu
Review
Quoted 9 lines
(quoted diff) +9
-0+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 $@ &&Using $@ in these expansions is wrong. You do not want to forward an argument list, but you want to construct a command line. $* is correct here. Then you can remove the single-quotes at the invocation, like so:
Quoted 7 lines
Quoted 5 lines
test_rebase_same_head success test_rebase_same_head success --onto B BFunction
test_rebase()could be done in the same way, but the first argument, expected, still needs quotes at the call site, of course.That's a good idea, let me run with it.
Indeed, this patch fixes it (see e.g.
https://dev.azure.com/Git-for-Windows/git/_build/results?buildId=34370):-- snipsnap --
Subject: [PATCH] fixup??? t3431: add rebase --fork-point testsTry to fix the Mac build, which currently fails thusly:
++ git reset --hard G
HEAD is now at d8775ba G
++ git rebase $'\177'
fatal: invalid upstream '?'
error: last command exited with $?=128Signed-off-by Johannes Schindelin <[email protected]>--- t/t3431-rebase-fork-point.sh | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-)t/t3431-rebase-fork-point.sh +0
-0@@ -29,29 +29,29 @@ test_expect_success setup ' test_rebase() {
t/t3431-rebase-fork-point.sh test_expect_success setup ' · line 37
Quoted 10 lines
t/t3431-rebase-fork-point.sh +2
-2expected="$1" && shift && - test_expect_success "git rebase $@" " + test_expect_success "git rebase $*" " git checkout master && git reset --hard E && git checkout side && git reset --hard G && - git rebase $@ && + eval git rebase $* &&
The 'eval' is not necessary, all Bash versions down to v3.0 work without it.
Quoted 35 lines
t/t3431-rebase-fork-point.sh +12
-13test_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_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 --2.21.0.windows.1.152.g5895f170b6
v4 cover rebase: teach rebase --keep-base
In reply to Denton LiuHi all,
I dug into it a little more and according to the bash 4.2->4.3 changelog,
bbb. Fixed a bug that caused spurious DEL characters (\177) to appear in double-quoted expansion where the RHS is evaluated to the empty string.
so, in particular, I've removed all of the '' from the tests.
I also downloaded bash 4.2 and tested everything with that. Seems to be working well!
Thanks for catching this!
---
This patchset now depends "[PATCH 1/8] tests (rebase): spell out the --keep-empty option" which is the first patch of Johannes's "Do not use abbreviated options in tests" patchset[1]. (Thanks for catching that, Johannes!)
Changes since v1:
* Squashed old set into one patch
* Fixed indentation style and dangling else
* Added more documentation after discussion with Ævar
Changes since v2:
* Add testing for rebase --fork-point behaviour
* Add testing for rebase fast-forward behaviour
* Make rebase --onto fast-forward in more cases
* Update documentation to include use-case
Changes since v3:
* Fix tests failing on bash 4.2
* Fix typo in t3431 comment
Denton Liu (4):
t3431: add rebase --fork-point tests
t3432: test rebase fast-forward behavior
rebase: fast-forward --onto in more cases
rebase: teach rebase --keep-base
Documentation/git-rebase.txt | 30 +++++++++++-- builtin/rebase.c | 72 +++++++++++++++++++++++--------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3416-rebase-onto-threedots.sh | 57 +++++++++++++++++++++++++ t/t3431-rebase-fork-point.sh | 57 +++++++++++++++++++++++++ t/t3432-rebase-fast-forward.sh | 62 +++++++++++++++++++++++++++ 7 files changed, 258 insertions(+), 24 deletions(-) create mode 100755 t/t3431-rebase-fork-point.sh create mode 100755 t/t3432-rebase-fast-forward.sh
Interdiff against v3:
t/t3431-rebase-fork-point.sh +15 -15
@@ -11,7 +11,7 @@ test_description='git rebase --fork-point test' # \ # C*---F---G (side) # -# C was formerly part of master but is side out +# C was formerly part of master but master was rewound to remove C # test_expect_success setup ' test_commit A &&
@@ -29,29 +29,29 @@ test_expect_success setup ' test_rebase() { expected="$1" && shift && - test_expect_success "git rebase $@" " + test_expect_success "git rebase $*" " git checkout master && git reset --hard E && git checkout side && git reset --hard G && - git rebase $@ && + 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_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
t/t3432-rebase-fast-forward.sh +18 -18
@@ -18,34 +18,34 @@ test_expect_success setup ' test_rebase_same_head() { status="$1" && shift && - test_expect_$status "git rebase $@ with $changes is no-op" " + test_expect_$status "git rebase $* with $changes is no-op" " oldhead=\$(git rev-parse HEAD) && test_when_finished 'git reset --hard \$oldhead' && - git rebase $@ && + git rebase $* && newhead=\$(git rev-parse HEAD) && test_cmp_rev \$oldhead \$newhead " } changes='no changes' -test_rebase_same_head success '' -test_rebase_same_head success 'master' -test_rebase_same_head success '--onto B B' -test_rebase_same_head success '--onto B... B' -test_rebase_same_head success '--onto master... master' -test_rebase_same_head success '--keep-base master' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master test_expect_success 'add work to side' ' test_commit E ' changes='our changes' -test_rebase_same_head success '' -test_rebase_same_head success 'master' -test_rebase_same_head success '--onto B B' -test_rebase_same_head success '--onto B... B' -test_rebase_same_head success '--onto master... master' -test_rebase_same_head success '--keep-base master' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master test_expect_success 'add work to upstream' ' git checkout master &&
@@ -54,9 +54,9 @@ test_expect_success 'add work to upstream' ' ' changes='our and their changes' -test_rebase_same_head success '--onto B B' -test_rebase_same_head success '--onto B... B' -test_rebase_same_head success '--onto master... master' -test_rebase_same_head success '--keep-base master' +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master test_done
Signature · 1 line
2.21.0.843.gd0ae0373aa
PATCH v4 1/4 t3431: add rebase --fork-point tests
--- t/t3431-rebase-fork-point.sh | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 t/t3431-rebase-fork-point.sh
t/t3431-rebase-fork-point.sh new file +53 -0
@@ -0,0 +1,53 @@ +#!/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 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 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 C E D B A' refs/heads/master +test_rebase 'G F C D B A' --onto D refs/heads/master + +test_done
Signature · 1 line
2.21.0.843.gd0ae0373aa
PATCH v4 2/4 t3432: test rebase fast-forward behavior
In reply to Denton LiuWhen rebase is run on a branch that can be fast-forwarded, this should automatically be done. Create test to ensure this behavior happens.
There is one case that currently does not pass. In the case where a feature and master have diverged, running "git rebase master... master" causes a full rebase to happen even though a fast-forward should happen. Mark this case as failure so we can fix it later.
--- t/t3432-rebase-fast-forward.sh | 59 ++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100755 t/t3432-rebase-fast-forward.sh
t/t3432-rebase-fast-forward.sh new file +59 -0
@@ -0,0 +1,59 @@ +#!/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="$1" && + shift && + test_expect_$status "git rebase $* with $changes is no-op" " + oldhead=\$(git rev-parse HEAD) && + test_when_finished 'git reset --hard \$oldhead' && + git rebase $* && + newhead=\$(git rev-parse HEAD) && + test_cmp_rev \$oldhead \$newhead + " +} + +changes='no changes' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master + +test_expect_success 'add work to side' ' + test_commit E +' + +changes='our changes' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master + +test_expect_success 'add work to upstream' ' + git checkout master && + test_commit F && + git checkout side +' + +changes='our and their changes' +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head failure --onto master... master + +test_done
Signature · 1 line
2.21.0.843.gd0ae0373aa
PATCH v4 4/4 rebase: teach rebase --keep-base
In reply to Denton LiuA 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'. However, while developing the patches, 'master' is also developed further and it is sometimes not the bst idea to keep rebasing on top of 'master', but to keep the base commit as-is.
Alternatively, 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.
--- Documentation/git-rebase.txt | 30 +++++++++++++++-- builtin/rebase.c | 32 ++++++++++++++---- t/t3416-rebase-onto-threedots.sh | 57 ++++++++++++++++++++++++++++++++ t/t3431-rebase-fork-point.sh | 4 +++ t/t3432-rebase-fast-forward.sh | 3 ++ 5 files changed, 117 insertions(+), 9 deletions(-)
Documentation/git-rebase.txt +27 -3
@@ -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
@@ -364,6 +382,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>::
@@ -539,6 +561,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 -----------------------
@@ -863,7 +887,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:
builtin/rebase.c +26 -6
@@ -27,8 +27,8 @@#include "branch.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" ),
@@ -1035,6 +1035,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) };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 ;
@@ -1068,6 +1069,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,
@@ -1234,6 +1237,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)usage_with_options (builtin_rebase_usage, builtin_rebase_options); +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 !=NO_ACTION && !in_progress)die (_ ("No rebase in progress?" ));setenv (GIT_REFLOG_ACTION_ENVIRONMENT ,"rebase" ,0 );
@@ -1558,12 +1568,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 {
t/t3416-rebase-onto-threedots.sh +57 -0
64 lines not on this page · raw message
t/t3431-rebase-fork-point.sh +4 -0
@@ -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
t/t3432-rebase-fast-forward.sh +3 -0
@@ -33,6 +33,7 @@ test_rebase_same_head success master test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master test_expect_success 'add work to side' ' test_commit E
@@ -44,6 +45,7 @@ test_rebase_same_head success master test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master test_expect_success 'add work to upstream' ' git checkout master &&
@@ -55,5 +57,6 @@ changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master test_done
Signature · 1 line
2.21.0.843.gd0ae0373aa
PATCH v4 3/4 rebase: fast-forward --onto in more cases
In reply to Denton LiuBefore, when we had the following graph,
A---B---C (master) \ D (side)
running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to can_fast_forward so that this case can be detected and a fast-forward will be performed. First of all, rewrite the function to use gotos which simplifies the logic. Next, since the
options.upstream && !oidcmp(&options.upstream->object.oid, &options.onto->object.oid)
conditions were removed in cmd_rebase, we reintroduce a substitute in can_fast_forward. In particular, checking the merge bases of upstream and head fixes a failing case in t3416.
The abbreviated graph for t3416 is as follows:
F---G topic / A---B---C---D---E master
and the failing command was
git rebase --onto master...topic F topic
Before, Git would see that there was one merge base (C), and the merge and onto were the same so it would incorrectly return 1, indicating that we could fast-forward. This would cause the rebased graph to be 'ABCFG' when we were expecting 'ABCG'.
With the additional logic, we detect that upstream and head's merge base is F. Since onto isn't F, it means we're not rebasing the full set of commits from master..topic. Since we're excluding some commits, a fast-forward cannot be performed and so we correctly return 0.
Add '-f' to test cases that failed as a result of this change because they were not expecting a fast-forward so that a rebase is forced.
While we're at it, remove a trailing whitespace from rebase.c.
--- builtin/rebase.c | 40 +++++++++++++++++++++++----------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 2 +- 4 files changed, 30 insertions(+), 16 deletions(-)
builtin/rebase.c +27 -13
@@ -895,12 +895,12 @@ 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 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 ;
@@ -908,12 +908,29 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid, 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); +if (!oideq (merge_base, &onto->object.oid)) + goto done; }else {oidcpy (merge_base, &null_oid); - res =0 ; + goto done; } + +if (!upstream) + goto done; +free_commit_list (merge_bases); + merge_bases =get_merge_bases (upstream, head); +if (merge_bases && !merge_bases->next) { +if (!oideq (&onto->object.oid, &merge_bases->item->object.oid)) + goto done; + }else + goto done; + + res =1 ; + +done: +if (merge_bases) +free_commit_list (merge_bases);return res &&is_linear_history (onto, head); }
@@ -1682,13 +1699,10 @@ 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. +* but this should be done if this is not 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.orig_head, &merge_base) && + !is_interactive (&options) && !options.restrict_revision) {int flag;if (!(options.flags &REBASE_FORCE )) {
@@ -1782,7 +1796,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)strbuf_addf (&msg,"%s: checkout %s" ,getenv (GIT_REFLOG_ACTION_ENVIRONMENT ), options.onto_name);if (reset_head (&options.onto->object.oid,"checkout" ,NULL , -RESET_HEAD_DETACH |RESET_ORIG_HEAD | +RESET_HEAD_DETACH |RESET_ORIG_HEAD |RESET_HEAD_RUN_POST_CHECKOUT_HOOK ,NULL , msg.buf))die (_ ("Could not detach HEAD" ));
t/t3400-rebase.sh +1 -1
@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh 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 )
t/t3404-rebase-interactive.sh +1 -1
@@ -1066,7 +1066,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 '
t/t3432-rebase-fast-forward.sh +1 -1
@@ -54,6 +54,6 @@ test_expect_success 'add work to upstream' ' changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B -test_rebase_same_head failure --onto master... master +test_rebase_same_head success --onto master... master test_done
Signature · 1 line
2.21.0.843.gd0ae0373aa
Re: [PATCH v3 0/4] rebase: teach rebase --keep-base
In reply to Denton LiuQuoted 3 lines · Denton Liu
Thanks again for your feedback, Ævar! I think we're both on the same page now. Hopefully I've addressed all of your high-level concerns with this patchset and we can move into a discussion on implementation detail.
Late in replying to this, have been off-list. This also applies for your v4.
The current version you have still doesn't explain the "Why would we redundantly rebase every time in this case..." question I had in
https://public-inbox.org/git/[email protected]/
I *think* it's closer to "it was easier to implement this in terms of --onto, which happens to behave that way now" than "it must work this way for --keep-base", which is fair enough.
Although I see when I forward-port my POC patch from that E-Mail that one test fails now, which is good, that wasn't the case before, but it looks like that might be testing something else than just the lazy behavior.
It would be good to know in terms of commit message or (better) explicit tests so that if we teach these various rebase modes the same lazyness --fork-point uses in the future it's clear if that's OK or not.
Re: [PATCH v3 0/4] rebase: teach rebase --keep-base
Quoted 10 lines · Ævar Arnfjörð Bjarmason
Quoted 3 lines · Denton Liu
Thanks again for your feedback, Ævar! I think we're both on the same page now. Hopefully I've addressed all of your high-level concerns with this patchset and we can move into a discussion on implementation detail.
Late in replying to this, have been off-list. This also applies for your v4.
Quoted 7 lines
The current version you have still doesn't explain the "Why would we redundantly rebase every time in this case..." question I had in
https://public-inbox.org/git/[email protected]/I *think* it's closer to "it was easier to implement this in terms of --onto, which happens to behave that way now" than "it must work this way for --keep-base", which is fair enough.
Correct, the reason why --keep-base was not lazy initially was because "--onto did it that way". You are correct in that --keep-base should be lazily rebasing so I changed --onto's behaviour in 3/4 because it would also benefit from laziness. Thus, now --keep-base lazily rebases because --onto also does.
Quoted 5 lines
Although I see when I forward-port my POC patch from that E-Mail that one test fails now, which is good, that wasn't the case before, but it looks like that might be testing something else than just the lazy behavior.
The test fails because the patch disables fork_point if --keep-base is set. So, with the patch applied, C is rebased even though it is excluded when fork_point is set.
Quoted 4 lines
It would be good to know in terms of commit message or (better) explicit tests so that if we teach these various rebase modes the same lazyness --fork-point uses in the future it's clear if that's OK or not.
Sorry, could you please clarify what you mean by the "lazyness --fork-point uses"? I don't understand what laziness is introduced by using --fork-point. Also, are the tests in t3432 not sufficient for testing fast-forwarding (aka lazy) behaviour?
Thanks,
Denton
Re: [PATCH v3 1/4] t3431: add rebase --fork-point tests
In reply to SZEDER GáborQuoted 14 lines · SZEDER Gábor
t/t3431-rebase-fork-point.sh line 44
Quoted 8 lines
t/t3431-rebase-fork-point.sh new file +8
-0+ test_expect_success "git rebase $@" " +... + git rebase $@ && +... + " +} + +test_rebase 'G F E D B A' ''It appears that this last empty argument triggers some bug in Bash v4.2 and older (and on macOS such an old Bash is the default
/bin/sh), as it turns that empty argument into something else, which in turn fails the test with:
Quoted 7 lines
<...>
++ git rebase $'\177'
fatal: invalid upstream '?'
error: last command exited with $?=128
not ok 2 - git rebase
Yeah, every time I see $@ that appears in any form other than "$@" (i.e. within a pair of double-quotes without anything else in it), it makes me feel very uneasy. Shouldn't the argument to the above "rebase" be spelled $* instead? I somehow do not think use of $@ there is buying us anything.
Of course, if we were really passing an arg with $IFS character in it, we could probably eval 'git rebase "$@"' it (with appropriate quoting to adjust for the surrounding quote pair).
Quoted 15 lines
Omitting that empty argument avoids this issue, and the test still checks what it was supposed to.
Quoted 12 lines
t/t3431-rebase-fork-point.sh new file +9
-0+test_rebase 'G F D B A' '--onto D' +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 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 C E D B A' 'refs/heads/master' +test_rebase 'G F C D B A' '--onto D refs/heads/master' + +test_doneSignature · 1 line
2.21.0.695.gaf8658f249
Re: [PATCH v3 0/4] rebase: teach rebase --keep-base
In reply to Denton LiuQuoted 36 lines · Denton Liu
Quoted 10 lines · Ævar Arnfjörð Bjarmason
Quoted 3 lines · Denton Liu
Thanks again for your feedback, Ævar! I think we're both on the same page now. Hopefully I've addressed all of your high-level concerns with this patchset and we can move into a discussion on implementation detail.
Late in replying to this, have been off-list. This also applies for your v4.
Quoted 7 lines
The current version you have still doesn't explain the "Why would we redundantly rebase every time in this case..." question I had in
https://public-inbox.org/git/[email protected]/I *think* it's closer to "it was easier to implement this in terms of --onto, which happens to behave that way now" than "it must work this way for --keep-base", which is fair enough.
Correct, the reason why --keep-base was not lazy initially was because "--onto did it that way". You are correct in that --keep-base should be lazily rebasing so I changed --onto's behaviour in 3/4 because it would also benefit from laziness. Thus, now --keep-base lazily rebases because --onto also does.
Quoted 5 lines
Although I see when I forward-port my POC patch from that E-Mail that one test fails now, which is good, that wasn't the case before, but it looks like that might be testing something else than just the lazy behavior.
The test fails because the patch disables
fork_pointif --keep-base is set. So, with the patch applied, C is rebased even though it is excluded whenfork_pointis set.Quoted 1 line
Quoted 8 lines
Quoted 3 lines
It would be good to know in terms of commit message or (better) explicit tests so that if we teach these various rebase modes the same lazyness --fork-point uses in the future it's clear if that's OK or not.
Sorry, could you please clarify what you mean by the "lazyness --fork-point uses"? I don't understand what laziness is introduced by using --fork-point. Also, are the tests in t3432 not sufficient for testing fast-forwarding (aka lazy) behaviour?
Late reply, sorry. I mean if I do e.g. on git.git:
git checkout -b avar/test 041f5ea1cf
I.e. branch of Junio's "The third batch" (we're now on the 4th) and then:
git branch --set-upstream-to origin/master
And commit a dummy change, I'm now:
On branch avar/test
Your branch and 'origin/master' have diverged, and have 1 and 33 different commits each, respectively.
Then I run a --keep-base rebase twice:
$ git log --oneline --no-decorate -2; ~/g/git/git --exec-path=$PWD rebase --keep-base; git rev-parse HEAD
fc3e916c5f foo
041f5ea1cf The third batch
First, rewinding head to replay your work on top of it...
Applying: foo
b10e672074dfee6b6e8b2901c9bb49f856a13708
$ git log --oneline --no-decorate -2; ~/g/git/git --exec-path=$PWD rebase --keep-base; git rev-parse HEAD
b10e672074 foo
041f5ea1cf The third batch
First, rewinding head to replay your work on top of it...
Applying: foo
fd3029e73b89f5a799653ff17199d00f2a6ee2e2I.e. I'll keep on getting a new commit, even though by any criteria I can think of for this type of case there's no work to do. I.e. we're already based on 041f5ea1cf, no need to rebase again.
Of course this is also currently the case with --fork-point without argument, which'll settle on (note fourth, not third):
$ git log --oneline --no-decorate -2; ~/g/git/git --exec-path=$PWD rebase --fork-point; git rev-parse HEAD
85a1861cec foo
e35b8cb8e2 The fourth batch
First, rewinding head to replay your work on top of it...
Applying: foo
c6046e97af29d71bf6270080acf188c095e0cb7c
$ git log --oneline --no-decorate -2; ~/g/git/git --exec-path=$PWD rebase --fork-point; git rev-parse HEAD
c6046e97af foo
e35b8cb8e2 The fourth batch
First, rewinding head to replay your work on top of it...
Applying: foo
672d22d58e9aa9b6a28054531c21e1f1b598b013Whereas a --keep-base in *this* case should be identical to:
$ git log --oneline --no-decorate -2; ~/g/git/git --exec-path=$PWD rebase $(git merge-base --fork-point origin/master HEAD); git rev-parse HEAD
fc3e916c5f foo
041f5ea1cf The third batch
Current branch avar/test is up to date.
fc3e916c5fc707cfc83f28b3faca81046306b305The reason I'm asking is that as noted in the thread starting at
https://public-inbox.org/git/[email protected]/
we used to in *some* cases do the same for:
rebase $(git merge-base --fork-point origin/master HEAD)
As for just:
rebase --fork-point
I.e. say "Current branch is up-to-date". I'm planning to loop back to that and fix it for the --fork-point case. So I was wondering if you could think of a reason for why --keep-base couldn't also do the same, or if there was some intrinsic difference I'm missing.
Anyway. Don't worry about it. When I poke at it it'll likely shake out of the respective tests.
I was just wondering if you having looked at many of the same things recently could understand my ramblings and knew if this was a case of "yup, we could add a bit more to that can_fast_forward(...) case and make it lazy too", or "no, --keep-base is special for reasons you're missing...".
v5 cover rebase: teach rebase --keep-base
In reply to Denton LiuThanks for the example, Ævar, it clarified a lot. I think that _now_ we're on the same page. ;)
I made can_fast_forward detect this case and now, it should behave as expected. The change can be seen in 4/5. Hopefully this saves you some work later.
---
This patchset now depends "[PATCH 1/8] tests (rebase): spell out the --keep-empty option" which is the first patch of Johannes's "Do not use abbreviated options in tests" patchset[1]. (Thanks for catching that, Johannes!)
Changes since v1:
* Squashed old set into one patch
* Fixed indentation style and dangling else
* Added more documentation after discussion with Ævar
Changes since v2:
* Add testing for rebase --fork-point behaviour
* Add testing for rebase fast-forward behaviour
* Make rebase --onto fast-forward in more cases
* Update documentation to include use-case
Changes since v3:
* Fix tests failing on bash 4.2
* Fix typo in t3431 comment
Changes since v4:
* Make rebase --fork-point fast-forward in more cases
Denton Liu (5):
t3431: add rebase --fork-point tests
t3432: test rebase fast-forward behavior
rebase: fast-forward --onto in more cases
rebase: fast-forward --fork-point in more cases
rebase: teach rebase --keep-base
Documentation/git-rebase.txt | 30 ++++++++++-- builtin/rebase.c | 77 +++++++++++++++++++++-------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3416-rebase-onto-threedots.sh | 57 ++++++++++++++++++++++ t/t3431-rebase-fork-point.sh | 57 ++++++++++++++++++++++ t/t3432-rebase-fast-forward.sh | 83 ++++++++++++++++++++++++++++++++ 7 files changed, 284 insertions(+), 24 deletions(-) create mode 100755 t/t3431-rebase-fork-point.sh create mode 100755 t/t3432-rebase-fast-forward.sh
Signature · 1 line
2.21.0.921.gb27c68c4e9
PATCH v5 1/5 t3431: add rebase --fork-point tests
--- t/t3431-rebase-fork-point.sh | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 t/t3431-rebase-fork-point.sh
t/t3431-rebase-fork-point.sh new file +53 -0
@@ -0,0 +1,53 @@ +#!/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 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 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 C E D B A' refs/heads/master +test_rebase 'G F C D B A' --onto D refs/heads/master + +test_done
Signature · 1 line
2.21.0.921.gb27c68c4e9
PATCH v5 2/5 t3432: test rebase fast-forward behavior
In reply to Denton LiuWhen rebase is run on a branch that can be fast-forwarded, this should automatically be done. Create test to ensure this behavior happens.
There are some cases that currently don't pass. The first case is where a feature and master have diverged, running
"git rebase master... master" causes a full rebase to happen even though a fast-forward should happen.
The second case is when we are doing "git rebase --fork-point" and a fork-point commit is found. Once again, a full rebase happens even though a fast-forward should happen.
Mark these cases as failure so we can fix it later.
--- t/t3432-rebase-fast-forward.sh | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100755 t/t3432-rebase-fast-forward.sh
t/t3432-rebase-fast-forward.sh new file +72 -0
@@ -0,0 +1,72 @@ +#!/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="$1" && + shift && + test_expect_$status "git rebase $* with $changes is no-op" " + oldhead=\$(git rev-parse HEAD) && + test_when_finished 'git reset --hard \$oldhead' && + git rebase $* && + newhead=\$(git rev-parse HEAD) && + test_cmp_rev \$oldhead \$newhead + " +} + +changes='no changes' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --no-fork-point +test_rebase_same_head success --fork-point master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto master... master + +test_expect_success 'add work to side' ' + test_commit E +' + +changes='our changes' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --no-fork-point +test_rebase_same_head success --fork-point master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto master... master + +test_expect_success 'add work to upstream' ' + git checkout master && + test_commit F && + git checkout side +' + +changes='our and their changes' +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head failure --onto master... master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head failure --fork-point --onto master... master + +test_done
Signature · 1 line
2.21.0.921.gb27c68c4e9
PATCH v5 3/5 rebase: fast-forward --onto in more cases
In reply to Denton LiuBefore, when we had the following graph,
A---B---C (master) \ D (side)
running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to can_fast_forward so that this case can be detected and a fast-forward will be performed. First of all, rewrite the function to use gotos which simplifies the logic. Next, since the
options.upstream && !oidcmp(&options.upstream->object.oid, &options.onto->object.oid)
conditions were removed in cmd_rebase, we reintroduce a substitute in can_fast_forward. In particular, checking the merge bases of upstream and head fixes a failing case in t3416.
The abbreviated graph for t3416 is as follows:
F---G topic / A---B---C---D---E master
and the failing command was
git rebase --onto master...topic F topic
Before, Git would see that there was one merge base (C), and the merge and onto were the same so it would incorrectly return 1, indicating that we could fast-forward. This would cause the rebased graph to be 'ABCFG' when we were expecting 'ABCG'.
With the additional logic, we detect that upstream and head's merge base is F. Since onto isn't F, it means we're not rebasing the full set of commits from master..topic. Since we're excluding some commits, a fast-forward cannot be performed and so we correctly return 0.
Add '-f' to test cases that failed as a result of this change because they were not expecting a fast-forward so that a rebase is forced.
While we're at it, remove a trailing whitespace from rebase.c.
--- builtin/rebase.c | 40 +++++++++++++++++++++++----------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 4 ++-- 4 files changed, 31 insertions(+), 17 deletions(-)
builtin/rebase.c +27 -13
@@ -895,12 +895,12 @@ 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 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 ;
@@ -908,12 +908,29 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid, 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); +if (!oideq (merge_base, &onto->object.oid)) + goto done; }else {oidcpy (merge_base, &null_oid); - res =0 ; + goto done; } + +if (!upstream) + goto done; +free_commit_list (merge_bases); + merge_bases =get_merge_bases (upstream, head); +if (merge_bases && !merge_bases->next) { +if (!oideq (&onto->object.oid, &merge_bases->item->object.oid)) + goto done; + }else + goto done; + + res =1 ; + +done: +if (merge_bases) +free_commit_list (merge_bases);return res &&is_linear_history (onto, head); }
@@ -1682,13 +1699,10 @@ 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. +* but this should be done if this is not 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.orig_head, &merge_base) && + !is_interactive (&options) && !options.restrict_revision) {int flag;if (!(options.flags &REBASE_FORCE )) {
@@ -1782,7 +1796,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)strbuf_addf (&msg,"%s: checkout %s" ,getenv (GIT_REFLOG_ACTION_ENVIRONMENT ), options.onto_name);if (reset_head (&options.onto->object.oid,"checkout" ,NULL , -RESET_HEAD_DETACH |RESET_ORIG_HEAD | +RESET_HEAD_DETACH |RESET_ORIG_HEAD |RESET_HEAD_RUN_POST_CHECKOUT_HOOK ,NULL , msg.buf))die (_ ("Could not detach HEAD" ));
t/t3400-rebase.sh +1 -1
@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh 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 )
t/t3404-rebase-interactive.sh +1 -1
@@ -1066,7 +1066,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 '
t/t3432-rebase-fast-forward.sh +2 -2
@@ -64,9 +64,9 @@ test_expect_success 'add work to upstream' ' changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B -test_rebase_same_head failure --onto master... master +test_rebase_same_head success --onto master... master test_rebase_same_head failure --fork-point --onto B B test_rebase_same_head failure --fork-point --onto B... B -test_rebase_same_head failure --fork-point --onto master... master +test_rebase_same_head success --fork-point --onto master... master test_done
Signature · 1 line
2.21.0.921.gb27c68c4e9
PATCH v5 4/5 rebase: fast-forward --fork-point in more cases
In reply to Denton LiuBefore, when we rebased with a --fork-point invocation where the fork-point wasn't empty, we would be setting options.restrict_revision. The fast-forward logic would automatically declare that the rebase was not fast-forwardable if it was set. However, this was painting with a very broad brush.
Refine the logic so that we can fast-forward in the case where the restricted revision is equal to the merge base, since we stop rebasing at the merge base anyway.
--- builtin/rebase.c | 9 +++++++-- t/t3432-rebase-fast-forward.sh | 12 ++++++------ 2 files changed, 13 insertions(+), 8 deletions(-)
builtin/rebase.c +7 -2
@@ -896,6 +896,7 @@ static int is_linear_history(struct commit *from, struct commit *to) }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);
@@ -915,6 +916,9 @@ static int can_fast_forward(struct commit *onto, struct commit *upstream, goto done; } +if (restrict_revision && !oideq (&restrict_revision->object.oid, merge_base)) + goto done; +if (!upstream) goto done;
@@ -1701,8 +1705,9 @@ 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 if this is not an interactive rebase. */ -if (can_fast_forward (options.onto, options.upstream, &options.orig_head, &merge_base) && - !is_interactive (&options) && !options.restrict_revision) { +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 )) {
t/t3432-rebase-fast-forward.sh +6 -6
@@ -35,8 +35,8 @@ test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master test_rebase_same_head success --no-fork-point test_rebase_same_head success --fork-point master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto B B +test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master test_expect_success 'add work to side' '
@@ -51,8 +51,8 @@ test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master test_rebase_same_head success --no-fork-point test_rebase_same_head success --fork-point master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto B B +test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master test_expect_success 'add work to upstream' '
@@ -65,8 +65,8 @@ changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto B B +test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master test_done
Signature · 1 line
2.21.0.921.gb27c68c4e9
PATCH v5 5/5 rebase: teach rebase --keep-base
In reply to Denton LiuA 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'. However, while developing the patches, 'master' is also developed further and it is sometimes not the bst idea to keep rebasing on top of 'master', but to keep the base commit as-is.
Alternatively, 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.
--- Documentation/git-rebase.txt | 30 +++++++++++++++-- builtin/rebase.c | 32 ++++++++++++++---- t/t3416-rebase-onto-threedots.sh | 57 ++++++++++++++++++++++++++++++++ t/t3431-rebase-fork-point.sh | 4 +++ t/t3432-rebase-fast-forward.sh | 11 ++++++ 5 files changed, 125 insertions(+), 9 deletions(-)
Documentation/git-rebase.txt +27 -3
@@ -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
@@ -364,6 +382,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>::
@@ -539,6 +561,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 -----------------------
@@ -863,7 +887,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:
builtin/rebase.c +26 -6
@@ -27,8 +27,8 @@#include "branch.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" ),
@@ -1039,6 +1039,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) };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 ;
@@ -1072,6 +1073,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,
@@ -1238,6 +1241,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)usage_with_options (builtin_rebase_usage, builtin_rebase_options); +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 !=NO_ACTION && !in_progress)die (_ ("No rebase in progress?" ));setenv (GIT_REFLOG_ACTION_ENVIRONMENT ,"rebase" ,0 );
@@ -1562,12 +1572,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 {
t/t3416-rebase-onto-threedots.sh +57 -0
64 lines not on this page · raw message
t/t3431-rebase-fork-point.sh +4 -0
@@ -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
t/t3432-rebase-fast-forward.sh +11 -0
@@ -33,11 +33,15 @@ test_rebase_same_head success master test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master +test_rebase_same_head success --keep-base test_rebase_same_head success --no-fork-point +test_rebase_same_head success --keep-base --no-fork-point test_rebase_same_head success --fork-point master test_rebase_same_head success --fork-point --onto B B test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master +test_rebase_same_head success --fork-point --keep-base master test_expect_success 'add work to side' ' test_commit E
@@ -49,11 +53,15 @@ test_rebase_same_head success master test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master +test_rebase_same_head success --keep-base test_rebase_same_head success --no-fork-point +test_rebase_same_head success --keep-base --no-fork-point test_rebase_same_head success --fork-point master test_rebase_same_head success --fork-point --onto B B test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master +test_rebase_same_head success --fork-point --keep-base master test_expect_success 'add work to upstream' ' git checkout master &&
@@ -65,8 +73,11 @@ changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master +test_rebase_same_head success --keep-base test_rebase_same_head success --fork-point --onto B B test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master +test_rebase_same_head success --fork-point --keep-base master test_done
Signature · 1 line
2.21.0.921.gb27c68c4e9
Re: [PATCH v5 3/5] rebase: fast-forward --onto in more cases
In reply to Denton LiuQuoted 5 lines · Denton Liu
Before, when we had the following graph,
A---B---C (master) \ D (side)
This is minor, but comparing the above with below
Quoted 3 lines
F---G topic / A---B---C---D---E master
you'll notice that branches growing downwards in your picture (this applies also to an illustration in your tests) are off by one column.
D
/
A---B---C
\
Ebuiltin/rebase.c int cmd_rebase(int argc, const char **argv, const char *prefix) · line 1703
Quoted 8 lines
builtin/rebase.c +1
-2@@ -1682,13 +1699,10 @@ 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. +* but this should be done if this is not an interactive rebase. */
Two issues.
- One is shared with the original (i.e. not a problem with this patch), but what "this" refers to is not what has already been stated in the sentence. We check, to see if we are in a situation where a specific optimization is possible. But this (== optimization to fast-forward without actually replaying the commit's changes) should be done only under such and such condition.
- The other is more grave. "should be done if this is not an interactive rebase" drops "only" from "only if" in the original, which changes the meaning of the sentence (it can be read as "we check if we can optimize, but the optimization should be done for rebase-i regardless of the result of that said check", which is not what you mean).
Perhaps
Check if we are ..., 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.
Re: [PATCH v5 3/5] rebase: fast-forward --onto in more cases
In reply to Denton LiuHi Denton
It's good to see rebase fast-forwarding properly when it should
Quoted 98 lines · Denton Liu
Before, when we had the following graph,
A---B---C (master) \ D (side)running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to
can_fast_forwardso that this case can be detected and a fast-forward will be performed. First of all, rewrite the function to use gotos which simplifies the logic. Next, since theoptions.upstream && !oidcmp(&options.upstream->object.oid, &options.onto->object.oid)conditions were removed in
cmd_rebase, we reintroduce a substitute incan_fast_forward. In particular, checking the merge bases ofupstreamandheadfixes a failing case in t3416.The abbreviated graph for t3416 is as follows:
F---G topic / A---B---C---D---E masterand the failing command was
git rebase --onto master...topic F topicBefore, Git would see that there was one merge base (C), and the merge and onto were the same so it would incorrectly return 1, indicating that we could fast-forward. This would cause the rebased graph to be 'ABCFG' when we were expecting 'ABCG'.
With the additional logic, we detect that upstream and head's merge base is F. Since onto isn't F, it means we're not rebasing the full set of commits from master..topic. Since we're excluding some commits, a fast-forward cannot be performed and so we correctly return 0.
Add '-f' to test cases that failed as a result of this change because they were not expecting a fast-forward so that a rebase is forced.
While we're at it, remove a trailing whitespace from
rebase.c.Signed-off-by Denton Liu <[email protected]>--- builtin/rebase.c | 40 +++++++++++++++++++++++----------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 4 ++-- 4 files changed, 31 insertions(+), 17 deletions(-)builtin/rebase.c +16
-6@@ -895,12 +895,12 @@ 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 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 ;@@ -908,12 +908,29 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid, 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); +if (!oideq (merge_base, &onto->object.oid)) + goto done; }else {oidcpy (merge_base, &null_oid); - res =0 ; + goto done; } + +if (!upstream) + goto done; +free_commit_list (merge_bases); + merge_bases =get_merge_bases (upstream, head); +if (merge_bases && !merge_bases->next) { +if (!oideq (&onto->object.oid, &merge_bases->item->object.oid)) + goto done; + }else
builtin/rebase.c static int can_fast_forward(struct commit *onto, struct object_id *head_oid, · line 936
Quoted 10 lines
builtin/rebase.c +7
-0+ goto done; + + res =1 ; + +done: +if (merge_bases) +free_commit_list (merge_bases);return res &&is_linear_history (onto, head); }
I had a hard time following all those gotos. When you 'goto done' in both branches of an if statement it is hard to work out which cases fall through to the rest of the code. If I've understood it correctly I think it is clearer as
merge_bases = get_merge_bases(onto, head);
if (merge_bases && !merge_bases->next) {
oidcpy(merge_base, &merge_bases->item->object.oid);
if (oideq(merge_base, &onto->object.oid) && upstream) {
free_commit_list(merge_bases);
merge_bases = get_merge_bases(upstream, head);
if (merge_bases && !merge_bases->next)
if (oideq(&onto->object.oid,
&merge_bases->item->object.oid))
res = 1;
}
} else {
oidcpy(merge_base, &null_oid);
}
if (merge_bases)
free_commit_list(merge_bases);
return res && is_linear_history(onto, head);
}The nested if's aren't great but I think it is easier to follow
builtin/rebase.c int cmd_rebase(int argc, const char **argv, const char *prefix) · line 1704
Quoted 13 lines
builtin/rebase.c +2
-6@@ -1682,13 +1699,10 @@ 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. +* but this should be done if this is not 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.orig_head, &merge_base) &&
This is rather long, perhaps break the argument list
Best Wishes
Phillip
Quoted 56 lines
builtin/rebase.c +2
-1+ !is_interactive(&options) && !options.restrict_revision) {int flag;if (!(options.flags &REBASE_FORCE )) {@@ -1782,7 +1796,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)strbuf_addf (&msg,"%s: checkout %s" ,getenv (GIT_REFLOG_ACTION_ENVIRONMENT ), options.onto_name);if (reset_head (&options.onto->object.oid,"checkout" ,NULL , -RESET_HEAD_DETACH |RESET_ORIG_HEAD | +RESET_HEAD_DETACH |RESET_ORIG_HEAD |RESET_HEAD_RUN_POST_CHECKOUT_HOOK ,NULL , msg.buf))die (_ ("Could not detach HEAD" ));t/t3400-rebase.sh +1
-1@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh 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 )t/t3404-rebase-interactive.sh +1
-1@@ -1066,7 +1066,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 't/t3432-rebase-fast-forward.sh +2
-2@@ -64,9 +64,9 @@ test_expect_success 'add work to upstream' ' changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B -test_rebase_same_head failure --onto master... master +test_rebase_same_head success --onto master... master test_rebase_same_head failure --fork-point --onto B B test_rebase_same_head failure --fork-point --onto B... B -test_rebase_same_head failure --fork-point --onto master... master +test_rebase_same_head success --fork-point --onto master... master test_done
Re: [PATCH v5 3/5] rebase: fast-forward --onto in more cases
Quoted 136 lines · Phillip Wood
Hi Denton
It's good to see rebase fast-forwarding properly when it should
Quoted 95 lines · Denton Liu
Before, when we had the following graph,
A---B---C (master) \ D (side)running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to
can_fast_forwardso that this case can be detected and a fast-forward will be performed. First of all, rewrite the function to use gotos which simplifies the logic. Next, since theoptions.upstream && !oidcmp(&options.upstream->object.oid, &options.onto->object.oid)conditions were removed in
cmd_rebase, we reintroduce a substitute incan_fast_forward. In particular, checking the merge bases ofupstreamandheadfixes a failing case in t3416.The abbreviated graph for t3416 is as follows:
F---G topic / A---B---C---D---E masterand the failing command was
git rebase --onto master...topic F topicBefore, Git would see that there was one merge base (C), and the merge and onto were the same so it would incorrectly return 1, indicating that we could fast-forward. This would cause the rebased graph to be 'ABCFG' when we were expecting 'ABCG'.
With the additional logic, we detect that upstream and head's merge base is F. Since onto isn't F, it means we're not rebasing the full set of commits from master..topic. Since we're excluding some commits, a fast-forward cannot be performed and so we correctly return 0.
Add '-f' to test cases that failed as a result of this change because they were not expecting a fast-forward so that a rebase is forced.
While we're at it, remove a trailing whitespace from
rebase.c.Signed-off-by Denton Liu <[email protected]>--- builtin/rebase.c | 40 +++++++++++++++++++++++----------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 4 ++-- 4 files changed, 31 insertions(+), 17 deletions(-)builtin/rebase.c +15
-6@@ -895,12 +895,12 @@ 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 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 ;@@ -908,12 +908,29 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid, 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); +if (!oideq (merge_base, &onto->object.oid)) + goto done; }else {oidcpy (merge_base, &null_oid); - res =0 ; + goto done; } + +if (!upstream) + goto done; +free_commit_list (merge_bases); + merge_bases =get_merge_bases (upstream, head); +if (merge_bases && !merge_bases->next) { +if (!oideq (&onto->object.oid, &merge_bases->item->object.oid)) + goto done;builtin/rebase.c static int can_fast_forward(struct commit *onto, struct object_id *head_oid, · line 935
Quoted 10 lines
builtin/rebase.c +8
-0+ }else + goto done; + + res =1 ; + +done: +if (merge_bases) +free_commit_list (merge_bases);return res &&is_linear_history (onto, head); }I had a hard time following all those gotos. When you 'goto done' in both branches of an if statement it is hard to work out which cases fall through to the rest of the code. If I've understood it correctly I think it is clearer as
merge_bases = get_merge_bases(onto, head); if (merge_bases && !merge_bases->next) { oidcpy(merge_base, &merge_bases->item->object.oid); if (oideq(merge_base, &onto->object.oid) && upstream) { free_commit_list(merge_bases); merge_bases = get_merge_bases(upstream, head); if (merge_bases && !merge_bases->next) if (oideq(&onto->object.oid, &merge_bases->item->object.oid)) res = 1; } } else { oidcpy(merge_base, &null_oid); } if (merge_bases) free_commit_list(merge_bases); return res && is_linear_history(onto, head); }
Quoted 1 line
The nested if's aren't great but I think it is easier to follow
I am pretty impartial between gotos and ifs. If no one else has any strong opinions between the two, I'll reroll with ifs.
Quoted 15 lines
builtin/rebase.c int cmd_rebase(int argc, const char **argv, const char *prefix) · line 1703
Quoted 13 lines
builtin/rebase.c +2
-6@@ -1682,13 +1699,10 @@ 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. +* but this should be done if this is not 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.orig_head, &merge_base) &&This is rather long, perhaps break the argument list
Thanks, will do.
Quoted 58 lines
Best Wishes
Phillip
Quoted 54 lines
builtin/rebase.c +2
-1+ !is_interactive(&options) && !options.restrict_revision) {int flag;if (!(options.flags &REBASE_FORCE )) {@@ -1782,7 +1796,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)strbuf_addf (&msg,"%s: checkout %s" ,getenv (GIT_REFLOG_ACTION_ENVIRONMENT ), options.onto_name);if (reset_head (&options.onto->object.oid,"checkout" ,NULL , -RESET_HEAD_DETACH |RESET_ORIG_HEAD | +RESET_HEAD_DETACH |RESET_ORIG_HEAD |RESET_HEAD_RUN_POST_CHECKOUT_HOOK ,NULL , msg.buf))die (_ ("Could not detach HEAD" ));t/t3400-rebase.sh +1
-1@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh 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 )t/t3404-rebase-interactive.sh +1
-1@@ -1066,7 +1066,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 't/t3432-rebase-fast-forward.sh +2
-2@@ -64,9 +64,9 @@ test_expect_success 'add work to upstream' ' changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B -test_rebase_same_head failure --onto master... master +test_rebase_same_head success --onto master... master test_rebase_same_head failure --fork-point --onto B B test_rebase_same_head failure --fork-point --onto B... B -test_rebase_same_head failure --fork-point --onto master... master +test_rebase_same_head success --fork-point --onto master... master test_done
Re: [PATCH v5 3/5] rebase: fast-forward --onto in more cases
Quoted 117 lines · Denton Liu
Quoted 116 lines · Phillip Wood
Hi Denton
It's good to see rebase fast-forwarding properly when it should
Quoted 95 lines · Denton Liu
Before, when we had the following graph,
A---B---C (master) \ D (side)running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to
can_fast_forwardso that this case can be detected and a fast-forward will be performed. First of all, rewrite the function to use gotos which simplifies the logic. Next, since theoptions.upstream && !oidcmp(&options.upstream->object.oid, &options.onto->object.oid)conditions were removed in
cmd_rebase, we reintroduce a substitute incan_fast_forward. In particular, checking the merge bases ofupstreamandheadfixes a failing case in t3416.The abbreviated graph for t3416 is as follows:
F---G topic / A---B---C---D---E masterand the failing command was
git rebase --onto master...topic F topicBefore, Git would see that there was one merge base (C), and the merge and onto were the same so it would incorrectly return 1, indicating that we could fast-forward. This would cause the rebased graph to be 'ABCFG' when we were expecting 'ABCG'.
With the additional logic, we detect that upstream and head's merge base is F. Since onto isn't F, it means we're not rebasing the full set of commits from master..topic. Since we're excluding some commits, a fast-forward cannot be performed and so we correctly return 0.
Add '-f' to test cases that failed as a result of this change because they were not expecting a fast-forward so that a rebase is forced.
While we're at it, remove a trailing whitespace from
rebase.c.Signed-off-by Denton Liu <[email protected]>--- builtin/rebase.c | 40 +++++++++++++++++++++++----------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 4 ++-- 4 files changed, 31 insertions(+), 17 deletions(-)builtin/rebase.c +15
-6@@ -895,12 +895,12 @@ 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 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 ;@@ -908,12 +908,29 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid, 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); +if (!oideq (merge_base, &onto->object.oid)) + goto done; }else {oidcpy (merge_base, &null_oid); - res =0 ; + goto done; } + +if (!upstream) + goto done; +free_commit_list (merge_bases); + merge_bases =get_merge_bases (upstream, head); +if (merge_bases && !merge_bases->next) { +if (!oideq (&onto->object.oid, &merge_bases->item->object.oid)) + goto done;builtin/rebase.c static int can_fast_forward(struct commit *onto, struct object_id *head_oid, · line 935
Quoted 10 lines
builtin/rebase.c +8
-0+ }else + goto done; + + res =1 ; + +done: +if (merge_bases) +free_commit_list (merge_bases);return res &&is_linear_history (onto, head); }I had a hard time following all those gotos. When you 'goto done' in both branches of an if statement it is hard to work out which cases fall through to the rest of the code. If I've understood it correctly I think it is clearer as
Quoted 10 lines
Quoted 10 lines
merge_bases = get_merge_bases(onto, head); if (merge_bases && !merge_bases->next) { oidcpy(merge_base, &merge_bases->item->object.oid); if (oideq(merge_base, &onto->object.oid) && upstream) { free_commit_list(merge_bases); merge_bases = get_merge_bases(upstream, head); if (merge_bases && !merge_bases->next) if (oideq(&onto->object.oid, &merge_bases->item->object.oid)) res = 1;
that would be better as
res = oideq(&onto->object.oid, &merge_bases->item->object.oid);
without the last if
Quoted 10 lines
Quoted 10 lines
} } else { oidcpy(merge_base, &null_oid); } if (merge_bases) free_commit_list(merge_bases); return res && is_linear_history(onto, head); }
Quoted 4 lines
Quoted 1 line
The nested if's aren't great but I think it is easier to follow
I am pretty impartial between gotos and ifs. If no one else has any strong opinions between the two, I'll reroll with ifs.
If you want to keep the goto approach then refactoring the ifs as follows is clearer as it avoids jumping from both arms, each stage is a simple single armed if (something) goto done
merge_bases = get_merge_bases(onto, head);
if (!merge_bases || merge_bases->next) {
oidcpy(merge_base, &null_oid);
goto done;
}
oidcpy(merge_base, &merge_bases->item->object.oid);
if (!oideq(merge_base, &onto->object.oid))
goto done;
if (!upstream)
goto done;
free_commit_list(merge_bases);
merge_bases = get_merge_bases(upstream, head);
if (merge_bases && !merge_bases->next)
res = oideq(&onto->object.oid, &merge_bases->item->object.oid);done:
if (merge_bases) free_commit_list(merge_bases); return res && is_linear_history(onto, head); }
Best Wishes
Phillip
Quoted 76 lines
Quoted 15 lines
builtin/rebase.c int cmd_rebase(int argc, const char **argv, const char *prefix) · line 1703
Quoted 13 lines
builtin/rebase.c +2
-6@@ -1682,13 +1699,10 @@ 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. +* but this should be done if this is not 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.orig_head, &merge_base) &&This is rather long, perhaps break the argument list
Thanks, will do.
Quoted 58 lines
Best Wishes
Phillip
Quoted 54 lines
builtin/rebase.c +2
-1+ !is_interactive(&options) && !options.restrict_revision) {int flag;if (!(options.flags &REBASE_FORCE )) {@@ -1782,7 +1796,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)strbuf_addf (&msg,"%s: checkout %s" ,getenv (GIT_REFLOG_ACTION_ENVIRONMENT ), options.onto_name);if (reset_head (&options.onto->object.oid,"checkout" ,NULL , -RESET_HEAD_DETACH |RESET_ORIG_HEAD | +RESET_HEAD_DETACH |RESET_ORIG_HEAD |RESET_HEAD_RUN_POST_CHECKOUT_HOOK ,NULL , msg.buf))die (_ ("Could not detach HEAD" ));t/t3400-rebase.sh +1
-1@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh 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 )t/t3404-rebase-interactive.sh +1
-1@@ -1066,7 +1066,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 't/t3432-rebase-fast-forward.sh +2
-2@@ -64,9 +64,9 @@ test_expect_success 'add work to upstream' ' changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B -test_rebase_same_head failure --onto master... master +test_rebase_same_head success --onto master... master test_rebase_same_head failure --fork-point --onto B B test_rebase_same_head failure --fork-point --onto B... B -test_rebase_same_head failure --fork-point --onto master... master +test_rebase_same_head success --fork-point --onto master... master test_done
v6 cover rebase: teach rebase --keep-base
In reply to Denton LiuThanks for the comments, Junio and Phillip.
I've fixed the ASCII art graphs and also refactored the if-else into a goto tower.
Hopefully, this will be the last reroll of this series ;)
---
This patchset now depends "[PATCH 1/8] tests (rebase): spell out the --keep-empty option" which is the first patch of Johannes's "Do not use abbreviated options in tests" patchset[1]. (Thanks for catching that, Johannes!)
Changes since v1:
* Squashed old set into one patch
* Fixed indentation style and dangling else
* Added more documentation after discussion with Ævar
Changes since v2:
* Add testing for rebase --fork-point behaviour
* Add testing for rebase fast-forward behaviour
* Make rebase --onto fast-forward in more cases
* Update documentation to include use-case
Changes since v3:
* Fix tests failing on bash 4.2
* Fix typo in t3431 comment
Changes since v4:
* Make rebase --fork-point fast-forward in more cases
Changes since v5:
* Fix graph illustrations so that the "branch off" is visually in the correct place
* Refactor if-else in can_fast_forward into one-level-deep ifs to increase clarity
Denton Liu (6):
t3431: add rebase --fork-point tests
t3432: test rebase fast-forward behavior
rebase: refactor can_fast_forward into goto tower
rebase: fast-forward --onto in more cases
rebase: fast-forward --fork-point in more cases
rebase: teach rebase --keep-base
Documentation/git-rebase.txt | 30 +++++++++-- builtin/rebase.c | 86 +++++++++++++++++++++++--------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3416-rebase-onto-threedots.sh | 57 +++++++++++++++++++++ t/t3431-rebase-fork-point.sh | 57 +++++++++++++++++++++ t/t3432-rebase-fast-forward.sh | 83 ++++++++++++++++++++++++++++++ 7 files changed, 289 insertions(+), 28 deletions(-) create mode 100755 t/t3431-rebase-fork-point.sh create mode 100755 t/t3432-rebase-fast-forward.sh
Range-diff against v5:
1: 0f1e9ac5c8 ! 1: eb64f6c91d t3431: add rebase --fork-point tests
@@ -18,9 +18,9 @@
+
+. ./test-lib.sh
+
-+# A---B---D---E (master)
-+# \
-+# C*---F---G (side)
++# A---B---D---E (master)
++# \
++# C*---F---G (side)
+#
+# C was formerly part of master but master was rewound to remove C
+#
2: 148027a14b = 2: 4c087ec041 t3432: test rebase fast-forward behavior
-: ---------- > 3: 3d348d2c37 rebase: refactor can_fast_forward into goto tower
3: ec55da0719 ! 4: 2559ab54a2 rebase: fast-forward --onto in more cases
@@ -5,8 +5,8 @@
Before, when we had the following graph,
A---B---C (master)
- \
- D (side)
+ \
+ D (side)
running 'git rebase --onto master... master side' would result in D
being always rebased, no matter what. However, the desired behavior is
@@ -49,6 +49,7 @@
While we're at it, remove a trailing whitespace from rebase.c.
+ Helped-by: Phillip Wood <[email protected]>
Signed-off-by: Denton Liu <[email protected]>
diff --git a/builtin/rebase.c b/builtin/rebase.c
@@ -64,45 +65,26 @@
+ 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;
+ struct commit_list *merge_bases = NULL;
@@
- 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);
-+ if (!oideq(merge_base, &onto->object.oid))
-+ goto done;
- } else {
- oidcpy(merge_base, &null_oid);
-- res = 0;
-+ goto done;
- }
-+
+ if (!oideq(merge_base, &onto->object.oid))
+ goto done;
+
+ if (!upstream)
+ goto done;
+
- free_commit_list(merge_bases);
++ free_commit_list(merge_bases);
+ merge_bases = get_merge_bases(upstream, head);
-+ if (merge_bases && !merge_bases->next) {
-+ if (!oideq(&onto->object.oid, &merge_bases->item->object.oid))
-+ goto done;
-+ } else
++ if (!merge_bases || merge_bases->next) {
+ goto done;
++ }
+
-+ res = 1;
++ if (!oideq(&onto->object.oid, &merge_bases->item->object.oid))
++ goto done;
+
-+done:
-+ if (merge_bases)
-+ free_commit_list(merge_bases);
- return res && is_linear_history(onto, head);
- }
+ res = 1;
+ done:
@@
/*
@@ -115,7 +97,8 @@
- !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.orig_head, &merge_base) &&
++ if (can_fast_forward(options.onto, options.upstream, &options.orig_head,
++ &merge_base) &&
+ !is_interactive(&options) && !options.restrict_revision) {
int flag;4: 2256a902c1 ! 5: 0a466e830f rebase: fast-forward --fork-point in more cases
@@ -27,8 +27,8 @@
{
struct commit *head = lookup_commit(the_repository, head_oid);
@@
+ if (!oideq(merge_base, &onto->object.oid))
goto done;
- }
+ if (restrict_revision && !oideq(&restrict_revision->object.oid, merge_base))
+ goto done;
@@ -40,7 +40,8 @@
* Check if we are already based on onto with linear history,
* but this should be done if this is not an interactive rebase.
*/
-- if (can_fast_forward(options.onto, options.upstream, &options.orig_head, &merge_base) &&
+- if (can_fast_forward(options.onto, options.upstream, &options.orig_head,
+- &merge_base) &&
- !is_interactive(&options) && !options.restrict_revision) {
+ if (can_fast_forward(options.onto, options.upstream, options.restrict_revision,
+ &options.orig_head, &merge_base) &&5: d6e7e1ca4b = 6: c542c7afc1 rebase: teach rebase --keep-base
Signature · 1 line
2.21.0.944.gce45564dfd
PATCH v6 1/6 t3431: add rebase --fork-point tests
--- t/t3431-rebase-fork-point.sh | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 t/t3431-rebase-fork-point.sh
t/t3431-rebase-fork-point.sh new file +53 -0
@@ -0,0 +1,53 @@ +#!/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 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 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 C E D B A' refs/heads/master +test_rebase 'G F C D B A' --onto D refs/heads/master + +test_done
Signature · 1 line
2.21.0.944.gce45564dfd
PATCH v6 2/6 t3432: test rebase fast-forward behavior
In reply to Denton LiuWhen rebase is run on a branch that can be fast-forwarded, this should automatically be done. Create test to ensure this behavior happens.
There are some cases that currently don't pass. The first case is where a feature and master have diverged, running
"git rebase master... master" causes a full rebase to happen even though a fast-forward should happen.
The second case is when we are doing "git rebase --fork-point" and a fork-point commit is found. Once again, a full rebase happens even though a fast-forward should happen.
Mark these cases as failure so we can fix it later.
--- t/t3432-rebase-fast-forward.sh | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100755 t/t3432-rebase-fast-forward.sh
t/t3432-rebase-fast-forward.sh new file +72 -0
@@ -0,0 +1,72 @@ +#!/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="$1" && + shift && + test_expect_$status "git rebase $* with $changes is no-op" " + oldhead=\$(git rev-parse HEAD) && + test_when_finished 'git reset --hard \$oldhead' && + git rebase $* && + newhead=\$(git rev-parse HEAD) && + test_cmp_rev \$oldhead \$newhead + " +} + +changes='no changes' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --no-fork-point +test_rebase_same_head success --fork-point master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto master... master + +test_expect_success 'add work to side' ' + test_commit E +' + +changes='our changes' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --no-fork-point +test_rebase_same_head success --fork-point master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto master... master + +test_expect_success 'add work to upstream' ' + git checkout master && + test_commit F && + git checkout side +' + +changes='our and their changes' +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head failure --onto master... master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head failure --fork-point --onto master... master + +test_done
Signature · 1 line
2.21.0.944.gce45564dfd
PATCH v6 3/6 rebase: refactor can_fast_forward into goto tower
In reply to Denton LiuBefore, can_fast_forward was written with an if-else statement. However, in the future, we may be adding more termination cases which would lead to deeply nested if statements.
Refactor to use a goto tower so that future cases can be easily inserted.
--- builtin/rebase.c | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-)
builtin/rebase.c +15 -9
@@ -899,21 +899,27 @@ static int can_fast_forward(struct commit *onto, 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; } -free_commit_list (merge_bases); + +oidcpy (merge_base, &merge_bases->item->object.oid); +if (!oideq (merge_base, &onto->object.oid)) + goto done; + + res =1 ; + +done: +if (merge_bases) +free_commit_list (merge_bases);return res &&is_linear_history (onto, head); }
Signature · 1 line
2.21.0.944.gce45564dfd
PATCH v6 4/6 rebase: fast-forward --onto in more cases
In reply to Denton LiuBefore, when we had the following graph,
A---B---C (master) \ D (side)
running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to can_fast_forward so that this case can be detected and a fast-forward will be performed. First of all, rewrite the function to use gotos which simplifies the logic. Next, since the
options.upstream && !oidcmp(&options.upstream->object.oid, &options.onto->object.oid)
conditions were removed in cmd_rebase, we reintroduce a substitute in can_fast_forward. In particular, checking the merge bases of upstream and head fixes a failing case in t3416.
The abbreviated graph for t3416 is as follows:
F---G topic / A---B---C---D---E master
and the failing command was
git rebase --onto master...topic F topic
Before, Git would see that there was one merge base (C), and the merge and onto were the same so it would incorrectly return 1, indicating that we could fast-forward. This would cause the rebased graph to be 'ABCFG' when we were expecting 'ABCG'.
With the additional logic, we detect that upstream and head's merge base is F. Since onto isn't F, it means we're not rebasing the full set of commits from master..topic. Since we're excluding some commits, a fast-forward cannot be performed and so we correctly return 0.
Add '-f' to test cases that failed as a result of this change because they were not expecting a fast-forward so that a rebase is forced.
While we're at it, remove a trailing whitespace from rebase.c.
--- builtin/rebase.c | 28 +++++++++++++++++++--------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 4 ++-- 4 files changed, 23 insertions(+), 13 deletions(-)
builtin/rebase.c +19 -9
@@ -895,8 +895,8 @@ 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 object_id *head_oid,struct object_id *merge_base) {struct commit *head =lookup_commit (the_repository, head_oid);struct commit_list *merge_bases =NULL ;
@@ -915,6 +915,18 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid,if (!oideq (merge_base, &onto->object.oid)) 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:
@@ -1688,13 +1700,11 @@ 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. +* but this should be done if this is not 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.orig_head, + &merge_base) && + !is_interactive (&options) && !options.restrict_revision) {int flag;if (!(options.flags &REBASE_FORCE )) {
@@ -1788,7 +1798,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)strbuf_addf (&msg,"%s: checkout %s" ,getenv (GIT_REFLOG_ACTION_ENVIRONMENT ), options.onto_name);if (reset_head (&options.onto->object.oid,"checkout" ,NULL , -RESET_HEAD_DETACH |RESET_ORIG_HEAD | +RESET_HEAD_DETACH |RESET_ORIG_HEAD |RESET_HEAD_RUN_POST_CHECKOUT_HOOK ,NULL , msg.buf))die (_ ("Could not detach HEAD" ));
t/t3400-rebase.sh +1 -1
@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh 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 )
t/t3404-rebase-interactive.sh +1 -1
@@ -1066,7 +1066,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 '
t/t3432-rebase-fast-forward.sh +2 -2
@@ -64,9 +64,9 @@ test_expect_success 'add work to upstream' ' changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B -test_rebase_same_head failure --onto master... master +test_rebase_same_head success --onto master... master test_rebase_same_head failure --fork-point --onto B B test_rebase_same_head failure --fork-point --onto B... B -test_rebase_same_head failure --fork-point --onto master... master +test_rebase_same_head success --fork-point --onto master... master test_done
Signature · 1 line
2.21.0.944.gce45564dfd
PATCH v6 5/6 rebase: fast-forward --fork-point in more cases
In reply to Denton LiuBefore, when we rebased with a --fork-point invocation where the fork-point wasn't empty, we would be setting options.restrict_revision. The fast-forward logic would automatically declare that the rebase was not fast-forwardable if it was set. However, this was painting with a very broad brush.
Refine the logic so that we can fast-forward in the case where the restricted revision is equal to the merge base, since we stop rebasing at the merge base anyway.
--- builtin/rebase.c | 10 +++++++--- t/t3432-rebase-fast-forward.sh | 12 ++++++------ 2 files changed, 13 insertions(+), 9 deletions(-)
builtin/rebase.c +7 -3
@@ -896,6 +896,7 @@ static int is_linear_history(struct commit *from, struct commit *to) }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);
@@ -915,6 +916,9 @@ static int can_fast_forward(struct commit *onto, struct commit *upstream,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;
@@ -1702,9 +1706,9 @@ 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 if this is not an interactive rebase. */ -if (can_fast_forward (options.onto, options.upstream, &options.orig_head, - &merge_base) && - !is_interactive (&options) && !options.restrict_revision) { +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 )) {
t/t3432-rebase-fast-forward.sh +6 -6
@@ -35,8 +35,8 @@ test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master test_rebase_same_head success --no-fork-point test_rebase_same_head success --fork-point master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto B B +test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master test_expect_success 'add work to side' '
@@ -51,8 +51,8 @@ test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master test_rebase_same_head success --no-fork-point test_rebase_same_head success --fork-point master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto B B +test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master test_expect_success 'add work to upstream' '
@@ -65,8 +65,8 @@ changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto B B +test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master test_done
Signature · 1 line
2.21.0.944.gce45564dfd
PATCH v6 6/6 rebase: teach rebase --keep-base
In reply to Denton LiuA 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'. However, while developing the patches, 'master' is also developed further and it is sometimes not the bst idea to keep rebasing on top of 'master', but to keep the base commit as-is.
Alternatively, 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.
--- Documentation/git-rebase.txt | 30 +++++++++++++++-- builtin/rebase.c | 32 ++++++++++++++---- t/t3416-rebase-onto-threedots.sh | 57 ++++++++++++++++++++++++++++++++ t/t3431-rebase-fork-point.sh | 4 +++ t/t3432-rebase-fast-forward.sh | 11 ++++++ 5 files changed, 125 insertions(+), 9 deletions(-)
Documentation/git-rebase.txt +27 -3
@@ -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
@@ -364,6 +382,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>::
@@ -539,6 +561,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 -----------------------
@@ -863,7 +887,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:
builtin/rebase.c +26 -6
@@ -27,8 +27,8 @@#include "branch.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" ),
@@ -1040,6 +1040,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) };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 ;
@@ -1073,6 +1074,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,
@@ -1239,6 +1242,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)usage_with_options (builtin_rebase_usage, builtin_rebase_options); +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 !=NO_ACTION && !in_progress)die (_ ("No rebase in progress?" ));setenv (GIT_REFLOG_ACTION_ENVIRONMENT ,"rebase" ,0 );
@@ -1563,12 +1573,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 {
t/t3416-rebase-onto-threedots.sh +57 -0
64 lines not on this page · raw message
t/t3431-rebase-fork-point.sh +4 -0
@@ -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
t/t3432-rebase-fast-forward.sh +11 -0
@@ -33,11 +33,15 @@ test_rebase_same_head success master test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master +test_rebase_same_head success --keep-base test_rebase_same_head success --no-fork-point +test_rebase_same_head success --keep-base --no-fork-point test_rebase_same_head success --fork-point master test_rebase_same_head success --fork-point --onto B B test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master +test_rebase_same_head success --fork-point --keep-base master test_expect_success 'add work to side' ' test_commit E
@@ -49,11 +53,15 @@ test_rebase_same_head success master test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master +test_rebase_same_head success --keep-base test_rebase_same_head success --no-fork-point +test_rebase_same_head success --keep-base --no-fork-point test_rebase_same_head success --fork-point master test_rebase_same_head success --fork-point --onto B B test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master +test_rebase_same_head success --fork-point --keep-base master test_expect_success 'add work to upstream' ' git checkout master &&
@@ -65,8 +73,11 @@ changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master +test_rebase_same_head success --keep-base test_rebase_same_head success --fork-point --onto B B test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master +test_rebase_same_head success --fork-point --keep-base master test_done
Signature · 1 line
2.21.0.944.gce45564dfd
Re: [PATCH v6 4/6] rebase: fast-forward --onto in more cases
In reply to Denton LiuHi Denton
Quoted 73 lines · Denton Liu
Before, when we had the following graph,
A---B---C (master) \ D (side)running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to
can_fast_forwardso that this case can be detected and a fast-forward will be performed. First of all, rewrite the function to use gotos which simplifies the logic. Next, since theoptions.upstream && !oidcmp(&options.upstream->object.oid, &options.onto->object.oid)conditions were removed in
cmd_rebase, we reintroduce a substitute incan_fast_forward. In particular, checking the merge bases ofupstreamandheadfixes a failing case in t3416.The abbreviated graph for t3416 is as follows:
F---G topic / A---B---C---D---E masterand the failing command was
git rebase --onto master...topic F topicBefore, Git would see that there was one merge base (C), and the merge and onto were the same so it would incorrectly return 1, indicating that we could fast-forward. This would cause the rebased graph to be 'ABCFG' when we were expecting 'ABCG'.
With the additional logic, we detect that upstream and head's merge base is F. Since onto isn't F, it means we're not rebasing the full set of commits from master..topic. Since we're excluding some commits, a fast-forward cannot be performed and so we correctly return 0.
Add '-f' to test cases that failed as a result of this change because they were not expecting a fast-forward so that a rebase is forced.
While we're at it, remove a trailing whitespace from
rebase.c.Helped-by Phillip Wood <[email protected]>Signed-off-by Denton Liu <[email protected]>--- builtin/rebase.c | 28 +++++++++++++++++++--------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 4 ++-- 4 files changed, 23 insertions(+), 13 deletions(-)builtin/rebase.c +2
-2@@ -895,8 +895,8 @@ 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 object_id *head_oid,struct object_id *merge_base) {struct commit *head =lookup_commit (the_repository, head_oid);struct commit_list *merge_bases =NULL ;@@ -915,6 +915,18 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid,if (!oideq (merge_base, &onto->object.oid))
builtin/rebase.c static int can_fast_forward(struct commit *onto, struct object_id *head_oid, · line 925
Quoted 10 lines
builtin/rebase.c +8
-0goto 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; + }
Thanks for changing the ifs in this patch and the previous one, I find it much easier to follow now. Just one style nit (probably not worth a reroll) - we don't put braces around a single conditional statement like this unless another branch of the same if requires them (then all branches should have them).
Best Wishes
Phillip
Quoted 77 lines
builtin/rebase.c +9
-7+ +if (!oideq (&onto->object.oid, &merge_bases->item->object.oid)) + goto done; + res =1 ; done:@@ -1688,13 +1700,11 @@ 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. +* but this should be done if this is not 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.orig_head, + &merge_base) && + !is_interactive (&options) && !options.restrict_revision) {int flag;if (!(options.flags &REBASE_FORCE )) {@@ -1788,7 +1798,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)strbuf_addf (&msg,"%s: checkout %s" ,getenv (GIT_REFLOG_ACTION_ENVIRONMENT ), options.onto_name);if (reset_head (&options.onto->object.oid,"checkout" ,NULL , -RESET_HEAD_DETACH |RESET_ORIG_HEAD | +RESET_HEAD_DETACH |RESET_ORIG_HEAD |RESET_HEAD_RUN_POST_CHECKOUT_HOOK ,NULL , msg.buf))die (_ ("Could not detach HEAD" ));t/t3400-rebase.sh +1
-1@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh 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 )t/t3404-rebase-interactive.sh +1
-1@@ -1066,7 +1066,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 't/t3432-rebase-fast-forward.sh +2
-2@@ -64,9 +64,9 @@ test_expect_success 'add work to upstream' ' changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B -test_rebase_same_head failure --onto master... master +test_rebase_same_head success --onto master... master test_rebase_same_head failure --fork-point --onto B B test_rebase_same_head failure --fork-point --onto B... B -test_rebase_same_head failure --fork-point --onto master... master +test_rebase_same_head success --fork-point --onto master... master test_done
Re: [PATCH v5 3/5] rebase: fast-forward --onto in more cases
In reply to Denton LiuHi Junio,
Quoted 109 lines · Denton Liu
Before, when we had the following graph,
A---B---C (master) \ D (side)running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to
can_fast_forwardso that this case can be detected and a fast-forward will be performed. First of all, rewrite the function to use gotos which simplifies the logic. Next, since theoptions.upstream && !oidcmp(&options.upstream->object.oid, &options.onto->object.oid)conditions were removed in
cmd_rebase, we reintroduce a substitute incan_fast_forward. In particular, checking the merge bases ofupstreamandheadfixes a failing case in t3416.The abbreviated graph for t3416 is as follows:
F---G topic / A---B---C---D---E masterand the failing command was
git rebase --onto master...topic F topicBefore, Git would see that there was one merge base (C), and the merge and onto were the same so it would incorrectly return 1, indicating that we could fast-forward. This would cause the rebased graph to be 'ABCFG' when we were expecting 'ABCG'.
With the additional logic, we detect that upstream and head's merge base is F. Since onto isn't F, it means we're not rebasing the full set of commits from master..topic. Since we're excluding some commits, a fast-forward cannot be performed and so we correctly return 0.
Add '-f' to test cases that failed as a result of this change because they were not expecting a fast-forward so that a rebase is forced.
While we're at it, remove a trailing whitespace from
rebase.c.Signed-off-by Denton Liu <[email protected]>--- builtin/rebase.c | 40 +++++++++++++++++++++++----------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 4 ++-- 4 files changed, 31 insertions(+), 17 deletions(-)builtin/rebase.c +23
-6@@ -895,12 +895,12 @@ 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 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 ;@@ -908,12 +908,29 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid, 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); +if (!oideq (merge_base, &onto->object.oid)) + goto done; }else {oidcpy (merge_base, &null_oid); - res =0 ; + goto done; } + +if (!upstream) + goto done; +free_commit_list (merge_bases); + merge_bases =get_merge_bases (upstream, head); +if (merge_bases && !merge_bases->next) { +if (!oideq (&onto->object.oid, &merge_bases->item->object.oid)) + goto done; + }else + goto done; + + res =1 ; + +done: +if (merge_bases) +free_commit_list (merge_bases);return res &&is_linear_history (onto, head); }
builtin/rebase.c int cmd_rebase(int argc, const char **argv, const char *prefix) · line 1703
Quoted 7 lines
builtin/rebase.c +1
-2@@ -1682,13 +1699,10 @@ 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. +* but this should be done if this is not an interactive rebase. */
I forgot to incorporate your comment about this comment block in the last reroll. I'm not sure if this is worth another reroll so could you please change the comment block to this:
/*
* Check if we are already based on onto with linear history,
* 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. */
Thanks,
Denton
Quoted 63 lines
builtin/rebase.c +3
-5-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.orig_head, &merge_base) && + !is_interactive (&options) && !options.restrict_revision) {int flag;if (!(options.flags &REBASE_FORCE )) {@@ -1782,7 +1796,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)strbuf_addf (&msg,"%s: checkout %s" ,getenv (GIT_REFLOG_ACTION_ENVIRONMENT ), options.onto_name);if (reset_head (&options.onto->object.oid,"checkout" ,NULL , -RESET_HEAD_DETACH |RESET_ORIG_HEAD | +RESET_HEAD_DETACH |RESET_ORIG_HEAD |RESET_HEAD_RUN_POST_CHECKOUT_HOOK ,NULL , msg.buf))die (_ ("Could not detach HEAD" ));t/t3400-rebase.sh +1
-1@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh 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 )t/t3404-rebase-interactive.sh +1
-1@@ -1066,7 +1066,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 't/t3432-rebase-fast-forward.sh +2
-2@@ -64,9 +64,9 @@ test_expect_success 'add work to upstream' ' changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B -test_rebase_same_head failure --onto master... master +test_rebase_same_head success --onto master... master test_rebase_same_head failure --fork-point --onto B B test_rebase_same_head failure --fork-point --onto B... B -test_rebase_same_head failure --fork-point --onto master... master +test_rebase_same_head success --fork-point --onto master... master test_doneSignature · 1 line
2.21.0.921.gb27c68c4e9
v7 cover rebase: learn --keep-base
In reply to Denton LiuHi all,
Sorry for yet another reroll. I realised earlier that rebase doesn't use --git-completion-helper for completion so I had to manually insert --keep-base into the completion script.
Between this and forgetting to update the comment around the can_fast_forward call, I figured with the number of mistakes to fix, it was worth another reroll.
Thanks,
Denton
---
This patchset now depends "[PATCH 1/8] tests (rebase): spell out the --keep-empty option" which is the first patch of Johannes's "Do not use abbreviated options in tests" patchset[1]. (Thanks for catching that, Johannes!)
Changes since v1:
* Squashed old set into one patch
* Fixed indentation style and dangling else
* Added more documentation after discussion with Ævar
Changes since v2:
* Add testing for rebase --fork-point behaviour
* Add testing for rebase fast-forward behaviour
* Make rebase --onto fast-forward in more cases
* Update documentation to include use-case
Changes since v3:
* Fix tests failing on bash 4.2
* Fix typo in t3431 comment
Changes since v4:
* Make rebase --fork-point fast-forward in more cases
Changes since v5:
* Fix graph illustrations so that the "branch off" is visually in the correct place
* Refactor if-else in can_fast_forward into one-level-deep ifs to increase clarity
Changes since v6:
* Remove redundant braces around if
* Update comment around can_fast_forward call
* Add completion for rebase
Denton Liu (6):
t3431: add rebase --fork-point tests
t3432: test rebase fast-forward behavior
rebase: refactor can_fast_forward into goto tower
rebase: fast-forward --onto in more cases
rebase: fast-forward --fork-point in more cases
rebase: teach rebase --keep-base
Documentation/git-rebase.txt | 30 ++++++++- builtin/rebase.c | 87 +++++++++++++++++++------- contrib/completion/git-completion.bash | 2 +- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3416-rebase-onto-threedots.sh | 57 +++++++++++++++++ t/t3431-rebase-fork-point.sh | 57 +++++++++++++++++ t/t3432-rebase-fast-forward.sh | 83 ++++++++++++++++++++++++ 8 files changed, 291 insertions(+), 29 deletions(-) create mode 100755 t/t3431-rebase-fork-point.sh create mode 100755 t/t3432-rebase-fast-forward.sh
Interdiff against v6:
builtin/rebase.c +3 -1
@@ -1723,7 +1723,9 @@ 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 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.upstream, options.restrict_revision, &options.orig_head, &merge_base) &&
contrib/completion/git-completion.bash +1 -1
@@ -2027,7 +2027,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
Signature · 1 line
2.21.0.967.gf85e14fd49
PATCH v7 1/6 t3431: add rebase --fork-point tests
--- t/t3431-rebase-fork-point.sh | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 t/t3431-rebase-fork-point.sh
t/t3431-rebase-fork-point.sh new file +53 -0
@@ -0,0 +1,53 @@ +#!/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 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 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 C E D B A' refs/heads/master +test_rebase 'G F C D B A' --onto D refs/heads/master + +test_done
Signature · 1 line
2.21.0.967.gf85e14fd49
PATCH v7 2/6 t3432: test rebase fast-forward behavior
In reply to Denton LiuWhen rebase is run on a branch that can be fast-forwarded, this should automatically be done. Create test to ensure this behavior happens.
There are some cases that currently don't pass. The first case is where a feature and master have diverged, running
"git rebase master... master" causes a full rebase to happen even though a fast-forward should happen.
The second case is when we are doing "git rebase --fork-point" and a fork-point commit is found. Once again, a full rebase happens even though a fast-forward should happen.
Mark these cases as failure so we can fix it later.
--- t/t3432-rebase-fast-forward.sh | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100755 t/t3432-rebase-fast-forward.sh
t/t3432-rebase-fast-forward.sh new file +72 -0
@@ -0,0 +1,72 @@ +#!/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="$1" && + shift && + test_expect_$status "git rebase $* with $changes is no-op" " + oldhead=\$(git rev-parse HEAD) && + test_when_finished 'git reset --hard \$oldhead' && + git rebase $* && + newhead=\$(git rev-parse HEAD) && + test_cmp_rev \$oldhead \$newhead + " +} + +changes='no changes' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --no-fork-point +test_rebase_same_head success --fork-point master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto master... master + +test_expect_success 'add work to side' ' + test_commit E +' + +changes='our changes' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --no-fork-point +test_rebase_same_head success --fork-point master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto master... master + +test_expect_success 'add work to upstream' ' + git checkout master && + test_commit F && + git checkout side +' + +changes='our and their changes' +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head failure --onto master... master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head failure --fork-point --onto master... master + +test_done
Signature · 1 line
2.21.0.967.gf85e14fd49
PATCH v7 3/6 rebase: refactor can_fast_forward into goto tower
In reply to Denton LiuBefore, can_fast_forward was written with an if-else statement. However, in the future, we may be adding more termination cases which would lead to deeply nested if statements.
Refactor to use a goto tower so that future cases can be easily inserted.
--- builtin/rebase.c | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-)
builtin/rebase.c +15 -9
@@ -899,21 +899,27 @@ static int can_fast_forward(struct commit *onto, 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; } -free_commit_list (merge_bases); + +oidcpy (merge_base, &merge_bases->item->object.oid); +if (!oideq (merge_base, &onto->object.oid)) + goto done; + + res =1 ; + +done: +if (merge_bases) +free_commit_list (merge_bases);return res &&is_linear_history (onto, head); }
Signature · 1 line
2.21.0.967.gf85e14fd49
PATCH v7 4/6 rebase: fast-forward --onto in more cases
In reply to Denton LiuBefore, when we had the following graph,
A---B---C (master) \ D (side)
running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to can_fast_forward so that this case can be detected and a fast-forward will be performed. First of all, rewrite the function to use gotos which simplifies the logic. Next, since the
options.upstream && !oidcmp(&options.upstream->object.oid, &options.onto->object.oid)
conditions were removed in cmd_rebase, we reintroduce a substitute in can_fast_forward. In particular, checking the merge bases of upstream and head fixes a failing case in t3416.
The abbreviated graph for t3416 is as follows:
F---G topic / A---B---C---D---E master
and the failing command was
git rebase --onto master...topic F topic
Before, Git would see that there was one merge base (C), and the merge and onto were the same so it would incorrectly return 1, indicating that we could fast-forward. This would cause the rebased graph to be 'ABCFG' when we were expecting 'ABCG'.
With the additional logic, we detect that upstream and head's merge base is F. Since onto isn't F, it means we're not rebasing the full set of commits from master..topic. Since we're excluding some commits, a fast-forward cannot be performed and so we correctly return 0.
Add '-f' to test cases that failed as a result of this change because they were not expecting a fast-forward so that a rebase is forced.
While we're at it, remove a trailing whitespace from rebase.c.
--- builtin/rebase.c | 29 ++++++++++++++++++++--------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 4 ++-- 4 files changed, 24 insertions(+), 13 deletions(-)
builtin/rebase.c +20 -9
@@ -895,8 +895,8 @@ 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 object_id *head_oid,struct object_id *merge_base) {struct commit *head =lookup_commit (the_repository, head_oid);struct commit_list *merge_bases =NULL ;
@@ -915,6 +915,17 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid,if (!oideq (merge_base, &onto->object.oid)) 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:
@@ -1688,13 +1699,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.orig_head, + &merge_base) && + !is_interactive (&options) && !options.restrict_revision) {int flag;if (!(options.flags &REBASE_FORCE )) {
@@ -1788,7 +1799,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)strbuf_addf (&msg,"%s: checkout %s" ,getenv (GIT_REFLOG_ACTION_ENVIRONMENT ), options.onto_name);if (reset_head (&options.onto->object.oid,"checkout" ,NULL , -RESET_HEAD_DETACH |RESET_ORIG_HEAD | +RESET_HEAD_DETACH |RESET_ORIG_HEAD |RESET_HEAD_RUN_POST_CHECKOUT_HOOK ,NULL , msg.buf))die (_ ("Could not detach HEAD" ));
t/t3400-rebase.sh +1 -1
@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh 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 )
t/t3404-rebase-interactive.sh +1 -1
@@ -1066,7 +1066,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 '
t/t3432-rebase-fast-forward.sh +2 -2
@@ -64,9 +64,9 @@ test_expect_success 'add work to upstream' ' changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B -test_rebase_same_head failure --onto master... master +test_rebase_same_head success --onto master... master test_rebase_same_head failure --fork-point --onto B B test_rebase_same_head failure --fork-point --onto B... B -test_rebase_same_head failure --fork-point --onto master... master +test_rebase_same_head success --fork-point --onto master... master test_done
Signature · 1 line
2.21.0.967.gf85e14fd49
PATCH v7 5/6 rebase: fast-forward --fork-point in more cases
In reply to Denton LiuBefore, when we rebased with a --fork-point invocation where the fork-point wasn't empty, we would be setting options.restrict_revision. The fast-forward logic would automatically declare that the rebase was not fast-forwardable if it was set. However, this was painting with a very broad brush.
Refine the logic so that we can fast-forward in the case where the restricted revision is equal to the merge base, since we stop rebasing at the merge base anyway.
--- builtin/rebase.c | 10 +++++++--- t/t3432-rebase-fast-forward.sh | 12 ++++++------ 2 files changed, 13 insertions(+), 9 deletions(-)
builtin/rebase.c +7 -3
@@ -896,6 +896,7 @@ static int is_linear_history(struct commit *from, struct commit *to) }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);
@@ -915,6 +916,9 @@ static int can_fast_forward(struct commit *onto, struct commit *upstream,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;
@@ -1703,9 +1707,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)* 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.upstream, &options.orig_head, - &merge_base) && - !is_interactive (&options) && !options.restrict_revision) { +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 )) {
t/t3432-rebase-fast-forward.sh +6 -6
@@ -35,8 +35,8 @@ test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master test_rebase_same_head success --no-fork-point test_rebase_same_head success --fork-point master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto B B +test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master test_expect_success 'add work to side' '
@@ -51,8 +51,8 @@ test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master test_rebase_same_head success --no-fork-point test_rebase_same_head success --fork-point master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto B B +test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master test_expect_success 'add work to upstream' '
@@ -65,8 +65,8 @@ changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto B B +test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master test_done
Signature · 1 line
2.21.0.967.gf85e14fd49
PATCH v7 6/6 rebase: teach rebase --keep-base
In reply to Denton LiuA 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'. However, while developing the patches, 'master' is also developed further and it is sometimes not the bst idea to keep rebasing on top of 'master', but to keep the base commit as-is.
Alternatively, 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.
--- Documentation/git-rebase.txt | 30 ++++++++++++-- builtin/rebase.c | 32 ++++++++++++--- contrib/completion/git-completion.bash | 2 +- t/t3416-rebase-onto-threedots.sh | 57 ++++++++++++++++++++++++++ t/t3431-rebase-fork-point.sh | 4 ++ t/t3432-rebase-fast-forward.sh | 11 +++++ 6 files changed, 126 insertions(+), 10 deletions(-)
Documentation/git-rebase.txt +27 -3
@@ -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
@@ -364,6 +382,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>::
@@ -539,6 +561,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 -----------------------
@@ -863,7 +887,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:
builtin/rebase.c +26 -6
@@ -27,8 +27,8 @@#include "branch.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" ),
@@ -1039,6 +1039,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) };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 ;
@@ -1072,6 +1073,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,
@@ -1238,6 +1241,13 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)usage_with_options (builtin_rebase_usage, builtin_rebase_options); +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 !=NO_ACTION && !in_progress)die (_ ("No rebase in progress?" ));setenv (GIT_REFLOG_ACTION_ENVIRONMENT ,"rebase" ,0 );
@@ -1562,12 +1572,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 {
contrib/completion/git-completion.bash +1 -1
@@ -2027,7 +2027,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
t/t3416-rebase-onto-threedots.sh +57 -0
64 lines not on this page · raw message
t/t3431-rebase-fork-point.sh +4 -0
@@ -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
t/t3432-rebase-fast-forward.sh +11 -0
@@ -33,11 +33,15 @@ test_rebase_same_head success master test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master +test_rebase_same_head success --keep-base test_rebase_same_head success --no-fork-point +test_rebase_same_head success --keep-base --no-fork-point test_rebase_same_head success --fork-point master test_rebase_same_head success --fork-point --onto B B test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master +test_rebase_same_head success --fork-point --keep-base master test_expect_success 'add work to side' ' test_commit E
@@ -49,11 +53,15 @@ test_rebase_same_head success master test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master +test_rebase_same_head success --keep-base test_rebase_same_head success --no-fork-point +test_rebase_same_head success --keep-base --no-fork-point test_rebase_same_head success --fork-point master test_rebase_same_head success --fork-point --onto B B test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master +test_rebase_same_head success --fork-point --keep-base master test_expect_success 'add work to upstream' ' git checkout master &&
@@ -65,8 +73,11 @@ changes='our and their changes' test_rebase_same_head success --onto B B test_rebase_same_head success --onto B... B test_rebase_same_head success --onto master... master +test_rebase_same_head success --keep-base master +test_rebase_same_head success --keep-base test_rebase_same_head success --fork-point --onto B B test_rebase_same_head success --fork-point --onto B... B test_rebase_same_head success --fork-point --onto master... master +test_rebase_same_head success --fork-point --keep-base master test_done
Signature · 1 line
2.21.0.967.gf85e14fd49
Re: [PATCH v7 1/6] t3431: add rebase --fork-point tests
In reply to Denton LiuHi Junio,
Quoted 31 lines · Denton Liu
Signed-off-by Denton Liu <[email protected]>--- t/t3431-rebase-fork-point.sh | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 t/t3431-rebase-fork-point.sht/t3431-rebase-fork-point.sh new file +19
-0@@ -0,0 +1,53 @@ +#!/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 &&
t/t3431-rebase-fork-point.sh line 29
Quoted 10 lines
t/t3431-rebase-fork-point.sh new file +10
-0+ 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() {
I read in an email thread earlier that the preferred style for function definitions is to include a space after the name. Sorry for not catching this earlier.
Could you please change this to test_rebase () { for me?
Also, same comment applies to test_rebase_same_head in 2/6.
Thanks so much,
Denton
Quoted 27 lines
t/t3431-rebase-fork-point.sh new file +24
-0+ 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 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 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 C E D B A' refs/heads/master +test_rebase 'G F C D B A' --onto D refs/heads/master + +test_doneSignature · 1 line
2.21.0.967.gf85e14fd49
v8 cover learn --keep-base & more
In reply to Denton LiuThis is a WIP series I have that I figured I'd send out as-is for comment since Junio said he'd be merging dl/rebase-i-keep-base down.
So I wanted to test it, and as seen early in this series in 3/13 and 4/13 we had significant blindspots in our tests, i.e. there were no tests for whether --no-ff bypassed the amended logic as it should.
As seen from those tests we may have some bugs here, either existing or new, needs more poking at it. We are also inconsistent with whether a --no-ff yields a different SHA-1 after the forced rebase, surely it always should, no?
Then in 9/13 and 10/13 I re-added the incomplete patches I had in
https://public-inbox.org/git/[email protected]/
to see if my tests passed with Denton's --fork-point code, they do. Yay!
Left them there because I was wondering if I needed to port some/all of the tests over, and maybe amend a commit message to reword some of my findings in
https://public-inbox.org/git/[email protected]/
Then I have 11/13 and 12/13 which seem pretty sensible to me as-is, and finally I wanted --preserve-merges and --rebase-merges to also benefit from this logic, so 13/13 is a WIP patch for that. The code should be done (although maybe there's a better way to do it...), but it needs a better commit message & tests.
Denton Liu (6):
t3431: add rebase --fork-point tests
t3432: test rebase fast-forward behavior
rebase: refactor can_fast_forward into goto tower
rebase: fast-forward --onto in more cases
rebase: fast-forward --fork-point in more cases
rebase: teach rebase --keep-base
Ævar Arnfjörð Bjarmason (7):
t3432: distinguish "noop-same" v.s. "work-same" in "same head" tests
t3432: test for --no-ff's interaction with fast-forward rebase tests: test linear branch topology
rebase: don't rebase linear topology with --fork-point
rebase: eliminate side-effects from can_fast_forward()
rebase: add a should_fast_forward() utility function
WIP: can_fast_forward() support for --preserve-merges and
--rebase-merges
Documentation/git-rebase.txt | 30 +++++- builtin/rebase.c | 117 +++++++++++++++++----- contrib/completion/git-completion.bash | 2 +- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3416-rebase-onto-threedots.sh | 57 +++++++++++ t/t3421-rebase-topology-linear.sh | 44 +++++++++ t/t3431-rebase-fork-point.sh | 57 +++++++++++ t/t3432-rebase-fast-forward.sh | 128 +++++++++++++++++++++++++ 9 files changed, 407 insertions(+), 32 deletions(-) create mode 100755 t/t3431-rebase-fork-point.sh create mode 100755 t/t3432-rebase-fast-forward.sh
Range-diff:
1: aececab3b4 = 1: 0c931f6275 t3431: add rebase --fork-point tests 2: 238bf1db83 = 2: b5bdca58db t3432: test rebase fast-forward behavior -: ---------- > 3: 5d057a1240 t3432: distinguish "noop-same" v.s. "work-same" in "same head" tests -: ---------- > 4: 06084d9891 t3432: test for --no-ff's interaction with fast-forward 3: 526c03b5a9 = 5: f5a2172398 rebase: refactor can_fast_forward into goto tower 4: 10572de16f ! 6: ea90934368 rebase: fast-forward --onto in more cases
@@ -146,13 +146,13 @@
+++ b/t/t3432-rebase-fast-forward.sh
@@
changes='our and their changes'
- test_rebase_same_head success --onto B B
- test_rebase_same_head success --onto B... B
--test_rebase_same_head failure --onto master... master
-+test_rebase_same_head success --onto master... master
- test_rebase_same_head failure --fork-point --onto B B
- test_rebase_same_head failure --fork-point --onto B... B --test_rebase_same_head failure --fork-point --onto master... master -+test_rebase_same_head success --fork-point --onto master... 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 failure work same success work diff --onto master... master ++test_rebase_same_head success noop same success work diff --onto master... master
+ 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 failure work same success work diff --fork-point --onto master... master ++test_rebase_same_head success noop same success work diff --fork-point --onto master... master
test_done
5: 446985193c ! 7: 46660cefe0 rebase: fast-forward --fork-point in more cases
@@ -55,35 +55,45 @@
--- a/t/t3432-rebase-fast-forward.sh
+++ b/t/t3432-rebase-fast-forward.sh
@@
- test_rebase_same_head success --onto master... master
- test_rebase_same_head success --no-fork-point
- test_rebase_same_head success --fork-point master --test_rebase_same_head failure --fork-point --onto B B --test_rebase_same_head failure --fork-point --onto B... B -+test_rebase_same_head success --fork-point --onto B B -+test_rebase_same_head success --fork-point --onto B... B
- test_rebase_same_head success --fork-point --onto master... master
+ }
- test_expect_success 'add work to side' '
-@@
- test_rebase_same_head success --onto master... master
- test_rebase_same_head success --no-fork-point
- test_rebase_same_head success --fork-point master
--test_rebase_same_head failure --fork-point --onto B B
--test_rebase_same_head failure --fork-point --onto B... B -+test_rebase_same_head success --fork-point --onto B B
-+test_rebase_same_head success --fork-point --onto B... B
- test_rebase_same_head success --fork-point --onto master... master
+ changes='no changes'
+-test_rebase_same_head success work same success work same ++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 --no-fork-point +-test_rebase_same_head success work same success work same --fork-point master +-test_rebase_same_head failure noop 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 work same success work same --fork-point --onto master... master ++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_expect_success 'add work to upstream' '
-@@
- test_rebase_same_head success --onto B B
- test_rebase_same_head success --onto B... B
- test_rebase_same_head success --onto master... master
--test_rebase_same_head failure --fork-point --onto B B
--test_rebase_same_head failure --fork-point --onto B... B -+test_rebase_same_head success --fork-point --onto B B
-+test_rebase_same_head success --fork-point --onto B... B
- test_rebase_same_head success --fork-point --onto master... master
+ test_expect_success 'add work same to side' '
+ test_commit E
+ '
+
+ changes='our changes'
+-test_rebase_same_head success work same success work same ++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 --no-fork-point +-test_rebase_same_head success work same success work same --fork-point master +-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 work same success work same --fork-point --onto master... master ++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_done
+ test_expect_success 'add work same to upstream' '
+ git checkout master &&
6: 88096495c2 ! 8: a59ff76704 rebase: teach rebase --keep-base
@@ -149,8 +149,8 @@
N_("allow pre-rebase hook to run")),
OPT_NEGBIT('q', "quiet", &options.flags,
@@
- usage_with_options(builtin_rebase_usage,
- builtin_rebase_options);
+ warning(_("git rebase --preserve-merges is deprecated. "
+ "Use --rebase-merges instead."));
+ if (keep_base) {
+ if (options.onto_name)
@@ -296,46 +296,46 @@
--- a/t/t3432-rebase-fast-forward.sh
+++ b/t/t3432-rebase-fast-forward.sh
@@
- test_rebase_same_head success --onto B B
- test_rebase_same_head success --onto B... B
- test_rebase_same_head success --onto master... master
-+test_rebase_same_head success --keep-base master
-+test_rebase_same_head success --keep-base
- test_rebase_same_head success --no-fork-point
-+test_rebase_same_head success --keep-base --no-fork-point
- test_rebase_same_head success --fork-point master
- test_rebase_same_head success --fork-point --onto B B
- test_rebase_same_head success --fork-point --onto B... B
- test_rebase_same_head success --fork-point --onto master... master -+test_rebase_same_head success --fork-point --keep-base 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 to side' '
+ test_expect_success 'add work same to side' '
test_commit E
@@
- test_rebase_same_head success --onto B B
- test_rebase_same_head success --onto B... B
- test_rebase_same_head success --onto master... master
-+test_rebase_same_head success --keep-base master
-+test_rebase_same_head success --keep-base
- test_rebase_same_head success --no-fork-point
-+test_rebase_same_head success --keep-base --no-fork-point
- test_rebase_same_head success --fork-point master
- test_rebase_same_head success --fork-point --onto B B
- test_rebase_same_head success --fork-point --onto B... B
- test_rebase_same_head success --fork-point --onto master... master -+test_rebase_same_head success --fork-point --keep-base 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 to upstream' '
+ test_expect_success 'add work same to upstream' '
git checkout master &&
@@
- test_rebase_same_head success --onto B B
- test_rebase_same_head success --onto B... B
- test_rebase_same_head success --onto master... master
-+test_rebase_same_head success --keep-base master
-+test_rebase_same_head success --keep-base
- test_rebase_same_head success --fork-point --onto B B
- test_rebase_same_head success --fork-point --onto B... B
- test_rebase_same_head success --fork-point --onto master... master -+test_rebase_same_head success --fork-point --keep-base 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 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
-: ---------- > 9: 46ccfca308 rebase tests: test linear branch topology -: ---------- > 10: b06e84c6d6 rebase: don't rebase linear topology with --fork-point -: ---------- > 11: bf13aa8a80 rebase: eliminate side-effects from can_fast_forward() -: ---------- > 12: 2ffbb6c342 rebase: add a should_fast_forward() utility function -: ---------- > 13: 20c38b78c7 WIP: can_fast_forward() support for --preserve-merges and --rebase-merges
Signature · 1 line
2.21.0.1020.gf2820cf01a
RFC PATCH v8 1/13 t3431: add rebase --fork-point tests
In reply to Denton LiuFrom: Denton Liu <[email protected]>
--- t/t3431-rebase-fork-point.sh | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 t/t3431-rebase-fork-point.sh
t/t3431-rebase-fork-point.sh new file +53 -0
@@ -0,0 +1,53 @@ +#!/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 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 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 C E D B A' refs/heads/master +test_rebase 'G F C D B A' --onto D refs/heads/master + +test_done
Signature · 1 line
2.21.0.1020.gf2820cf01a
RFC PATCH v8 2/13 t3432: test rebase fast-forward behavior
In reply to Denton LiuFrom: Denton Liu <[email protected]>
When rebase is run on a branch that can be fast-forwarded, this should automatically be done. Create test to ensure this behavior happens.
There are some cases that currently don't pass. The first case is where a feature and master have diverged, running
"git rebase master... master" causes a full rebase to happen even though a fast-forward should happen.
The second case is when we are doing "git rebase --fork-point" and a fork-point commit is found. Once again, a full rebase happens even though a fast-forward should happen.
Mark these cases as failure so we can fix it later.
--- t/t3432-rebase-fast-forward.sh | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100755 t/t3432-rebase-fast-forward.sh
t/t3432-rebase-fast-forward.sh new file +72 -0
@@ -0,0 +1,72 @@ +#!/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="$1" && + shift && + test_expect_$status "git rebase $* with $changes is no-op" " + oldhead=\$(git rev-parse HEAD) && + test_when_finished 'git reset --hard \$oldhead' && + git rebase $* && + newhead=\$(git rev-parse HEAD) && + test_cmp_rev \$oldhead \$newhead + " +} + +changes='no changes' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --no-fork-point +test_rebase_same_head success --fork-point master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto master... master + +test_expect_success 'add work to side' ' + test_commit E +' + +changes='our changes' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --no-fork-point +test_rebase_same_head success --fork-point master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto master... master + +test_expect_success 'add work to upstream' ' + git checkout master && + test_commit F && + git checkout side +' + +changes='our and their changes' +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head failure --onto master... master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head failure --fork-point --onto master... master + +test_done
Signature · 1 line
2.21.0.1020.gf2820cf01a
RFC PATCH v8 3/13 t3432: distinguish "noop-same" v.s. "work-same" in "same head" tests
In reply to Denton LiuChange "same head" introduced in the preceding commit to check whether the rebase.c code lands in the can_fast_forward() case in, and thus prints out an "is up to date" and aborts early.
In some of these cases we make it past that and to "rewinding head", then do a rebase, only to find out there's nothing to change so HEAD stays at the same OID.
These tests presumed these two cases were the same thing. In terms of where HEAD ends up they are, but we're not only interested in rebase semantics, but also whether or not we're needlessly doing work when we could avoid it entirely.
I'm adding "same" and "diff" here because I'll follow-up and add --no-ff tests, where some of those will be "diff"-erent, so add the "diff" code already.
--- t/t3432-rebase-fast-forward.sh | 79 +++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 31 deletions(-)
t/t3432-rebase-fast-forward.sh +48 -31
@@ -18,55 +18,72 @@ test_expect_success setup ' test_rebase_same_head () { status="$1" && shift && - test_expect_$status "git rebase $* with $changes is no-op" " + what="$1" && + shift && + cmp="$1" && + shift && + test_expect_$status "git rebase $* with $changes is $what" " oldhead=\$(git rev-parse HEAD) && test_when_finished 'git reset --hard \$oldhead' && - git rebase $* && + git rebase $* >stdout && + if test $what = work + then + test_i18ngrep 'rewinding head' stdout + elif test $what = noop + then + test_i18ngrep 'is up to date' stdout + fi && newhead=\$(git rev-parse HEAD) && - test_cmp_rev \$oldhead \$newhead + 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 -test_rebase_same_head success master -test_rebase_same_head success --onto B B -test_rebase_same_head success --onto B... B -test_rebase_same_head success --onto master... master -test_rebase_same_head success --no-fork-point -test_rebase_same_head success --fork-point master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B -test_rebase_same_head success --fork-point --onto master... master +test_rebase_same_head success work same +test_rebase_same_head success noop same master +test_rebase_same_head success noop same --onto B B +test_rebase_same_head success noop same --onto B... B +test_rebase_same_head success noop same --onto master... master +test_rebase_same_head success noop same --no-fork-point +test_rebase_same_head success work same --fork-point master +test_rebase_same_head failure noop same --fork-point --onto B B +test_rebase_same_head failure work same --fork-point --onto B... B +test_rebase_same_head success work same --fork-point --onto master... master -test_expect_success 'add work to side' ' +test_expect_success 'add work same to side' ' test_commit E ' changes='our changes' -test_rebase_same_head success -test_rebase_same_head success master -test_rebase_same_head success --onto B B -test_rebase_same_head success --onto B... B -test_rebase_same_head success --onto master... master -test_rebase_same_head success --no-fork-point -test_rebase_same_head success --fork-point master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B -test_rebase_same_head success --fork-point --onto master... master +test_rebase_same_head success work same +test_rebase_same_head success noop same master +test_rebase_same_head success noop same --onto B B +test_rebase_same_head success noop same --onto B... B +test_rebase_same_head success noop same --onto master... master +test_rebase_same_head success noop same --no-fork-point +test_rebase_same_head success work same --fork-point master +test_rebase_same_head failure work same --fork-point --onto B B +test_rebase_same_head failure work same --fork-point --onto B... B +test_rebase_same_head success work same --fork-point --onto master... master -test_expect_success 'add work to upstream' ' +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 --onto B B -test_rebase_same_head success --onto B... B -test_rebase_same_head failure --onto master... master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B -test_rebase_same_head failure --fork-point --onto master... master +test_rebase_same_head success noop same --onto B B +test_rebase_same_head success noop same --onto B... B +test_rebase_same_head failure work same --onto master... master +test_rebase_same_head failure work same --fork-point --onto B B +test_rebase_same_head failure work same --fork-point --onto B... B +test_rebase_same_head failure work same --fork-point --onto master... master test_done
Signature · 1 line
2.21.0.1020.gf2820cf01a
RFC PATCH v8 4/13 t3432: test for --no-ff's interaction with fast-forward
In reply to Denton LiuAdd more stress tests for the can_fast_forward() case in rebase.c. These tests are getting rather verbose, but now we can see under --ff and --no-ff whether we skip work, or whether we're forced to run the rebase.
These tests aren't supposed to endorse the status quo, just test for what we're currently doing.
--- t/t3432-rebase-fast-forward.sh | 83 ++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 29 deletions(-)
t/t3432-rebase-fast-forward.sh +54 -29
@@ -16,22 +16,47 @@ test_expect_success setup ' ' 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 && - test_expect_$status "git rebase $* with $changes is $what" " + 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 $* >stdout && + 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 '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
@@ -45,32 +70,32 @@ test_rebase_same_head () { } changes='no changes' -test_rebase_same_head success work same -test_rebase_same_head success noop same master -test_rebase_same_head success noop same --onto B B -test_rebase_same_head success noop same --onto B... B -test_rebase_same_head success noop same --onto master... master -test_rebase_same_head success noop same --no-fork-point -test_rebase_same_head success work same --fork-point master -test_rebase_same_head failure noop same --fork-point --onto B B -test_rebase_same_head failure work same --fork-point --onto B... B -test_rebase_same_head success work same --fork-point --onto master... master +test_rebase_same_head success work 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 --no-fork-point +test_rebase_same_head success work same success work same --fork-point master +test_rebase_same_head failure noop 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 work same success work same --fork-point --onto master... master test_expect_success 'add work same to side' ' test_commit E ' changes='our changes' -test_rebase_same_head success work same -test_rebase_same_head success noop same master -test_rebase_same_head success noop same --onto B B -test_rebase_same_head success noop same --onto B... B -test_rebase_same_head success noop same --onto master... master -test_rebase_same_head success noop same --no-fork-point -test_rebase_same_head success work same --fork-point master -test_rebase_same_head failure work same --fork-point --onto B B -test_rebase_same_head failure work same --fork-point --onto B... B -test_rebase_same_head success work same --fork-point --onto master... master +test_rebase_same_head success work 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 --no-fork-point +test_rebase_same_head success work same success work same --fork-point master +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 work same success work same --fork-point --onto master... master test_expect_success 'add work same to upstream' ' git checkout master &&
@@ -79,11 +104,11 @@ test_expect_success 'add work same to upstream' ' ' changes='our and their changes' -test_rebase_same_head success noop same --onto B B -test_rebase_same_head success noop same --onto B... B -test_rebase_same_head failure work same --onto master... master -test_rebase_same_head failure work same --fork-point --onto B B -test_rebase_same_head failure work same --fork-point --onto B... B -test_rebase_same_head failure work same --fork-point --onto master... 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 failure work same success work diff --onto master... master +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 failure work same success work diff --fork-point --onto master... master test_done
Signature · 1 line
2.21.0.1020.gf2820cf01a
RFC PATCH v8 5/13 rebase: refactor can_fast_forward into goto tower
In reply to Denton LiuFrom: Denton Liu <[email protected]>
Before, can_fast_forward was written with an if-else statement. However, in the future, we may be adding more termination cases which would lead to deeply nested if statements.
Refactor to use a goto tower so that future cases can be easily inserted.
--- builtin/rebase.c | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-)
builtin/rebase.c +15 -9
@@ -882,21 +882,27 @@ static int can_fast_forward(struct commit *onto, 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; } -free_commit_list (merge_bases); + +oidcpy (merge_base, &merge_bases->item->object.oid); +if (!oideq (merge_base, &onto->object.oid)) + goto done; + + res =1 ; + +done: +if (merge_bases) +free_commit_list (merge_bases);return res &&is_linear_history (onto, head); }
Signature · 1 line
2.21.0.1020.gf2820cf01a
RFC PATCH v8 6/13 rebase: fast-forward --onto in more cases
In reply to Denton LiuFrom: Denton Liu <[email protected]>
Before, when we had the following graph,
A---B---C (master) \ D (side)
running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to can_fast_forward so that this case can be detected and a fast-forward will be performed. First of all, rewrite the function to use gotos which simplifies the logic. Next, since the
options.upstream && !oidcmp(&options.upstream->object.oid, &options.onto->object.oid)
conditions were removed in cmd_rebase, we reintroduce a substitute in can_fast_forward. In particular, checking the merge bases of upstream and head fixes a failing case in t3416.
The abbreviated graph for t3416 is as follows:
F---G topic / A---B---C---D---E master
and the failing command was
git rebase --onto master...topic F topic
Before, Git would see that there was one merge base (C), and the merge and onto were the same so it would incorrectly return 1, indicating that we could fast-forward. This would cause the rebased graph to be 'ABCFG' when we were expecting 'ABCG'.
With the additional logic, we detect that upstream and head's merge base is F. Since onto isn't F, it means we're not rebasing the full set of commits from master..topic. Since we're excluding some commits, a fast-forward cannot be performed and so we correctly return 0.
Add '-f' to test cases that failed as a result of this change because they were not expecting a fast-forward so that a rebase is forced.
While we're at it, remove a trailing whitespace from rebase.c.
--- builtin/rebase.c | 29 ++++++++++++++++++++--------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 4 ++-- 4 files changed, 24 insertions(+), 13 deletions(-)
builtin/rebase.c +20 -9
@@ -878,8 +878,8 @@ 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 object_id *head_oid,struct object_id *merge_base) {struct commit *head =lookup_commit (the_repository, head_oid);struct commit_list *merge_bases =NULL ;
@@ -898,6 +898,17 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid,if (!oideq (merge_base, &onto->object.oid)) 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:
@@ -1664,13 +1675,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.orig_head, + &merge_base) && + !is_interactive (&options) && !options.restrict_revision) {int flag;if (!(options.flags &REBASE_FORCE )) {
@@ -1764,7 +1775,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)strbuf_addf (&msg,"%s: checkout %s" ,getenv (GIT_REFLOG_ACTION_ENVIRONMENT ), options.onto_name);if (reset_head (&options.onto->object.oid,"checkout" ,NULL , -RESET_HEAD_DETACH |RESET_ORIG_HEAD | +RESET_HEAD_DETACH |RESET_ORIG_HEAD |RESET_HEAD_RUN_POST_CHECKOUT_HOOK ,NULL , msg.buf))die (_ ("Could not detach HEAD" ));
t/t3400-rebase.sh +1 -1
@@ -295,7 +295,7 @@ test_expect_success 'rebase--am.sh 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 )
t/t3404-rebase-interactive.sh +1 -1
@@ -1063,7 +1063,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 '
t/t3432-rebase-fast-forward.sh +2 -2
@@ -106,9 +106,9 @@ test_expect_success 'add work same to upstream' ' 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 failure work same success work diff --onto master... master +test_rebase_same_head success noop same success work diff --onto master... master 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 failure work same success work diff --fork-point --onto master... master +test_rebase_same_head success noop same success work diff --fork-point --onto master... master test_done
Signature · 1 line
2.21.0.1020.gf2820cf01a
RFC PATCH v8 7/13 rebase: fast-forward --fork-point in more cases
In reply to Denton LiuFrom: Denton Liu <[email protected]>
Before, when we rebased with a --fork-point invocation where the fork-point wasn't empty, we would be setting options.restrict_revision. The fast-forward logic would automatically declare that the rebase was not fast-forwardable if it was set. However, this was painting with a very broad brush.
Refine the logic so that we can fast-forward in the case where the restricted revision is equal to the merge base, since we stop rebasing at the merge base anyway.
--- builtin/rebase.c | 10 +++++++--- t/t3432-rebase-fast-forward.sh | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-)
builtin/rebase.c +7 -3
@@ -879,6 +879,7 @@ static int is_linear_history(struct commit *from, struct commit *to) }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);
@@ -898,6 +899,9 @@ static int can_fast_forward(struct commit *onto, struct commit *upstream,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;
@@ -1679,9 +1683,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)* 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.upstream, &options.orig_head, - &merge_base) && - !is_interactive (&options) && !options.restrict_revision) { +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 )) {
t/t3432-rebase-fast-forward.sh +10 -10
@@ -70,32 +70,32 @@ test_rebase_same_head_ () { } changes='no changes' -test_rebase_same_head success work same success work same +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 --no-fork-point -test_rebase_same_head success work same success work same --fork-point master -test_rebase_same_head failure noop 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 work same success work same --fork-point --onto master... master +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_expect_success 'add work same to side' ' test_commit E ' changes='our changes' -test_rebase_same_head success work same success work same +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 --no-fork-point -test_rebase_same_head success work same success work same --fork-point master -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 work same success work same --fork-point --onto master... master +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_expect_success 'add work same to upstream' ' git checkout master &&
Signature · 1 line
2.21.0.1020.gf2820cf01a
RFC PATCH v8 8/13 rebase: teach rebase --keep-base
In reply to Denton LiuFrom: Denton Liu <[email protected]>
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'. However, while developing the patches, 'master' is also developed further and it is sometimes not the bst idea to keep rebasing on top of 'master', but to keep the base commit as-is.
Alternatively, 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.
--- Documentation/git-rebase.txt | 30 ++++++++++++-- builtin/rebase.c | 32 ++++++++++++--- contrib/completion/git-completion.bash | 2 +- t/t3416-rebase-onto-threedots.sh | 57 ++++++++++++++++++++++++++ t/t3431-rebase-fork-point.sh | 4 ++ t/t3432-rebase-fast-forward.sh | 11 +++++ 6 files changed, 126 insertions(+), 10 deletions(-)
Documentation/git-rebase.txt +27 -3
@@ -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 -----------------------
@@ -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:
builtin/rebase.c +26 -6
@@ -27,8 +27,8 @@#include "branch.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" ),
@@ -1022,6 +1022,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) };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 ;
@@ -1055,6 +1056,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,
@@ -1214,6 +1217,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 !=NO_ACTION && !in_progress)die (_ ("No rebase in progress?" ));setenv (GIT_REFLOG_ACTION_ENVIRONMENT ,"rebase" ,0 );
@@ -1538,12 +1548,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 {
contrib/completion/git-completion.bash +1 -1
@@ -2028,7 +2028,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
t/t3416-rebase-onto-threedots.sh +57 -0
64 lines not on this page · raw message
t/t3431-rebase-fork-point.sh +4 -0
@@ -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
t/t3432-rebase-fast-forward.sh +11 -0
@@ -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
Signature · 1 line
2.21.0.1020.gf2820cf01a
RFC PATCH v8 9/13 rebase tests: test linear branch topology
In reply to Denton LiuAdd tests rebasing a linear branch topology to linear rebase tests added in 2aad7cace2 ("add simple tests of consistency across rebase types", 2013-06-06).
These tests are duplicates of two surrounding tests that do the same with tags pointing to the same objects. Right now there's no change in behavior being introduced, but as we'll see in a subsequent change rebase can have different behaviors when working implicitly with remote tracking branches.
While I'm at it add a --fork-point test, strictly speaking this is redundant to the existing '' test, as no argument to rebase implies --fork-point. But now it's easier to grep for tests that explicitly stress --fork-point.
--- t/t3421-rebase-topology-linear.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+)
t/t3421-rebase-topology-linear.sh +29 -0
@@ -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,10 +67,28 @@ 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 +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
@@ -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
Signature · 1 line
2.21.0.1020.gf2820cf01a
RFC PATCH v8 10/13 rebase: don't rebase linear topology with --fork-point
In reply to Denton LiuFix a regression introduced in 4f21454b55 ("merge-base: handle --fork-point without reflog", 2016-10-12).
Before that change having a linear history on top of an upstream master would with --fork-point (aka argument-less rebase) tell us there was nothing to be done:
$ git rebase
Current branch master is up to date.After that change "rebase" will always redundantly find that it has work to do (it doesn't):
$ git rebase
First, rewinding head to replay your work on top of it...
Applying: [...]Whereas equivalently running:
$ git rebase @{upstream}
$ git rebase $(git merge-base --fork-point @{u})Gives us the old behavior of doing nothing.
Now, why did we have this regression? Fully digging into it yields an interesting combination of causes:
Way back in 1308c17b3e ("Allow rebase to run if upstream is completely merged", 2007-07-04) "rebase" learned to not do this redundant work when asked to rebase on a commit that was already an ancestor of the current commit.
Then in 1e0dacdbdb ("rebase: omit patch-identical commits with --fork-point", 2014-07-16) a rebase bug was fixed for a case where the history to be rebased was divergent by entirely skipping the 2007-era logic if --fork-point was provided.
But here's the critical thing, *only* if the --fork-point was divergent. At that time "git merge-base --fork-point A B" would return nothing if the two commits weren't divergent.
Then in 4f21454b55 ("merge-base: handle --fork-point without reflog", 2016-10-12) which introduced the regression being fixed here, a bug fix for "git merge-base --fork-point" being run stand-alone by proxy broke this use-case git-rebase.sh was relying on, since it was still assuming that if we didn't have divergent history we'd have no output.
Finally, when "rebase" was rewritten in C a combination of 9a48a615b4 ("builtin rebase: try to fast forward when possible", 2018-09-04), 103148aad8 ("merge-base --fork-point: extract libified function", 2018-09-04) and 92d0d74e8d ("builtin rebase: support fork-point option", 2018-09-04) faithfully re-implemented the then-buggy behavior.
So let's fix this. It's easy enough, we just stop explicitly excluding --fork-point from the can_fast_forward(...) test we're doing, which as discussed above is faithfully ported over from buggy shellscript-era logic.
I'm not bothering to fix this in the legacy rebase mode. As discussed in 9aea5e9286 ("rebase: fix regression in rebase.useBuiltin=false test mode", 2019-02-13) it'll be going away shortly after 2.21.0 lands.
--- t/t3421-rebase-topology-linear.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+)
t/t3421-rebase-topology-linear.sh +15 -0
@@ -55,6 +55,21 @@ 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 $* is no-op if remote upstream is an ancestor" " + reset_rebase && + GIT_TEST_REBASE_USE_BUILTIN=true git rebase $* branch-b branch-e && + test_cmp_rev e HEAD + " +} +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
Signature · 1 line
2.21.0.1020.gf2820cf01a
RFC PATCH v8 11/13 rebase: eliminate side-effects from can_fast_forward()
In reply to Denton LiuThe can_fast_forward() function is potentially much more expensive than is_interactive() since it e.g. might need to call is_linear_history().
So reversing the two looks like an obvious improvement, but doing so reveals a previously hidden caveat: We need the can_fast_forward() function to populate data used later, namely the "merge_bases" variable. This has been the case since it was added in 9a48a615b4 ("builtin rebase: try to fast forward when possible", 2018-09-04).
So let's refactor it into two functions. Now we'll always call populate_merge_bases(), and then only call can_fast_forward() if is_interactive() is false, making this both faster in pathological cases, and more importantly easier to follow.
--- builtin/rebase.c | 41 +++++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 14 deletions(-)
builtin/rebase.c +27 -14
@@ -878,24 +878,30 @@ static int is_linear_history(struct commit *from, struct commit *to)return 1 ; } -static int can_fast_forward (struct commit *onto,struct commit *upstream, +static void populate_merge_bases (struct commit *head,struct commit *onto, +struct commit_list *merge_bases, +struct object_id *merge_base) +{ + merge_bases =get_merge_bases (onto, head); +if (!merge_bases || merge_bases->next) { +oidcpy (merge_base, &null_oid); +return ; + } +oidcpy (merge_base, &merge_bases->item->object.oid); +} + +static int can_fast_forward (struct commit *head, +struct commit *onto,struct commit *upstream,struct commit *restrict_revision, -struct object_id *head_oid,struct object_id *merge_base) +struct object_id *head_oid, +struct commit_list *merge_bases, +struct object_id *merge_base) { -struct commit *head =lookup_commit (the_repository, head_oid); -struct commit_list *merge_bases =NULL ;int res =0 ;if (!head) goto done; - merge_bases =get_merge_bases (onto, head); -if (!merge_bases || merge_bases->next) { -oidcpy (merge_base, &null_oid); - goto done; - } - -oidcpy (merge_base, &merge_bases->item->object.oid);if (!oideq (merge_base, &onto->object.oid)) goto done;
@@ -1154,6 +1160,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) OPT_END(), };int i; +struct commit *head_commit; +struct commit_list *merge_bases =NULL ;if (argc ==2 && !strcmp (argv[1 ],"-h" )) usage_with_options(builtin_rebase_usage,
@@ -1703,9 +1711,14 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)* 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.upstream, options.restrict_revision, - &options.orig_head, &merge_base) && - !is_interactive (&options)) { + head_commit =lookup_commit (the_repository, &options.orig_head); +if (head_commit) +populate_merge_bases (head_commit, options.onto, merge_bases, + &merge_base); +if (!is_interactive (&options) && +can_fast_forward (head_commit, options.onto, options.upstream, + options.restrict_revision, &options.orig_head, + merge_bases, &merge_base)) {int flag;if (!(options.flags &REBASE_FORCE )) {
Signature · 1 line
2.21.0.1020.gf2820cf01a
RFC PATCH v8 12/13 rebase: add a should_fast_forward() utility function
In reply to Denton LiuThe is_interactive() test gets tricky around --rebase-merges. Let's split our use of it for the purposes of whether we should try fast-forwarding a rebase into a utility function to prepare for adding more logic to that specific codepath.
--- builtin/rebase.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-)
builtin/rebase.c +6 -1
@@ -890,6 +890,11 @@ static void populate_merge_bases(struct commit *head, struct commit *onto,oidcpy (merge_base, &merge_bases->item->object.oid); } +static int should_fast_forward (struct rebase_options *opts) +{ +return !is_interactive (opts); +} + static int can_fast_forward(struct commit *head, struct commit *onto, struct commit *upstream, struct commit *restrict_revision,
@@ -1715,7 +1720,7 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)if (head_commit)populate_merge_bases (head_commit, options.onto, merge_bases, &merge_base); -if (!is_interactive (&options) && +if (should_fast_forward (&options) &&can_fast_forward (head_commit, options.onto, options.upstream, options.restrict_revision, &options.orig_head, merge_bases, &merge_base)) {
Signature · 1 line
2.21.0.1020.gf2820cf01a
RFC PATCH v8 13/13 WIP: can_fast_forward() support for --preserve-merges and --rebase-merges
In reply to Denton LiuThis seems to work, needs more tests etc...
--- builtin/rebase.c | 6 ++++++ t/t3432-rebase-fast-forward.sh | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-)
builtin/rebase.c +6 -0
@@ -892,6 +892,12 @@ static void populate_merge_bases(struct commit *head, struct commit *onto,static int should_fast_forward (struct rebase_options *opts) { +if (!(opts->flags &REBASE_INTERACTIVE_EXPLICIT )) { +if (opts->rebase_merges) +return 1 ; +if (opts->type ==REBASE_PRESERVE_MERGES ) +return 1 ; + }return !is_interactive (opts); }
t/t3432-rebase-fast-forward.sh +5 -2
@@ -44,12 +44,13 @@ test_rebase_same_head_ () { 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 && + git rebase$flag $* >stdout 2>stderr && 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 + test_i18ngrep 'rewinding head' stdout || + test_i18ngrep 'is up to date, rebase forced' stdout elif test $what = noop then test_i18ngrep 'is up to date' stdout &&
@@ -79,6 +80,8 @@ test_rebase_same_head success noop same success noop-force same --keep-base mast 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 noop-force same --preserve-merges +test_rebase_same_head success noop same success noop-force same --rebase-merges 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
Signature · 1 line
2.21.0.1020.gf2820cf01a
Re: [RFC WIP PATCH v8 00/13] learn --keep-base & more
In reply to Ævar Arnfjörð BjarmasonQuoted 9 lines · Ævar Arnfjörð Bjarmason
This is a WIP series I have that I figured I'd send out as-is for comment since Junio said he'd be merging dl/rebase-i-keep-base down.
So I wanted to test it, and as seen early in this series in 3/13 and 4/13 we had significant blindspots in our tests, i.e. there were no tests for whether --no-ff bypassed the amended logic as it should.
As seen from those tests we may have some bugs here, either existing or new, needs more poking at it.
Thanks. I am actually OK to keep dl/rebase-i-keep-base out of 'next' to iron out the kinks. It's not like we are in a hurry to deliber an important fix to our users---the topic is adding a new feature and attempting to fix a minor irritation (i.e. lost opportunity to fast-forward).
Quoted 15 lines
Then in 9/13 and 10/13 I re-added the incomplete patches I had in
https://public-inbox.org/git/[email protected]/
to see if my tests passed with Denton's --fork-point code, they do. Yay!Left them there because I was wondering if I needed to port some/all of the tests over, and maybe amend a commit message to reword some of my findings in
https://public-inbox.org/git/[email protected]/Then I have 11/13 and 12/13 which seem pretty sensible to me as-is, and finally I wanted --preserve-merges and --rebase-merges to also benefit from this logic, so 13/13 is a WIP patch for that. The code should be done (although maybe there's a better way to do it...), but it needs a better commit message & tests.
Re: [RFC WIP PATCH v8 11/13] rebase: eliminate side-effects from can_fast_forward()
In reply to Ævar Arnfjörð BjarmasonHi Ævar,
Quoted 11 lines · Ævar Arnfjörð Bjarmason
The
can_fast_forward()function is potentially much more expensive thanis_interactive()since it e.g. might need to callis_linear_history().So reversing the two looks like an obvious improvement, but doing so reveals a previously hidden caveat: We need the
can_fast_forward()function to populate data used later, namely the "merge_bases" variable. This has been the case since it was added in9a48a615b4("builtin rebase: try to fast forward when possible", 2018-09-04).
Quoted 4 lines
So let's refactor it into two functions. Now we'll always call
populate_merge_bases(), and then only callcan_fast_forward()ifis_interactive()is false, making this both faster in pathological cases, and more importantly easier to follow.
True.
We might want to mention, though, what exactly that this "pathological case" is: rebasing commits onto an unrelated history.
Quoted 5 lines
builtin/rebase.c +0
-0
builtin/rebase.c static int is_linear_history(struct commit *from, struct commit *to) · line 885
Quoted 9 lines
builtin/rebase.c +5
-1@@ -878,24 +878,30 @@ static int is_linear_history(struct commit *from, struct commit *to)return 1 ; } -static int can_fast_forward(struct commit *onto, struct commit *upstream, +static void populate_merge_bases (struct commit *head,struct commit *onto, +struct commit_list *merge_bases, +struct object_id *merge_base) +{ + merge_bases =get_merge_bases (onto, head);
Hmm. This overrides whatever was passed in via the parameter. Did you mean to make the parameter of type struct commit_list **, i.e. a *pointer to a pointer*?
Quoted 19 lines
builtin/rebase.c +12
-3+if (!merge_bases || merge_bases->next) { +oidcpy (merge_base, &null_oid); +return ; + } +oidcpy (merge_base, &merge_bases->item->object.oid); +} + +static int can_fast_forward (struct commit *head, +struct commit *onto,struct commit *upstream,struct commit *restrict_revision, - struct object_id *head_oid, struct object_id *merge_base) +struct object_id *head_oid, +struct commit_list *merge_bases, +struct object_id *merge_base) { - struct commit *head = lookup_commit(the_repository, head_oid); -struct commit_list *merge_bases =NULL ;int res =0 ;
builtin/rebase.c static int is_linear_history(struct commit *from, struct commit *to) · line 898
Quoted 10 lines
builtin/rebase.c +0
-7if (!head) goto done; - merge_bases =get_merge_bases (onto, head); -if (!merge_bases || merge_bases->next) { -oidcpy (merge_base, &null_oid); - goto done; - } - -oidcpy (merge_base, &merge_bases->item->object.oid);
Do we even use merge_bases after this anymore? I guess not. So we should get rid of this function parameter at this point.
builtin/rebase.c static int is_linear_history(struct commit *from, struct commit *to) · line 905
Quoted 1 line
builtin/rebase.c +0
-0if (!oideq (merge_base, &onto->object.oid))
Uh oh. With this patch, merge_base can be NULL at this point, namely when trying to rebase commits onto an unrelated history.
Quoted 12 lines
builtin/rebase.c +2
-0goto done;@@ -1154,6 +1160,8 @@ int cmd_rebase(int argc, const char **argv, const char *prefix) OPT_END(), };int i; +struct commit *head_commit; +struct commit_list *merge_bases =NULL ;if (argc ==2 && !strcmp (argv[1 ],"-h" )) usage_with_options(builtin_rebase_usage,
builtin/rebase.c int cmd_rebase(int argc, const char **argv, const char *prefix) · line 1714
Quoted 7 lines
builtin/rebase.c +1
-3@@ -1703,9 +1711,14 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)* 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.upstream, options.restrict_revision, - &options.orig_head, &merge_base) && - !is_interactive (&options)) { + head_commit =lookup_commit (the_repository, &options.orig_head);
Hmm. I do not see this line (or any equivalent code) removed in this patch. Is it possible that we do not need to (re-?)initialize head_commit at this point?
I guess I should read the entire patch series because that head_commit isn't there in the worktree I am looking at (oh, if only I had an easy way to review code, but I have to review static patches instead, not even in a proper IDE... ;-)). Will try to find time to do that next week.
Ciao,
Dscho
Quoted 14 lines
builtin/rebase.c +7
-1+if (head_commit) +populate_merge_bases (head_commit, options.onto, merge_bases, + &merge_base); +if (!is_interactive (&options) && +can_fast_forward (head_commit, options.onto, options.upstream, + options.restrict_revision, &options.orig_head, + merge_bases, &merge_base)) {int flag;if (!(options.flags &REBASE_FORCE )) { --2.21.0.1020.gf2820cf01a
Re: [RFC WIP PATCH v8 13/13] WIP: can_fast_forward() support for --preserve-merges and --rebase-merges
In reply to Ævar Arnfjörð BjarmasonHi Ævar,
Quoted 1 line · Ævar Arnfjörð Bjarmason
This seems to work, needs more tests etc...
I can see how it works, but it is a bit limited, and at the same time overzealous.
The reason why we do not enter the fast-forwarding block in the interactive case would appear to me to be that the interactive rebase *might* want to avoid fast-forwarding e.g. with --force-rebase.
However, that is not the only instance where we must not simply
fast-forward: Think --exec. There might be others, too. (I just saw that --signoff sets FORCE_REBASE, but --exec does not, so you cannot even use the FORCE_REBASE flag as indicator.)
Since git rebase -rx "make -j8" is something I use myself, this here patch would therefore break *my* workflow.
This is the "overzealous" part. Now for the "limited" part:
Let's take a step back and ask how the interactive rebase handles these potentially fast-forwarding cases? Via skip_unnecessary_picks().
And that function is ill-prepared for rebasing merges (I specifically do *not* think about --preserve-merges at this point, for all I care, it is already deprecated *and* dropped).
Even if this function *was* well-prepared for rebasing merges, I think that would miss the mark. Take this todo list, for example:
label onto
# Branch dscho
reset onto
pick a123 first
label dscho
# Branch avar
reset onto
pick b789 second
label avar
reset onto
merge -C c124 dscho
merge -C d314 avar
Two branches, both one patch deep, both merged, one after the other. Now, if you insert pick abc zeroth before the first pick, obviously the first branch would no longer be skippable, but the second one totally would be!
This is the "limited" part.
To remedy this, I think what we would need is code in pick_commits(), right where TODO_RESET is handled (or more toward the beginning of that function), that would:
- parse the argument (this is currently done in do_reset() and would have to be refactored out) and pretend that it is HEAD,
- look at the following command: if it is
- a pick, and if its parent agrees with HEAD, pretend that the pick was actually a reset, update the pretended HEAD and keep looking at the next command,
- a merge, and if its option was -C <orig-merge> (not lower-case -c!), and if its parent agrees with HEAD, and if its merge head(s) agree with the original merge commit's (if any), pretend that it was actually a reset <orig-merge>, update the pretended HEAD and keep looking at the next command,
- a label, perform it, but with the pretended HEAD, and keep looking for the next command,
- a reset, update the done and git-rebase-todo files and start the entire spiel from the top,
- otherwise perform the reset.
- all while skipping, this code would need to take care of updating the done and git-rebase-todo files,
- if a reset is necessary, and if it fails, the done and git-rebase-todo files should *not* be updated, but the original reset should be re-scheduled, and
- since this adds quite a bit of code, it should probably be done in a separate function.
Instead of marking this as a left-over bit (which I would either forget, or whose status would be hard to track), I decided to open a ticket:
https://github.com/gitgitgadget/git/issues/209
(I opened GitGitGadget's issues for exactly this kind of use case, because I recently tried to find some useful left-over bits as easy project starters, and even *I* found it super hard to find those, let alone figure out whether they are being/have been addressed already, a mailing list is just not a good bug tracker, even if it is still better than trying to report a bug on Twitter, where I could not even have written this paragraph in a single Tweet.)
Ciao,
Dscho
Quoted 56 lines
Signed-off-by Ævar Arnfjörð Bjarmason <[email protected]>--- builtin/rebase.c | 6 ++++++ t/t3432-rebase-fast-forward.sh | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-)builtin/rebase.c +6
-0@@ -892,6 +892,12 @@ static void populate_merge_bases(struct commit *head, struct commit *onto,static int should_fast_forward (struct rebase_options *opts) { +if (!(opts->flags &REBASE_INTERACTIVE_EXPLICIT )) { +if (opts->rebase_merges) +return 1 ; +if (opts->type ==REBASE_PRESERVE_MERGES ) +return 1 ; + }return !is_interactive (opts); }t/t3432-rebase-fast-forward.sh +5
-3@@ -44,12 +44,13 @@ test_rebase_same_head_ () { 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 && + git rebase$flag $* >stdout 2>stderr && 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 + test_i18ngrep 'rewinding head' stdout || + test_i18ngrep 'is up to date, rebase forced' stdout elif test $what = noop then test_i18ngrep 'is up to date' stdout &&@@ -79,6 +80,8 @@ test_rebase_same_head success noop same success noop-force same --keep-base mast 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 noop-force same --preserve-merges +test_rebase_same_head success noop same success noop-force same --rebase-merges 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 --2.21.0.1020.gf2820cf01a
Re: [RFC WIP PATCH v8 00/13] learn --keep-base & more
In reply to Junio C HamanoQuoted 9 lines · Junio C Hamano
Quoted 7 lines · Ævar Arnfjörð Bjarmason
This is a WIP series I have that I figured I'd send out as-is for comment since Junio said he'd be merging dl/rebase-i-keep-base down.
So I wanted to test it, and as seen early in this series in 3/13 and 4/13 we had significant blindspots in our tests, i.e. there were no tests for whether --no-ff bypassed the amended logic as it should.
Quoted 8 lines
Quoted 2 lines
As seen from those tests we may have some bugs here, either existing or new, needs more poking at it.
Thanks. I am actually OK to keep dl/rebase-i-keep-base out of 'next' to iron out the kinks. It's not like we are in a hurry to deliber an important fix to our users---the topic is adding a new feature and attempting to fix a minor irritation (i.e. lost opportunity to fast-forward).
The 'next' branch has thinned down sufficiently that it may be a good time to resurrect topics like this one that we postponed. If there is (still) interest in them, that is, which I do not know ;-)
Re: [RFC WIP PATCH v8 00/13] learn --keep-base & more
Quoted 20 lines · Junio C Hamano
Quoted 9 lines · Junio C Hamano
Quoted 7 lines · Ævar Arnfjörð Bjarmason
This is a WIP series I have that I figured I'd send out as-is for comment since Junio said he'd be merging dl/rebase-i-keep-base down.
So I wanted to test it, and as seen early in this series in 3/13 and 4/13 we had significant blindspots in our tests, i.e. there were no tests for whether --no-ff bypassed the amended logic as it should.
Quoted 8 lines
Quoted 2 lines
As seen from those tests we may have some bugs here, either existing or new, needs more poking at it.
Thanks. I am actually OK to keep dl/rebase-i-keep-base out of 'next' to iron out the kinks. It's not like we are in a hurry to deliber an important fix to our users---the topic is adding a new feature and attempting to fix a minor irritation (i.e. lost opportunity to fast-forward).
Quoted 4 lines
The 'next' branch has thinned down sufficiently that it may be a good time to resurrect topics like this one that we postponed. If there is (still) interest in them, that is, which I do not know ;-)
I'm definitely still interested in this topic but I haven't had much time to contribute since school's been busy lately.
If Ævar doesn't get to it by around a month from now, I'll polish up the RFC and resubmit.
v9 cover rebase: learn --keep-base and improvements on fast-forward behaviour
In reply to Ævar Arnfjörð BjarmasonHi all, it's been a while but I guess now's a good time as any to resurrect this topic. This is basically a resubmission of Ævar's WIP v8 but I fixed a couple of minor whitespace issues.
In addition, I opted to drop patches 9-13 from v8 since I don't think I can do a good enough job polishing them up and I don't really understand the intricacies of this part of the rebase code. Hopefully, Ævar will pick them up at a later time.
Changes since v1:
* Squashed old set into one patch
* Fixed indentation style and dangling else
* Added more documentation after discussion with Ævar
Changes since v2:
* Add testing for rebase --fork-point behaviour
* Add testing for rebase fast-forward behaviour
* Make rebase --onto fast-forward in more cases
* Update documentation to include use-case
Changes since v3:
* Fix tests failing on bash 4.2
* Fix typo in t3431 comment
Changes since v4:
* Make rebase --fork-point fast-forward in more cases
Changes since v5:
* Fix graph illustrations so that the "branch off" is visually in the correct place
* Refactor if-else in can_fast_forward into one-level-deep ifs to increase clarity
Changes since v6:
* Remove redundant braces around if
* Update comment around can_fast_forward call
* Add completion for rebase
Changes since v7:
* Ævar sent in his WIP patchset
Changes since v8:
* Drop patches 9-13
* Fix some minor whitespace issues from v7
Denton Liu (6):
t3431: add rebase --fork-point tests
t3432: test rebase fast-forward behavior
rebase: refactor can_fast_forward into goto tower
rebase: fast-forward --onto in more cases
rebase: fast-forward --fork-point in more cases
rebase: teach rebase --keep-base
Ævar Arnfjörð Bjarmason (3):
t3432: distinguish "noop-same" v.s. "work-same" in "same head" tests
t3432: test for --no-ff's interaction with fast-forward rebase tests: test linear branch topology
Documentation/git-rebase.txt | 30 +++++- builtin/rebase.c | 85 ++++++++++++----- contrib/completion/git-completion.bash | 2 +- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3416-rebase-onto-threedots.sh | 57 +++++++++++ t/t3421-rebase-topology-linear.sh | 29 ++++++ t/t3431-rebase-fork-point.sh | 57 +++++++++++ t/t3432-rebase-fast-forward.sh | 125 +++++++++++++++++++++++++ 9 files changed, 361 insertions(+), 28 deletions(-) create mode 100755 t/t3431-rebase-fork-point.sh create mode 100755 t/t3432-rebase-fast-forward.sh
Range-diff against v8:
1: eb64f6c91d ! 1: 03f769d410 t3431: add rebase --fork-point tests
@@ t/t3431-rebase-fork-point.sh (new)
+ test_commit G
+'
+
-+test_rebase() {
++test_rebase () {
+ expected="$1" &&
+ shift &&
+ test_expect_success "git rebase $*" "
2: 4c087ec041 ! 2: bc8998079d t3432: test rebase fast-forward behavior
@@ t/t3432-rebase-fast-forward.sh (new)
+ git checkout -t -b side
+'
+
-+test_rebase_same_head() {
++test_rebase_same_head () {
+ status="$1" &&
+ shift &&
+ test_expect_$status "git rebase $* with $changes is no-op" "
-: ---------- > 3: 5c08e2b81f t3432: distinguish "noop-same" v.s. "work-same" in "same head" tests
-: ---------- > 4: 48b4e41a17 t3432: test for --no-ff's interaction with fast-forward
3: 3d348d2c37 = 5: 9bd34b4a40 rebase: refactor can_fast_forward into goto tower
4: 27cbcfaeae ! 6: becb924232 rebase: fast-forward --onto in more cases @@ Commit message
Add '-f' to test cases that failed as a result of this change because they were not expecting a fast-forward so that a rebase is forced.
- While we're at it, remove a trailing whitespace from rebase.c.
-
@@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix
int flag;
if (!(options.flags & REBASE_FORCE)) {
-@@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)
- strbuf_addf(&msg, "%s: checkout %s",
- getenv(GIT_REFLOG_ACTION_ENVIRONMENT), options.onto_name);
- if (reset_head(&options.onto->object.oid, "checkout", NULL,
-- RESET_HEAD_DETACH | RESET_ORIG_HEAD |
-+ RESET_HEAD_DETACH | RESET_ORIG_HEAD |
- RESET_HEAD_RUN_POST_CHECKOUT_HOOK,
- NULL, msg.buf))
- die(_("Could not detach HEAD"));
## t/t3400-rebase.sh ##
-@@ t/t3400-rebase.sh: test_expect_success 'rebase--am.sh and --show-current-patch' ' +@@ t/t3400-rebase.sh: test_expect_success 'rebase --am and --show-current-patch' '
echo two >>init.t &&
git commit -a -m two &&
git tag two &&
@@ t/t3404-rebase-interactive.sh: test_expect_success C_LOCALE_OUTPUT 'rebase --edi
'
## t/t3432-rebase-fast-forward.sh ##
-@@ t/t3432-rebase-fast-forward.sh: test_expect_success 'add work to upstream' ' +@@ t/t3432-rebase-fast-forward.sh: test_expect_success 'add work same to upstream' '
changes='our and their changes'
- test_rebase_same_head success --onto B B
- test_rebase_same_head success --onto B... B
--test_rebase_same_head failure --onto master... master
-+test_rebase_same_head success --onto master... master
- test_rebase_same_head failure --fork-point --onto B B
- test_rebase_same_head failure --fork-point --onto B... B --test_rebase_same_head failure --fork-point --onto master... master -+test_rebase_same_head success --fork-point --onto master... 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 failure work same success work diff --onto master... master ++test_rebase_same_head success noop same success work diff --onto master... master
+ 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 failure work same success work diff --fork-point --onto master... master ++test_rebase_same_head success noop same success work diff --fork-point --onto master... master
test_done
5: 8254730810 ! 7: 4dab5847cb rebase: fast-forward --fork-point in more cases
@@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix
if (!(options.flags & REBASE_FORCE)) {
## t/t3432-rebase-fast-forward.sh ##
-@@ t/t3432-rebase-fast-forward.sh: test_rebase_same_head success --onto B... B
- test_rebase_same_head success --onto master... master
- test_rebase_same_head success --no-fork-point
- test_rebase_same_head success --fork-point master
--test_rebase_same_head failure --fork-point --onto B B
--test_rebase_same_head failure --fork-point --onto B... B -+test_rebase_same_head success --fork-point --onto B B
-+test_rebase_same_head success --fork-point --onto B... B
- test_rebase_same_head success --fork-point --onto master... master
+@@ t/t3432-rebase-fast-forward.sh: test_rebase_same_head_ () {
+ }
+
+ changes='no changes'
+-test_rebase_same_head success work same success work same ++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 --no-fork-point +-test_rebase_same_head success work same success work same --fork-point master +-test_rebase_same_head failure noop 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 work same success work same --fork-point --onto master... master ++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_expect_success 'add work to side' '
-@@ t/t3432-rebase-fast-forward.sh: test_rebase_same_head success --onto B... B
- test_rebase_same_head success --onto master... master
- test_rebase_same_head success --no-fork-point
- test_rebase_same_head success --fork-point master --test_rebase_same_head failure --fork-point --onto B B --test_rebase_same_head failure --fork-point --onto B... B -+test_rebase_same_head success --fork-point --onto B B -+test_rebase_same_head success --fork-point --onto B... B
- test_rebase_same_head success --fork-point --onto master... master
+ test_expect_success 'add work same to side' '
+ test_commit E
+ '
- test_expect_success 'add work to upstream' '
-@@ t/t3432-rebase-fast-forward.sh: changes='our and their changes'
- test_rebase_same_head success --onto B B
- test_rebase_same_head success --onto B... B
- test_rebase_same_head success --onto master... master
--test_rebase_same_head failure --fork-point --onto B B
--test_rebase_same_head failure --fork-point --onto B... B -+test_rebase_same_head success --fork-point --onto B B
-+test_rebase_same_head success --fork-point --onto B... B
- test_rebase_same_head success --fork-point --onto master... master
+ changes='our changes'
+-test_rebase_same_head success work same success work same ++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 --no-fork-point +-test_rebase_same_head success work same success work same --fork-point master +-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 work same success work same --fork-point --onto master... master ++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_done
+ test_expect_success 'add work same to upstream' '
+ git checkout master &&
-: ---------- > 8: 4699a90993 rebase tests: test linear branch topology 6: f18d3ee3fa ! 9: 6927aba617 rebase: teach rebase --keep-base
@@ Commit message
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'. However, while developing the patches, 'master' is
- also developed further and it is sometimes not the bst idea to keep
+ 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.
Alternatively, a user wishing to test individual commits in a topic
@@ Documentation/git-rebase.txt: git-rebase - Reapply commits on top of another bas
+ [--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
+ 'git rebase' (--continue | --skip | --abort | --quit | --edit-todo | --show-current-patch) @@ Documentation/git-rebase.txt: 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.
@@ Documentation/git-rebase.txt: NOTE: While an "easy case recovery" sometimes appe
## builtin/rebase.c ##
@@
- #include "branch.h"
+ #include "rebase-interactive.h"
static char const * const builtin_rebase_usage[] = {
- N_("git rebase [-i] [options] [--exec <cmd>] [--onto <newbase>] "
@@ builtin/rebase.c
"--root [<branch>]"),
N_("git rebase --continue | --abort | --skip | --edit-todo"),
@@ builtin/rebase.c: 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;
@@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix
N_("allow pre-rebase hook to run")),
OPT_NEGBIT('q', "quiet", &options.flags,
@@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)
- usage_with_options(builtin_rebase_usage,
- builtin_rebase_options);
+ warning(_("git rebase --preserve-merges is deprecated. "
+ "Use --rebase-merges instead."));
+ if (keep_base) {
+ if (options.onto_name)
@@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix
+ die(_("cannot combine '--keep-base' with '--root'"));
+ }
+
- if (action != NO_ACTION && !in_progress)
+ if (action != ACTION_NONE && !in_progress)
die(_("No rebase in progress?"));
setenv(GIT_REFLOG_ACTION_ENVIRONMENT, "rebase", 0);
@@ builtin/rebase.c: int cmd_rebase(int argc, const char **argv, const char *prefix)
@@ t/t3416-rebase-onto-threedots.sh: test_expect_success 'rebase -i --onto master..
test_done
## t/t3431-rebase-fork-point.sh ##
-@@ t/t3431-rebase-fork-point.sh: test_rebase() { +@@ t/t3431-rebase-fork-point.sh: test_rebase () {
test_rebase 'G F E D B A'
test_rebase 'G F D B A' --onto D
@@ t/t3431-rebase-fork-point.sh: test_rebase() {
test_done
## t/t3432-rebase-fast-forward.sh ##
-@@ t/t3432-rebase-fast-forward.sh: test_rebase_same_head success master
- test_rebase_same_head success --onto B B
- test_rebase_same_head success --onto B... B
- test_rebase_same_head success --onto master... master
-+test_rebase_same_head success --keep-base master
-+test_rebase_same_head success --keep-base
- test_rebase_same_head success --no-fork-point
-+test_rebase_same_head success --keep-base --no-fork-point
- test_rebase_same_head success --fork-point master
- test_rebase_same_head success --fork-point --onto B B
- test_rebase_same_head success --fork-point --onto B... B
- test_rebase_same_head success --fork-point --onto master... master -+test_rebase_same_head success --fork-point --keep-base master
+@@ t/t3432-rebase-fast-forward.sh: 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 to side' '
+ test_expect_success 'add work same to side' '
test_commit E
-@@ t/t3432-rebase-fast-forward.sh: test_rebase_same_head success master
- test_rebase_same_head success --onto B B
- test_rebase_same_head success --onto B... B
- test_rebase_same_head success --onto master... master
-+test_rebase_same_head success --keep-base master
-+test_rebase_same_head success --keep-base
- test_rebase_same_head success --no-fork-point
-+test_rebase_same_head success --keep-base --no-fork-point
- test_rebase_same_head success --fork-point master
- test_rebase_same_head success --fork-point --onto B B
- test_rebase_same_head success --fork-point --onto B... B
- test_rebase_same_head success --fork-point --onto master... master -+test_rebase_same_head success --fork-point --keep-base master
+@@ t/t3432-rebase-fast-forward.sh: 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 to upstream' '
+ test_expect_success 'add work same to upstream' '
git checkout master &&
@@ t/t3432-rebase-fast-forward.sh: changes='our and their changes'
- test_rebase_same_head success --onto B B
- test_rebase_same_head success --onto B... B
- test_rebase_same_head success --onto master... master
-+test_rebase_same_head success --keep-base master
-+test_rebase_same_head success --keep-base
- test_rebase_same_head success --fork-point --onto B B
- test_rebase_same_head success --fork-point --onto B... B
- test_rebase_same_head success --fork-point --onto master... master -+test_rebase_same_head success --fork-point --keep-base 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 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
Signature · 1 line
2.23.0.248.g3a9dd8fb08
PATCH v9 1/9 t3431: add rebase --fork-point tests
--- t/t3431-rebase-fork-point.sh | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 t/t3431-rebase-fork-point.sh
t/t3431-rebase-fork-point.sh new file +53 -0
@@ -0,0 +1,53 @@ +#!/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 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 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 C E D B A' refs/heads/master +test_rebase 'G F C D B A' --onto D refs/heads/master + +test_done
Signature · 1 line
2.23.0.248.g3a9dd8fb08
PATCH v9 2/9 t3432: test rebase fast-forward behavior
In reply to Denton LiuWhen rebase is run on a branch that can be fast-forwarded, this should automatically be done. Create test to ensure this behavior happens.
There are some cases that currently don't pass. The first case is where a feature and master have diverged, running
"git rebase master... master" causes a full rebase to happen even though a fast-forward should happen.
The second case is when we are doing "git rebase --fork-point" and a fork-point commit is found. Once again, a full rebase happens even though a fast-forward should happen.
Mark these cases as failure so we can fix it later.
--- t/t3432-rebase-fast-forward.sh | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100755 t/t3432-rebase-fast-forward.sh
t/t3432-rebase-fast-forward.sh new file +72 -0
@@ -0,0 +1,72 @@ +#!/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="$1" && + shift && + test_expect_$status "git rebase $* with $changes is no-op" " + oldhead=\$(git rev-parse HEAD) && + test_when_finished 'git reset --hard \$oldhead' && + git rebase $* && + newhead=\$(git rev-parse HEAD) && + test_cmp_rev \$oldhead \$newhead + " +} + +changes='no changes' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --no-fork-point +test_rebase_same_head success --fork-point master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto master... master + +test_expect_success 'add work to side' ' + test_commit E +' + +changes='our changes' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --no-fork-point +test_rebase_same_head success --fork-point master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto master... master + +test_expect_success 'add work to upstream' ' + git checkout master && + test_commit F && + git checkout side +' + +changes='our and their changes' +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head failure --onto master... master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head failure --fork-point --onto master... master + +test_done
Signature · 1 line
2.23.0.248.g3a9dd8fb08
PATCH v9 3/9 t3432: distinguish "noop-same" v.s. "work-same" in "same head" tests
In reply to Denton LiuFrom: Ævar Arnfjörð Bjarmason <[email protected]>
Change "same head" introduced in the preceding commit to check whether the rebase.c code lands in the can_fast_forward() case in, and thus prints out an "is up to date" and aborts early.
In some of these cases we make it past that and to "rewinding head", then do a rebase, only to find out there's nothing to change so HEAD stays at the same OID.
These tests presumed these two cases were the same thing. In terms of where HEAD ends up they are, but we're not only interested in rebase semantics, but also whether or not we're needlessly doing work when we could avoid it entirely.
I'm adding "same" and "diff" here because I'll follow-up and add --no-ff tests, where some of those will be "diff"-erent, so add the "diff" code already.
--- t/t3432-rebase-fast-forward.sh | 79 +++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 31 deletions(-)
t/t3432-rebase-fast-forward.sh +48 -31
@@ -18,55 +18,72 @@ test_expect_success setup ' test_rebase_same_head () { status="$1" && shift && - test_expect_$status "git rebase $* with $changes is no-op" " + what="$1" && + shift && + cmp="$1" && + shift && + test_expect_$status "git rebase $* with $changes is $what" " oldhead=\$(git rev-parse HEAD) && test_when_finished 'git reset --hard \$oldhead' && - git rebase $* && + git rebase $* >stdout && + if test $what = work + then + test_i18ngrep 'rewinding head' stdout + elif test $what = noop + then + test_i18ngrep 'is up to date' stdout + fi && newhead=\$(git rev-parse HEAD) && - test_cmp_rev \$oldhead \$newhead + 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 -test_rebase_same_head success master -test_rebase_same_head success --onto B B -test_rebase_same_head success --onto B... B -test_rebase_same_head success --onto master... master -test_rebase_same_head success --no-fork-point -test_rebase_same_head success --fork-point master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B -test_rebase_same_head success --fork-point --onto master... master +test_rebase_same_head success work same +test_rebase_same_head success noop same master +test_rebase_same_head success noop same --onto B B +test_rebase_same_head success noop same --onto B... B +test_rebase_same_head success noop same --onto master... master +test_rebase_same_head success noop same --no-fork-point +test_rebase_same_head success work same --fork-point master +test_rebase_same_head failure noop same --fork-point --onto B B +test_rebase_same_head failure work same --fork-point --onto B... B +test_rebase_same_head success work same --fork-point --onto master... master -test_expect_success 'add work to side' ' +test_expect_success 'add work same to side' ' test_commit E ' changes='our changes' -test_rebase_same_head success -test_rebase_same_head success master -test_rebase_same_head success --onto B B -test_rebase_same_head success --onto B... B -test_rebase_same_head success --onto master... master -test_rebase_same_head success --no-fork-point -test_rebase_same_head success --fork-point master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B -test_rebase_same_head success --fork-point --onto master... master +test_rebase_same_head success work same +test_rebase_same_head success noop same master +test_rebase_same_head success noop same --onto B B +test_rebase_same_head success noop same --onto B... B +test_rebase_same_head success noop same --onto master... master +test_rebase_same_head success noop same --no-fork-point +test_rebase_same_head success work same --fork-point master +test_rebase_same_head failure work same --fork-point --onto B B +test_rebase_same_head failure work same --fork-point --onto B... B +test_rebase_same_head success work same --fork-point --onto master... master -test_expect_success 'add work to upstream' ' +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 --onto B B -test_rebase_same_head success --onto B... B -test_rebase_same_head failure --onto master... master -test_rebase_same_head failure --fork-point --onto B B -test_rebase_same_head failure --fork-point --onto B... B -test_rebase_same_head failure --fork-point --onto master... master +test_rebase_same_head success noop same --onto B B +test_rebase_same_head success noop same --onto B... B +test_rebase_same_head failure work same --onto master... master +test_rebase_same_head failure work same --fork-point --onto B B +test_rebase_same_head failure work same --fork-point --onto B... B +test_rebase_same_head failure work same --fork-point --onto master... master test_done
Signature · 1 line
2.23.0.248.g3a9dd8fb08
PATCH v9 4/9 t3432: test for --no-ff's interaction with fast-forward
In reply to Denton LiuFrom: Ævar Arnfjörð Bjarmason <[email protected]>
Add more stress tests for the can_fast_forward() case in rebase.c. These tests are getting rather verbose, but now we can see under --ff and --no-ff whether we skip work, or whether we're forced to run the rebase.
These tests aren't supposed to endorse the status quo, just test for what we're currently doing.
--- t/t3432-rebase-fast-forward.sh | 83 ++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 29 deletions(-)
t/t3432-rebase-fast-forward.sh +54 -29
@@ -16,22 +16,47 @@ test_expect_success setup ' ' 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 && - test_expect_$status "git rebase $* with $changes is $what" " + 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 $* >stdout && + 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 '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
@@ -45,32 +70,32 @@ test_rebase_same_head () { } changes='no changes' -test_rebase_same_head success work same -test_rebase_same_head success noop same master -test_rebase_same_head success noop same --onto B B -test_rebase_same_head success noop same --onto B... B -test_rebase_same_head success noop same --onto master... master -test_rebase_same_head success noop same --no-fork-point -test_rebase_same_head success work same --fork-point master -test_rebase_same_head failure noop same --fork-point --onto B B -test_rebase_same_head failure work same --fork-point --onto B... B -test_rebase_same_head success work same --fork-point --onto master... master +test_rebase_same_head success work 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 --no-fork-point +test_rebase_same_head success work same success work same --fork-point master +test_rebase_same_head failure noop 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 work same success work same --fork-point --onto master... master test_expect_success 'add work same to side' ' test_commit E ' changes='our changes' -test_rebase_same_head success work same -test_rebase_same_head success noop same master -test_rebase_same_head success noop same --onto B B -test_rebase_same_head success noop same --onto B... B -test_rebase_same_head success noop same --onto master... master -test_rebase_same_head success noop same --no-fork-point -test_rebase_same_head success work same --fork-point master -test_rebase_same_head failure work same --fork-point --onto B B -test_rebase_same_head failure work same --fork-point --onto B... B -test_rebase_same_head success work same --fork-point --onto master... master +test_rebase_same_head success work 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 --no-fork-point +test_rebase_same_head success work same success work same --fork-point master +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 work same success work same --fork-point --onto master... master test_expect_success 'add work same to upstream' ' git checkout master &&
@@ -79,11 +104,11 @@ test_expect_success 'add work same to upstream' ' ' changes='our and their changes' -test_rebase_same_head success noop same --onto B B -test_rebase_same_head success noop same --onto B... B -test_rebase_same_head failure work same --onto master... master -test_rebase_same_head failure work same --fork-point --onto B B -test_rebase_same_head failure work same --fork-point --onto B... B -test_rebase_same_head failure work same --fork-point --onto master... 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 failure work same success work diff --onto master... master +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 failure work same success work diff --fork-point --onto master... master test_done
Signature · 1 line
2.23.0.248.g3a9dd8fb08
PATCH v9 5/9 rebase: refactor can_fast_forward into goto tower
In reply to Denton LiuBefore, can_fast_forward was written with an if-else statement. However, in the future, we may be adding more termination cases which would lead to deeply nested if statements.
Refactor to use a goto tower so that future cases can be easily inserted.
--- builtin/rebase.c | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-)
builtin/rebase.c +15 -9
@@ -1264,21 +1264,27 @@ static int can_fast_forward(struct commit *onto, 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; } -free_commit_list (merge_bases); + +oidcpy (merge_base, &merge_bases->item->object.oid); +if (!oideq (merge_base, &onto->object.oid)) + goto done; + + res =1 ; + +done: +if (merge_bases) +free_commit_list (merge_bases);return res &&is_linear_history (onto, head); }
Signature · 1 line
2.23.0.248.g3a9dd8fb08
PATCH v9 6/9 rebase: fast-forward --onto in more cases
In reply to Denton LiuBefore, when we had the following graph,
A---B---C (master) \ D (side)
running 'git rebase --onto master... master side' would result in D being always rebased, no matter what. However, the desired behavior is that rebase should notice that this is fast-forwardable and do that instead.
Add detection to can_fast_forward so that this case can be detected and a fast-forward will be performed. First of all, rewrite the function to use gotos which simplifies the logic. Next, since the
options.upstream && !oidcmp(&options.upstream->object.oid, &options.onto->object.oid)
conditions were removed in cmd_rebase, we reintroduce a substitute in can_fast_forward. In particular, checking the merge bases of upstream and head fixes a failing case in t3416.
The abbreviated graph for t3416 is as follows:
F---G topic / A---B---C---D---E master
and the failing command was
git rebase --onto master...topic F topic
Before, Git would see that there was one merge base (C), and the merge and onto were the same so it would incorrectly return 1, indicating that we could fast-forward. This would cause the rebased graph to be 'ABCFG' when we were expecting 'ABCG'.
With the additional logic, we detect that upstream and head's merge base is F. Since onto isn't F, it means we're not rebasing the full set of commits from master..topic. Since we're excluding some commits, a fast-forward cannot be performed and so we correctly return 0.
Add '-f' to test cases that failed as a result of this change because they were not expecting a fast-forward so that a rebase is forced.
--- builtin/rebase.c | 27 +++++++++++++++++++-------- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3432-rebase-fast-forward.sh | 4 ++-- 4 files changed, 23 insertions(+), 12 deletions(-)
builtin/rebase.c +19 -8
@@ -1260,8 +1260,8 @@ 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 object_id *head_oid,struct object_id *merge_base) {struct commit *head =lookup_commit (the_repository, head_oid);struct commit_list *merge_bases =NULL ;
@@ -1280,6 +1280,17 @@ static int can_fast_forward(struct commit *onto, struct object_id *head_oid,if (!oideq (merge_base, &onto->object.oid)) 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:
@@ -2028,13 +2039,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.orig_head, + &merge_base) && + !is_interactive (&options) && !options.restrict_revision) {int flag;if (!(options.flags &REBASE_FORCE )) {
t/t3400-rebase.sh +1 -1
@@ -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 )
t/t3404-rebase-interactive.sh +1 -1
@@ -1058,7 +1058,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 '
t/t3432-rebase-fast-forward.sh +2 -2
@@ -106,9 +106,9 @@ test_expect_success 'add work same to upstream' ' 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 failure work same success work diff --onto master... master +test_rebase_same_head success noop same success work diff --onto master... master 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 failure work same success work diff --fork-point --onto master... master +test_rebase_same_head success noop same success work diff --fork-point --onto master... master test_done
Signature · 1 line
2.23.0.248.g3a9dd8fb08
PATCH v9 7/9 rebase: fast-forward --fork-point in more cases
In reply to Denton LiuBefore, when we rebased with a --fork-point invocation where the fork-point wasn't empty, we would be setting options.restrict_revision. The fast-forward logic would automatically declare that the rebase was not fast-forwardable if it was set. However, this was painting with a very broad brush.
Refine the logic so that we can fast-forward in the case where the restricted revision is equal to the merge base, since we stop rebasing at the merge base anyway.
--- builtin/rebase.c | 10 +++++++--- t/t3432-rebase-fast-forward.sh | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 13 deletions(-)
builtin/rebase.c +7 -3
@@ -1261,6 +1261,7 @@ static int is_linear_history(struct commit *from, struct commit *to) }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);
@@ -1280,6 +1281,9 @@ static int can_fast_forward(struct commit *onto, struct commit *upstream,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;
@@ -2043,9 +2047,9 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)* 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.upstream, &options.orig_head, - &merge_base) && - !is_interactive (&options) && !options.restrict_revision) { +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 )) {
t/t3432-rebase-fast-forward.sh +10 -10
@@ -70,32 +70,32 @@ test_rebase_same_head_ () { } changes='no changes' -test_rebase_same_head success work same success work same +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 --no-fork-point -test_rebase_same_head success work same success work same --fork-point master -test_rebase_same_head failure noop 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 work same success work same --fork-point --onto master... master +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_expect_success 'add work same to side' ' test_commit E ' changes='our changes' -test_rebase_same_head success work same success work same +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 --no-fork-point -test_rebase_same_head success work same success work same --fork-point master -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 work same success work same --fork-point --onto master... master +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_expect_success 'add work same to upstream' ' git checkout master &&
Signature · 1 line
2.23.0.248.g3a9dd8fb08
PATCH v9 8/9 rebase tests: test linear branch topology
In reply to Denton LiuFrom: Ævar Arnfjörð Bjarmason <[email protected]>
Add tests rebasing a linear branch topology to linear rebase tests added in 2aad7cace2 ("add simple tests of consistency across rebase types", 2013-06-06).
These tests are duplicates of two surrounding tests that do the same with tags pointing to the same objects. Right now there's no change in behavior being introduced, but as we'll see in a subsequent change rebase can have different behaviors when working implicitly with remote tracking branches.
While I'm at it add a --fork-point test, strictly speaking this is redundant to the existing '' test, as no argument to rebase implies --fork-point. But now it's easier to grep for tests that explicitly stress --fork-point.
--- t/t3421-rebase-topology-linear.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+)
t/t3421-rebase-topology-linear.sh +29 -0
@@ -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,10 +67,28 @@ 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 +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
@@ -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
Signature · 1 line
2.23.0.248.g3a9dd8fb08
PATCH v9 9/9 rebase: teach rebase --keep-base
In reply to Denton LiuA 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'. However, 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.
Alternatively, 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.
--- Documentation/git-rebase.txt | 30 ++++++++++++-- builtin/rebase.c | 32 ++++++++++++--- contrib/completion/git-completion.bash | 2 +- t/t3416-rebase-onto-threedots.sh | 57 ++++++++++++++++++++++++++ t/t3431-rebase-fork-point.sh | 4 ++ t/t3432-rebase-fast-forward.sh | 11 +++++ 6 files changed, 126 insertions(+), 10 deletions(-)
Documentation/git-rebase.txt +27 -3
@@ -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:
builtin/rebase.c +26 -6
@@ -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" ),
@@ -1397,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 ;
@@ -1415,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,
@@ -1568,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 );
@@ -1903,12 +1913,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 {
contrib/completion/git-completion.bash +1 -1
@@ -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
t/t3416-rebase-onto-threedots.sh +57 -0
64 lines not on this page · raw message
t/t3431-rebase-fork-point.sh +4 -0
@@ -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
t/t3432-rebase-fast-forward.sh +11 -0
@@ -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
Signature · 1 line
2.23.0.248.g3a9dd8fb08
Re: [PATCH v9 5/9] rebase: refactor can_fast_forward into goto tower
In reply to Denton LiuQuoted 40 lines · Denton Liu
Before,
can_fast_forwardwas written with an if-else statement. However, in the future, we may be adding more termination cases which would lead to deeply nested if statements.Refactor to use a goto tower so that future cases can be easily inserted.
Signed-off-by Denton Liu <[email protected]>--- builtin/rebase.c | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-)builtin/rebase.c +5
-9@@ -1264,21 +1264,27 @@ static int can_fast_forward(struct commit *onto, 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; } -free_commit_list (merge_bases);
builtin/rebase.c static int can_fast_forward(struct commit *onto, struct object_id *head_oid, · line 1287
Quoted 10 lines
builtin/rebase.c +10
-0+ +oidcpy (merge_base, &merge_bases->item->object.oid); +if (!oideq (merge_base, &onto->object.oid)) + goto done; + + res =1 ; + +done: +if (merge_bases) +free_commit_list (merge_bases);
free_commit_list() returns immediately when a NULL pointer is passed in, so I'm not sure if this check is really necessary. I think it is a reasonable assumption to make that free* functions work well with NULL inputs.
builtin/rebase.c static int can_fast_forward(struct commit *onto, struct object_id *head_oid, · line 1290
Quoted 3 lines
builtin/rebase.c +0
-0return res &&is_linear_history (onto, head); }
Out of curiosity, since you are going with a goto tower, why not do something like:
done_merge_bases:free_commit_list(merge_bases);
done:
return res && is_linear_history(onto, head);
You jump to done_merge_bases after you have initialized merge_bases, and directly to done before initializing it.
I'm not advocating for either way, just curious if there is a specific reason to do it your way. Anyway, if you drop the if (merge_bases), then it doesn't really matter I suppose.
Signature · 2 lines
Regards, Pratyush Yadav
Re: [PATCH v9 9/9] rebase: teach rebase --keep-base
In reply to Denton LiuQuoted 9 lines · Denton Liu
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... masterin 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'.
This 'However' part doesn't fit the flow. Do you mean 'while initially developing...', or is the choice of 'However' wrong?
Quoted 4 lines
However, 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.
This 'Alternatively' appears to be part of the first set of common scenarios. Maybe it's just 3 common scenarios, all justifying the same improvement.
Quoted 10 lines
Alternatively, 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
Quoted 7 lines
git rebase -i --keep-base masterand
git rebase -x ./test.sh --keep-base masterrespectively.
[remainder snipped]
[also Eric Sunshine dropped due to temporary sending difficulties]
Philip
Re: [PATCH v9 0/9] rebase: learn --keep-base and improvements on fast-forward behaviour
In reply to Denton LiuQuoted 8 lines · Denton Liu
Hi all, it's been a while but I guess now's a good time as any to resurrect this topic. This is basically a resubmission of Ævar's WIP v8 but I fixed a couple of minor whitespace issues.
In addition, I opted to drop patches 9-13 from v8 since I don't think I can do a good enough job polishing them up and I don't really understand the intricacies of this part of the rebase code. Hopefully, Ævar will pick them up at a later time.
Thanks; let's queue this with an updated base (the tip of master) as we are now in a new cycle.
Re: [PATCH v9 5/9] rebase: refactor can_fast_forward into goto tower
In reply to Pratyush YadavQuoted 48 lines · Pratyush Yadav
Quoted 47 lines · Denton Liu
Before,
can_fast_forwardwas written with an if-else statement. However, in the future, we may be adding more termination cases which would lead to deeply nested if statements.Refactor to use a goto tower so that future cases can be easily inserted.
Signed-off-by Denton Liu <[email protected]>--- builtin/rebase.c | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-)builtin/rebase.c +12
-9@@ -1264,21 +1264,27 @@ static int can_fast_forward(struct commit *onto, 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; } -free_commit_list (merge_bases); + +oidcpy (merge_base, &merge_bases->item->object.oid); +if (!oideq (merge_base, &onto->object.oid)) + goto done; + + res =1 ; +
Quoted 8 lines
builtin/rebase.c static int can_fast_forward(struct commit *onto, struct object_id *head_oid, · line 1287
Quoted 3 lines
builtin/rebase.c +3
-0+done: +if (merge_bases) +free_commit_list (merge_bases);
free_commit_list()returns immediately when a NULL pointer is passed in, so I'm not sure if this check is really necessary. I think it is a reasonable assumption to make that free* functions work well with NULL inputs.
I didn't realise that free_commit_list() would freely accept a NULL pointer without segfaulting. I'll remove the surrounding if.
Quoted 8 lines
builtin/rebase.c static int can_fast_forward(struct commit *onto, struct object_id *head_oid, · line 1290
Quoted 4 lines
builtin/rebase.c +0
-0return res &&is_linear_history (onto, head); }Out of curiosity, since you are going with a goto tower, why not do something like:
Quoted 7 lines
done_merge_bases:free_commit_list(merge_bases);
done:
return res &&is_linear_history(onto, head);You jump to
done_merge_basesafter you have initializedmerge_bases, and directly to done before initializing it.
I opted to do it this way since I figured it was less complexity to have a common jump target at the end. Also, in case in the future we decided to add more logic before merge_bases was initialised, we wouldn't have to worry about figuring out which target to jump to.
Quoted 4 lines
I'm not advocating for either way, just curious if there is a specific reason to do it your way. Anyway, if you drop the if (
merge_bases), then it doesn't really matter I suppose.
I'm pretty impartial as well. I'll drop the if and leave the rest as is unless someone else feels strongly about this.
Quoted 4 lines
Signature · 3 lines
-- Regards, Pratyush Yadav
v10 cover rebase: learn --keep-base and improvements on fast-forward behaviour
In reply to Denton LiuThanks for your suggestions, Pratyush and Philip. I've incorporated both of them into this reroll.
Changes since v1:
* Squashed old set into one patch
* Fixed indentation style and dangling else
* Added more documentation after discussion with Ævar
Changes since v2:
* Add testing for rebase --fork-point behaviour
* Add testing for rebase fast-forward behaviour
* Make rebase --onto fast-forward in more cases
* Update documentation to include use-case
Changes since v3:
* Fix tests failing on bash 4.2
* Fix typo in t3431 comment
Changes since v4:
* Make rebase --fork-point fast-forward in more cases
Changes since v5:
* Fix graph illustrations so that the "branch off" is visually in the correct place
* Refactor if-else in can_fast_forward into one-level-deep ifs to increase clarity
Changes since v6:
* Remove redundant braces around if
* Update comment around can_fast_forward call
* Add completion for rebase
Changes since v7:
* Ævar sent in his WIP patchset
Changes since v8:
* Drop patches 9-13
* Fix some minor whitespace issues from v7
Changes since v9:
* Remove unnecessary if-statement in patch 6/9
* Update commit message for patch 9/9 for clarity according to Philip's suggestions
Denton Liu (6):
t3431: add rebase --fork-point tests
t3432: test rebase fast-forward behavior
rebase: refactor can_fast_forward into goto tower
rebase: fast-forward --onto in more cases
rebase: fast-forward --fork-point in more cases
rebase: teach rebase --keep-base
Ævar Arnfjörð Bjarmason (3):
t3432: distinguish "noop-same" v.s. "work-same" in "same head" tests
t3432: test for --no-ff's interaction with fast-forward rebase tests: test linear branch topology
Documentation/git-rebase.txt | 30 +++++- builtin/rebase.c | 84 ++++++++++++----- contrib/completion/git-completion.bash | 2 +- t/t3400-rebase.sh | 2 +- t/t3404-rebase-interactive.sh | 2 +- t/t3416-rebase-onto-threedots.sh | 57 +++++++++++ t/t3421-rebase-topology-linear.sh | 29 ++++++ t/t3431-rebase-fork-point.sh | 57 +++++++++++ t/t3432-rebase-fast-forward.sh | 125 +++++++++++++++++++++++++ 9 files changed, 360 insertions(+), 28 deletions(-) create mode 100755 t/t3431-rebase-fork-point.sh create mode 100755 t/t3432-rebase-fast-forward.sh
Range-diff against v9:
1: 03f769d410 = 1: 03f769d410 t3431: add rebase --fork-point tests
2: bc8998079d = 2: bc8998079d t3432: test rebase fast-forward behavior
3: 5c08e2b81f = 3: 5c08e2b81f t3432: distinguish "noop-same" v.s. "work-same" in "same head" tests
4: 48b4e41a17 = 4: 48b4e41a17 t3432: test for --no-ff's interaction with fast-forward
5: 9bd34b4a40 ! 5: 9acce7c911 rebase: refactor can_fast_forward into goto tower
@@ builtin/rebase.c: static int can_fast_forward(struct commit *onto, struct object
- res = 0;
+ goto done;
}
-- free_commit_list(merge_bases);
+
+ oidcpy(merge_base, &merge_bases->item->object.oid);
+ if (!oideq(merge_base, &onto->object.oid))
@@ builtin/rebase.c: static int can_fast_forward(struct commit *onto, struct object
+ res = 1;
+
+done:
-+ if (merge_bases)
-+ free_commit_list(merge_bases);
+ free_commit_list(merge_bases);
return res && is_linear_history(onto, head);
}
-
6: becb924232 = 6: 3f208421d6 rebase: fast-forward --onto in more cases
7: 4dab5847cb = 7: 126c20a95d rebase: fast-forward --fork-point in more cases
8: 4699a90993 = 8: bf6cc6a610 rebase tests: test linear branch topology
9: 6927aba617 ! 9: 6bc7423ac1 rebase: teach rebase --keep-base@@ Commit message
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'. However, 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.
+ 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.
- Alternatively, a user wishing to test individual commits in a topic
- branch without changing anything may run
+ 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
Signature · 1 line
2.23.0.248.g3a9dd8fb08
PATCH v10 1/9 t3431: add rebase --fork-point tests
--- t/t3431-rebase-fork-point.sh | 53 ++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100755 t/t3431-rebase-fork-point.sh
t/t3431-rebase-fork-point.sh new file +53 -0
@@ -0,0 +1,53 @@ +#!/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 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 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 C E D B A' refs/heads/master +test_rebase 'G F C D B A' --onto D refs/heads/master + +test_done
Signature · 1 line
2.23.0.248.g3a9dd8fb08
PATCH v10 2/9 t3432: test rebase fast-forward behavior
In reply to Denton LiuWhen rebase is run on a branch that can be fast-forwarded, this should automatically be done. Create test to ensure this behavior happens.
There are some cases that currently don't pass. The first case is where a feature and master have diverged, running
"git rebase master... master" causes a full rebase to happen even though a fast-forward should happen.
The second case is when we are doing "git rebase --fork-point" and a fork-point commit is found. Once again, a full rebase happens even though a fast-forward should happen.
Mark these cases as failure so we can fix it later.
--- t/t3432-rebase-fast-forward.sh | 72 ++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100755 t/t3432-rebase-fast-forward.sh
t/t3432-rebase-fast-forward.sh new file +72 -0
@@ -0,0 +1,72 @@ +#!/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="$1" && + shift && + test_expect_$status "git rebase $* with $changes is no-op" " + oldhead=\$(git rev-parse HEAD) && + test_when_finished 'git reset --hard \$oldhead' && + git rebase $* && + newhead=\$(git rev-parse HEAD) && + test_cmp_rev \$oldhead \$newhead + " +} + +changes='no changes' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --no-fork-point +test_rebase_same_head success --fork-point master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto master... master + +test_expect_success 'add work to side' ' + test_commit E +' + +changes='our changes' +test_rebase_same_head success +test_rebase_same_head success master +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head success --onto master... master +test_rebase_same_head success --no-fork-point +test_rebase_same_head success --fork-point master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head success --fork-point --onto master... master + +test_expect_success 'add work to upstream' ' + git checkout master && + test_commit F && + git checkout side +' + +changes='our and their changes' +test_rebase_same_head success --onto B B +test_rebase_same_head success --onto B... B +test_rebase_same_head failure --onto master... master +test_rebase_same_head failure --fork-point --onto B B +test_rebase_same_head failure --fork-point --onto B... B +test_rebase_same_head failure --fork-point --onto master... master + +test_done
Signature · 1 line
2.23.0.248.g3a9dd8fb08