三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

12 — 分支就是便利贴,HEAD 就是指路牌

12 — 分支就是便利贴,HEAD 就是指路牌

12 — 分支就是便利贴,HEAD 就是指路牌

摘要:本文深入解析 Git 中「引用」和「HEAD」的核心概念。分支本质上是.git/refs/heads/目录下的文件,仅包含一行 commit 哈希值,如同贴在档案上的便利贴;HEAD 则是.git/HEAD文件,指示当前所在位置,如同指路牌。文章通过「便利贴」和「指路牌」的生动比喻,拆解了分支创建、切换、删除及提交时的底层文件变化,解释了分离 HEAD 的状态与风险,并提供了动手实验和真实场景示例,帮助读者从文件系统层面理解 Git 引用机制,从而掌握分支操作的底层逻辑。

写在前面:这一章要解决什么

你可能已经用了很多次分支——git branchgit switch——但你有没有想过:分支到底是什么?它是复制了一份文件?还是复制了一个目录?

都不是。分支就是一张便利贴。这一章帮你彻底搞清楚「引用」和「HEAD」到底是什么,为什么理解了它们,Git 的很多操作就不再神秘。

学完后,你应该能:

  1. 用自己的话说出「分支是什么」(不是复制文件夹,是便利贴)
  2. 分清 HEAD、分支引用、远程跟踪引用的区别
  3. 知道「分离 HEAD」是什么状态,怎么进去、怎么出来
  4. 看懂.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/HEAD
ref: refs/heads/main

白话翻译:我站在main这个标签上。

当你提交新 commit 时,Git 会做两件事:

  1. 创建新的 commit 对象
  2. 把 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/feature

3.3 删除分支git branch -d feature

底层:删除.git/refs/heads/feature这个文件。

白话:撕掉feature这张便利贴。档案柜里的东西一个都没动。

注意:如果feature指向的 commit 没有其他引用或 reflog 指向它,这些对象最终会被垃圾回收。但git branch -d有安全检查——只有当分支已经合并到当前分支时才允许删除。

3.4 提交时发生了什么

gitswitch featureecho"新功能">feature.txtgitaddfeature.txtgitcommit-m"添加新功能"

底层:

  1. 创建 blob 对象(存 feature.txt 的内容)
  2. 创建 tree 对象(记录新的目录结构)
  3. 创建 commit 对象(指向新的 tree,parent 是上一个 commit)
  4. feature引用移动到新 commit
  5. 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/HEADGit 自动能改,但不要不会
本地分支.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 checkoutswitch语义更明确,不会意外进入分离 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^2merge commit 的第二个父
@HEAD 的简写
@{2}HEAD 2 步之前的位置(reflog)

10.2 packed-refs

当分支很多时,Git 会把引用打包到一个文件.git/packed-refs里以提升性能。这不影响使用,但如果你找不到.git/refs/heads/某个分支,可能它在 packed-refs 里。

10.3 远程引用不会自动更新

origin/main只有在你git fetchgit pull时才会更新。它不会因为你本地提交了就变——它反映的是「远程那边最新到哪了」。


11. 小实验

实验 A:亲手看引用

  1. 建一个仓库,做两次提交
  2. cat .git/HEAD看 HEAD 指向什么
  3. cat .git/refs/heads/main看分支引用指向什么
  4. 创建新分支,确认新分支和 main 指向同一个 commit

通过标准:能说出 HEAD 指向分支名,分支名指向 commit 哈希。

实验 B:体验分离 HEAD

  1. git checkout到一个历史 commit
  2. 确认 HEAD 直接指向哈希(而不是分支名)
  3. 做一个新提交
  4. 切回 main,用git reflog找回刚才的提交

通过标准:理解分离 HEAD 下提交的 commit 可能丢失,知道用 reflog 找回。

实验 C:引用移动追踪

  1. 在 main 上提交一次
  2. 创建并切换到 feature 分支
  3. 在 feature 上提交一次
  4. 分别查看 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文件,通常指向某个分支名
分离 HEADHEAD 直接指向 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
← 返回列表