LabHub
学习 学习路径 课程

Git 实战

选战略只需问一个问题

在 LabHub 中继续学习

一句话总结

选择分支策略时要问的,不是“哪种图形漂亮”,而是“发生故障时,计划如何在这个 repository 中找到致因 commit”。

概念图: 从 Git 的视角看,该 branch 从未合并过 · 每个 commit 都能构建并运行,而且 commit 要小。 · 新的 commit · 回退合并后再次合并同一个 branch,不会带入任何变更。

为什么需要了解这一点

Git Flow、GitHub Flow 和 Trunk-Based 分别以不同的部署周期为前提。忽视前提只照搬名称,每天都会产生摩擦。

策略 分支类型 合并频率 适用场景
Git Flow 5 种以上 每周 1~2 次 定期发布、移动应用、package
GitHub Flow 2 种 每天 1~3 次 实践 CD 的 SaaS
Trunk-Based 1~2 种 每天 5~10 次以上 分支寿命不超过 1 天,必须使用 Feature Flag

Trunk-Based 对 CI 的要求非常高。没有 Feature Flag 就开始使用,未完成的代码会原样部署。相反,在 GitOps 环境中,Git Flow 的 release branch 与自动同步模式摩擦很大。因此,使用 GitOps 时建议选择 GitHub Flow 或 Trunk-Based。

有一条通用规则在任何地方都成立:存活超过 3 天的 feature branch 是 merge conflict 的温床。

工作原理

合并方式有三种,各自留下不同的信息。

cherry-pick 不是剪下已保存的 diff 再粘贴。Git 本来就不存储 diff,只存储 snapshot。因此,它会把目标 commit 的 parent tree 当作共同祖先,执行 three-way merge。这正是 cherry-pick 会发生冲突的原因。

实际工作中的表现

在 release branch 中修复 hotfix 却没有反映到 main,导致同一缺陷在下次 release 时复活,几乎每个团队都至少经历过一次。运营 release branch 时,必须把“hotfix 也必须带回 main”明确写成 policy。

tag 也是如此。lightweight tag 只是 pointer,不会记录由谁、何时、为何添加。release tag 必须用 -a 添加 annotation,之后才能用于调查。

历史是为调查而保留的

分支策略的价值不在平时,而会在事故发生时显现。面对“昨天还正常,今天却不行”的情况,查找致因 commit 的标准工具是 bisect。Git 通过 git bisect 半自动完成这项工作。告诉它一个正常 commit 和一个故障 commit,它会 checkout 中间点;人只需回答好或坏,范围便会逐次减半。在 1,000 个 commit 中寻找原因,十次就足够了。

要让 bisect 有效,历史必须满足两个条件:每个 commit 都能构建并运行,而且 commit 要小。 如果中间夹有无法运行的 commit,人就不能回答好或坏,只能跳过该点;如果通过 squash 合并让整个 PR 变成一个 commit,搜索会停在这个 PR 面前。也就是说,只能查到“就在这个 PR 中某处”,余下内容仍要人工阅读。前面所说 squash 合并会把 bisect 分辨率固定为 PR 大小,指的就是这里。

找到致因 commit 后,也要预先确定下一步选择。回退方法有两种,性质完全不同。

回退 merge commit 时还需要额外一步。因为它有两个 parent,必须用 -m 1 指定保留哪一条主线,通常 1 号是接收合并的一方(main)。而且,回退合并后再次合并同一个 branch,不会带入任何变更。 因为在 Git 看来,这些 commit 已经合并过。要重新引入已回退的内容,必须再次回退那个 revert commit;最好不要在紧急时刻才第一次知道这一点。

下个练习将做什么

从头创建 repository,用 --no-ff 合并 feature branch 以保留集成记录,通过 fast-forward 提交小修改,添加 release tag,把 release branch 中的 hotfix 通过 cherry-pick 带回 main,然后只选择已合并 branch 进行清理。最后统计这个 repository 的实际数字,编写策略对比表。