Bitbucket 让 pull request变得更强大,可即刻提升团队代码质量

如果你正在使用Git,那你很有可能也在使用pull request。在分布式版本控制系统(DVCS)诞生之初,pull request就已经以某种形式出现了。在Bitbucket 和GitHub 等流行的 web 服务诞生之前,一个pull request可能只是一封来自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想让我们拉取的所有变更集。

乍一看,这似乎是一种审阅 pull request 中变化的合理的方法。事实上,在撰写本文时,大部分git托管服务都是用这种三点式方法来运行pull request的diff算法。

然而使用这种 “三点式” 的变更对比算法来给 pull request 生成比对结果会有一些问题。在真正的项目中,master 分支会严重的偏离(diverge)功能分支(feature branch)。因为,其他的开发者会在他们自己的分支上工作,然后合并他们的代码到master。一旦master向前发展了,在功能分支上运行简单的git diff就不能反映两个分支之间的真正变化了。你只能看到当前功能分支与master分支的一个较老的版本间的变化。

“三点式”的git diffmaster...alice/master命令不会考虑到master的变化


在pull request diff看不到这些变更有可能带来什么后果呢?有两个。

合并冲突

第一个问题可能你会经常遇到:合并冲突。如果你在你的功能分支上修改了一个文件,而且文件恰好也在master上被修改了,git diff 依旧会显示你的功能分支上的修改。而 git merge 在另一方面则会显示出错误,并且把冲突提示 (conflict marker) 在你的工作副本中弄的到处都是,提示你,你的分支有不可调和的差别。或者,至少这些差别让 Git 复杂的合并策略也无能为力。

没有人“喜欢”化解合并冲突,但这是任何版本控制系统都无法避免的。至少,在不支持在文件层次上给文件上锁的版本控制系统,而给文件上锁本身也有其自身的问题。

但是,比起使用“三点式”的git diff来调用pull request比对结果带来的另外一个问题来说,合并冲突要好多了。这另外一个问题就是本可以顺利合并却把微妙的错误偷偷引入你的代码库中的特别的逻辑冲突

干净合并的逻辑冲突

如果开发人员在不同的分支上修改同一个文件的不同部分,你也许会有麻烦。在某些情况下,各自可以正常运行的不同的变更貌似可以无冲突顺利地合并,但实际上在合并后却会引起逻辑错误。

在几种情况下会产生这样的逻辑冲突,但常见的是当两个或更多的开发人员偶然发现并在不同的分支上修复了同一个错误。让我们看看以下计算票价的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在入境费前加上关税:


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的修复也很类似,只是他在入境费之后的那一行加上关税:

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;
}

(这个例子显然是刻意编出来的,但重复代码或逻辑确实会造成相当严重的后果:[gotofail](https://www.imperialviolet.org/2014/02/22/applebug.html)。大家有没有遇到过类似的错误?

假设你已经先把Alice的pull request合并进入了master,以下就是Bob的pull request使用“三点式”git diff计算从分支顶点到合并基准的代码变化所生成的比对结果。

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

因为你看到的对比结果是基于源代码的评估,所以根本没有警告一旦你按了合并按钮将会导致什么样的严重后果。

你真正想从pull request看到的是:如果把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;
}

这个变更对比清楚地说明了问题所在。一个pull request的查看器可以识别代码重复行(希望如此)并让Bob清楚的了解到哪些代码需要重新调整,从而防止重大漏洞危及master,最终影响输出。

这就是我们决定在Bitbucket和BitbucketCloud中的pullrequest能够完美实现变更对比功能。在Bitbucket中,当您查看一个pullrequest时,你所看到的正是生成的合并提交的实际预览。我们实现这一点的机理是在后台实际创建一个合并提交的分支,并向你展示该合并提交与目标分支的区别。

Git diff C和D(D表示merge commit)展示了两分支的所有区别(图片注释)。

为满足你的好奇心,我将同一代码库推送(push)到若干不同的主机服务器上,这样你就能看到各种有效的diff算法。

Bitbucket和Bitbucket Cloud中所用的“merge commit”变更对比功能显示了合并时将作出的实际变更。难题在于实施起来较为棘手,且成本高昂。

移动目标

首要问题在于merge commit D实际上并不存在,而创建mergecommit的代价较高。第二个问题是,单单创建D并不能治本。Mergecommit的父对象B和C随时都可能改变。我们改变其中一个父对象,重新设定pull request的作用域,因为这样能有效调整pull request合并时使用的变更对比算法。如果您的pull request的目标分支是十分重要的高负荷分支,则您的pull request很可能会频繁改变作用域。


任何一条分支改变时,都会创建merge commit。

实际上,每当有人将分支推送或合并至master或特征分支中时,Bitbucket或Bitbucket Cloud可能需要对新的合并操作进行计算,以显示准确的变更对比结果。


处理合并冲突

通过合并生成pull request 变更对比会产生另一个问题,即,时常会引发合并冲突。因为这种情况下,你的git服务器以非交互方式运行,但却没人来解决这个问题。这就使问题变得更加棘手,但实际上这也有可能转变成一大优势。在Bitbucket和Bitbucket Cloud中,我们实际上会提交冲突标记作为merge commit D的组成要素,然后在变更对比结果中标出这标记,以说明你的pullrequest如何发生冲突。

在 Bitbucket 和 Bitbucket Cloud 的变更记录中:绿色代表增加,红色代表删除,黄色表示代码冲突

这意味着我们不仅能提前监测到 pull request 冲突,而且同时能让审查者讨论冲突应该怎样解决。 冲突永远都会涉及到至少两方面的开发者,所以我们觉得 pull request 是研究合理解决方案的最好的地方。

抛开额外的复杂度和成本不谈,我相信我们在 Bitbucket Cloud 和 Bitbucket 中采用的方式提供了最准确和有用的 pull request 变更对比。如果你有其它问题或是反馈,请在文章下面留言。如果你喜欢,你可以加我 ([@kannonboy] 。我会时常发一些 Git,Bitbucket 的更新和其它的干货。


CSDN开发服务为企业提供ALM(应用全生命周期管理)解决方案,致力于打造基于研发管理前沿、开放的工具产品集群(如Atlassian、Sonar、Jenkins等),结合CSDN CODE等研发工具的高效率、高质量和高可靠性企业级研发管理平台,为企业软件开发生命周期内各阶段、各部门、各角色提供全流程、全方位的跟踪和综合管理。截止目前,CSDN ALM解决方案已服务于包括华为、中国移动通信研究院、嘀嘀打车、广联达、招商银行、南粤银行等在内的数百家行业企业及互联网企业。



阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页