版本控制与 Git
Git 常被当作"存差异的数据库",实际是内容寻址的快照图——commit 是快照而非差异。本章从数据结构角度讲 Git,让常见操作不再神秘。
Git 的存储是四类内容寻址(SHA-1 哈希即名字)的对象:
- blob:文件内容(不含文件名);
- tree:目录(名字 → blob/tree 的映射);
- commit:快照(指向一个 tree + 父 commit + 元信息);
- tag:带签名的指针。
commit 是快照而非差异:每个 commit 指向完整的目录树;"差异"只在展示时动态计算(git diff)。这解释了 Git 的多数行为:
- 检出任意 commit 是 (只取变化的 blob,打包压缩去重);
- 两个 commit 的 diff 是树比较,不是链表回放;
- 分支只是指针(一个 40 字节文件)——创建分支是写一个文件,O(1),这是 Git 分支便宜的原因(对比:合并复杂、切换要动工作区)。
commit A ──┐ ├─ tree (src/) ── blob (main.c)commit B ──┘分支、合并与变基
Section titled “分支、合并与变基”- merge:两个分支的快照合成一个新快照(双亲 commit)——merge 保留历史:历史图分叉又汇合,"谁在什么时候做了什么"原样保留;
- rebase:把一串 commit 在新基底上重放——rebase 重写历史:commit 哈希全变,历史变成一条直线;
- 取舍:merge 的历史真实但杂乱(分叉图),rebase 的历史线性但虚假(重写过)——团队约定的常见折中:本地未推送的 commit 用 rebase 整理,已推送的公共分支用 merge(重写别人可能依赖的历史是 rebase 的大忌)。
冲突 = 两个快照对同一区域给出不同的内容——Git 不知道谁对,只能标记、让人裁决。冲突的粒度是"行"(更细的 hunk),因此小而频繁的提交让冲突少而小——大分支长期分叉才是冲突灾难的根源。
常见操作的"真相"
Section titled “常见操作的"真相"”| 操作 | 真相 |
|---|---|
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 | 把远端指针指到重写过的历史(危险) |
分支策略与代码评审
Section titled “分支策略与代码评审”- trunk-based(主干开发):短命分支、频繁合入、依赖特性开关——持续交付的配套;
- GitHub Flow:分支 + PR + 评审 + 合并——小团队的主流;
- Git Flow(develop/release/hotfix 多分支):适合有版本化发布的软件,对 Web 服务过重。
小而频繁的提交便于评审与回滚:一个 commit 一件事(原子提交),评审的粒度与回滚的粒度都对齐。代码评审的价值不在抓 bug(测试更擅长),在知识扩散与设计共识——评审是团队的同步机制。
- commit 是快照而非差异;对象内容寻址(哈希即名字)
- 分支只是指针(O(1));切换的成本在工作区不在 Git
- rebase 重写历史,merge 保留历史;公共分支别 rebase
- 冲突的本质是两个快照的不同:小而频繁的提交让冲突少而小
- 评审的价值在知识扩散与设计共识,不在抓 bug