rebase: learn --keep-base
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
Re: [RFC WIP PATCH v8 11/13] rebase: eliminate side-effects from can_fast_forward()
Hi Æ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
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 13/13] WIP: can_fast_forward() support for --preserve-merges and --rebase-merges
Hi Æ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: [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 0/4] rebase: teach rebase --keep-base
Quoted 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...".