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解決方案已服務於包括華爲、中國移動通信研究院、嘀嘀打車、廣聯達、招商銀行、南粵銀行等在內的數百家行業企業及互聯網企業。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章