"Git 不仅仅是一个版本控制工具,它是一套关于协作与数据完整性的哲学。"
1. 进化论:为什么分布式 (Distributed) 是降维打击?#
在 Git 出现之前,主流工具(如 SVN)是集中式的。
- 集中式 (Centralized):
- 逻辑:服务器是唯一的真理。
- 痛点:服务器不可用时同步受限,离线查看未缓存的历史和服务端操作受限;仍可通过服务器备份恢复,并非必然丢失全部数据。
- 分布式 (Distributed):
- 逻辑:普通完整克隆通常有可达历史的本地副本;浅克隆和 partial clone 可能不完整。
- 优势:极速离线操作(在飞机上也能 commit/查看历史);分支切换几乎瞬间完成;对象校验帮助检测内容损坏,但不代替身份认证、签名和备份。
2. 存储模型:快照 (Snapshots) vs. 差异 (Deltas)#
这是理解 Git 速度和可靠性的关键。
- 差异模型 (传统工具):类似于“差量备份”。记录文件 A 在版本 2 增加了哪几行。底层可能结合完整版本与差异存储,访问成本取决于实现,不能对所有传统工具一概而论。
- 快照模型 (Git):每次提交,Git 记录的是整个项目在这一刻的“全量照片”。
为什么“全快照”不会导致存储爆炸?
Git 实现了“逻辑全快照,物理增量存”的奇迹,依靠以下三大机制:
- 内容寻址 (Blob 复用):Git 依据内容而非文件名存数据。如果你有 1000 个文件但在新提交中只改了 1 个,Git 只为那 1 个文件创建新存储,其余 999 个直接通过哈希指针指向已存在的旧数据。
- 树对象 (Tree) 的结构复用:目录结构也是对象。如果一个子目录内的内容完全没变,父级目录(Tree 对象)会直接引用旧子目录的哈希值,无需重复存储相同的树对象;工作区状态扫描是另一个机制。
- Packfiles (打包压缩):Git 会在维护时打包松散对象,传输也可以生成 pack;增量压缩减少相似对象占用。delta 基对象与版本顺序不构成固定保证,push 也不等于必然执行完整 gc。
3. 核心物理模型:三棵树 (The Three Areas)#
掌握这“三棵树”的流转,就掌握了 Git 的 80% 常用命令。Git 的工作本质就是在这些区域之间搬运快照。
graph LR
WD[工作区 / Working Directory / 物理文件]
SA[暂存区 / Staging Area / Index / 待提交清单]
RP[本地仓库 / Repository / .git 目录]
WD -- "git add (追踪/快照)" --> SA
SA -- "git commit (永久存档)" --> RP
RP -- "git checkout/switch (恢复到工作区)" --> WD
SA -- "git restore (从暂存区撤销)" --> WD深度解析:为什么非要一个“暂存区”?
很多初学者觉得 add 是多余的。其实,暂存区(Index)是 Git 最具艺术感的设计:
- 解耦修改与提交:你在工作区可能修改了 10 处代码。其中 3 处是 Bug 修复,7 处是新功能。
- 你可以先
git add那 3 处修复,进行一次commit; - 然后再
add剩下的 7 处,进行另一次commit。 - 结果:提交历史极其整洁,每一个 commit 都只有一个明确的主题。
- 你可以先
- 构建“下一次快照”的蓝图:
git commit并不是从你的硬盘上抓取文件,而是根据暂存区里的清单直接打包。这保证了提交的原子性和可预测性。
区域状态定义
- 工作区 (Working Directory):你在文件管理器里看到的真实文件。
- 暂存区 (Staging Area):一个简单的索引文件(
.git/index),记录了即将进入下一次提交的文件指纹。 - 本地仓库 (Repository):Git 存放所有压缩对象和历史记录的数据库(
.git/objects)。
4. Git 的原子组件:对象数据库 (Objects)#
在 .git/objects 目录下,存储着 Git 的灵魂。
- Blob:存储内容(不含文件名)。
- Tree:存储结构(类似文件夹,记录文件名及指向 Blob 的哈希)。
- Commit:存储元数据(作者、时间戳、父提交 ID、指向哪个 Tree)。
对象哈希:传统 SHA-1 仓库以 40 位十六进制对象标识引用内容;SHA-256 仓库使用不同长度。对象标识帮助检测损坏和寻址,不提供绝对安全保证,也不验证提交者身份。
5. 指针的艺术:HEAD 与 分支#
Git 并不像老式工具那样在硬盘上按文件夹存版本。它的历史是靠“指针连连看”串起来的。
提交链 (Commit Chaining):历史是如何串联的?
当你创建一个新的 Commit 时,这个对象内部包含一个字段:parent。
- 机制:普通提交记录一个父提交,初始提交没有父提交,合并提交可能有多个父提交。
- 结果:通过这种回溯机制,形成了一条从当前快照一直指向初始快照的有向无环图(DAG)。这就是所谓的“历史记录”。
Branch (分支):轻如鸿毛的指针
- 本质:分支并不是一个代码副本,它是指向提交的命名引用;松散 refs 可位于
.git/refs/heads/,也可使用 packed-refs 或其他引用存储,不能按单个文本文件理解所有实现。 - 动态性:当你处于
master/main分支并提交代码时,Git 会创建一个新 Commit,并自动把指针文件的内容更新为这个新哈希。分支指针随着你的提交自动向前“漂移”。
默认分支名称
master 与 main 对 Git 核心机制没有差别。初始化默认名可通过 git init -b main 或 init.defaultBranch 指定,托管平台与已有仓库的名称各自配置,不应假定所有仓库已经统一为 main。
分支的区别与共用
- 如何区别:两个分支
master和dev的区别仅仅是它们指向了不同的 Commit。 - 如何共用:如果
dev是从master分出来的,在分叉点之前,它们的所有提交对象都是完全共用的。Git 只需要存储一份数据。 - 分叉 (Diverge):当
dev往前走而master停在原地(或走向另一个方向)时,它们才开始拥有各自独特的 Commit 指针。
HEAD:你的“第一人称”视角
- HEAD 是一个特殊指针,指向你当前所在的那个分支(通常:
HEAD->master->Commit C)。 - 游离状态 (Detached HEAD):如果你让 HEAD 直接指向某个 Commit 而非分支,你就处于游离态。此时提交的代码就像没有挂载点的浮云,如果不及时新建分支关联,可能会被 Git 的垃圾回收机制清理掉。
6. 只增不减的适用边界#
Git 对象通常在创建后不改写,reset 或 rebase 先改变引用及可达关系,不会立刻抹除旧对象。已提交内容常可通过其他分支或本地 reflog 找到。
但“提交过”不是永久备份:引用可能删除,reflog 会过期,GC 可回收不可达对象,磁盘也可能损坏。从未提交或暂存的工作区编辑更不能靠 reflog 保证恢复。危险操作前检查 git status 和 diff,并保留独立备份或明确的恢复引用。
参考与延伸
根据技术笔记重新整理,示例使用通用名称。