使用 git rebase 提高 PR 質量

doodlewind發表於2017-09-24

在 Github 上以提交 PR 的方式參與開源專案是十分簡單的。不過由於 Git 本身自由度較高,有些隨意提出的 PR 實際上是會影響專案歷史記錄的【髒】PR。下文介紹何時會發生這種情況,以及如何通過 rebase 工作流改進它。

什麼是髒 PR

我們知道,如果你想為某個開源專案貢獻程式碼,通用的流程是:

  1. fork 專案到自己的倉庫。
  2. 在新開的分支上提交。
  3. 提出 PR 請求維護者將你的新分支合併至原專案。

在最後一步中,你所提交的 PR 會包括新分支上全部的歷史記錄。這時候,如果出現下面的幾種情況之一,在這裡我們就認為這個 PR 屬於【髒】PR:

  1. PR 分支和原倉庫的目標分支存在衝突
  2. PR 包含了許多瑣碎的 commit 記錄,如 fix bug / add dev 等缺乏實際意義的提交資訊。
  3. PR 包含了多個不必要的 Merge 記錄。一般來說,fork 出的倉庫和原倉庫保持同步的最簡單方式,是 fetch 原倉庫後將 HEAD merge 到當前分支。這個操作每執行一次,就會在當前分支留下一個類似 Merge xxx 的 commit 記錄。
  4. PR 包含了與主題不符的改動,如留下冗餘的日誌檔案、在其它模組中新增了額外的除錯用程式碼等。

如何處理髒 PR

內部專案

上述的幾種情況,在開發託管在 Stash 或 Gitlab 上的內部專案時,其實都不是問題,都有著非常簡單的解決方案:

  1. 衝突了?直接拉主分支拉下來改啊,反正大家都是管理員✌️
  2. commit 怎麼寫有問題嗎?本來不就是每天下班準時 commit 一次嗎?
  3. 看看我們的 git log --graph,多麼壯觀!大家都很努力的好嗎!
  4. 能按時提測就行,不要在意這些細節?。

並不是說這麼處理有什麼問題,尤其在中國特色天天趕進度的業務專案中,這麼做也基本上是最佳實踐了。下面,我們重點討論的是在較為正式地向外提交 PR 時,提升 PR 質量的方法。

merge --squash

Github 在很早之前就支援了強制 squash 功能。通過這種方式,原倉庫的維護者可以在將 PR 提交的分支所更改的內容,squash 到主倉庫的一次提交中。這樣,不管提出 PR 的分支有多【髒】,都可以在併入時得到淨化了。這大致相當於命令列下這樣的操作:

git merge forked_lib/new_branch --squash
git commmit -m 'something from new_branch'複製程式碼

這是得到 Github 官方支援的實踐,但這麼處理有什麼侷限性呢?主要是這兩點:

  1. 需要原倉庫維護者解決衝突並整理歷史,而不是 PR 提出者。
  2. 只能將多個 commit 整理為一個,而不是若干個。

這個方式最棘手的問題實際上在於:它把編輯提交歷史的責任丟給了原倉庫的維護者,PR 提交者並不能在提交 PR 前清理歷史記錄。是否有更好的方案呢?

rebase

通過 git rebase 命令,我們能夠獲得對 git 提交歷史更大的掌控。不過,這也是一個存在風險的命令,因此在實際使用前建議稍加了解其原理。

首先假設專案主幹分支是 master,你在 fork 而來的倉庫下新增了 dev 分支。你從 master 的 m1 提交開始,在 dev 提交了 d1、d2 和 d3 三次提交。這時,master 也更新了 m2 和 m3 兩次提交。這時候版本樹大致長這樣:

m0 -- m1 -- m2 -- m3
      |
      d1 -- d2 -- d3複製程式碼

這時你的目標是將三次 dev 上的 commit 合併為一個新的 d,讓 dev 的歷史變成這樣:

m0 -- m1 -- m2 -- m3 -- d複製程式碼

為了實現這一點,你可以在 dev 上 rebase 到 master:

git checkout dev
git rebase -i master複製程式碼

rebase 的原理是:

  1. 首先找到兩個分支(dev 和 master)的最近共同祖先 m1。
  2. 對比當前 dev 分支相比 m1 的歷次提交,提取修改,儲存為臨時檔案。
  3. 將分支指向 master 最新的 m3。
  4. 依次應用修改。

在【依次應用修改】的這一步中,你可以進一步選擇如何對待 d1、d2 和 d3 的 commit message。在以 -i 引數啟動了互動式的 rebase 後,會進入 vim 介面,由你選擇如何操作 dev 上的提交記錄,形如這樣:

pick 91398f93 d1
pick 65efc762 d2
pick b82e050d d3

# Rebase 4652f96d..b82e050d onto 4652f96d (3 commands)
#
# Commands:
# p, pick = use commit
# r, reword = use commit, but edit the commit message
# e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message
# x, exec = run command (the rest of the line) using shell
# d, drop = remove commit
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out複製程式碼

你可以編輯對 dev 上這幾個 commit 的處理,如輸入 pick 為保留,輸入 squash 則將該 commit 內容併入上一個 commit 等。在完成操作選擇後(這裡我們可以選擇 fixup d1 和 d2,並 reword d3),輸入 :wq 儲存退出,會進入一個新的 vim 視窗,在此你可以進一步編輯新的 commit message,儲存後 rebase 即可生效。注意,你至少需要選擇一個需要 use 的 commit,否則會報錯。

rebase 生效後再查閱分支歷史記錄,是不是清淨多了呢?在這個狀態下提交更清爽的 PR 吧?

在此額外提醒一點,對於已經被 fork 出多份的倉庫,rebase 原倉庫的主幹是危險操作。除此之外,使用 rebase 修改私有分支的歷史記錄是很安全的。

回頭看看髒 PR 的幾個問題,如何通過 rebase 解決呢?

  1. 遇到和遠端主庫的衝突時,可以先將遠端倉庫 fetch 下來,而後將自己的 dev 分支 rebase 到新的 HEAD 上。
  2. 冗餘的 commit 記錄可以直接 rebase 合併。
  3. 和 1 類似地,通過將自己的 dev 分支 rebase 到新的遠端庫 HEAD 的方式,不會留下冗餘的 Merge 記錄。
  4. 提交一個新 commit 修復問題,而後 rebase 即可。

到此,對 rebase 的介紹大體上就結束了。希望對大家更好地參與開源專案有所幫助。

參考:

相關文章