git rebase --onto <commitB> <commitA> <commitD>
实际上,使用commit (<commitD>)作为最后一个参数,而不是分支名称(topic),应该创建一个分离的分支:
A --- B --- C master
\ \
\ -- D' <detached HEAD>
\
\-- D topic
除了……在Git 2.36之前,在某些情况下不会:
"git rebase $base $non_branch_commit"(man),当$base是一个祖先或$non_branch_commit时,修改了当前的分支,这已在git 2.36 (Q2 2022)中更正。
参见John Cai (John - Cai)的commit bdff97a, commit 77ab58c (18 Mar 2022)。
(由Junio C Hamano - gitster -在提交f818536, 2022年3月29日合并)
rebase:在checkout_up_to_date()中设置REF_HEAD_DETACH
报道:Michael McClimon
签字人:John Cai
"git rebase A B"(man) where B is not a commit should behave as if the HEAD got detached at B and then the detached HEAD got rebased on top of A.
A bug however overwrites the current branch to point at B, when B is a descendant of A (i.e. the rebase ends up being a fast-forward).
See this thread 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 B is not a valid branch but an oid, rebase sets the head_name 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().
Then update_refs() checks ropts.branch to decide whether or not to switch branches.
If ropts.branch is NULL, it 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.
We want the HEAD detached at B instead.
Fix this bug by adding the RESET_HEAD_DETACH flag in checkout_up_to_date if B 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.