以 Changeset 为中心重构 GitHub Pull Request
原文由 Mitchell Hashimoto 于 发布,订阅该博客
我既以维护者的身份参与过拥有上千名贡献者的大型开源项目,也以工程师的身份参与过大型闭源企业项目,各种规模更小的项目也都经历过。回望至今的所有经历,在 GitHub 上我几乎把所有时间都花在了 Pull Request 上,而它恰恰也是 GitHub 上最让我感到沮丧的部分。
关于 Pull Request,我希望看到很多改进,但其中很大一部分问题,都可以通过一个核心功能来解决:changeset。这篇博文将阐述这一设想以及我所期待的样子。
声明:这里的想法并非原创!我并不声称这些想法是我首创的。我的建议基于已被充分探索的 Git 工作流,并且在 Gerrit、Phabricator 或最传统的基于邮件的补丁审核等产品中已经得到部分甚至完整的实现。
当下的问题
如今 GitHub 上一个 Pull Request 的生命周期,本质上就是一个巨大的、可变的 changeset。这简直一团糟!
如今一个典型的 PR 是这样的:贡献者向一个分支推送一组提交,发起 PR,此后这个 PR 就代表了这个分支。大家通过评论来讨论 PR。当贡献者推送新的改动时,这些改动会直接出现在同一个 PR 上并立即更新它。审核者可以留言,贡献者也可以同时推送改动,所有操作都会更新同一个 PR。
这会带来很多问题:
审核者可能针对 PR 的某个旧状态留下了 review,但就在审核过程中贡献者推送了新的改动,导致这份 review 立刻过时。
更糟的是,review 可能会部分过时,剩下的反馈在贡献者推送的新改动的上下文中就变得不知所云。例如,一条行级评论可能写着“同上一条评论的意见”,但由于贡献者推送的改动移动了那些代码行,上一条评论已经消失/被隐藏了。1
Review 中并不包含它所依附的 commit 的任何元数据,只有提交时的时间戳。用户只能大致通过时间戳去对应 commit,但这并不完全准确,因为如果在审核过程中有新的 commit 进来,时间戳看起来会像是针对最新 commit 之后的 review,但你的 review 实际上可能是针对上一个 commit 的。😕
为回应 review 意见而产生的、尚未完成的工作提交,一旦推送到分支就会立刻可见。这迫使贡献者必须在一次提交中回应所有反馈,否则审核者就不得不面对只改了一半的反馈。
你很难回溯到 PR 的先前状态。如果你想只看早期的某组提交而忽略后来的提交,要么得手动构建一个“compare”视图,要么得在本地检出(我通常用后者)。但无论哪种方式,你得到的都只有代码变动,看不到当时时间点的审核意见!
与上面类似,如果贡献者一次性推送了多个新提交,你很难将这组新提交与旧的提交进行对比。你一次基本上只能逐个提交地查看。为此,你又得退回到本地用
git手动生成 diff。还有更多……后面还会再提到一些,但我想已经足以说明问题了。
我确信上面某些点的细节可能说得不准确。肯定会有人说“他本来只要做这个就能解决第 5(a) 个问题”。这很有帮助!但是,我想表达的重点是,如果退一步看,导致这些问题的根本原因才是真正的问题所在。也就是,单个可变的 changeset 以逐 commit 的方式来跟踪一个分支。
Changeset
解决办法就是 changeset:Pull Request 可以通过一个单调递增的版本号(v1、v2……)来实现可版本化。这些版本通常就被称为“changeset”。
每个 changeset 都指向分支在某一固定时刻的状态。这些版本是不可变的:当推送新的提交时,它们会成为一个新的 changeset 的一部分。如果贡献者强制推送了分支,那也会形成一个新的 changeset。之前的 changeset 会被永久保留。
新的 changeset 可以立即发布(每个 commit 一次),也可以暂缓发布,直到贡献者决定提交一个新版本以供审核。后者允许贡献者通过多个提交来回应先前的反馈,等到自己觉得准备好了再一次性发布这些改动。
在 changeset 的世界里,反馈是依附于某个 changeset 的。如果审核者开始审核某个 changeset 时发布了新的 changeset,也没关系,因为这份 review 作为一个原子单元是附着在之前的那个 changeset 上的。
在后续的 changeset 中,通常很有用的一点是,能标示出某个文件或某行在之前的 changeset 中还有未解决的评论。这可以确保早期 changeset 中的反馈不会丢失,并且在任何一个 changeset 被接受之前都必须得到处理。
通常,每个 changeset 都由不同的 Git 引用来表示。例如,如今的 GitHub Pull Request 通常是 refs/pr/1234,你可以在本地用 git 这样检出任意一个 Pull Request。而 changeset 可能是类似 refs/pr/1234/v2(假设)这样的形式,这样你也可以单独检出每一个 changeset。
审核者不再是“批准”整个 PR 并合并,而是批准某个changeset。这意味着贡献者甚至可以在同一个 PR 中提交采用不同方案的多个 changeset,而维护者可以选择其中并非最新的某个 changeset 作为最终想要合并的改动。
GitHub,拜托了!
Changeset 是众多开源项目和公司中一种久经验证的模式。在 Gerrit 和 Phabricator 这类现有产品中,与之相关的用户体验问题也已经被充分探索。我也相信 changeset 可以以一种非破坏性的方式引入(因为现有的 PR 就相当于单可变 changeset 模式)。
Changeset 会让 Pull Request 在更大型的项目和组织中更具可扩展性。除了可扩展性之外,它还能让 Pull Request 相关双方的审核流程都变得更清晰、更可靠。
当然,我只能代表我自己和我的经历,但仅这一个重要功能,就能极大地改善我在使用 GitHub 时的体验和效率2。
脚注
随机一篇博客
评论
登录后参与讨论