1. 概述

在 Git 操作中遇到神秘状态并不罕见,其中最常见的就是 "分离 HEAD"(detached HEAD)。本教程将深入探讨什么是分离 HEAD、它的工作原理,以及如何进入和退出这种状态。

2. Git 中的 HEAD 是什么

Git 在创建提交时会存储仓库中所有文件的状态记录。HEAD 是另一种重要的引用类型。HEAD 的核心作用是追踪 Git 仓库的当前位置。简单来说,HEAD 回答了这个问题:"我现在在哪里?":

$ git log --oneline
a795255 (HEAD -> master) create 2nd file
5282c7c appending more info
b0e1887 creating first file

例如,当我们使用 log 命令时,Git 如何知道从哪个提交开始显示结果?HEAD 提供了答案。当我们创建新提交时,其父提交由 HEAD 当前指向的位置决定

由于 Git 拥有强大的版本追踪功能,我们可以随时回到仓库的任意历史节点查看内容。查看历史提交的能力也让我们能了解仓库或特定文件随时间的变化。**当我们检出一个不属于分支的提交时,就会进入"分离 HEAD 状态"**。这意味着我们正在查看的提交不是仓库中的最新提交。

3. 分离 HEAD 示例

大多数情况下,HEAD 指向分支名称。当我们添加新提交时,分支引用会更新指向新提交,但 HEAD 保持不变。当我们切换分支时,HEAD 会更新指向新分支。这意味着在上述场景中,HEAD 等同于"当前分支的最新提交"。这是 HEAD 附加到分支的正常状态:

HEAD 指向分支

如图所示,HEAD 指向 master 分支,而 master 分支指向最新提交。一切正常。但执行以下命令后,仓库会进入分离 HEAD 状态:

$ git checkout 5282c7c
Note: switching to '5282c7c'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

HEAD is now at 5282c7c appending more info

users@ubuntu01: MINGW64 ~/git/detached-head-demo ((5282c7c...))

以下是当前 Git HEAD 的图形化表示。由于我们检出到一个历史提交,现在 HEAD 指向 5282c7c 提交,而 master 分支仍然指向原位置:

分离 HEAD 示意图

4. Git 分离 HEAD 的优势

通过检出特定提交(如 5282c7c)进入分离 HEAD 状态后,我们可以回到项目历史的任意时间点。

假设我们需要检查某个 bug 在上周二是否已经存在。我们可以使用带日期过滤的 log 命令找到相关提交哈希,然后检出该提交并通过手动测试或自动化测试套件来验证应用。

分离 HEAD 不仅能让我们查看历史,还能修改历史!以下命令展示了如何操作:

echo "understanding git detached head scenarios" > sample-file.txt
git add .
git commit -m "Create new sample file"
echo "Another line" >> sample-file.txt
git commit -a -m "Add a new line to the file"

现在我们基于第二个提交创建了两个新提交。执行 git log --oneline 查看结果:

$ git log --oneline
7a367ef (HEAD) Add a new line to the file
d423c8c create new sample file
5282c7c appending more info
b0e1887 creating first file

最初 HEAD 指向 5282c7c 提交,然后我们添加了 d423c8c7a367ef 两个新提交。以下是在分离 HEAD 上创建提交的图形化表示,显示现在 HEAD 指向最新提交 7a367ef

分离 HEAD 上的提交

如果想保留这些更改或返回之前的状态,我们将在下一节讨论具体操作。

5. 常见场景

5.1. 意外进入分离 HEAD

如果意外进入分离 HEAD 状态(即本意不是检出提交),返回很简单。只需使用以下命令检出之前的分支:

git switch <branch-name> 

git checkout <branch-name>

5.2. 实验性更改需要丢弃

在某些场景中,我们可能在分离 HEAD 后进行了更改(测试功能或定位 bug),但不想将这些更改合并到原分支。这时可以直接使用上一场景的命令返回原分支,这些更改会被自动丢弃。

5.3. 实验性更改需要保留

如果想保留分离 HEAD 状态下的更改,只需创建新分支并切换过去。我们可以在刚进入分离 HEAD 时创建,也可以在创建一个或多个提交后创建,结果相同。唯一限制是必须在返回原分支前操作。在演示仓库中,创建提交后执行以下命令:

git branch experimental
git checkout experimental

注意 git log --oneline 的结果与之前完全相同,唯一区别是最新提交显示的分支名称:

$ git log --oneline
7a367ef (HEAD -> experimental) Add a new line to the file
d423c8c create new sample file
5282c7c appending more info
b0e1887 creating first file

6. 总结

如本文所述,分离 HEAD 并不意味着仓库出了问题。它只是仓库的一种非常规状态。分离 HEAD 不仅不是错误,反而相当实用,允许我们进行实验性操作,然后根据需要选择保留或丢弃这些更改。掌握这个特性能让你在版本控制中更加游刃有余。


原始标题:Understanding Detached HEAD in Git