跳转到内容

版本控制与 Git

Git 常被当作"存差异的数据库",实际是内容寻址的快照图——commit 是快照而非差异。本章从数据结构角度讲 Git,让常见操作不再神秘。

Git 的存储是四类内容寻址(SHA-1 哈希即名字)的对象:

  • blob:文件内容(不含文件名);
  • tree:目录(名字 → blob/tree 的映射);
  • commit:快照(指向一个 tree + 父 commit + 元信息);
  • tag:带签名的指针。

commit 是快照而非差异:每个 commit 指向完整的目录树;"差异"只在展示时动态计算(git diff)。这解释了 Git 的多数行为:

  • 检出任意 commit 是 O(改动)O(\text{改动})(只取变化的 blob,打包压缩去重);
  • 两个 commit 的 diff 是树比较,不是链表回放;
  • 分支只是指针(一个 40 字节文件)——创建分支是写一个文件,O(1),这是 Git 分支便宜的原因(对比:合并复杂、切换要动工作区)。
commit A ──┐
├─ tree (src/) ── blob (main.c)
commit B ──┘
  • merge:两个分支的快照合成一个新快照(双亲 commit)——merge 保留历史:历史图分叉又汇合,"谁在什么时候做了什么"原样保留;
  • rebase:把一串 commit 在新基底上重放——rebase 重写历史:commit 哈希全变,历史变成一条直线;
  • 取舍:merge 的历史真实但杂乱(分叉图),rebase 的历史线性但虚假(重写过)——团队约定的常见折中:本地未推送的 commit 用 rebase 整理,已推送的公共分支用 merge(重写别人可能依赖的历史是 rebase 的大忌)。

冲突 = 两个快照对同一区域给出不同的内容——Git 不知道谁对,只能标记、让人裁决。冲突的粒度是"行"(更细的 hunk),因此小而频繁的提交让冲突少而小——大分支长期分叉才是冲突灾难的根源。

操作真相
git commit写 tree + commit 对象,移动分支指针
git branch写一个 40 字节文件
git checkout/switch把 tree 的内容铺到工作区
git reset移动分支指针(+ 可选动工作区)
git revert生成"逆向补丁"的新 commit(不改历史)
git cherry-pick在当前分支重放某个 commit
git rebase重放一串 commit,生成新哈希
git push --force把远端指针指到重写过的历史(危险)
  • trunk-based(主干开发):短命分支、频繁合入、依赖特性开关——持续交付的配套;
  • GitHub Flow:分支 + PR + 评审 + 合并——小团队的主流;
  • Git Flow(develop/release/hotfix 多分支):适合有版本化发布的软件,对 Web 服务过重。

小而频繁的提交便于评审与回滚:一个 commit 一件事(原子提交),评审的粒度与回滚的粒度都对齐。代码评审的价值不在抓 bug(测试更擅长),在知识扩散与设计共识——评审是团队的同步机制。

  • commit 是快照而非差异;对象内容寻址(哈希即名字)
  • 分支只是指针(O(1));切换的成本在工作区不在 Git
  • rebase 重写历史,merge 保留历史;公共分支别 rebase
  • 冲突的本质是两个快照的不同:小而频繁的提交让冲突少而小
  • 评审的价值在知识扩散与设计共识,不在抓 bug