让 GitHub Pull Request 围绕 Changesets 重新设计
我既作为维护者参与过拥有 1000 多名贡献者的超大型开源项目,也作为工程师参与过超大型闭源企业项目,以及各种规模更小的项目。基于这些经历直到今天,我在 GitHub 上几乎把所有时间都花在 pull request 上,而遗憾的是,它也是 GitHub 上最让我沮丧的部分。
关于 pull request,我希望看到的改进有很多,但其中很大一部分问题都可以通过一个重要功能来解决:Changesets(变更集)。这篇博客文章描述了这个建议,以及我希望看到的样子。
免责声明:我的这些想法并非原创!我不声称这些想法是我提出的。这里的建议基于已被充分探索的 Git 工作流,并且部分或全部地被其他产品实现过,例如 Gerrit、Phabricator,或者老式的基于电子邮件的补丁评审。
如今的问题
如今 GitHub pull request 的生命周期,实际上就是一个巨大的可变 changeset。这简直是一团糟!
以下是一个典型的 PR:贡献者向一个分支推送一组提交,打开一个 PR,此后这个 PR 就代表该分支。人们通过评论讨论这个 PR。当贡献者推送新的更改时,这些更改会直接出现在同一个 PR 上并立即更新它。评审者可以留下评论,同时贡献者也可以推送更改,而这一切都更新同一个 PR。
这带来了许多问题:
评审者可以针对 PR 的某个先前状态进行评审,但该评审可能立即过时,因为在评审进行期间贡献者又推送了更改。
更糟的是,评审可能部分过时,其余的反馈在贡献者所推送更改的上下文中可能变得没有意义。例如,一条行内评论可能写着“与上一条评论相同的反馈”,但那条先前的评论现在已经消失/被隐藏了,因为贡献者推送的更改移动了那些行。1
评审不包含任何关于其所附着的提交的元数据,只有提交时的时间戳。用户可以大致把时间戳与提交对应起来,但这并不完全准确,因为如果一次提交发生在评审进行期间,时间戳会让人以为评审是针对最新提交的,但实际上你的评审可能是针对上一个提交进行的。😕
针对评审反馈进行修改的进行中提交(work-in-progress commits)一旦推送到分支就会立即可见。这迫使贡献者必须在单个提交中解决所有反馈,或者让评审者去面对只解决了一部分的反馈。
你无法方便地回溯查看 PR 的先前状态。如果你想评审较早的一组提交而忽略后来的提交,你要么得手动构建一个 “compare” 视图,要么使用本地检出(我用的是后者)。但无论哪种方式,你都只能看到代码更改,却看不到当时那个时间点的评审者反馈!
与上面类似,如果贡献者一次推送多个新提交,你无法方便地将新的一组提交与旧的进行比较。你实际上只能逐个提交地回看。为此,你又不得不退回到本地
git手动构建 diff。还有更多……我稍后会谈到一些其他问题,但我想我已经说明了我的观点。
我确信在上面的某些要点上,我对某些细节的理解可能有误。很可能有人会说“他其实只要做这个就能解决问题 5(a)”。这很有帮助!但我想表达的是,如果你退一步看,导致这些问题的根本原因才是真正的问题所在。具体来说,就是以逐个提交的方式跟踪分支的单个可变 changeset。
Changesets(变更集)
解决方案就是 changesets:pull request 通过一个单调递增的编号(v1、v2、……)实现可版本化。这些版本通常被称为 “changesets”。
每个 changeset 指向某个分支在固定时刻的状态。这些版本是不可变的:当新的提交被推送时,它们会成为新 changeset 的一部分。如果贡献者强制推送(force push)该分支,那也会成为新 changeset 的一部分。先前的 changeset 会被永久保存。
新的 changeset 可以立即发布(每次提交都发布),也可以推迟到贡献者决定提出新版本供评审时再发布。后者允许贡献者用多个提交来解决先前的反馈,并在自己觉得准备就绪时才发布这些更改。
在 changesets 的世界里,反馈附着在 changeset 上。如果一位评审者开始评审某个 changeset 时发布了新的 changeset,那也没关系,因为作为一个原子单元的评审仍然附着在先前的 changeset 上。
在后续的 changeset 中,标示某个文件或某一行在先前 changeset 中存在未解决的评论往往很有用。这可以确保对早期 changeset 的反馈不会丢失,并且在任何 changeset 被接受之前必须得到处理。
通常,每个 changeset 由不同的 Git ref 表示。例如,如今的 GitHub pull request 通常对应 refs/pr/1234,你可以在本地使用 git 以这种方式检出任意 pull request。而一个 changeset 则类似于 refs/pr/1234/v2(假设的形式),这样你也可以检出任意的单个 changeset。
评审者不再“批准”整个 PR 并合并,而是批准某个changeset。这意味着贡献者也可以在同一个 PR 中发布多个采用不同思路解决问题的 changesets,而维护者有可能选择非最新的 changeset 作为他们想要合并的那组更改。
GitHub,拜托了!
Changesets 是众多开源项目和企业中早已确立的模式。在 Gerrit 和 Phabricator 等现有产品中,它们已经是被充分探索过的用户体验问题。我也相信 changesets 能够以非破坏性的方式引入(因为当前的 PR 就相当于单一可变 changeset 模式)。
对于更大的项目和组织来说,changesets 会让 pull request 的扩展性好得多。除了可扩展性之外,它们还能让 pull request 双方参与的评审过程更加干净、更加安全。
当然,我只能代表我自己和我的经历发言,但仅这一个重大功能就将极大地改善我使用 GitHub 时的生活质量和工作能力2。
脚注
随机一篇博客