12 — 分支就是便利贴,HEAD 就是指路牌
摘要:本文深入解析 Git 中「引用」和「HEAD」的核心概念。分支本质上是.git/refs/heads/目录下的文件,仅包含一行 commit 哈希值,如同贴在档案上的便利贴;HEAD 则是.git/HEAD文件,指示当前所在位置,如同指路牌。文章通过「便利贴」和「指路牌」的生动比喻,拆解了分支创建、切换、删除及提交时的底层文件变化,解释了分离 HEAD 的状态与风险,并提供了动手实验和真实场景示例,帮助读者从文件系统层面理解 Git 引用机制,从而掌握分支操作的底层逻辑。
写在前面:这一章要解决什么
你可能已经用了很多次分支——git branch、git switch——但你有没有想过:分支到底是什么?它是复制了一份文件?还是复制了一个目录?
都不是。分支就是一张便利贴。这一章帮你彻底搞清楚「引用」和「HEAD」到底是什么,为什么理解了它们,Git 的很多操作就不再神秘。
学完后,你应该能:
- 用自己的话说出「分支是什么」(不是复制文件夹,是便利贴)
- 分清 HEAD、分支引用、远程跟踪引用的区别
- 知道「分离 HEAD」是什么状态,怎么进去、怎么出来
- 看懂
.git/refs/目录里的文件内容
读者设定:大一同学,已经学过分支操作(第 05 章)和对象模型(第 10 章),但还不理解分支的底层逻辑。
1. 定位:为什么要搞懂引用
1.1 一句话先记住
分支是一个文件,里面只有一行文字——某个 commit 的哈希值。就像便利贴上只写了一个编号,贴在档案柜的某个隔层上。
1.2 不知道引用是什么会怎样
| 不理解引用 | 理解了之后 |
|---|---|
| 以为开分支会复制整个项目 | 知道分支只是多了一张便利贴,秒开 |
| 以为删分支会丢数据 | 知道删分支只是撕了便利贴,文件还在 |
| 看到「分离 HEAD」就慌 | 知道只是 HEAD 直接指向了一个 commit,不是出错了 |
不知道为什么git push有时候被拒绝 | 知道推送是移动远程引用,有冲突就不让推 |
1.3 和你已经会的事对比
| 你已经会的 | 和这一章的关系 |
|---|---|
| 第 05 章的分支操作 | 这一章告诉你分支的底层是什么 |
第 09 章的.git/目录结构 | 这一章带你看.git/refs/里的引用文件 |
| 第 11 章的「命令与文件变化」 | 这一章深入看「移动引用」到底移动了什么 |
2. 本质:引用到底长什么样
2.1 亲眼看一个分支文件
打开你的 Git 仓库,执行:
cat.git/refs/heads/main你会看到类似这样的输出:
a1b2c3d4e5f6...(40 个字符的哈希值)就这么一行。这就是整个main分支——一个文件,里面写着它指向的那个 commit 的哈希值。
白话翻译:main这张便利贴上写的是「第 a1b2c3 号存档」。
2.2 引用的层级
Git 里有好几层引用,它们的关系就像这样:
HEAD(指路牌:你现在在哪) │ ├── 指向分支引用(如 main) │ └── 指向某个 commit │ └── 或者直接指向某个 commit(这就是「分离 HEAD」)| 引用 | 存在哪 | 指向什么 | 白话 |
|---|---|---|---|
| HEAD | .git/HEAD | 当前分支或当前 commit | 你现在站的位置(指路牌) |
| 分支引用 | .git/refs/heads/分支名 | 某个 commit | 贴在档案上的便利贴 |
| 远程跟踪引用 | .git/refs/remotes/origin/分支名 | 远程的某个 commit | 别人档案上的便利贴(只读) |
| 标签引用 | .git/refs/tags/标签名 | 某个 commit | 贴着版本号的便利贴 |
2.3 HEAD:最重要的引用
HEAD 是 Git 里最核心的概念之一。它告诉 Git「你现在在哪」。
正常情况下,HEAD 指向一个分支名:
cat.git/HEADref: refs/heads/main白话翻译:我站在main这个标签上。
当你提交新 commit 时,Git 会做两件事:
- 创建新的 commit 对象
- 把 HEAD 指向的分支引用移动到新 commit
所以是分支跟着你一起往前走——因为 HEAD 指向分支,分支指向新 commit。
图:引用(分支、标签)指向 commit 对象。
图:分支只是指向某个 commit 的指针。
3. 分支操作底层拆解
3.1 创建分支git branch feature
底层:在.git/refs/heads/下新建一个文件feature,内容是当前 commit 的哈希。
白话:多贴了一张叫feature的便利贴,贴的位置和main的便利贴在同一页。
gitbranch feature# 查看两个分支是否指向同一个 commitcat.git/refs/heads/maincat.git/refs/heads/feature两个文件的哈希值完全相同——这就是分支创建瞬间,「两个标签贴在同一页」。
3.2 切换分支git switch feature
底层:把.git/HEAD的内容从ref: refs/heads/main改成ref: refs/heads/feature,然后根据feature指向的 commit 还原工作区和暂存区。
白话:你从main标签走到了feature标签,看到的文件变成了feature这个位置的。
# 切换前cat.git/HEAD# 输出:ref: refs/heads/maingitswitch feature# 切换后cat.git/HEAD# 输出:ref: refs/heads/feature3.3 删除分支git branch -d feature
底层:删除.git/refs/heads/feature这个文件。
白话:撕掉feature这张便利贴。档案柜里的东西一个都没动。
注意:如果feature指向的 commit 没有其他引用或 reflog 指向它,这些对象最终会被垃圾回收。但git branch -d有安全检查——只有当分支已经合并到当前分支时才允许删除。
3.4 提交时发生了什么
gitswitch featureecho"新功能">feature.txtgitaddfeature.txtgitcommit-m"添加新功能"底层:
- 创建 blob 对象(存 feature.txt 的内容)
- 创建 tree 对象(记录新的目录结构)
- 创建 commit 对象(指向新的 tree,parent 是上一个 commit)
- 把
feature引用移动到新 commit - HEAD 继续指向
feature,而feature已经移到新 commit 了
白话:你在feature标签的位置上新增了一份存档,然后把feature标签往前挪到了这份新存档上。main标签还在原来的位置,没有动。
# 验证:main 和 feature 已经不同了cat.git/refs/heads/main# 旧哈希cat.git/refs/heads/feature# 新哈希4. 分离 HEAD:HEAD 不指向分支了
4.1 什么是分离 HEAD
正常情况下,HEAD 指向一个分支名。但如果你直接 checkout 到某个 commit:
gitcheckout a1b2c3d此时 HEAD 不再指向分支,而是直接指向那个 commit:
cat.git/HEAD# 输出:a1b2c3d4e5f6...(直接是哈希,不再是 ref: refs/heads/...)白话:指路牌不再指向某个标签,而是直接指向档案柜里的某一页。
4.2 分离 HEAD 的风险
在分离 HEAD 状态下提交 commit:
echo"测试">test.txtgitaddtest.txtgitcommit-m"分离 HEAD 下的提交"这次提交会创建一个新的 commit,但没有分支指向它!如果你切走了(git switch main),就再也找不到它了——除非你记住了哈希,或者 reflog 里有记录。
白话:你在一页没有标签的档案上写了字,走开之后不知道怎么翻回那一页了。
4.3 什么时候会进入分离 HEAD
| 操作 | 场景 |
|---|---|
git checkout <哈希> | 你想看某个历史版本 |
git checkout <标签名> | 你想看某个打标签的版本 |
| rebase 过程中 | Git 暂时把 HEAD 指向中间 commit |
git submodule操作 | 子模块的默认状态 |
4.4 怎么离开分离 HEAD
方法一:如果你在分离 HEAD 下做了提交,想保留
gitbranch save-my-workgitswitch save-my-work白话:给当前位置贴个标签,然后站到那个标签上去。
方法二:如果没有做新提交,直接切回去
gitswitch main方法三:用 reflog 找回丢失的提交
gitreflog# 找到你提交的哈希,然后切过去建分支gitswitch-crecovered<哈希>5. 动手准备
建一个可丢弃的练习目录:
mkdirlearn-git-12cdlearn-git-12gitinit-bmainecho"第一版">f.txt&&gitaddf.txt&&gitcommit-m"第一次提交"echo"第二版">f.txt&&gitaddf.txt&&gitcommit-m"第二次提交"演示身份:Ada Example <ada@example.com>
6. 跟着做
实验 1:亲手看引用文件
cat.git/HEAD# 输出:ref: refs/heads/maincat.git/refs/heads/main# 输出:某个 40 字符的哈希gitbranch featurecat.git/refs/heads/feature# 输出:和 main 相同的哈希!实验 2:体验分离 HEAD
gitlog--oneline# 假设输出:a1b2c3 第二次提交# d4e5f6 第一次提交gitcheckout d4e5f6cat.git/HEAD# 输出:d4e5f6...(直接是哈希,不再是 ref: 开头)gitswitch main# 回到正常状态实验 3:在分支上提交,观察引用移动
gitbranchtestgitswitchtestecho"测试内容">test.txtgitaddtest.txtgitcommit-m"在 test 上提交"# main 没动cat.git/refs/heads/main# test 移动了cat.git/refs/heads/test# 两个哈希不同了!7. 对照表:各种引用的区别
| 引用类型 | 文件位置 | 谁创建 | 能手动改吗 | 会不会被推送 |
|---|---|---|---|---|
| HEAD | .git/HEAD | Git 自动 | 能改,但不要 | 不会 |
| 本地分支 | .git/refs/heads/名称 | 你 | 能改,但不建议 | 不会(推送后远程有对应追踪分支) |
| 远程跟踪分支 | .git/refs/remotes/origin/名称 | git fetch | 不应该改 | 这是远程的镜像 |
| 标签 | .git/refs/tags/名称 | 你用git tag创建 | 能改,但不建议 | 推送时可以推标签 |
| reflog | .git/logs/ | Git 自动 | 不建议改 | 不会 |
8. 安全习惯
| 规矩 | 为什么 |
|---|---|
不要手动编辑.git/refs/下的文件 | 手滑写错哈希会导致引用损坏 |
| 分离 HEAD 下做了提交,要立刻建分支 | 否则切走后可能找不到这些提交 |
用git switch而不是git checkout | switch语义更明确,不会意外进入分离 HEAD |
定期检查git branch -a | 看看有哪些分支和远程追踪分支 |
9. 真实场景
场景一:用分离 HEAD 看历史版本
老师让你看三个月前的代码长什么样:
gitlog--oneline# 找到那个 commit 的哈希gitcheckout<哈希># 进入分离 HEAD# 看代码、运行代码……gitswitch main# 看完了,回到正常状态场景二:误删了分支,怎么恢复
# 不小心删了一个还没合并的分支gitbranch-Dimportant-work# 恢复!用 reflog 找到最后一次指向它的 commitgitreflog# 找到哈希后gitbranch important-work<哈希>场景三:为什么推送有时被拒绝
你本地main指向 commit A,远程origin/main指向 commit B(别人推的新代码)。你执行git push,Git 发现远程引用要「倒退」才能接受你的提交,所以拒绝了。正确做法:先git pull合并远程新代码,再 push。
10. 进阶补充(首读可跳过)
10.1 符号引用
Git 提供了几个特殊的符号引用,不需要记哈希:
| 符号 | 含义 |
|---|---|
HEAD | 当前位置 |
HEAD~1 | 当前位置的上一代(父提交) |
HEAD~3 | 当前位置往上数第 3 代 |
HEAD^ | 父提交(merge commit 的第一个父) |
HEAD^2 | merge commit 的第二个父 |
@ | HEAD 的简写 |
@{2} | HEAD 2 步之前的位置(reflog) |
10.2 packed-refs
当分支很多时,Git 会把引用打包到一个文件.git/packed-refs里以提升性能。这不影响使用,但如果你找不到.git/refs/heads/某个分支,可能它在 packed-refs 里。
10.3 远程引用不会自动更新
origin/main只有在你git fetch或git pull时才会更新。它不会因为你本地提交了就变——它反映的是「远程那边最新到哪了」。
11. 小实验
实验 A:亲手看引用
- 建一个仓库,做两次提交
cat .git/HEAD看 HEAD 指向什么cat .git/refs/heads/main看分支引用指向什么- 创建新分支,确认新分支和 main 指向同一个 commit
通过标准:能说出 HEAD 指向分支名,分支名指向 commit 哈希。
实验 B:体验分离 HEAD
git checkout到一个历史 commit- 确认 HEAD 直接指向哈希(而不是分支名)
- 做一个新提交
- 切回 main,用
git reflog找回刚才的提交
通过标准:理解分离 HEAD 下提交的 commit 可能丢失,知道用 reflog 找回。
实验 C:引用移动追踪
- 在 main 上提交一次
- 创建并切换到 feature 分支
- 在 feature 上提交一次
- 分别查看 main 和 feature 的哈希,确认 main 没动、feature 移动了
通过标准:理解「在哪个分支上提交,哪个分支的引用就移动」。
12. 常见问题
问:分支和标签有什么区别?
分支是会移动的便利贴——每次提交它就往前挪。标签是贴好了就不动的便利贴——它永远指向你打标签那一刻的 commit。
问:远程跟踪分支能改吗?
不应该手动改。它是远程仓库的镜像,只有git fetch才应该更新它。
问:分离 HEAD 下的提交会丢吗?
切走之后没有引用指向它,但 reflog 会记住 90 天。90 天内可以用git reflog找回来。超过 90 天且没有引用指向它,垃圾回收会清理掉。
问:为什么git checkout会进入分离 HEAD,但git switch不会?
git switch只接受分支名,不接受裸哈希。如果你想看历史的某个 commit,用git checkout。但日常切换分支用git switch更安全。
问:.git/refs/heads/里没有某个分支的文件?
可能被打包到了.git/packed-refs文件里。用git branch查看所有分支,不需要去文件系统里找。
13. 总结
13.1 一页速记
| 项目 | 要点 |
|---|---|
| 分支 | .git/refs/heads/名称下的一个文件,内容是 commit 的哈希 |
| HEAD | .git/HEAD文件,通常指向某个分支名 |
| 分离 HEAD | HEAD 直接指向 commit 哈希,不指向任何分支 |
| 创建分支 | 新建一个引用文件,内容是当前 commit 的哈希 |
| 删除分支 | 删除引用文件,不影响对象 |
| 提交时 | 在哪个分支上提交,哪个分支的引用就移动到新 commit |
13.2 本系列中的位置
10 对象模型 → 11 命令与文件 → 12 引用与 HEAD(你在这里) → 13 暂存区深入 ↑ 搞懂「移动引用」到底移动了什么13.3 思维升华
分支是便利贴,HEAD 是指路牌。记住这个比喻,Git 80% 的操作你都能理解:创建分支 = 贴标签,切换分支 = 走到另一个标签,提交 = 往前挪标签。简单吧?
13.4 延伸阅读
- Pro Git — Git 引用
- gitrevisions 手册(符号引用的完整说明)
- git-switch 文档
- git-checkout 文档
- 本仓库图示署名:
assets/diagrams/ATTRIBUTION.md
13.5 检查清单
- 能说出分支文件只包含一行哈希
- 能区分 HEAD 指向分支名 vs HEAD 直接指向哈希
- 知道分离 HEAD 是什么、怎么进入、怎么离开
- 知道删分支只是删引用,对象不会丢
- 完成了实验 A、B、C