Git篇(5):HEAD指针——Git的灵魂概念

125 阅读8分钟

下面我们走到 Git 的“灵魂概念”——HEAD 指针。理解 HEAD,等于理解 Git 的行为逻辑。


为什么 Git 要有 HEAD?

Git 是一个内容寻址文件系统,核心是提交(commit)。
但是问题来了:

  • 用户需要一个“当前位置”的概念。
    就像在文件系统里,你得知道自己当前在哪个目录一样。
  • 在 Git 里,HEAD 就是当前所处提交的“游标”

👉 它的作用是:告诉 Git「当前工作目录和暂存区应该基于哪个提交」


HEAD 是什么?

  • HEAD 是一个 特殊引用(ref)。
  • 它通常指向一个分支,而分支再指向某个 commit。

例子:

HEAD → refs/heads/master → cC1234...

这表示:

  • 你当前在 master 分支上工作。
  • master 分支指向 commit cC1234...
  • 即:HEAD 指向分支,分支再指向 commit。这样你提交时,分支会前进,HEAD 也跟着前进。

HEAD 的两种状态

正常状态:HEAD → branch

  • HEAD 指向某个分支。

  • 你提交时,Git 会:

    1. 新建 commit
    2. 分支指针 向前移动
    3. HEAD 自然“跟着走”
  • 保证版本历史有序、稳定演进,提交总能落在分支上。

比如:

HEAD → master → c1

提交一次:

HEAD → master → c2

游离状态:detached HEAD

  • HEAD 直接指向某个 commit,而不是分支。
  • 允许你“临时走一走”,不污染分支历史。
  • 可以探索历史、测试代码、调试 bug,而不用动分支指针。
HEAD → c1
  • 这时如果你提交:

    • 新 commit 生成后,HEAD 直接指过去
    • 没有分支名字来保存它

结果是:

HEAD → c2
(但没有 branch 指向 c2,这是重点!)
  1. 如果你切回其他分支,那么HEAD会指向其他分支!
  2. 此时没有指针指向c2,那么c2 就变成“悬挂提交”,最终被垃圾回收掉。

游离 HEAD 的意义

那为什么 Git 要允许“游离状态”?而不是强制必须在分支上?

答案是:灵活性

  • 如果强制所有提交都要在分支上 → 历史会被搞得很乱,每次实验都多一个分支。

  • 游离 HEAD → 就像“临时工作区”,让你可以自由探索:

    • 检查旧版本代码
    • 打补丁
    • 在某个历史点编译运行
    • 实验性修改

等到觉得实验有价值,再 git checkout -b new-branch 把这些提交“捞起来”。

下面列举几个重要场景:

(1)回溯历史

你想看看某个老版本的代码运行效果:

git checkout c2

HEAD 游离,但没关系,你只是查看,并不会提交。

(2)基于历史做实验

比如你想在某个老版本上尝试修 bug:

git checkout c2
...修改...
git commit -m "test fix"

此时 HEAD → 新提交(但没有分支名字)。
如果觉得这个实验值得保留,可以创建一个分支保存它:

git switch -c fix-old-bug

这样 HEAD 又回归正常状态,提交就不会丢。

(3)打 tag 的意义

当你打一个 tag 并 checkout,它进入游离状态。
这在“发布版本回溯”时很常用。

👉 所以游离 HEAD = “在历史某点上自由探索的模式”。


游离状态下的提交
  • 在游离状态下创建的提交,如果没有 HEAD/branch/tag 指向它,它就是悬挂提交(也可以叫孤儿提交,没指针管)。
  • Git 会在一段时间(默认 90 天左右,取决于 reflog 配置)后,自动清理掉这些无法到达的提交(GC 机制)。

例子:

# 执行
git checkout c2

# 效果
HEAD → c2

# 执行
git commit → c2_new

# 效果
c2 -> ac2_new
HEAD -> c2

如果你切回 master:

HEAD → master → c3
c2_new ←没人指向,过一阵会被清理

👉 所以游离状态提交不是“马上丢”,而是“没人管的话会过期”。


悬挂提交与Git回收机制

什么是悬挂提交(Dangling Commit)?
  • 悬挂提交:就是没有任何引用(branch、tag、HEAD、stash 等)指向的 commit

  • 在 Git 的数据模型里:

    • 提交(commit)就像一块“石头”,
    • 分支/标签就像“绳子”系住它。
  • 一旦你 reset --hardrebasebranch -D 等操作,可能让某些提交 失去引用,它们就变成了悬挂状态。

👉 举例:

原来:
main: c1 -- c2 -- c3
HEAD -> main

执行 git reset --hard c2

现在:
main: c1 -- c2
HEAD -> main
悬挂:c3  (没人指向它了)

c3 就成了一个悬挂提交。


Git 会立刻删除悬挂提交吗?

不会!❌
Git 的对象存储机制决定了:

  • 提交对象一旦写入 .git/objects,就不会立刻被删除
  • Git 采用 垃圾回收(GC, garbage collection)机制 来清理悬挂提交。

Git 的回收机制(GC)

Git 的 GC 规则是:

  1. 所有有引用的对象都保留

    • 有分支、tag、HEAD 指向的 commit,不会被删。
  2. 悬挂对象会保留一段时间(默认 90 天)

    • Git 在 .git/logs/ 目录下保存操作历史(reflog)。
    • 在这段时间内,悬挂提交可以通过 git reflog 找回。
  3. 超过保留期的悬挂对象会被清理

    • 运行 git gc 或 Git 自动触发时,悬挂超过阈值的对象会被清理掉(永久删除)。

悬挂提交的意义

你可能会问:既然最后要删,为什么还要有“悬挂”状态?

  • 安全兜底:避免误操作(reset/rebase/checkout)导致提交立刻丢失。
  • 数据恢复:给用户一个后悔的机会,用 git refloggit fsck --lost-found 把悬挂提交捞回来。
  • 过渡状态:有些命令执行过程中(如 merge、rebase 失败)会生成临时提交,它们也可能是短暂的悬挂。

如何找回悬挂提交?

方法一: git reflog

git reflog

它会显示 HEAD 的移动记录,即使提交“丢了”,也能看到旧的哈希。

然后:

# rescue是分支名,可自定义
git branch rescue <hash> 

就能恢复分支,挂回那个提交。

方法二: git fsck --lost-found

git fsck --lost-found

Git 会扫描所有悬挂对象,输出“dangling commit xxx”。
你可以用 git show xxx 查看内容,再决定要不要救回来。


回收流程直观演示

假设:

main: c1 -- c2 -- c3
HEAD -> main

操作:

git reset --hard c1

状态:

main: c1
HEAD -> main
悬挂: c2, c3

恢复:

git reflog         # 找到 c3 的哈希
git branch rescue <c3-hash>

结果:

main:   c1
rescue: c1 -- c2 -- c3
HEAD -> main

c3 就“复活”了。


总结
  • 悬挂提交 = 没有引用指向的提交。
  • Git 不会立刻删除,而是通过 GC 延迟清理。
  • 只要没被 GC 掉,都能用 reflogfsck 找回。

HEAD 的两种状态解决了什么问题?

  • 正常 HEAD:保证版本历史有序、稳定演进,提交总能落在分支上。
  • 游离 HEAD:提供试验田,不必每次创建分支,可以轻量探索历史。

👉 这是 Git 的哲学:
既要有“秩序”(分支有序推进),也要有“混沌的自由”(游离 HEAD 任意实验)。
而 HEAD 就是这个秩序与自由的切换枢纽


案例巩固

我们来用一个具体流程,把 HEAD、分支、提交的关系演示出来:


1. 初始状态

你有一条分支 dev,上面有三个提交:

c1 -- c2 -- c3
             ↑
           dev, HEAD

此时 HEAD 正常状态,指向 devdev 指向 c3


2. checkout 到 c1(进入游离 HEAD)

你执行:

git checkout c1

结果:

c1 -- c2 -- c3
↑
HEAD        dev
  • HEAD 不再指向 dev,而是直接指向提交对象 c1
  • 这时就是 游离 HEAD

3. 在 c1 上提交(产生 c2_new)

你修改文件并提交:

git commit -m "new work"

结果:

c2_new
         ↑
c1 -- c2 -- c3
↑
HEAD        dev
  • 新提交 c2_newc1 分叉出来了。
  • HEAD 指向 c2_new,但注意:没有分支名指向它,只有 HEAD 临时挂着。

4. 后果

此时情况是:

  • 旧分支 dev 仍然在 c3,没有动。
  • 你在游离状态,提交历史“另起炉灶”。
  • 如果不创建新分支,这些提交最终可能会被 Git 垃圾回收(因为没有持久引用)。

5. 如何保存这条分叉?

如果你觉得 c2_new 很有价值,可以把它接到一个新分支上:

git switch -c feature 

git checkout -b feature 

现在 feature 分支指向了 c2_new,不会丢。


案例总结

现象:HEAD指向commit后提交,即游离后提交,提交链从 c1 分叉了!
结论:游离 HEAD 提交的本质就是——在没有分支指针的情况下,生成一条新的分叉链。


为什么说“所有操作都是移动 HEAD”?

checkout

  • 本质:移动 HEAD 到另一个分支或提交
HEAD → master   → c3
git checkout dev
HEAD → dev      → d5

reset

  • 本质:移动 分支指针到指定 commit,并把 HEAD 拉过去
master: c1 → c2 → c3 (HEAD)
git reset c1
master: c1 (HEAD)

所以 reset 就是“硬生生挪动分支和 HEAD”。

rebase

  • 本质:让 HEAD 带着一堆 commit,重新“挪到”另一个基底上。
AB (master)
     \
      C → D (dev, HEAD)
git rebase master

dev 的 HEAD 被移动到 B 后面,C/D 被重新复制一遍。

👉 所以 Git 的一切“花哨操作”,本质上就是 移动 HEAD(直接移动或间接移动分支再跟过去)