變基與合併¶
通常來說,維護一個子系統需要熟悉 Git 原始碼管理系統。Git 是一個功能強大的工具,擁有許多特性;正如這類工具常見的情況一樣,使用這些特性也有正確和錯誤的方式。本文將特別關注變基(rebasing)和合並(merging)的用法。維護者在使用這些工具不當時常常會遇到麻煩,但避免問題實際上並不困難。
總的來說,需要注意的一點是,與許多其他專案不同,核心社群並不害怕在其開發歷史中看到合併提交。事實上,鑑於專案的規模,避免合併幾乎是不可能的。維護者遇到的一些問題源於避免合併的願望,而另一些則源於合併過於頻繁。
變基¶
“變基”是更改倉庫中一系列提交歷史的過程。有兩種不同型別的操作被稱為變基,因為它們都透過 git rebase 命令完成,但它們之間存在顯著差異
更改構建一系列補丁所基於的父(起始)提交。例如,變基操作可以將基於上一個核心版本構建的補丁集,轉而基於當前版本。在下面的討論中,我們將此操作稱為“重新定基(reparenting)”。
透過修復(或刪除)損壞的提交、新增補丁、為提交更新日誌新增標籤,或更改提交應用的順序來改變一組補丁的歷史。在下面的文字中,此類操作將被稱為“歷史修改(history modification)”。
術語“變基”將用於指代上述兩種操作。如果使用得當,變基可以產生更乾淨、更清晰的開發歷史;如果使用不當,它可能會混淆歷史並引入錯誤。
有一些經驗法則可以幫助開發者避免變基最嚴重的危險
通常不應更改已暴露給私有系統之外的歷史記錄。其他人可能已經拉取了你的程式碼樹並在此基礎上進行構建;修改你的程式碼樹會給他們帶來麻煩。如果工作需要變基,這通常表明它尚未準備好提交到公共倉庫。
話雖如此,總有例外。一些程式碼樹(例如 linux-next 就是一個重要的例子)由於其性質會頻繁變基,開發者也知道不應在此基礎上進行開發。開發者有時會公開一個不穩定的分支供他人測試或用於自動化測試服務。如果你以這種方式公開了一個可能不穩定的分支,請確保潛在使用者知道不要在此基礎上進行開發。
不要對包含他人建立的歷史記錄的分支進行變基。如果你已從其他開發者的倉庫中拉取了更改,你現在就是他們歷史記錄的保管者。你不應該更改它。除了少數例外情況,例如,這種程式碼樹中的損壞提交應該明確地回滾,而不是通過歷史修改使其消失。
沒有充分理由不要重新定基(reparent)程式碼樹。僅僅是為了使用更新的基礎或避免與上游倉庫合併,通常不是一個好的理由。
如果你必須重新定基(reparent)一個倉庫,不要選擇某個隨機的核心提交作為新的基礎。核心在釋出點之間通常處於相對不穩定的狀態;將開發基於這些點之一會增加遇到意外錯誤的機率。當一個補丁系列必須移動到一個新的基礎時,請選擇一個穩定的點(例如某個 -rc 版本)進行移動。
請注意,重新定基一個補丁系列(或進行重大的歷史修改)會改變其開發環境,並很可能使之前的大部分測試失效。一般來說,重新定基後的補丁系列應被視為新程式碼,並從頭開始重新測試。
合併視窗期間常見的問題是,當 Linus 收到一個明顯被重新定基的補丁系列(通常是基於一個隨機提交),而且是在傳送拉取請求前不久才完成時。這類系列經過充分測試的可能性相對較低——同樣,該拉取請求被處理的可能性也較低。
相反,如果變基僅限於私有程式碼樹,提交基於一個眾所周知的起始點,並且經過充分測試,那麼出現問題的可能性就會很低。
合併¶
合併是核心開發過程中常見的操作;5.1 開發週期包含了 1,126 次合併提交——幾乎佔總數的 9%。核心工作積累在 100 多個不同的子系統程式碼樹中,每個程式碼樹可能包含多個主題分支;每個分支通常獨立於其他分支進行開發。因此,在任何給定分支進入上游倉庫之前,自然至少需要一次合併。
許多專案要求拉取請求中的分支基於當前的 trunk(主幹),以便歷史記錄中不出現合併提交。核心並非這樣的專案;任何為了避免合併而對分支進行的變基操作,很可能會導致麻煩。
子系統維護者會發現自己必須進行兩種型別的合併:從較低級別的子系統程式碼樹合併,以及從其他程式碼樹(無論是兄弟程式碼樹還是主線)合併。在這兩種情況下,應遵循的最佳實踐有所不同。
從較低級別程式碼樹合併¶
較大的子系統往往有多個級別的維護者,較低級別的維護者向較高級別傳送拉取請求。處理此類拉取請求幾乎肯定會生成一個合併提交;這正是應該如此的。事實上,在極少數情況下,當合並提交通常不會被建立時,子系統維護者可能希望使用 --no-ff 標誌強制新增一個合併提交,以便記錄合併的原因。任何型別的合併,其更新日誌都應說明合併的原因。對於較低級別的程式碼樹,“原因”通常是該拉取請求所帶來的更改的摘要。
所有級別的維護者都應在他們的拉取請求上使用簽名標籤,上游維護者在拉取分支時應驗證這些標籤。未能這樣做將威脅到整個開發過程的安全性。
根據上述規則,一旦你將他人的歷史合併到你的程式碼樹中,你就不能對該分支進行變基,即使在其他情況下你可能能夠這樣做。
從兄弟或上游程式碼樹合併¶
雖然來自下游的合併是常見且不足為奇的,但當需要將分支推送到上游時,來自其他程式碼樹的合併往往是一個危險訊號。此類合併需要仔細考慮並充分證明其合理性,否則後續的拉取請求很有可能會被拒絕。
將主分支合併到倉庫中是很自然的願望;這種合併通常被稱為“回溯合併(back merge)”。回溯合併有助於確保與並行開發沒有衝突,並且通常會給人一種及時更新的舒適感。但幾乎所有時候都應該避免這種誘惑。
為什麼會這樣?回溯合併會混淆你自己的分支的開發歷史。它們會顯著增加你遇到來自社群其他地方的錯誤的機率,並使你難以確保你管理的工作是穩定的並已準備好提交到上游。頻繁的合併還可能掩蓋你的程式碼樹中開發過程的問題;它們可能會隱藏與其他程式碼樹的互動,而這些互動在一個管理良好的分支中不應(經常)發生。
話雖如此,回溯合併偶爾也是必需的;當這種情況發生時,請務必在提交資訊中說明其原因。一如既往,合併到一個眾所周知的穩定點,而不是某個隨機提交。即使如此,你也不應該回溯合併到你直接上游程式碼樹之上的程式碼樹;如果確實需要更高級別的回溯合併,上游程式碼樹應首先進行。
與合併相關的問題最常見的原因之一是,維護者在傳送拉取請求之前與上游合併以解決合併衝突。再次強調,這種誘惑很容易理解,但它絕對應該避免。對於最終的拉取請求尤其如此:Linus 堅決表示,他寧願看到合併衝突,也不願看到不必要的回溯合併。看到衝突讓他知道潛在的問題區域在哪裡。他進行了大量的合併(在 5.1 開發週期中進行了 382 次),並且在衝突解決方面做得相當好——通常比涉及的開發者做得更好。
那麼,當維護者的子系統分支與主線之間存在衝突時,他們應該怎麼做?最重要的一步是在拉取請求中警告 Linus 將會發生衝突;至少,這表明你瞭解自己的分支如何融入整個專案。對於特別困難的衝突,建立一個並推送一個單獨的分支來展示你將如何解決問題。在你的拉取請求中提及該分支,但拉取請求本身應針對未合併的分支。
即使沒有已知的衝突,在傳送拉取請求之前進行一次測試合併也是一個好主意。它可能會提醒你一些你從 linux-next 中未發現的問題,並幫助你準確理解你要求上游做什麼。
進行上游或其他子系統程式碼樹合併的另一個原因是解決依賴關係。這些依賴問題有時會發生,有時與其他程式碼樹進行交叉合併是解決它們的最佳方式;一如既往,在這種情況下,合併提交應解釋合併的原因。花點時間正確地完成它;人們會閱讀那些更新日誌。
然而,依賴問題通常表明需要改變方法。合併另一個子系統程式碼樹來解決依賴關係會冒引入其他錯誤的風險,並且幾乎不應該這樣做。如果該子系統程式碼樹未能被拉取到上游,它存在的任何問題也會阻礙你的程式碼樹的合併。更優的選擇包括與維護者協商,在其中一個程式碼樹中承載兩組更改,或者建立一個專門用於先決提交的主題分支,該分支可以合併到兩個程式碼樹中。如果依賴關係與重大的基礎設施更改有關,正確的解決方案可能是將依賴提交推遲一個開發週期,以便這些更改有時間在主線中穩定下來。
最後¶
在開發週期開始時,為了吸收程式碼樹中其他地方所做的更改和修復,與主線合併是相對常見的做法。一如既往,這樣的合併應選擇一個眾所周知的釋出點,而不是某個隨機位置。如果你的上游繫結分支在合併視窗期間已完全合併到主線中,你可以使用類似以下命令將其向前拉取:
git merge --ff-only v5.2-rc1
上述準則僅是:準則。總會有需要不同解決方案的情況,這些準則不應阻止開發者在需要時做正確的事情。但是,人們應該始終思考是否真的出現了這種需求,並準備好解釋為什麼需要採取異常措施。