65.9K
CodeProject 正在变化。 阅读更多。
Home

更好的 Pull Request

2017年3月1日

CPOL

7分钟阅读

viewsIcon

15674

Bitbucket 提供了最准确、最有用的拉取请求差异。

如果你正在使用 Git,你可能正在使用拉取请求。它们以这样或那样的形式存在于分布式版本控制系统 (DVCS) 的初期。早在 Bitbucket 和 GitHub 构建花哨的网页 UI 之前,拉取请求可能只是 Alice 发来的一封电子邮件,请你从她的仓库中拉取一些更改。如果这听起来是个好主意,你可以运行一些命令将这些更改拉取到你的 master 分支中。

$ git remote add alice git://bitbucket.org/alice/bleak.git
$ git checkout master
$ git pull alice master

当然,随意将 Alice 的更改拉取到 master 中并不是一个好主意。master 代表你打算交付给客户的代码,因此你通常希望密切关注合并的内容。与其拉取到 master 中,更好的模式是将其拉取到单独的分支中,并在合并之前检查更改。

$ git fetch alice
$ git diff master...alice/master

使用 git diff 的“三点”语法向我们展示了 alice/master 顶端与其本地 master 分支的合并基础(或共同祖先)之间的更改。这有效地向我们展示了 Alice 希望我们拉取的所有更改。

git diff master...alice/master 等同于 git diff A B

乍一看,这似乎是审查拉取请求所涉及更改的合理方式。事实上,在撰写本文时,这似乎是大多数 Git 托管工具实现其拉取请求差异算法的方式。

然而,使用“三点”差异方法为拉取请求生成差异存在几个问题。在真实项目中,master 分支将与任何给定的 feature 分支显著分歧。其他开发人员将在他们自己的分支上工作并将其合并到 master。一旦 master 取得了进展,一个简单的 git difffeature 分支的顶端到其合并基础不再足以显示两个分支之间的真实差异。你只看到了分支顶端与 master 的一些旧版本之间的差异。

“三点”git diff master...alice/master 不考虑 master 的更改

为什么在拉取请求差异中看不到这些更改是一个问题?有两个原因。

合并冲突

第一个问题是你可能经常遇到的问题:合并冲突。如果你在你的功能分支上修改了一个文件,该文件也在 master 上被修改,git diff 仍然只会向你显示在你的功能分支上所做的更改。而 git merge 则会报错并在你的工作副本中散布冲突标记,表明你的分支存在不可调和的差异。或者至少是超出 Git 复杂合并策略能力的差异。

没有人喜欢解决合并冲突,但它们是所有版本控制系统的现实。至少,那些不支持文件级锁定的版本控制系统,这也有它自己的问题。

但是合并冲突远比如果你使用“三点”git diff 处理拉取请求可能遇到的第二个问题要好:一种特殊的逻辑冲突,它会干净地合并,但可能会在你的代码库中引入微妙的错误。

干净合并的逻辑冲突

如果开发人员在不同的分支上修改了同一个文件的不同部分,你可能会遇到一些麻烦。在某些情况下,独立工作并看似愉快地合并而没有冲突的不同更改,实际上在组合时可能会产生逻辑错误。

这可以通过几种不同的方式发生,但一种常见的方式是当两个或更多开发人员在两个不同的分支上偶然注意到并修复了相同的错误时。考虑以下用于计算机票价格的 JavaScript 代码:

// flat fees and taxes
var customsFee          = 5.5;
var immigrationFee      = 7;
var federalTransportTax = .025;

function calculateAirfare(baseFare) {
    var fare = baseFare;                
    fare += immigrationFee;
    fare *= (1 + federalTransportTax);
    return fare;
}

这里有一个明显的错误——作者在计算中忽略了包含关税!

现在想象一下,两个不同的开发人员,Alice 和 Bob,各自注意到这个错误并独立地在两个不同的分支上修复它。

Alice 在 immigrationFee 之前添加了 customsFee

function calculateAirfare(baseFare) {
    var fare = baseFare;                
+++ fare += customsFee; // Fixed it! Phew. Glad we didn't ship that! - Alice
    fare += immigrationFee;
    fare *= (1 + federalTransportTax);
    return fare;
}

而 Bob 做了类似的修复,但它在 immigrationFee 之后的行上。

function calculateAirfare(baseFare) {
    var fare = baseFare;                
    fare += immigrationFee;
+++ fare += customsFee; // Fixed it! Gee, lucky I caught that one. - Bob
    fare *= (1 + federalTransportTax);
    return fare;
}

由于每个分支上修改了不同的行,这两个分支都将一个接一个地干净地合并到 master 中。然而,master 将同时包含这两行。并且,一个严重的错误将导致客户被双重收取关税。

function calculateAirfare(baseFare) {
    var fare = baseFare;                
    fare += customsFee; // Fixed it! Phew. Glad we didn't ship that! - Alice
    fare += immigrationFee;
    fare += customsFee; // Fixed it! Gee, lucky I caught that one. - Bob
    fare *= (1 + federalTransportTax);
    return fare;
}

(这显然是一个人为的例子,但重复的代码或逻辑可能会导致非常严重的问题:有人记得 goto fail; 吗?)

假设你首先将 Alice 的拉取请求合并到 master 中,如果你使用从分支顶端到共同祖先的“三点”git diff,Bob 的拉取请求将如下所示:

function calculateAirfare(baseFare) {
    var fare = baseFare;                
    fare += immigrationFee;
+++ fare += customsFee; // Fixed it! Gee, lucky I caught that one. - Bob
    fare *= (1 + federalTransportTax);
    return fare;
}

由于你正在审查与祖先的差异,所以当你点击合并按钮时,没有即将发生的灾难的警告。

你真正想在拉取请求中看到的是,当你合并 Bob 的分支时 master 将如何改变。

function calculateAirfare(baseFare) {
    var fare = baseFare;                
    fare += customsFee; // Fixed it! Phew. Glad we didn't ship that! - Alice
    fare += immigrationFee;
+++ fare += customsFee; // Fixed it! Gee, lucky I caught that one. - Bob
    fare *= (1 + federalTransportTax);
    return fare;
}

这个差异清楚地显示了问题。拉取请求评审员将(希望)发现重复的行,并告知 Bob 代码需要一些返工,从而防止严重的错误到达 master 并最终投入生产。

这就是我们决定在 Bitbucket 和 Stash 中实现拉取请求差异的方式。当你查看拉取请求时,你看到的是生成的合并提交实际的样子。我们通过在幕后实际创建一个合并提交,并向你展示它与目标分支顶端的差异来做到这一点。

git diff C D,其中 D 是合并提交,显示了两个分支之间的所有差异。

如果你好奇,我已将同一个仓库推送到几个不同的托管服务商,以便你可以看到不同的差异算法的实际效果。

Bitbucket 和 Stash 中使用的“合并提交”差异显示了你合并时将实际应用的更改。缺点是它实现起来更复杂,执行成本也更高。

移动目标

第一个问题是合并提交 D 实际上尚不存在,并且创建合并提交是一个相对昂贵的过程。第二个问题是您不能简单地创建 D 就完事。BC,即我们的合并提交的父项,随时都可能更改。我们将对其中一个父项的更改称为重新定范围拉取请求,因为它实际上改变了当拉取请求合并时将应用的差异。如果您的拉取请求针对一个繁忙的分支,例如 master,您的拉取请求很可能会非常频繁地被重新定范围。

每当任一分支更改时,都会创建合并提交。

事实上,每次有人推送到或将分支合并到 master 或您的功能分支时,Bitbucket 或 Stash 都可能需要计算一个新的合并才能向您显示准确的差异。

处理合并冲突

通过执行合并来生成拉取请求差异的另一个问题是,您时不时会遇到合并冲突。由于您的 Git 服务器是非交互式运行的,所以没有人会来解决它们。这使事情变得有点复杂,但实际上却是一个优势。在 Bitbucket 和 Stash 中,我们实际上将冲突标记作为合并提交 D 的一部分进行提交,然后将它们在差异中标记出来,以向您展示您的拉取请求是如何冲突的。

在 Bitbucket 和 Stash 的差异中:绿线表示添加,红线表示删除,橙线表示冲突。

这意味着我们不仅可以提前检测到您的拉取请求存在冲突,还可以让评审员讨论如何解决冲突。由于冲突总是涉及至少两方,我们认为拉取请求是确定适当解决方案的最佳场所。

尽管增加了复杂性和成本,但我相信我们在 Stash 和 Bitbucket 中采取的方法提供了最准确和最有用的拉取请求差异。如果您有疑问或反馈,请在评论或 Twitter 上告诉我。如果您愿意,可以关注我(@kannonboy),获取有关 Git、Bitbucket 和其他精彩内容的偶尔更新。

© . All rights reserved.