1. 别急着记命令:先搞清楚Git到底在“管”什么
很多朋友学Git,都是靠“背命令”起步的。git add .、git commit -m "xxx"、git push,一套流程滚瓜烂熟,但一旦遇到冲突、分支乱套、误操作回滚,就完全抓瞎,只能上网搜“Git 如何撤销”然后复制粘贴。我也是从那个阶段过来的,直到某天认真读了一遍Git的内部原理,回头看之前踩过的坑,几乎每个都有了解释。
Git与其他版本管理工具最大的区别在于:它管理的是“快照”,而不是“差异”。SVN这类老牌工具存的是文件之间的变化记录(delta),你要知道某个文件某一天长什么样,得从初始版本开始一个个补丁往后算。Git不一样,每一次commit都是整个项目的一个完整快照,只不过它用了非常聪明的压缩手段,不会让仓库体积爆炸。
但快照这个概念还是不够底层。真正让Git强大到能支撑Linux内核这种超大项目的,是它底层的对象模型——一切皆对象,所有历史都是一张有向无环的对象图。当你理解了这张图,Git的几乎一切行为都变得合乎逻辑了。
本文不啰嗦安装配置,直接剖開核心架构,把下面这张图塞进你脑子里:
code复制工作区 (Working Directory)
│ git add
▼
暂存区 (Staging Area / Index)
│ git commit
▼
版本库 (Repository / .git)
├── 对象库 (Objects) ←── Blob / Tree / Commit / Tag
└── 引用 (References) ←── branches / HEAD / tags
这张图是本文的地图。你可能会觉得“这不就是网上烂大街的三区模型吗”,但真正值得深入的是:三个区之间流转的数据到底是什么形态、对象之间怎么互相指向、引用在整个过程中扮演什么角色。把这些搞清楚,你才算真正“会”了Git。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 对象库:四种对象如何构成一张“代码流动网”
先直接说结论:Git的版本库里只有四种对象——Blob(数据对象)、Tree(树对象)、Commit(提交对象)、Tag(标签对象)。每一次commit、每一个分支、每一次tag,最终都归结为这四种对象之间互相引用的关系。
2.1 Blob:Git“眼中”的最小文件单位
当你执行git add时,Git会把文件内容压缩后存进对象库,生成一个Blob对象。关键在于:Blob里只有文件内容,没有文件名、没有路径、没有任何元信息。换句话说,两个不同路径下内容完全相同的文件,在Git对象库里只存一份Blob。
这带来一个大好处——Git天然具备内容去重的能力。比如你把某个文件复制到另一个目录,内容一字不改,Git不会为它生成第二个Blob。同时这也带来一个反直觉的结论:Git并不追踪“文件重命名”这件事。你在git status里看到“renamed”提示,那是Git根据内容相似度“猜”出来的,底层并没有存储任何重命名记录。
你可以在项目里亲手验证一下:
bash复制# 随便找一个已被Git跟踪的文件
git hash-object <文件路径>
这个命令会输出一个40位的SHA-1哈希(新版Git可能用SHA-256),它就是该文件内容对应的Blob唯一标识。注意,是内容决定哈希,跟文件名无关。你改一个字节,哈希就完全变了。
2.2 Tree:还原目录结构的关键先生
Blob只有内容,那目录结构、文件名去哪了?靠的是Tree对象。Tree可以类比为文件系统里的目录:一个Tree节点下面可以包含若干Blob节点(代表文件),也可以包含若干子Tree节点(代表子目录)。
code复制Tree对象(对应项目根目录)
├── Blob "README.md"
├── Tree "src"
│ ├── Blob "main.c"
│ └── Blob "utils.h"
└── Tree "docs"
└── Blob "manual.md"
每次commit,Git都会把当时暂存区里的完整目录结构打包成一个Tree对象。文件内容的任何修改,都会生成新的Blob,进而生成包含新Blob引用的新Tree。但注意,未被修改的目录,其Tree对象会直接被复用——这就是Git快照机制下存储效率依然很高的秘
