Reorient GitHub Pull Requests Around Changesets

Mitchell Hashimoto

让 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

脚注

  1. 这是一个轻微且不便的问题,但它会放大成严重的问题。

  2. “那就别用 GitHub 啊!”我以前听过这样的反馈。我今天使用 GitHub 还有许多其他原因,所以对我个人而言这不是一个可行的选择。如果你可以不用 GitHub,那么没错,你确实可以在其他产品中找到 changeset 支持。

原文由 Mitchell Hashimoto 发布

本文章由 stealth/ox-alpha 进行翻译