跳转至

Commit 与 DAG

很多人第一次接触 Git 提交历史时,会把它想成“提交列表”。
这个理解能勉强应付 git log,但一旦遇到分支和 merge,就不够了。

更准确的说法是:

Git 的提交历史是一个有向无环图,也就是 DAG。

commit 里到底有什么

一个 commit 最核心的信息包括:

  • 指向根 tree 的对象 ID;
  • 一个或多个父提交;
  • 作者和提交者信息;
  • 时间戳;
  • 提交说明。

commit 本身不直接保存“分支名”。
分支名是 refs 系统的工作,不是 commit 对象的一部分。

为什么历史是图,不是线

如果每个 commit 都只有一个父提交,那历史看起来像一条链。
但 Git 允许:

  • 从旧提交上新开分支;
  • 合并另一条分支;
  • 形成拥有两个父提交的 merge commit。

一旦 merge commit 出现,历史就不再是线性列表,而是图结构。

这也是为什么很多 Git 功能必须思考“图关系”,而不是只思考“前后顺序”。

什么是 DAG

DAG 是 Directed Acyclic Graph,中文叫有向无环图。

拆开看:

  • 有向:提交关系是有方向的,子提交指向父提交;
  • 无环:不会出现某个提交最终又指回自己祖先后的自己。

这两个性质让 Git 能可靠地做:

  • 祖先判断;
  • 最近公共祖先搜索;
  • 快进判断;
  • 历史遍历;
  • merge base 计算。

branch 为什么这么轻

一旦你理解 commit 图,就会发现 branch 根本不是“复制一份项目”。

branch 本质只是一个 ref,指向图中的某个 commit。
新建分支时,不是复制对象库,而只是新增一个名字。

这也是 Git 分支为什么非常轻量的原因。

HEAD 表示“当前检出的目标”。

通常情况下,HEAD 会符号引用某个分支,比如:

ref: refs/heads/main

这意味着当前工作区和提交行为都跟着 main 走。
如果你 checkout 到某个 commit 而不是某个分支,HEAD 就会进入 detached 状态。

log 为什么通常按第一父链看

你平时看到的很多历史输出,默认是沿着父提交链往回走。
在简单场景里,这很像链表遍历。

但只要出现 merge commit,历史就不再只有一个父节点。
因此:

  • 有些命令看第一父链;
  • 有些命令要看所有父边;
  • merge 时更要在图里找最近公共祖先。

最近公共祖先为什么重要

假设你有两条分支:

  • ours
  • theirs

想把它们合并,不能只比较两个分支头。
你还必须知道:它们共同从哪里分叉出来。

这个共同祖先就是 merge 的基准点。
没有它,你就无法准确判断:

  • 哪些改动只有一边做了;
  • 哪些改动两边都做了;
  • 哪些改动发生了冲突。

python-git-reproduction 中的意义

这个项目里:

  • commit-tree 创建 commit 对象;
  • commit 更新当前分支 ref;
  • log 沿历史读提交;
  • merge 通过 DAG 找最近公共祖先;
  • 分支、标签和 HEAD 都是围绕 commit 图在移动。

所以理解 commit 与 DAG,不只是学 Git,也是在学这个项目的整体骨架。