git/list[1] front-page[2] threads[3] people[4] search[5] about
 

Re: [PATCH v3 2/2] rebase: set REF_HEAD_DETACH in checkout_up_to_date()

From
Ævar Arnfjörð Bjarmason <avarab@gmail.com>
Date
Mar 17, 2022, 13:42 UTC
Message-ID
<220317.86r170d6zs.gmgdl@evledraar.gmail.com>
In-Reply-To
<bd1c9537ffc503707690ed173b9e6e808d58ce5d.1647487001.git.gitgitgadget@gmail.com>
On Thu, Mar 17 2022, John Cai via GitGitGadget wrote:
Show 19 quoted lines
> From: John Cai <johncai86@gmail.com>
>
> Fixes a bug whereby rebase updates the deferenced reference HEAD points
> to instead of HEAD directly.
>
> If HEAD is on main and if the following is a fast-forward operation,
>
> git rebase $(git rev-parse main) $(git rev-parse topic)
>
> Instead of HEAD being set to $(git rev-parse topic), rebase erroneously
> dereferences HEAD and sets main to $(git rev-parse topic). See [1] for
> the original bug report.
>
> The callstack from checkout_up_to_date() is the following:
>
> cmd_rebase() -> checkout_up_to_date() -> reset_head() -> update_refs()
>  -> update_ref()
>
> When <branch> is not a valid branch but a sha, rebase sets the head_name
..but an OID...
> of rebase_options to NULL. This value gets passed down this call chain
> through the branch member of reset_head_opts also getting set to NULL
> all the way to update_refs(). update_refs() checks ropts.branch to
> decide whether or not to switch brancheds. If ropts.branch is NULL, it
brancheds -> branches.

And maybe a new paragraph before "update_refs()"? I.e. "\n\nThen update_refs() checks..." ?

Show 14 quoted lines
> calls update_ref() to update HEAD. At this point however, from rebase's
> point of view, we want a detached HEAD. But, since checkout_up_to_date()
> does not set the RESET_HEAD_DETACH flag, the update_ref() call will
> deference HEAD and update the branch its pointing to, which in the above
> example is main.
>
> The correct behavior is that git rebase should update HEAD to $(git
> rev-parse topic) without dereferencing it.
>
> Fix this bug by adding the RESET_HEAD_DETACH flag in checkout_up_to_date
> if <branch> is not a valid branch. so that once reset_head() calls
> update_refs(), it calls update_ref() with REF_NO_DEREF which updates
> HEAD directly intead of deferencing it and updating the branch that HEAD
> points to.
But on the "tell" v.s. show... (more below)...
Show 29 quoted lines
>
> Also add a test to ensure this behavior.
>
> 1. https://lore.kernel.org/git/xmqqsfrpbepd.fsf@gitster.g/
>
> Reported-by: Michael McClimon <michael@mcclimon.org>
> Signed-off-by: John Cai <johncai86@gmail.com>
> ---
>  builtin/rebase.c  | 5 ++++-
>  t/t3400-rebase.sh | 9 +++++++++
>  2 files changed, 13 insertions(+), 1 deletion(-)
>
> diff --git a/builtin/rebase.c b/builtin/rebase.c
> index b29ad2b65e7..5ae7fa2a169 100644
> --- a/builtin/rebase.c
> +++ b/builtin/rebase.c
> @@ -827,8 +827,11 @@ static int checkout_up_to_date(struct rebase_options *options)
>  		    getenv(GIT_REFLOG_ACTION_ENVIRONMENT),
>  		    options->switch_to);
>  	ropts.oid = &options->orig_head;
> -	ropts.branch = options->head_name;
>  	ropts.flags = RESET_HEAD_RUN_POST_CHECKOUT_HOOK;
> +	if (options->head_name)
> +		ropts.branch = options->head_name;
> +	else
> +		ropts.flags |=  RESET_HEAD_DETACH;
>  	ropts.head_msg = buf.buf;
>  	if (reset_head(the_repository, &ropts) < 0)
>  		ret = error(_("could not switch to %s"), options->switch_to);
In this case a smaller change of:
    if (!ropts.branch)
		ropts.flags |=  RESET_HEAD_DETACH;
will do the same.

I wonder if just converting it to a designated initializer while we're at it (or a pre-cleanup commit) would be better, i.e.:

	
	diff --git a/builtin/rebase.c b/builtin/rebase.c
	index 5ae7fa2a169..bf4fd4d2920 100644
	--- a/builtin/rebase.c
	+++ b/builtin/rebase.c
	@@ -820,18 +820,18 @@ static int rebase_config(const char *var, const char *value, void *data)
	 static int checkout_up_to_date(struct rebase_options *options)
	 {
	 	struct strbuf buf = STRBUF_INIT;
	-	struct reset_head_opts ropts = { 0 };
	+	struct reset_head_opts ropts = {
	+		.oid = &options->orig_head,
	+		.branch = options->head_name,
	+		.flags = (RESET_HEAD_RUN_POST_CHECKOUT_HOOK |
	+			  (options->head_name ? 0 : RESET_HEAD_DETACH)),
	+	};
	 	int ret = 0;
	 
	 	strbuf_addf(&buf, "%s: checkout %s",
	 		    getenv(GIT_REFLOG_ACTION_ENVIRONMENT),
	 		    options->switch_to);
	-	ropts.oid = &options->orig_head;
	-	ropts.flags = RESET_HEAD_RUN_POST_CHECKOUT_HOOK;
	-	if (options->head_name)
	-		ropts.branch = options->head_name;
	-	else
	-		ropts.flags |=  RESET_HEAD_DETACH;
	+
	 	ropts.head_msg = buf.buf;
	 	if (reset_head(the_repository, &ropts) < 0)
	 		ret = error(_("could not switch to %s"), options->switch_to);
But in any case the functionality will be the same, so this is just
bikeshedding.
	
Show 20 quoted lines
> diff --git a/t/t3400-rebase.sh b/t/t3400-rebase.sh
> index 0643d015255..d5a8ee39fc4 100755
> --- a/t/t3400-rebase.sh
> +++ b/t/t3400-rebase.sh
> @@ -394,6 +394,15 @@ test_expect_success 'switch to branch not checked out' '
>  	git rebase main other
>  '
>  
> +test_expect_success 'switch to non-branch detaches HEAD' '
> +	git checkout main &&
> +	old_main=$(git rev-parse HEAD) &&
> +	git rebase First Second^0 &&
> +	test_cmp_rev HEAD Second &&
> +	test_cmp_rev main $old_main &&
> +	test_must_fail git symbolic-ref HEAD
> +'
> +
>  test_expect_success 'refuse to switch to branch checked out elsewhere' '
>  	git checkout main &&
>  	git worktree add wt &&

I think it's *much* easier to review these sorts of changes where there's a preceding commit that positively asserts what we do now, and we'll then in the "fix" commit change the behavior.

So more "show" v.s. "tell".

I.e. in this case do the "test_cmp_rev" to the "wrong" tip with a TODO comment or whatever, then in the fix just adjust it, then it's clear what we had before/after.

Previous: John Cai via GitGitGadgetNext: Junio C Hamano
Message 22 of 41 in “rebase: set REF_HEAD_DETACH in checkout_up_to_date()”
  1. rebase: set REF_HEAD_DETACH in checkout_up_to_date()John Cai via GitGitGadget, Mar 11, 2022
  2. Junio C HamanoMar 11, 2022
  3. Junio C HamanoMar 11, 2022
  4. John CaiMar 11, 2022
  5. Phillip WoodMar 11, 2022
  6. John CaiMar 11, 2022
  7. John CaiMar 11, 2022
  8. Phillip WoodMar 11, 2022
  9. 0/2 rebase: update HEAD when is an oidJohn Cai via GitGitGadget, Mar 11, 2022
  10. 1/2 rebase: use test_commit helper in setupJohn Cai via GitGitGadget, Mar 11, 2022
  11. Junio C HamanoMar 13, 2022
  12. Phillip WoodMar 14, 2022
  13. Junio C HamanoMar 14, 2022
  14. 2/2 rebase: set REF_HEAD_DETACH in checkout_up_to_date()John Cai via GitGitGadget, Mar 11, 2022
  15. Junio C HamanoMar 13, 2022
  16. Phillip WoodMar 14, 2022
  17. Phillip WoodMar 14, 2022
  18. John CaiMar 14, 2022
  19. Phillip WoodMar 14, 2022
  20. 0/2 rebase: update HEAD when is an oidJohn Cai via GitGitGadget, Mar 17, 2022
  21. 2/2 rebase: set REF_HEAD_DETACH in checkout_up_to_date()John Cai via GitGitGadget, Mar 17, 2022
  22. Ævar Arnfjörð BjarmasonMar 17, 2022
  23. Junio C HamanoMar 17, 2022
  24. 1/2 rebase: use test_commit helper in setupJohn Cai via GitGitGadget, Mar 17, 2022
  25. Ævar Arnfjörð BjarmasonMar 17, 2022
  26. 0/3 rebase: update HEAD when is an oidJohn Cai via GitGitGadget, Mar 17, 2022
  27. 1/3 rebase: test showing bug in rebase with non-branchJohn Cai via GitGitGadget, Mar 17, 2022
  28. Junio C HamanoMar 17, 2022
  29. Junio C HamanoMar 17, 2022
  30. John CaiMar 17, 2022
  31. 2/3 rebase: use test_commit helper in setupJohn Cai via GitGitGadget, Mar 17, 2022
  32. Phillip WoodMar 18, 2022
  33. John CaiMar 18, 2022
  34. 3/3 rebase: set REF_HEAD_DETACH in checkout_up_to_date()John Cai via GitGitGadget, Mar 17, 2022
  35. Junio C HamanoMar 17, 2022
  36. John CaiMar 18, 2022
  37. 0/2 rebase: update HEAD when is an oidJohn Cai via GitGitGadget, Mar 18, 2022
  38. 1/2 rebase: use test_commit helper in setupJohn Cai via GitGitGadget, Mar 18, 2022
  39. Junio C HamanoMar 18, 2022
  40. 2/2 rebase: set REF_HEAD_DETACH in checkout_up_to_date()John Cai via GitGitGadget, Mar 18, 2022
  41. Junio C HamanoMar 18, 2022

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.