OFFSET偏移
← 文章目录OFFSET / FIELD NOTESNO. 093

Git 的底层模型:对象、快照与引用

用快照、对象数据库和引用理解 Git 操作改变的内容。

所在专题Git:从快照到协作与恢复 ↗
约 8 分钟阅读开始阅读

"Git 不仅仅是一个版本控制工具,它是一套关于协作与数据完整性的哲学。"

1. 进化论:为什么分布式 (Distributed) 是降维打击?#

在 Git 出现之前,主流工具(如 SVN)是集中式的。

  • 集中式 (Centralized):
    • 逻辑:服务器是唯一的真理。
    • 痛点:服务器不可用时同步受限,离线查看未缓存的历史和服务端操作受限;仍可通过服务器备份恢复,并非必然丢失全部数据。
  • 分布式 (Distributed):
    • 逻辑:普通完整克隆通常有可达历史的本地副本;浅克隆和 partial clone 可能不完整。
    • 优势:极速离线操作(在飞机上也能 commit/查看历史);分支切换几乎瞬间完成;对象校验帮助检测内容损坏,但不代替身份认证、签名和备份。

2. 存储模型:快照 (Snapshots) vs. 差异 (Deltas)#

这是理解 Git 速度和可靠性的关键。

  • 差异模型 (传统工具):类似于“差量备份”。记录文件 A 在版本 2 增加了哪几行。底层可能结合完整版本与差异存储,访问成本取决于实现,不能对所有传统工具一概而论。
  • 快照模型 (Git):每次提交,Git 记录的是整个项目在这一刻的“全量照片”。

为什么“全快照”不会导致存储爆炸?

Git 实现了“逻辑全快照,物理增量存”的奇迹,依靠以下三大机制:

  1. 内容寻址 (Blob 复用):Git 依据内容而非文件名存数据。如果你有 1000 个文件但在新提交中只改了 1 个,Git 只为那 1 个文件创建新存储,其余 999 个直接通过哈希指针指向已存在的旧数据。
  2. 树对象 (Tree) 的结构复用:目录结构也是对象。如果一个子目录内的内容完全没变,父级目录(Tree 对象)会直接引用旧子目录的哈希值,无需重复存储相同的树对象;工作区状态扫描是另一个机制。
  3. 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 最具艺术感的设计:

  1. 解耦修改与提交:你在工作区可能修改了 10 处代码。其中 3 处是 Bug 修复,7 处是新功能。
    • 你可以先 git add 那 3 处修复,进行一次 commit;
    • 然后再 add 剩下的 7 处,进行另一次 commit。
    • 结果:提交历史极其整洁,每一个 commit 都只有一个明确的主题。
  2. 构建“下一次快照”的蓝图:git commit 并不是从你的硬盘上抓取文件,而是根据暂存区里的清单直接打包。这保证了提交的原子性和可预测性。

区域状态定义

  • 工作区 (Working Directory):你在文件管理器里看到的真实文件。
  • 暂存区 (Staging Area):一个简单的索引文件(.git/index),记录了即将进入下一次提交的文件指纹。
  • 本地仓库 (Repository):Git 存放所有压缩对象和历史记录的数据库(.git/objects)。

4. Git 的原子组件:对象数据库 (Objects)#

在 .git/objects 目录下,存储着 Git 的灵魂。

  1. Blob:存储内容(不含文件名)。
  2. Tree:存储结构(类似文件夹,记录文件名及指向 Blob 的哈希)。
  3. 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,并保留独立备份或明确的恢复引用。

参考与延伸

  1. Pro Git:Git 对象 ↗
  2. Pro Git:Packfiles ↗
  3. Git 引用日志 ↗

根据技术笔记重新整理,示例使用通用名称。

沿专题继续阅读

Git:从快照到协作与恢复

  1. 01Git 的底层模型:对象、快照与引用
  2. 02从第一次 add,到撤回一次修改。
  3. 03Git 分支:合并、冲突与现场保存
  4. 04Git 远程协作:抓取、追踪与双远端
  5. 05Git 进阶:变基、拣选与二分排错
  6. 06Git 场景手册:修正、撤销与恢复
CONTINUE READING / 下一篇Git 分支:合并、冲突与现场保存

理解快进和三方合并,并用 stash 管理尚未完成的工作。