Git 底层原理系列 · 第8讲 — HEAD 与 detached HEAD

15 阅读5分钟

Git 底层原理系列 · 第8讲 — HEAD 与 detached HEAD

⏱️ 预计阅读时间:14 分钟

目录


📚 学习导航

项目内容
前置知识第6讲:分支与引用
核心问题Q1: HEAD 是什么?
Q2: Detached HEAD 为什么危险?
Q3: Detached HEAD 有什么实际用途?
预计收获彻底理解 HEAD 的工作原理;安全地使用 detached HEAD

⚡ 认知冲突

你以为 detached HEAD 是一个"错误状态"?

实际上 detached HEAD 是一个非常有用(但也容易误用)的状态。Git 的很多核心操作——比如 git rebase -igit bisect——都在 detached HEAD 下工作。它不是错误,而是"指向了 commit 而非分支"的状态。


1 HEAD 的本质

HEAD 是一个指针,它告诉 Git"你现在在哪里"。它的内容很简单:

cat .git/HEAD
# 形式一:ref: refs/heads/main    ← 在分支 main 上
# 形式二:a1b2c3d4e5f6a7b8...    ← 直接指向某个 commit(detached)

HEAD 决定了三件事:

1. 下一次 commit 的 parent
   → 新 commit 的 parent = $(git rev-parse HEAD)

2. 工作区的内容
   → checkout 时把 HEAD 指向的 commit 的 tree 解压到工作区

3. 当前在哪个分支上(git branch 的输出)

2 Attached HEAD:在分支上

这是"正常"状态——HEAD 指向一个分支,分支指向一个 commit:

HEAD → refs/heads/main → commit abc

在分支上时 git commit 的流程

# 1. git write-tree → 从 Index 创建 tree
# 2. git commit-tree TREE -p $(git rev-parse HEAD) -m "msg"
# 3. git update-ref refs/heads/main NEW_COMMIT
#    ↑ HEAD 指向 refs/heads/main,所以自动更新了 main

即:git commit → 新 commit → update-ref refs/heads/main → HEAD 不动(分支动了)

在分支上时 git checkout 的流程

git checkout feature
# 1. 读取 refs/heads/feature → commit hash
# 2. 更新 .git/HEAD → "ref: refs/heads/feature"
# 3. 解压 feature 的 tree 到工作区

3 Detached HEAD:不在分支上

HEAD 直接指向一个 commit:

HEAD → commit abc(没有经过任何分支)

进入 Detached HEAD 的方式

# 1. checkout 一个 commit hash
git checkout a1b2c3d4

# 2. checkout 一个 tag
git checkout v1.0

# 3. checkout 一个相对引用
git checkout HEAD~3

# 4. checkout 一个远程分支
git checkout origin/main

# 5. 某些命令内部进入(rebase -i, bisect 等)
git rebase -i HEAD~3  # rebase 过程中进入 detached HEAD

Detached HEAD 下 git commit 的流程

# 1. git write-tree → 创建 tree
# 2. git commit-tree TREE -p $(git rev-parse HEAD) -m "msg"
# 3. 没有 update-ref!因为 HEAD 不指向任何分支
#    HEAD 直接更新为指向新 commit

即:git commit → 新 commit → HEAD 指向新 commit(没有分支被更新)


4 Detached HEAD 下 commit 为什么"丢失"?

丢失的根本原因

# 在 detached HEAD 下创建了两个 commit
git checkout v1.0
# → HEAD is now at a1b2c3...

echo "change" > file.txt
git add file.txt && git commit -m "fix 1"
# → HEAD → d4e5f6(新 commit)

echo "more changes" > file.txt
git add file.txt && git commit -m "fix 2"
# → HEAD → g7h8i9(新 commit,parent=d4e5f6)

# 现在切换到 main
git checkout main
# → HEAD 指向 main,但 g7h8i9 和 d4e5f6 没有被任何引用指向!

此时 commit 图:

a1b2c3 (v1.0) ── d4e5f6 ── g7h8i9
                               ↑ HEAD 刚才在这里
                               
main ── C1 ── C2 ── C3
↑ HEAD 现在在这里

d4e5f6g7h8i9 没有任何分支指向它们。它们成为"悬空对象"(dangling commits)。

悬空对象的后续

# 查看悬空对象
git fsck --lost-found
# → dangling commit d4e5f6...
# → dangling commit g7h8i9...

# 它们不会立即消失
# 直到 git gc 执行时才会被清理
# 默认保留 2 周(gc.reflogExpire)

5 如何安全地在 Detached HEAD 下工作

方法一:创建分支

# 在切换之前创建分支
git checkout v1.0
git switch -c hotfix-v1.0
# 或
git checkout -b hotfix-v1.0

# 现在 HEAD → refs/heads/hotfix-v1.0 → commit
# commit 不会丢失

方法二:在切换之前创建分支

git checkout v1.0
# ... 创建了两个 commit ...
git branch hotfix-v1.0  # 不切换,只创建分支指向当前 HEAD
# 或
git switch -c hotfix-v1.0  # 创建并切换到新分支

# 现在这两个 commit 被 hotfix-v1.0 引用了

方法三:用 reflog 找回丢失的 commit

# 如果不小心切走了
git checkout main

# 找回刚才在 detached HEAD 下创建的 commit
git reflog
# → g7h8i9 HEAD@{0}: commit: fix 2
# → d4e5f6 HEAD@{1}: commit: fix 1
# → a1b2c3 HEAD@{2}: checkout: moving from main to v1.0

# 创建分支指向它
git branch recovered g7h8i9

6 ORIG_HEAD 和特殊引用

Git 有一些特殊的引用,它们只存在于内存中或临时文件中:

# ORIG_HEAD:危险操作前的 HEAD 备份
git merge feature
# → Git 在合并前把 HEAD 保存到 ORIG_HEAD

# 如果合并出了问题
git reset --hard ORIG_HEAD
# 回到合并前的状态
特殊引用何时创建用途
ORIG_HEADmerge、rebase、reset 前撤销危险操作
FETCH_HEADgit fetch记录从远程获取的分支信息
MERGE_HEADmerge 进行中记录被合并分支的 commit
CHERRY_PICK_HEADcherry-pick 进行中记录被 cherry-pick 的 commit
BISECT_HEADbisect 进行中记录 bisect 的当前 commit

7 动手实验:感受 Detached HEAD

mkdir ~/sandbox/detached-lab && cd ~/sandbox/detached-lab
git init
echo "v1" > file.txt && git add . && git commit -m "v1"
echo "v2" > file.txt && git add . && git commit -m "v2"
echo "v3" > file.txt && git add . && git commit -m "v3"
git tag v1.0 HEAD~2

# 进入 detached HEAD
git checkout v1.0
git log --oneline  # 发现只有 v1

# 在 detached HEAD 下创建 commit
echo "hotfix" > file.txt && git add . && git commit -m "hotfix on v1"

# 观察状态
git log --oneline
git branch -a  # 没有分支指向新 commit

# 切走看看
git checkout main
git log --all --oneline  # hotfix commit 还在吗?
# 不在了(git log 不显示悬空对象)

# 用 reflog 找回
git reflog
git branch recovered HEAD@{1}
git log recovered --oneline  # 找回来了!

总结

状态HEAD 内容git commit 行为commit 安全性适用场景
Attachedref: refs/heads/xxx更新分支引用✅ 永远安全日常开发
Detachedcommit hash只移动 HEAD⚠️ 可能丢失临时查看、rebase、bisect

自测卡片

Q1:HEAD 文件里存了什么?

A: 两种可能:① ref: refs/heads/main(在分支上)② 一个 commit hash(detached)。HEAD 告诉 Git"你在哪里"。

Q2:为什么 detached HEAD 下 commit 会丢失?

A: 因为新 commit 没有更新任何分支引用(HEAD 不指向分支)。切换到其他地方后,新 commit 没有任何引用指向它,变成悬空对象。

Q3:如何找回 detached HEAD 下丢失的 commit?

A: 用 git reflog 找到 commit 的 hash,然后用 git branch <name> <hash> 创建分支指向它。Reflog 记录了 HEAD 曾经指向过的所有位置。

Q4:什么场景需要主动使用 detached HEAD?

A: ① git rebase -i(交互式变基)② git bisect(二分查找引入 bug 的 commit)③ 临时查看历史版本并做实验性修改 ④ checkout tag 查看某个版本。


第8讲完。下一讲:重置与还原 — git reset / git revert / git restore 的底层原理。