Docs Library

Git 原理专题

从对象数据库、tree 与 commit、引用、HEAD、packfiles 到常见命令的底层影响,系统建立 Git 的内部心智模型。

一句话理解

Git 的核心不是“文件版本列表”,而是一个以内容寻址对象数据库为基础、再通过引用把这些对象组织成可读历史的系统。

为什么学习 Git 原理值得

很多 Git 命令看起来像技巧题,真正难的是不知道命令在底层改了什么。一旦把内部模型建立起来,很多原本零散的现象就会连起来:

  • 为什么一次 commit 会生成新的对象
  • 为什么 branch 创建几乎是瞬时的
  • 为什么 rebase 会产生新的提交 ID
  • 为什么 reset 看起来像“回退”,但对象常常还在
  • 为什么 reflog 能在误操作后帮你定位旧位置

1. Git 是内容寻址对象数据库

Pro Git 对 Git 对象模型的第一层解释非常重要:Git 底层是一个 key-value 数据存储。你把内容交给 Git,它会基于内容计算出对象 ID,并把对象存进 .git/objects

这意味着两件事:

  1. Git 更关心“内容是什么”,而不是“这个文件叫啥”
  2. 只要对象内容不变,它的对象 ID 就稳定

2. 四类最常见对象

blob

blob 只保存文件内容本身,不保存文件名。

tree

tree 用来组织目录结构,记录“这个目录下有哪些条目,它们分别指向哪些 blob 或 tree”。

commit

commit 会把一次项目状态固定下来。它通常会指向:

  • 一个 tree
  • 一个父提交或多个父提交
  • 作者、时间、提交说明

tag

tag 可以为某个对象附加更稳定、可读的名字,常见于发布版本。

3. 一次 commit 到底发生了什么

从内部模型看,一次 commit 并不是“只记了一行日志”,而是大致发生了这些事情:

  1. Git 根据暂存区内容写出 tree
  2. Git 创建一个新的 commit 对象
  3. commit 指向这次快照对应的 tree
  4. 当前分支引用移动到这个新提交
  5. HEAD 因为指向当前分支,也就等于看到了新位置

所以 commit 的本质是“新增节点并移动分支引用”,而不是在旧历史上直接修改。

4. 分支其实只是一个可移动引用

这是理解 Git 的关键转折点。分支不是仓库副本,也不是一条被单独复制出来的历史线;它只是一个指向某个提交的名字。

这解释了很多现象:

  • 新建分支很快,因为只是新增一个引用
  • 切换分支快,是因为 HEAD 改为跟随另一个引用
  • 删除分支不一定等于提交马上消失,因为对象可能仍被别的引用或 reflog 持有

5. HEAD 是你当前观察历史的位置

在正常情况下,HEAD 是一个符号引用,它指向当前分支;当前分支再指向某个提交。

当你进入 detached HEAD 时,HEAD 不再跟随分支名,而是直接指向一个具体提交。这也是为什么你在这种状态下继续提交时,会生成一段暂时没有分支名接住的新历史。

6. 历史本质上是图,不是单纯列表

很多人把 Git 日志想成线性时间线,但 Git 真正维护的是提交图。

普通提交

普通提交通常只有一个父节点,所以看起来像一条线。

merge 提交

merge commit 往往有两个父节点,用来表达两条历史线在这里汇合。

rebase 之后

rebase 不是“改旧提交”,而是把旧改动重新生成到新基底上,所以图结构会被重新表达。

7. 为什么 rebase 会改写历史

因为 commit 对象里包含父提交信息。只要父提交变了,新的 commit 对象 ID 也会变。于是你虽然看起来“内容差不多”,但底层其实是一组新的提交对象。

这也是为什么:

  • rebase 适合整理未共享历史
  • rebase 不适合随便改写已共享历史

8. reset 到底改了什么

从内部角度看,reset 最重要的动作是移动引用,外加根据模式决定是否同步索引和工作区。

  • --soft:主要移动 HEAD / 当前分支
  • --mixed:再把索引一起调整
  • --hard:再把工作区也覆盖

对象数据库里的旧提交通常不会在这一刻立刻消失,它们只是可能失去容易访问的名字。

9. fetch、push 和远端跟踪引用

理解远端协作时,最好把这些名字分开:

  • main:你的本地分支
  • origin/main:本地记录的远端跟踪引用

fetch 更新的是后者,而不是直接改写前者。这样 Git 才能让你先“知道远端现在在哪里”,再自己决定是否 merge、rebase 或 reset。

10. Packfiles 为什么重要

官方书还解释了 packfiles。简单理解,它们是 Git 为了更高效存储和传输对象而做的打包压缩形式。

这件事提醒我们:Git 的仓库不是简单地把每个版本完整复制一份,而是通过对象、压缩和复用来管理历史。

11. 一个理解 Git 的实用心智模型

可以把 Git 分成三层:

内容层

blob、tree、commit、tag 这些对象本身

指针层

branch、tag、HEAD、remote-tracking refs 这些名字

工作层

工作区、暂存区、命令操作体验

很多常见困惑,都是把这三层混在一起了。

12. 用这个模型重新理解常见命令

git add

把工作区中的内容准备进索引,朝下一次 tree/commit 靠近。

git commit

生成新对象并推动当前分支向前。

git merge

新增一个把两条历史线汇合起来的提交节点。

git rebase

基于新的父提交重新生成一组提交对象。

git checkout / git switch

改变 HEAD 所跟随的目标,并同步工作区视图。

git restore

把路径内容恢复到某个对象状态,不一定涉及分支移动。

13. 为什么原理能直接帮助实战

理解原理不是为了炫技,而是为了让判断更稳。比如:

  • 当你看到 merge commit,就知道它是图中的汇合点
  • 当你看到 detached HEAD,就知道是“指针状态变化”而不是仓库损坏
  • 当你准备 reset 或 rebase,就更容易先考虑引用和恢复路径

一个最值得记住的结论

很多 Git 命令真正做的事,并不是“修改文件”这么简单,而是:

  1. 创建或复用对象
  2. 移动引用
  3. 同步工作区与索引的可见状态

只要你把这三件事分开理解,Git 就会清楚很多。

继续学习建议

建议顺着这条线继续读:

  1. Git 历史说明
  2. refs and HEAD
  3. git rebase
  4. reflog recovery

参考来源