我在一个分支上工作,需要撤销几个提交。所以我执行了以下操作:
$> git reset --hard bd53134
这产生了预期的效果,但我现在有一个分离的头 :(
$> git status
HEAD detached at bd53134
我该如何修复这个问题?
看起来您可能还在摸索中,这里给出一些背景。您在评论中提到,您启动了一个git rebase
并卡住了,git rebase --continue
始终在抱怨。(这真的应该是原始问题的一部分)。
首先要注意的是:HEAD
可以是正常的(“未分离”?)或“分离”的。一个“正常的”HEAD
仅由一个分支名称组成。如果您“在分支master
上”,HEAD
只说“在分支master上”。一个“分离的HEAD”,听起来像法国大革命中的某件事,代替它的是原始的SHA-1提交ID。在下面的图示中,我喜欢将已连接的HEAD写成HEAD=branch
,然后使用箭头指向特定提交的分支点。
在开始rebase之前,您可能具有类似于以下内容的提交图:
...- A - B - E - F <-- master, origin/master
\
C - D <-- HEAD=branch
在这种情况下,我假设您从master
分支执行了 git checkout -b branch
命令,并在branch
上进行了几次提交(创建了 C
和 D
),然后可能执行了 git checkout master
和 git pull
命令,从 "remoteorigin
" 拉取了提交记录 E
和 F
。接着,您使用 git checkout branch
命令返回到了 branch
分支(因此HEAD=branch
)。
然后,您决定将C
和D
两个提交记录在F
之上重新应用,以获得更线性的提交序列:
...- A - B - E - F <-- master, origin/master
\
C' - D' <-- HEAD=branch
(我马上会展示为什么它们是C'
和D'
,而不是C
和D
。) 所以你运行git rebase master
去“移动”提交记录C
和D
到master
的顶部。
Rebase实际上并没有移动提交记录。它所做的是复制一些已经存在的提交记录,创建新的提交记录来“做同样的事情”并且是“同样好的”(我们希望如此!)。所以,rebase保留了提交记录C
和D
。它使用一个特殊的标签ORIG_HEAD
来跟踪提交记录D
(还有一堆额外的.git/rebase-apply/
文件来跟踪整个rebase操作的所有进度——实际上这些文件是git知道rebase正在“进行中”的方法)。
Rebase通过添加这个ORIG_HEAD
和“分离HEAD”来开始这个过程。它将“分离的HEAD”设置为直接指向rebase目标(在本例中是提交记录F
)。(通过探索,似乎文档稍微说错了关于重置分支的部分。但这在较早版本的git中可能会有所不同;我认为文档在某个时刻是准确的。)因此:
...- A - B - E - F <-- master, origin/master
\ \
\ \...... HEAD [detached]
\
C - D <-- ORIG_HEAD, branch
然后,针对每个提交(这里是C
和D
),获取所做的更改并尝试将这些更改应用于HEAD
提交。 如果一切顺利——通常如此——它将创建一个与旧提交相同消息的新提交。 这就是提交C'
:
...- A - B - E - F <-- master, origin/master
\ \
\ C'..... HEAD [detached]
\
C - D <-- ORIG_HEAD, branch
成功将C
应用于F
,生成新的代码 C'
之后,rebase 命令尝试将代码 D
应用于最新的(但是仍然是分离的)HEAD
,即 C'
。但是这一次出现了问题:补丁无法应用:
CONFLICT ...
Failed to merge in the changes.
Patch failed at ...
The copy of the patch that failed is found in:
...
When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
此时,您的提交图看起来像我上面画的那个(有点凌乱)。
如rebase所述,您可以:
git rebase --continue
,或者git rebase --skip
删除提交(在本例中为D
),或者git rebase --abort
退出整个过程。选择最后一种选项会告诉rebase停止rebase尝试,将HEAD
恢复到原来的位置,并删除“正在进行的rebase”状态/跟踪文件。 这也放弃了所有新的链式提交(但尚未用分支标记)。 (从技术上讲,它们仍然在reflog中。通常情况下,您看不到它们。)
进行第一个选项(“手动解决”),或选择中间选项(“跳过”)会让rebase继续向前。 假设您解决了问题并使用了git rebase --continue
。 一旦rebase用完提交,并且D
是最后一个提交,它将将分支名称移动到最终提交,并再次将HEAD
设置为分支名称。 所以在这种情况下,您会得到这个:
...- A - B - E - F <-- master, origin/master
\ \
\ C' - D' <-- HEAD=branch
\
C - D <-- ORIG_HEAD
由于 ORIG_HEAD
指向的提交通常不会显示出来,因此看起来像 C
和 D
已经不存在了,而副本 (C'
和 D'
) 是唯一留下的提交。(但是需要注意的是,C
和 D
实际上仍然存在,并且将保留一段时间,直到 reflog 条目过期。)
考虑到一切情况,一旦进入“分离头指针”状态,git reset --hard
就无法影响到分支名称。它将移动 HEAD
并更改工作目录,但 HEAD
仍然直接指向提交。 因此,在重新设置基础过程中进行任何 git reset
操作都不太正常。(如果继续执行重新设置,它们将干扰重新设置的操作,因为继续重新设置只会添加到当前 HEAD
指向的位置。)
在更正常的情况下(当你在“分支上”时),HEAD
当前指向的就是这个分支的名称。然后(仅在此情况下),git reset
可以并确实更改分支名称所指向的提交。
如何确定 git reset
将采取哪些操作以及发生了什么?答案是使用 git status
。如果不确定正在发生什么,git status
可以提供帮助。它在过去几年中变得更加有用! :-)
如果您在一个分支上,git reset --hard bd53134
应该会更新该分支,并让您留在该分支上。因此,我怀疑您实际上并不在一个分支上。
如果是这样的话,一种解决方法是返回到该分支:
git checkout <branch>
然后再次运行您已经使用过的相同的git reset
命令。
HEAD
总是分离状态。使用git rebase --abort
来终止rebase操作,保持原始分支不变(git会将HEAD
放回未经rebase的分支末端)。 - torek