Git 底层原理系列 · 第8讲 --- HEAD 与 detached HEAD
⏱️ 预计阅读时间:14 分钟
目录
- [📚 学习导航](#📚 学习导航 "#-%E5%AD%A6%E4%B9%A0%E5%AF%BC%E8%88%AA")
- [⚡ 认知冲突](#⚡ 认知冲突 "#-%E8%AE%A4%E7%9F%A5%E5%86%B2%E7%AA%81")
- [1 HEAD 的本质](#1 HEAD 的本质 "#1-head-%E7%9A%84%E6%9C%AC%E8%B4%A8")
- [2 Attached HEAD:在分支上](#2 Attached HEAD:在分支上 "#2-attached-head%E5%9C%A8%E5%88%86%E6%94%AF%E4%B8%8A")
- [3 Detached HEAD:不在分支上](#3 Detached HEAD:不在分支上 "#3-detached-head%E4%B8%8D%E5%9C%A8%E5%88%86%E6%94%AF%E4%B8%8A")
- [4 Detached HEAD 下 commit 为什么"丢失"?](#4 Detached HEAD 下 commit 为什么"丢失"? "#4-detached-head-%E4%B8%8B-commit-%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%A2%E5%A4%B1")
- [5 如何安全地在 Detached HEAD 下工作](#5 如何安全地在 Detached HEAD 下工作 "#5-%E5%A6%82%E4%BD%95%E5%AE%89%E5%85%A8%E5%9C%B0%E5%9C%A8-detached-head-%E4%B8%8B%E5%B7%A5%E4%BD%9C")
- [6 ORIG_HEAD 和特殊引用](#6 ORIG_HEAD 和特殊引用 "#6-orig_head-%E5%92%8C%E7%89%B9%E6%AE%8A%E5%BC%95%E7%94%A8")
- [7 动手实验:感受 Detached HEAD](#7 动手实验:感受 Detached HEAD "#7-%E5%8A%A8%E6%89%8B%E5%AE%9E%E9%AA%8C%E6%84%9F%E5%8F%97-detached-head")
- 总结
- 自测卡片
📚 学习导航
| 项目 | 内容 |
|---|---|
| 前置知识 | 第6讲:分支与引用 |
| 核心问题 | Q1: HEAD 是什么? Q2: Detached HEAD 为什么危险? Q3: Detached HEAD 有什么实际用途? |
| 预计收获 | 彻底理解 HEAD 的工作原理;安全地使用 detached HEAD |
⚡ 认知冲突
你以为 detached HEAD 是一个"错误状态"?
实际上 detached HEAD 是一个非常有用(但也容易误用)的状态。Git 的很多核心操作------比如
git rebase -i、git bisect------都在 detached HEAD 下工作。它不是错误,而是"指向了 commit 而非分支"的状态。
1 HEAD 的本质
HEAD 是一个指针,它告诉 Git"你现在在哪里"。它的内容很简单:
bash
cat .git/HEAD
# 形式一:ref: refs/heads/main ← 在分支 main 上
# 形式二:a1b2c3d4e5f6a7b8... ← 直接指向某个 commit(detached)
HEAD 决定了三件事:
markdown
1. 下一次 commit 的 parent
→ 新 commit 的 parent = $(git rev-parse HEAD)
2. 工作区的内容
→ checkout 时把 HEAD 指向的 commit 的 tree 解压到工作区
3. 当前在哪个分支上(git branch 的输出)
2 Attached HEAD:在分支上
这是"正常"状态------HEAD 指向一个分支,分支指向一个 commit:
bash
HEAD → refs/heads/main → commit abc
在分支上时 git commit 的流程
bash
# 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 的流程
bash
git checkout feature
# 1. 读取 refs/heads/feature → commit hash
# 2. 更新 .git/HEAD → "ref: refs/heads/feature"
# 3. 解压 feature 的 tree 到工作区
3 Detached HEAD:不在分支上
HEAD 直接指向一个 commit:
sql
HEAD → commit abc(没有经过任何分支)
进入 Detached HEAD 的方式
bash
# 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 的流程
bash
# 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 为什么"丢失"?
丢失的根本原因
bash
# 在 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 图:
css
a1b2c3 (v1.0) ── d4e5f6 ── g7h8i9
↑ HEAD 刚才在这里
main ── C1 ── C2 ── C3
↑ HEAD 现在在这里
d4e5f6 和 g7h8i9 没有任何分支指向它们。它们成为"悬空对象"(dangling commits)。
悬空对象的后续
bash
# 查看悬空对象
git fsck --lost-found
# → dangling commit d4e5f6...
# → dangling commit g7h8i9...
# 它们不会立即消失
# 直到 git gc 执行时才会被清理
# 默认保留 2 周(gc.reflogExpire)
5 如何安全地在 Detached HEAD 下工作
方法一:创建分支
bash
# 在切换之前创建分支
git checkout v1.0
git switch -c hotfix-v1.0
# 或
git checkout -b hotfix-v1.0
# 现在 HEAD → refs/heads/hotfix-v1.0 → commit
# commit 不会丢失
方法二:在切换之前创建分支
bash
git checkout v1.0
# ... 创建了两个 commit ...
git branch hotfix-v1.0 # 不切换,只创建分支指向当前 HEAD
# 或
git switch -c hotfix-v1.0 # 创建并切换到新分支
# 现在这两个 commit 被 hotfix-v1.0 引用了
方法三:用 reflog 找回丢失的 commit
bash
# 如果不小心切走了
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 有一些特殊的引用,它们只存在于内存中或临时文件中:
bash
# ORIG_HEAD:危险操作前的 HEAD 备份
git merge feature
# → Git 在合并前把 HEAD 保存到 ORIG_HEAD
# 如果合并出了问题
git reset --hard ORIG_HEAD
# 回到合并前的状态
| 特殊引用 | 何时创建 | 用途 |
|---|---|---|
| ORIG_HEAD | merge、rebase、reset 前 | 撤销危险操作 |
| FETCH_HEAD | git fetch 时 |
记录从远程获取的分支信息 |
| MERGE_HEAD | merge 进行中 | 记录被合并分支的 commit |
| CHERRY_PICK_HEAD | cherry-pick 进行中 | 记录被 cherry-pick 的 commit |
| BISECT_HEAD | bisect 进行中 | 记录 bisect 的当前 commit |
7 动手实验:感受 Detached HEAD
bash
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 安全性 | 适用场景 |
|---|---|---|---|---|
| Attached | ref: refs/heads/xxx |
更新分支引用 | ✅ 永远安全 | 日常开发 |
| Detached | commit 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 的底层原理。