基於 patch 的版本控制系統是如何處理合並的 -- 一種新思路

基於 patch 的版本控制系統……

patch 是版本控制系統中最爲淵遠流長的概念之一,平常的各類操做中都須要跟它打個照面。好比 git diff 輸出格式就是個 patch 文件,git cherry-pick 會把摘取的修改以 patch 的形式應用到目標分支上。此種例子比比皆是。不過須要指出的是,當前普遍使用的版本控制系統,好比 svn/git/hg,都是基於 snapshot 而不是 patch 的。基於 snapshot 的版本控制系統,以 snapshot 的方式存儲當前版本。雖然這一類版本控制系統也會用到 patch,不過它們只有在須要時才計算出 patch 文件來。patch 是這一類版本控制系統的產物,而非基石。
(注意:切勿混淆 commit 和 snapshot 的概念,二者並不等價。Git 顯然不會在每一個 commit 中存儲對整個倉庫的 snapshot,這麼作太佔空間。事實上,Git 的 commit 只包含指向 snapshot tree 的指針,參見:Git-內部原理-Git-對象git

天然存在基於 patch 的版本控制系統,好比 darcs 和 pijul,只是較爲默默無聞。它們會是本文的主角。在基於 patch 的版本控制系統當中,當前版本由歷史上一系列 patch 決定。須要在開頭澄清的是,儘管本文着力於基於 patch 的版本控制系統的優點,但這並不表示我的認爲基於 patch 的版本控制系統是更好的選擇。基於 snapshot 的版本控制系統之因此流行,天然有它的優點所在。本文的目的是介紹基於 patch 的版本控制系統,尤爲聚焦於這一類系統是如何處理合並的,旨在提供一種新思路。當咱們須要借鑑版本控制系統來解決一類問題時,除了參考 git 和 svn,有時候也能夠看下 pijul。github

……是如何處理合並的

基於 patch 的版本控制系統,在跟 Git 比較時,一般會拿 git cherry-pick 說事。咱們知道,git cherry-pick 會拿出給定 commit 的修改,應用到當前版本上。初看上去,Git 提取了給定 commit 到當前版本上。然而仔細觀察後會發現,cherry-pick 以後新增的 commit,跟給定的 commit,其 ID 並不相同。事實上,git cherry-pick 只是提取了給定 commit 的修改到當前版本上。換句話說,cherry-pick 的是改動的內容,而非 commit 自己。若是過後又合併了當初 git cherry-pick 的 commit,在 Git 的眼裏,它認爲一樣的修改發生了兩次。數據結構

假設如下的場景:在開發 feature 分支上,發現 master 分支上有一個 bug,影響到新功能的開發,因此在 feature 分支上修了,而後 cherry-pick 到 master 分支上來。後來因爲業務上的變更,master 分支去掉了這個修復。當咱們合併 feature 分支後,這個修復又會從新出如今 master 分支。svn

在基於 patch 的版本控制系統沒有這個問題,在它們眼裏,不管在哪一個分支上,一樣的修改都是同一個 patch。在合併時,它們比較的是 patch 的多寡,而非 snapshot 的異同。一樣的道理,基於 patch 的版本控制系統,在處理 cherry-pickrevertblame 時,也會更加簡單。版本控制

基於 snapshot 的版本控制系統,在合併時採用三路合併(three-way merge)。好比 Git 中合併就是採用遞歸三路合併。所謂的三路合併,就是 theirs(A) 和 ours(B) 兩個版本先計算出公共祖先 merge_base(C),接着分別作 theirs-merge_base 和 ours-merge_base 的 diff,而後合併這兩個 diff。當兩個 diff 修改了一樣的地方時,就會產生合併衝突。指針

若是是基於 patch 的版本控制系統,會把對方分支上多出來的 patch 添加到當前分支上。效果看上去就像 git rebase 同樣。若是添加過程當中發生了衝突怎麼辦?code

patch 有兩個重要的屬性:對象

  1. 假設 patch B 依賴於 patch A,patch C 依賴於 patch B,B 能夠在 A 和 C 之間自由移動而不改變最終結果。這種移動操做稱之爲 commute。
  2. 每一個 patch 都有一個對應的 inverse patch,能夠把這個 path 引入的修改去掉。

darcs 在處理合並衝突時,會先添加若干個 inverse patch,回退到能夠直接添加 patch 的時候。額外添加的 inverse patch 和以前有衝突的 patch 合併在一塊兒,成爲一個新的 patch。這其中可能還會有 commute 操做來移動 patch 到適當的位置。遞歸

一般對於差異較大的 Git 分支,不建議用 rebase 操做,由於 rebase 過程當中,可能會發生由於修復衝突帶來的日後更多的衝突 - 衝突的滾雪球效應。darcs 的合併,也會有一樣的問題,一個合併操做耗費的時間可能會趕上指數爆炸。three

pijul 經過引入名爲有向圖文件(directed graph file,如下簡稱爲 digle)的數據結構,解決了這個問題。拋開我所不瞭解的具體細節不談(對於細節感興趣的讀者,看這篇文章),由 digle 表示的數據結構可以保證不會發生合併衝突。這意味着,咱們能夠用 digle 做爲 patch 的內部實現,這樣兩個 patch 的合併就是兩個 digle 的合併,而 digle 的合併是不會產生衝突的。這麼一來,合併過程當中就不會有滾雪球效應了,咱們能夠在最後把 digle 具象成實際的 patch 的時候,纔開始解決合併衝突。

pijul 的 merge 有兩個優勢:

  1. 最終結果跟用了 git rebase 同樣,可以保證歷史是單線條的。
  2. 因爲 merge 過程當中可以參考中間各個 patch 的信息,合併的效果理論上應該比簡單粗暴的三路合併要好。

感興趣的讀者能夠看看 pijul 的代碼,深究其內部實現。

參考資料:

相關文章
相關標籤/搜索