Re: [PATCH] rebase: set REF_HEAD_DETACH in checkout_up_to_date()
- From
John Cai <johncai86@gmail.com>
- Date
- Mar 11, 2022, 14:14 UTC
- Message-ID
- <B1A94338-404E-43F2-AF3C-889097FD938B@gmail.com>
- In-Reply-To
- <xmqq5yolav8l.fsf@gitster.g>
Hi Junio,
On 11 Mar 2022, at 0:55, Junio C Hamano wrote:
Show 30 quoted lines
> "John Cai via GitGitGadget" <gitgitgadget@gmail.com> writes: > >> This is happening because on a fast foward with an oid as a <branch>, >> update_refs() will only call update_ref() with REF_NO_DEREF if >> RESET_HEAD_DETACH is set. This change was made in 176f5d96 (built-in rebase >> --autostash: leave the current branch alone if possible, >> 2018-11-07). In rebase, we are not setting the RESET_HEAD_DETACH flag, >> which means that the update_ref() call ends up dereferencing >> HEAD and updating it to the oid used as <branch>. >> >> The correct behavior is that git rebase should update HEAD to $(git >> rev-parse topic) without dereferencing it. > > It is unintuitive that unconditionally setting the RESET_HEAD_DETACH > bit is the right solution. > > If the command weren't "rebase master side^0" but "rebase master > side", i.e. "please rebase the side branch itself, not an unnamed > branch created out of the side branch, on master", according to > <reset.h>, we ought to end up being on a detached HEAD, as > reset_head() with the bit > > /* Request a detached checkout */ > #define RESET_HEAD_DETACH (1<<0) > > requests a detached checkout. But that apparently is not what would > happen with your patch applied. > > Puzzled. The solution to the puzzle probably deserves to be in the > proposed log message.
Good point. Thinking aloud, here is the callstack.
checkout_up_to_date() -> reset_head() -> update_refs() -> update_ref()
if <branch> is not a valid ref, rebase_options head_name is set to NULL. This eventually leads update_refs() to decide that it doesn't need to switch to a branch via its switch_to_branch variable.
reset.c:
if (!switch_to_branch)
ret = update_ref(reflog_head, "HEAD", oid, head,
detach_head ? REF_NO_DEREF : 0,
UPDATE_REFS_MSG_ON_ERR);
else {
ret = update_ref(reflog_branch ? reflog_branch : reflog_head,
switch_to_branch, oid, NULL, 0,
UPDATE_REFS_MSG_ON_ERR);
if (!ret)
ret = create_symref("HEAD", switch_to_branch,
reflog_head);
}since the flags do not include RESET_HEAD_DETACH, detach_head is set to false and we get a deferenced HEAD update.
The solution I came up with works because when <branch> __is__ a valid branch, udpate_refs() takes a different code path that calls create_symref() with a branch, which is why we don't end up with a detached HEAD.
I see why this is confusing though. From checkout_up_to_date's perspective it looks like we are unconditionally detaching HEAD. So what we could do is only set the flag in checkout_up_to_date() when, from checkout_up_to_date's perspective, we will end up with a detached head. something like this:
diff --git a/builtin/rebase.c b/builtin/rebase.c index b29ad2b65e72..f0403fb12421 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -827,8 +827,10 @@ 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; + ropts.branch = options->head_name; + if (!ropts.branch) + 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); Otherwise, checkout_up_to_date() has to implicitly know the downstream logic in update_refs(). I believe that's the main source of the confusion--is that right? > > Thanks. Thanks John