Git 核心概念:三个区域、对象模型与 HEAD
Git 本质是一个内容寻址的文件系统。理解三个区域怎么搬数据、四类对象怎么互相指向、分支与 HEAD 为什么只是指针,绝大多数「Git 为什么这样」都能自己推出来。
最后更新:2026-10-30
三句话理解 Git 的内核
- Git 本质是一个内容寻址的文件系统:文件内容算哈希,哈希就是地址。
- 日常操作只在三个区域之间搬数据:工作区 → 暂存区 → 仓库。
- 分支、标签、HEAD 都只是指向提交的指针,移动指针比复制文件便宜得多。
理解这三点,绝大多数「Git 为什么这样」都能自己推出来。
三个工作区域
| 区域 | 存什么 | 相关命令 |
|---|---|---|
| 工作区 Working Tree | 你看到的文件,可随意改 | git status / git diff |
| 暂存区 Index / Staging | 下一次提交的快照清单 | git add / git restore --staged |
| 仓库 Repository(.git) | 所有提交与对象,只追加 | git commit / git log |
数据单向流动:工作区 —add→ 暂存区 —commit→ 仓库。反方向用 restore / reset / checkout。
对象模型:四类对象
- blob:文件内容(不含文件名)
- tree:目录快照,记录「文件名 → blob / tree 的哈希」
- commit:一次提交,指向一个 tree + 父提交 + 作者 / 时间 / 说明
- tag(annotated):给某个提交起的带注释别名
git cat-file -t <sha> # 看对象类型
git cat-file -p <sha> # 看对象内容内容寻址:同样的内容永远得到同样的哈希。所以相同的两份文件只占一份空间,判断内容是否变化也只需比哈希。
引用与 HEAD
- 分支只是一个指向提交的文件(如
.git/refs/heads/main),提交时自动前移 - HEAD 指向「当前分支」;
detached HEAD时直接指向某个提交,所以此时提交容易「丢」 - 远程跟踪分支(
origin/main)是本地对远程状态的缓存,只有fetch才更新 - 标签指向提交且默认不动,适合标记版本
git rev-parse HEAD # 当前提交
git symbolic-ref HEAD # 当前分支(detached 时会报错)
cat .git/HEAD # 直接看文件内容暂存区(index)到底是什么
暂存区不是「暂存文件夹」,而是一份待提交的树快照。
git add= 把当前文件内容做成 blob,并把这条记录写进 index(快照,不是「加入队列」)- 所以 add 之后再改文件,
git status会同时显示「已暂存」和「未暂存」两个版本 git diff比的是工作区与 index;git diff --staged比的是 index 与 HEAD
理解 index 是快照,就能解释「为什么 add 之后还要再 add」。
三棵树与 diff 的正确用法
| 命令 | 比较对象 |
|---|---|
| git diff | 工作区 ↔ 暂存区(还没 add 的改动) |
| git diff --staged | 暂存区 ↔ 最后一次提交(准备提交的内容) |
| git diff HEAD | 工作区 ↔ 最后一次提交(全部未提交改动) |
| git diff HEAD~1 HEAD | 两次提交之间的差异 |
记住这条线:工作区 — git diff — 暂存区 — git diff --staged — HEAD。
对象存在哪:.git/objects 探秘
- 松散对象:
.git/objects/ab/cdef...(哈希前两位做目录名) - 打包对象:
git gc后合并为.git/objects/pack/*.pack,体积更小 git count-objects -vH查看对象数量与占用- 对象只追加不修改:改文件产生新 blob,旧对象仍在(直到 gc)
git count-objects -vH
git fsck --lost-found # 找出不可达对象「Git 丢不了东西」的底气就在这里:只要对象没被 gc,就有机会用 reflog / fsck 找回。
常见问题
为什么 Git 用哈希而不是文件名来标识内容?
内容寻址让比较、去重和完整性校验都变得极廉价:同样内容哈希相同可以自动去重,比哈希就能判断内容是否变化,任何一位损坏都会导致哈希不匹配。文件名会变、会重复,不适合当地址。新版 Git 也支持 SHA-256 仓库格式。
Git 的分支和标签有什么区别?
分支是会移动的指针,每次提交自动前移;标签默认固定指向某个提交,用来标记版本。分支删掉后提交可能变成不可达(可借助 reflog 找回),标签则是给提交起一个稳定别名。
暂存区(index)到底有什么用,能不能跳过?
暂存区是「下一次提交的快照」,让你能挑选性提交(git add -p 只挑部分改动)、提交前预览(git diff --staged)。可以跳过:git commit -a 会跳过 add 直接提交已跟踪文件,git commit <file> 直接提交指定文件;但要做精细的提交拆分,index 是核心工具。