谈谈Github上如何交流(4): {Feature,Pull} Request
这篇文章说说用户怎么提出好的 feature request / pull request, 以及维护者如何对待它们.
这里, 我们忽略那种特别简单的 (例如 10 行代码以内可以实现的) request, 只考虑 non-trivial 的 feature request 和 pull request.
好的 feature request¶
首先, 一个残忍的事实是, 开源项目中大多数的 feature requests 不会得到 maintainer 的回应. 理由也很简单: 项目的资源是有限的, 而修 bug, 维护现有 feature 的优先级自然会更高. 当项目有额外的开发资源时, 一般也会优先推进团队自己原有的开发计划 / roadmap, 或优先为项目的赞助方 (如背后的公司) 实现 feature. 路人的 feature request 优先级可以说是最低的, 排在所有这些之后.
下图是 vscode 社区处理 feature request 的流程: (来源)
Vscode 是一个非常注重社区的项目, 因为编辑器必须要有好的生态才能成功. 因此我们才能看到 vscode 把用户的 "upvote" 也考虑在内. 绝大多数项目并没有这最后一步: 和项目 roadmap 不 align 的 feature request, 一般就直接进入 backlog 了.
在这种情况下, 要想提出一个 "好的 feature request", 并得到 maintainer 的重视, 当然不是那么容易. 一个好的 feature request 一般至少要在以下某一点中比较突出:
- 特别好的 idea: 提供 maintainer 可能还不了解, 或没想到的信息, 让 maintainer 看到尚未注意到的重要 feature
- 例如: "我发现很多用户需要对现有的 feature A 加一个 workaround, 如果我们能够做 X 的话, 就能够提升用户体验"
- 例如: "现在的 feature A 做了某个假设, 但是对于很多用户这个假设其实不成立. 我们希望通过实现 X 来放松这个假设"
- 描述这个 feature 的设计和实现: 更多的细节能节省 maintainer 的时间, 让人更容易判断这个 feature
是否值得投入
- 例如, 用 pseudo-code 来描述用户想要的新接口 / 功能 / 输入输出; 描述接口内部的可能实现; 甚至给出多种备选方案, 等等
- 愿意贡献: 如果用户表示自己愿意在 maintainer 帮助下完成这个 feature, 那么 maintainer 可能就更愿意投入时间来利用好社区资源
要做到这些, 有时候确实需要用户对项目有一定的深入了解, 能够把握住项目的 direction. 毕竟想要项目的 developer 改变原定的计划, 自己没两把刷子是不行的.
反过来, 一个 "平凡的 / 不好的 feature request" 可能会有如下特征:
- 平凡的 idea: nice to have, 但是只对少数用户有用
- 已经能够被用户以扩展的方式自己实现, 不一定需要加到项目中
- 目标很好, 但不知道怎么实现
- 例如: "请把这个程序变快" 就不是一个好的 feature request. 这甚至不算是一个有效的 feature request, 因为优化是无止境的, 这个 request 什么时候算完成?
- "程序的 X 模块可以通过 ABC 的方式变快" 就是一个好的 request. "程序比上个版本慢" 可以是一个 bug report.
- 描述不清楚: 使用过多有歧义的人类语言, 不知道 feature 到底是什么. 如果可以, 用代码来描述会有更少歧义.
- Out of scope: feature 离项目的核心功能太远
当然, 一个平凡的 feature request 照样值得提出, 虽然它可能会进入 backlog 暂时无人问津, 但是也许在沉寂一段时间之后会引发更有价值的讨论和实现.
好的 pull request¶
Pull request 是社区向项目贡献代码, 因此一般更受 maintainer 欢迎, 但也不全是. 围绕 pull request 的主要矛盾是 可维护性 : 当 maintainer 同意接受一个 PR 时, 就意味着 maintainer 同意负责维护这段别人写的代码, 这对代码的可维护性是有要求的.
因此, 用户应该认识到, maintainer 关注的绝不仅仅是一个 PR 是否 "work", 而是会考虑更多的因素:
- PR 的设计和实现方案是否是最合适的?
- 最适合维护的方案并不一定是最容易实现的方案: 例如科学的实现一个新 feature 可能依赖某些小的重构. 这时, maintainer 的高标准就会与用户 "快速解决自身需求" 的目标不一致.
- PR 是否有足够的 test coverage?
- Maintainer 对 PR 的测试要求甚至可能会比对自己的代码更严格, 因为维护别人的代码, 难度本来就更大.
- PR 是否引入了新的 dependency?
- 每个 dependency 都是一个额外的维护负担. 在我曾经做的 House3D 这个小小的项目里, 就发现了至少5个不同dependency中的bug. Maintainer 会避免添加 dependency.
- Lint / documentation 等
Jeff Geerling 的 Why I Close PRs 和 The Burden of an Open Source Maintainer 也介绍了什么样的 PR 是 maintainer 更乐于见到的. 文章写的很好, 且另外提到了一条重要的沟通原则:
- 对于大的改动, 先在 issue 里讨论再 PR.
- 这个 feature 是否值得做? 是否会被接受? 如何设计? 这些问题都没得到认可就发个大 PR, 可能会白白浪费作者的时间.
- 关于这一点, 胡渊鸣的如何优雅地参与开源开发也有更详细的解释.
Maintainer 应在CONTRIBUTING.md
或 .github/pull_request_template.md
里为 contributor 提供引导,
包括介绍提交 PR 的注意事项, PR 被接收的原则, 项目的 coding style, 如何使用 linter, 如何测试, 如何更新 documentation, 等等.
例如 detectron2 的 contributing.md
和 pull_request_template.md.
Extensions¶
开源社区中, 用户会有无数不同的需求. 即使 maintainer 有时间 (大部分 maintainer 没有) 去处理 feature request / pull request, 也会有很多人的需求无法满足.
在这种现实下, 面对没有精力实现 / 维护的 {feature, pull} request, maintainer / contributor 可以采取的一个好的策略是: 通过一些改动让项目变得更 extensible, 使得 feature 可以被用户以扩展 / extensions 的方式独立实现, 而不是在项目中实现.
具体要怎么做到这一点, 是一个系统设计问题, 这篇文章就不跑题多说了. 采用这种方式的好处是:
- 鼓励 contributor 将一些 pull request 变为它们自己的项目, 由 contributor 自己负责维护
- maintainer 可以专心负责核心系统的核心功能, 减轻负担. 这也更符合 "只做好一件事" 的 UNIX 哲学
- 将大量不会被 maintainer 接受的 feature request 看作 "out of scope": 它们可以看作这个项目的 "applications", 因为用户可以在项目的基础上实现它们
- 好的 extensible design 是 future proof 的: 它不仅能解决眼前的一个 feature, 还能够支持一大类未来可能会出现的需求
- 对于一些不成熟的功能, 可以让它们在项目之外经过迭代后, 再吸收回项目中由官方负责维护
- 建立一个更丰富的生态, 每个人都可以发挥自己的创造力, 不被 core maintainer 的精力限制
很多成功的开源项目都是靠着可扩展性创建了优秀的生态.
- Vim, Emacs 等编辑器的核心功能都不多, 要靠海量的扩展实现各类功能.
- Vscode 对很多 feature request 会关闭并加上一个 extension candidate 的 label, 意思是这个 feature request 适合作为扩展来独立实现.
- PyTorch 也十分注重可扩展性, 除了 module/operator 上的扩展之外, 甚至有允许用户实现自己的
Tensor
subclass, 自己的 device 等非常夸张的扩展. 最近的torch.fx
也是在给用户实现 graph transformation 扩展的机会. PyTorch 团队会使用 "extension points" 这个词, 指系统中可以由用户实现扩展的部位.
Detectron2 也从最初就尽量走这条路, 把 "尽量让所有模块都可扩展 / 可替换" 作为一个设计目标. Facebook 与之相关的 research project 就都以 detectron2 扩展的形式开源. 除此之外也有不少来自社区的优秀扩展, 例如 AdelaiDet, YOLOv7 等.
维护的负担 ¶
如果 pull request 并不容易被接受, 那么开发者是不是应该干脆自己 fork 项目, 来实现自己想要的改动呢? 要回答这个问题, 要先想清楚将这些改动开源的目的是什么:
如果只是一个 proof-of-concept, 为了公开的展示这个改动的内容, 那么 fork 是没问题甚至更合适的:
- 例如一些实验性的大型改动可以作为 fork 来向别人展示
- 例如论文作者发表可以复现论文结果的代码, 可以用 fork 的形式, 甚至可以把所有 dependency 的版本都固定住
开发者也要意识到, 如果认为自己的工作不只是一个 proof-of-concept/toy, 想要让自己的 fork 真的被人严肃的使用的话, 就不得不自己承担维护的责任. 而维护的负担是很重的, 挑几个点来说:
- 在开源世界中, 即使代码不变, 环境也在变, bug 也会自己找上门来
- 只要有人用, 就会有人报告问题
- 经验: 项目中很多看似奇怪的决定, 或实现细节, 很可能是有其历史原因,
或是在踩了坑之后总结出来的.
在自己的 fork 中魔改, 没有与官方维护者合作交流, 很多坑可能要再踩一次.
有个与此相关的 Chesterton's Fence 原则:
Do not remove a fence until you know why it was put up in the first place
- 测试: 例如 PyTorch 这样的项目, 每天花在跑测试上的钱都是个天文数字. 每个用户也是天然的在帮助项目做测试. fork 没有这样的测试资源和用户量, 如何保证自己的版本仍然正确?
- 我就遇到不止一次这样的情形: 我的开源项目中的一些代码, 早期有问题的版本被别人复制转手了多次, 转了一圈居然还回到了我参与的其它项目里, 让我被自己多年前已经修好的 bug 给坑了. 这就是由于一些没有承担维护责任的 fork
因此, 虽然一个成功的 pull request 要付出额外的交流, 但它换来的是项目维护者的维护工作. 如果开发者想加入新 feature, 又没有自信能胜任整个项目的维护, 与其另起炉灶, 不如多参与交流, 与维护者讨论一个更可维护的方案 (pull request 或 extension).