Hi Junio, On 11 Mar 2022, at 0:55, Junio C Hamano wrote: > "John Cai via GitGitGadget" <gitgitgadget@xxxxxxxxx> 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