在 Github 上以提交 PR 的方式參與開源專案是十分簡單的。不過由於 Git 本身自由度較高,有些隨意提出的 PR 實際上是會影響專案歷史記錄的【髒】PR。下文介紹何時會發生這種情況,以及如何通過 rebase 工作流改進它。
什麼是髒 PR
我們知道,如果你想為某個開源專案貢獻程式碼,通用的流程是:
- fork 專案到自己的倉庫。
- 在新開的分支上提交。
- 提出 PR 請求維護者將你的新分支合併至原專案。
在最後一步中,你所提交的 PR 會包括新分支上全部的歷史記錄。這時候,如果出現下面的幾種情況之一,在這裡我們就認為這個 PR 屬於【髒】PR:
- PR 分支和原倉庫的目標分支存在衝突。
- PR 包含了許多瑣碎的 commit 記錄,如
fix bug
/add dev
等缺乏實際意義的提交資訊。 - PR 包含了多個不必要的 Merge 記錄。一般來說,fork 出的倉庫和原倉庫保持同步的最簡單方式,是
fetch
原倉庫後將 HEADmerge
到當前分支。這個操作每執行一次,就會在當前分支留下一個類似Merge xxx
的 commit 記錄。 - PR 包含了與主題不符的改動,如留下冗餘的日誌檔案、在其它模組中新增了額外的除錯用程式碼等。
如何處理髒 PR
內部專案
上述的幾種情況,在開發託管在 Stash 或 Gitlab 上的內部專案時,其實都不是問題,都有著非常簡單的解決方案:
- 衝突了?直接拉主分支拉下來改啊,反正大家都是管理員✌️
- commit 怎麼寫有問題嗎?本來不就是每天下班準時 commit 一次嗎?
- 看看我們的
git log --graph
,多麼壯觀!大家都很努力的好嗎! - 能按時提測就行,不要在意這些細節?。
並不是說這麼處理有什麼問題,尤其在中國特色天天趕進度的業務專案中,這麼做也基本上是最佳實踐了。下面,我們重點討論的是在較為正式地向外提交 PR 時,提升 PR 質量的方法。
merge --squash
Github 在很早之前就支援了強制 squash 功能。通過這種方式,原倉庫的維護者可以在將 PR 提交的分支所更改的內容,squash 到主倉庫的一次提交中。這樣,不管提出 PR 的分支有多【髒】,都可以在併入時得到淨化了。這大致相當於命令列下這樣的操作:
git merge forked_lib/new_branch --squash
git commmit -m 'something from new_branch'複製程式碼
這是得到 Github 官方支援的實踐,但這麼處理有什麼侷限性呢?主要是這兩點:
- 需要原倉庫維護者解決衝突並整理歷史,而不是 PR 提出者。
- 只能將多個 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 的原理是:
- 首先找到兩個分支(dev 和 master)的最近共同祖先 m1。
- 對比當前 dev 分支相比 m1 的歷次提交,提取修改,儲存為臨時檔案。
- 將分支指向 master 最新的 m3。
- 依次應用修改。
在【依次應用修改】的這一步中,你可以進一步選擇如何對待 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 解決呢?
- 遇到和遠端主庫的衝突時,可以先將遠端倉庫 fetch 下來,而後將自己的 dev 分支 rebase 到新的 HEAD 上。
- 冗餘的 commit 記錄可以直接 rebase 合併。
- 和 1 類似地,通過將自己的 dev 分支 rebase 到新的遠端庫 HEAD 的方式,不會留下冗餘的 Merge 記錄。
- 提交一個新 commit 修復問題,而後 rebase 即可。
到此,對 rebase 的介紹大體上就結束了。希望對大家更好地參與開源專案有所幫助。
參考: