Git 版本管理系统由 Linux 的作者 Linus Torvalds 于 2005 年创造,至今不到二十年。
起初,Git 用于 Linux Kernel 的协同开发,用于替代不再提供免费许可的 BitKeeper 软件。随后,这一提供轻量级分支的分布式版本管理系统得到了开源开发者的广泛喜爱,在大量开源项目中投入使用。如今,Git 几乎是版本管理系统的同义词。
Git 最大的创新就是轻量级的分支实现,鼓励分布式的开发者群体创建自己的分支。每个分支都是平等的,只是原始作者的分支或者实现最好的分支会被社群公认为上游,其他分支被认为是下游。下游分支的修改通过邮件列表发送补丁或 GitHub 发起 Pull Request 的方式向上游申请合并,最后大部分用户从上游分支取得源代码使用。
在这个模型下,如何协同不同分支的开发,当上游发布了多个版本,尤其是并行维护多个发布版本时,如何管理分支,就是一个亟需解决的问题。Git 自身的设计不解决这个问题,也不对此做建模。它只提供分支创建和合并等基本功能,而把具体的分支管理策略留给开源软件的开发者。
基础软件的分支策略
一路向前的 Curator
对于绝大部分的开源软件来说,既没有维护多个版本的需求,又没有重量级的发版检查,最适合自己的分支策略就是唯一上游分支,一路向前。
Apache Curator[1] 采用了这种策略:主分支 master 是唯一的上游分支,版本号一路向前,没有在以前的功能版本发新的补丁版本的说法。
所有的下游修改,一般也是一个小修改一个分支,做完以后迅速提交到上游评审合并,几乎所有用户获取的版本都是从 master 分支上打 tag 得到的。
这种简单的策略被广泛使用,甚至可以做成自动化的流水线:
spotless[2] 用一个专门的发布流水线来手动触发从 master 分支分析 changelog 并发布新版本的工作。
griseo[3] 的流水线则是在推送新的 tag 的时候就触发把 tag 关联的新版本发布到 GitHub Release 页面和 PyPI 仓库上。这参考了 githubkit[4] 的方案。
setup-zig[5] 依赖 npm 生态的 semantic-release[6] 工具集,实现了彻底的自动化:每次主分支合并代码后,自动分析出 changelog 并根据 changelog 的语义判断是否应该发布新版本,应该发布什么版本。
齐头并进的 Flink
Apache Flink[7] 是一个典型的并行维护多个发布版本的开源软件。
Flink Release Management[8] 提到,Flink 上游社群维护最近的两个特性版本,而其过往发布记录大致如下:
Flink 版本发布历史
值得注意的是,其版本发布时间并不随着语义版本号单调递增,例如 1.16.0 的发布日期(2022-10-28)就早于 1.15.4 的发布日期(2023-03-15)。
根据语义化版本的定义,patch releases 只包含必要的修复,而不应该包含新功能。如果仍然采取一路向前的分支策略,那么在发布了带有新功能的 1.16.0 版本后,再发布 1.15.4 版本,难道还能 revert 所有功能变更吗?这不现实。
所以 Flink 采取的是和并行维护发布版本线对应的分支策略:
master 分支是不稳定的开发分支,对应 X.Y-SNAPSHOT 的快照版本,其中 X.Y 是下一个即将发布的功能版本。例如,现在最新的功能版本是 1.17 版本,那么 master 上的版本号就是 1.18-SNAPSHOT 了。
release-X.Y 分支是 X.Y 系列版本的基础分支,该分支将接受 bug fix 类的提交。
分支是不稳定的 Git 引用,不同时间 check out 同一个分支可能得到不同的结果。Flink 在实际做版本发布的时候,选择的是 tag 的形式来发布不可变的版本:
release-X.Y.Z-RCn 是一个标签版本,是一个静态的版本,对应 X.Y.Z 的第 n 次预发版本。由于 Flink 系统复杂,发布周期内需要进行大量测试,很可能有多次预发,为了避免强行覆盖 tag 导致破坏 tag 引用内容不可变的语义,Flink 用这一 tag 命名模式来给预发版本起名。