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-pick
,revert
和 blame
時,也會更加簡單。版本控制
基於 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 有兩個重要的屬性:對象
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 有兩個優勢:
git rebase
同樣,可以保證歷史是單線條的。感興趣的讀者能夠看看 pijul 的代碼,深究其內部實現。
參考資料: