(本文寫作於 2014 年 7 月,所有相關論述均以此時間點為準)
Mercurial 是1款分散式的版本管理軟體。
所謂版本管理軟體,是1種可在程式開發過程中,有規律地保存程式碼的歷史訊息、讓人能放心肠做各種開發實驗,並在開發不幸走進死胡同時,將程式碼回復到舊有版本的系統……
細節很複雜,1言以蔽之,就是1種程式碼管理器。詳細說明網路上可以找到很多,我就不在此囉唆贅述了。
Mercurial 經常被拿來和另外一款同類軟體 Git 比較,但是不知是故意貶低或缺少了解,大部分能在網路上讀到的中文文章,都傾向於認為 Mercurial 比 Git 弱小、彈性差、功能低落,乃至只是個「教學用軟體」。但隨著我同時跨足使用這兩套系統後,我發現實況卻非如此--乃至大部分時候都是反過來的。
Mercurial 沒能得到它該有的評價,這10分惋惜。是故我決定撰文將輿論平衡1下,以餉讀者。
原則上,凡本文提到的特性,無論是 Git 或 Mercurial ,咱都有親自實驗過,各位大致上能夠安心閱讀。
1如題目所言,這篇文章是兩者的比較文,本文將以版本管理使用者的角度來說明,為什麼 Mercurial 比 Git 更好?
做人要實在,優點等會兒再說,先從 Mercurial 的弱點開始說起好了。
Mercurial 現在主要的弱項在於跨平台時,處理非英文檔名可能會有編碼問題。
略微強調1下:
總地來說,即便您在源碼庫中用了中文檔名,只要您的開發平台都是 utf8 的,就不需要擔心這件事。
相關說明請見:http://mercurial.selenic.com/wiki/EncodingStrategy#Overview
如今用 Git 的人比較多(特別是在中文圈中),對於開發者來說,和他人合作時碰到 Git 的機會比較大。
不過話說回來,Mercurial 的社群也絕不算小。您可以在網路上找到大批用 Mercurial 維護的專案,從 OpenJDK、Python、Go、Nginx、Vim 等多不勝數。其中 Nginx 是全球市佔率超過 20% 的網站後端,平均您每看5個網站就有1個是用 Nginx 跑起來的,而如果只算前 10000 個受歡迎的網站,這個比例乃至可以上升到 40 %;而 Vim 就更不用提了,他乃至永久影響了全球程式設計師的文化體系。
在 http://stackoverflow.com 中,我們能查到幾乎所有能想像得到的,關於 Mercurial 的討論與情報。總之社群支持度無需擔心。
受限於 Mercurial 初期的資料結構限制,如果您將檔案重命名,或僅僅只是變更檔案放在原碼庫中的路徑,則原碼庫中會產生1份額外的副本,消耗兩倍的硬碟空間。
這蠢死了。聽說 Git 就沒有這種問題(儘管我沒試)。
這個缺点讓人心情不爽,不過話又說回來,如果您沒在倉庫中堆滿1堆巨大的2進位檔案,又3天兩頭搬動它,也不會造成什麼很嚴重的問題。
儘管有以上這些瑕疵,Mercurial 依然是1款棒呆了的版本管理系統,因為 Mercurial 有著足以抵消上述1切缺点的眾多性感特徵。我馬上就會和各位說明。
本文主要的比較對象是 Git,這篇文章才剛開始呢。
1.1. 分支分離
請看以下分支圖:
用戶下命令「請分別列出 Branch 1 與 Branch 2 中含有的提交」時……
Mercurial 的回答是……
Git 的回答是……
毫無疑問 Mercurial 的答案才是您想要的。
(請想像1下 branch 1 是穩定分支,而 Branch 2 是某功能分支的狀況)
這不是 Git 用戶下錯指令,事實上,Git 從資料結構層面就沒辦法紀錄 Mercurial 在上述問題中所能提供的訊息。GIT 就是做不到。(註1)
1.2. 保存但隱藏分支
Mercurial 可以「關閉分支」,但 Git 則否。
在 Mercurial 中,被關閉的分支預設不會出現在分支列表裡,因此 Mercurial 可以在不砍任何分支的条件下,確保分支列表清新好管理。另外,因為分支資料仍然存在,所有分支都能在必要時刻被完全召回。
反之,Git 不能將分支封存起來。要嘛砍掉,要嘛任由分支曝露在外。想讓兩者並行,它就是做不到。
換個方式問,如果不去刪分支呢--用戶能處理1個擁有1千個分支項目的列表嗎?嗯,馮紐曼大概可以吧,反正我不行就是了。當 Git 用戶得意地說他們的分支很廉價可以隨創隨刪時,他們同時也是在說,Git 絕大部份的分支無法被保存下去。
不管用哪種分支模型進行開發,我們總有些時候想保存不再使用的分支--有時乃至想保存住大部份的分支資訊,但是 Git 並沒有給我們這種選擇權。為了管理,我們非砍它不可。(註2)
1.3. 绝不遜於 Git 的輕量性分支
有些用戶誤以為 Mercurial 的分支比較笨重遲鈍,而 Git 的分支比較輕,但事實上絕非如此。
Mercurial 的分支 (branch),與 Git 中的 branch 不同,不是將標籤紀錄在頭部,而是在「每個提交」中紀錄1個「這個提交屬於哪個 branch」的訊息標籤,每次提交時因此額外消耗的容量充其量就幾10 bytes 而已,這顯然不會對速度與硬碟容量造成任何負面影響。
反過來說,這種作法帶來的直接好處,您可以在前文「分支分離」段落中看到。
另外,Mercurial 也支援 Git 式的分支模型--也就是將標籤記在單1變更集上,並隨提交而自動往前推進的方式。這種類似 Git 運作方式的分支,在 Mercurial 中被稱為 bookmark。
如果用戶希望,Mercurial 也能够以 Git 用戶慣用的方式,單獨運用 bookmark 分支,乃至混用 branch 與 bookmark 兩種分支方式--比方說,branch 產生的分支可用在大家同享的大工程上,而 bookmark 類型的分支標記則留在本機供自己使用(bookmark 同樣可以推送給其他人,但預設不推送,這和 Git 1樣)。同時使用這兩者不會產生絲毫衝突,而不甩其中1方,單用其中1種分支方法也毫無問題,這僅僅只是不同專案的分支習慣不同而已。
反過來看, Git 則無法以 Mercurial 的方式進行分支,它從根本上就沒辦法。
1.4. Mercurial 的分支不能刪
有人宣稱 Mercurial 的分支 (branch) 不能刪除,這是它用起來得谨慎翼翼,感覺很「重」的缘由。
嗯,這點倒是沒錯,Mercurial 的 branch 確實是不能刪的。但這有什麼不好嗎?對於 Mercurial 而言,分支 (branch) 就和變更集本身1樣,是版本庫中不可分割的重要訊息。Mercurial 的分支訊息是版本庫的1部分,而不是像 Git 那樣僅僅把它當成某種「外加」的東西--Git 的分支訊息與歷史無關,隨時想插就插想拔就拔,刪掉找不回來也無所謂。
認真對待 branch 歷史是 Mercurial 的核心設計邏輯,這和 Git 完全不同。
有時候,這種設計邏輯無法滿足用戶對臨時分支的要求,但別擔心,Mercurial 也允許例外。
如果您想在 Mercurial 中隨手建立1些幾天後就不再需要的暫時性分支,或您就是不想永久錄下分支資訊,您應該要用的是 Bookmark,若用分支 (branch) 就是用錯了工具。因為很重要所以再重複1次:如果您覺得某些分支訊息不重要(或僅僅在某些時候覺得不重要),沒那個意思將分支訊息永久保存下來的話,您應該要用 bookmark,而不是 branch。
只是,我建議您再多考慮1下用 branch 永久保有分支資訊的好處。雖然您不能刪它,但就算您弄出了 10000 個分支那又怎樣?它又不會咬你。
1.5. 匿名的超輕量分支
受益於強大的變更集定位能力(見後),Mercurial 中還有比 bookmark 分支更輕量的分支方式存在--也就是「匿名分支」。
▲當 A, B, C, D 提交路線已經完成後,退回 B,再進行1次提交,就會自動產生1個 E 分支。
如果用 Mercurial 進行以上操作,技術上來說 ABCDE 都會處於同1個 branch(指 Mercurial 中的 branch,見前文說明)中,換句話說1個 branch 中可能有兩個以上的「頭部 (此例中為 D 與 E)」。其中變更集 E 是最後加入倉庫的,所以切換到這個 Mercurial branch 時,預設會切到 E 版上。這時另外一邊的 D 就被視為1個沒有標記的「匿名分支」。
您可以把 E 重新合併回 D,或是乾脆就這樣把 CD 變更集放棄繞過去,改為沿著 E 路徑繼續開發。這不需要額外處理,只要修改想修改的地方後直接提交就行,非常輕鬆。
與 Git 相比,Git 也能如上這樣僅靠提交產生新的頭部,但用戶幾乎無法追蹤它(除非記住 hash 值),1旦沒取名又跳離了它,很難再把它找回來,不但沒有實用性反而更像是1種操作錯誤的結果,如果將 Git 切到這種沒取名的變更集上工作還會給您跳正告。但在 Mercurial 中,因為 revset(見後)的強大定位能力,不取名也無妨,使用起來非常容易。儘管因為匿名分支沒有語義標記而不適合推到遠端與他人同享,但在本地臨時修些小 bug 時仍然相當好用。(註3)
註:有些 Git 用戶認為 Mercurial 的匿名分支是個危險的蠢主张,因為在 Git 用戶的觀念中這種無名分支難以追蹤的緣故。不過對 Mercurial 來說,追蹤匿名分支轻而易举,完全不是問題。
2.1. 定位變更集
Mercurial 中有1系列定位變更集的作法。
2.1.1. 數字序列
Mercurial 可以用序列數字,如 1、3、15 來指定某個特定的提交,而不用非打 hash code 不可。
舉例來說……
Mercurial:
hg update 133
Git:
git checkout 4a9cb402d55357b534ef28f74b85ca7bb16c87ed
前者遠比後者簡單,也容易識別很多,最少用戶光看數字就可以認知到不同變更集之間的先後關係,和兩個變更集的距離大致有多遠,這在實際操作時直覺又實用。
另外一方面,因為在實際使用中 hash code 實在很難處理,這也意味了 Git 用戶通常是以分支為粒度進行各種操作,而 Mercurial 用戶則可以以每次提交為粒度進行操作--就和喝水吃飯1樣容易。
2.1.2. Revset
Mercurial 可以用1種稱為 revset 的語句,來幫助使用者定位「1個或1系列」的變更集。這種語句可以清晰地對大量變更集進行操作。
以下是些官網提供的例子……
hg log -r "branch(default)"
列出 default 分支中的所有變更集。
hg log -r "branch(default) and 1.5:: and not merge()"
列出 default 分支中,繼承 1.5 版變更集的後續所有變更集,但 merge 除外。
hg log -r "head() and not closed()"
列出所有未被關閉的分支末端。
hg log -r "1.3::1.5 and keyword(bug) and file('hgext/*')"
列出 1.3 到 1.5 兩個變更集之間,(在註解或用戶名等處)含有關鍵字 “bug”,且有更動過 “hgext” 資料夾下內容的變更集。
hg log -r "sort(date('May 2008'), user)"
列出 2008 年 5 月提交的所有變更集。列出時,以提交者排序。
hg log -r "(keyword(bug) or keyword(issue)) and not ancestors(tag())"
列出触及關鍵字 “bug” 或 “issue” 的提交,且這些版本必須还没有被包括在任1個被 tag 標記過的版本中。
感到振奮嗎?這是當然的!從這些例子中您可看出它的無窮威力。這是 Git 難以望其項背的,Git 根本沒法做到這些,更別說如此清晰且語義化地去達成它。正因為缺少這種優雅的版本定位能力,Git 雖然能將資料存在版本倉庫中,但卻無法輕易拿出使用。這是 Mercurial 的巨大優勢。
順便1提,revset 還可以設定 alias,您可以將複雜的長串運算整理成1個簡單的--乃至可以加入變數的函式版本,然後隨意運用之。
revset 詳細語法說明請見:
2.2. 清晰的指令介面
Mercurial 的指令介面遠比 Git 更容易使用。
2.2.1. 功能切割
Mercurial 與 Git 的指令設計邏輯不同。
Mercurial 每個指令專注於較少的事情,並提供更多指令供我們使用,而 Git 則更傾向於用1個大指令,將1堆種類完全不同的操作框在1起,然後透過1長串參數來操控它。
網路上有人這樣形容:「Git 提供1柄瑞士刀,而 Mercurial 提供1間裝備齊全的廚房。」
可以想見,Git 的指令設計理念,使得它每個指令文件都非常地難讀難查找,更別說因為功能混在1起,很多時候就連要查哪個指令文件都不知道。初學者哪能知道 git checkout 既可以切換分支,又能創建分支,還能復原工作目錄中檔案的舊版本……許多全然不同的操作全混在1起了。
請務必觀观察看 git help checkout (接近 400 行) 與 hg help update + hg help revert + hg help branch (合計 110 行) 的壓倒性差異。缺少容易理解的介面與文件,讓用戶在使用 Git 時很容易被咬--乃至也更容易因此讓資料在操作錯誤下意外損失掉!
2.2.2. 預設提供簡打
Mercurial 所有指令都有預設提供簡打,如:
Git 也能由用戶自行設定簡打。但它就是沒有內建。換個角度看,Mercurial 也能由用戶自行設定自己專屬的簡打,簡打擴展性方面並不輸給 Git。
雖然簡打稱不上多重要。但每天都要與之打交道的工具,1點方便就會讓人舒服許多。Mercurial 绝不吝嗇地提供了這種方便--而且沒有因此下降任何指令的可識別性。更別說您乃至可以在別人的機器上使用這些簡打指令,用簡打和他人溝通也沒問題。
2.3. Mercurial 有防呆
Mercurial 預設不允許使用者變更歷史紀錄。這意味著 Mercurial 的初學者,使用 Mercurial 時幾乎不可能失手摧毀自己或他人的倉庫。
(但如果您是進階使用者,想要获得更多的控制能力,只需要打開某些擴充套件就可以這麼作。見後。)
初學者不用擔心摧毀版本庫,使本來就很好上手的 Mercurial 變得更友善。1旦我們能放膽去玩去嘗試它,自然也能更快熟习這整套系統--就算某個指令不懂,直接試用也不怕。反過來看,Git 某個指令不懂,直接試用看看通常表示妳要惹上大麻煩了。使用 Git 時,因為 reset 或 rebase 把東西玩壞的經驗,差不多每個人都會遇上個那麼幾次。
2.4. Mercurial 便於同享
與 Git 相比,Mercurial 的設計哲學使它更適合與他人協同工作,分享工作成果。
比方說 push 的預設處理範圍就不同……
這意味著即便是 Mercurial 初學者,也能自但是然地使本地端與遠端資料正確同步,而不會因為分支未同步或沒有正確設定關聯,而弄出現些稀里糊涂的蠢問題。
本地真个所有分支資料會很自然地備份到遠端,這讓使用者不容易因本地問題而遺失資料之餘,也方便和其他開發者同享開發狀況。
另外,Mercurial 分支避免引入命名空間的概念,故從根本上就減少了誤解;也沒有像 Git 那樣引入「遠端分支名稱可能與本地分支名稱不同」等不知何時才能派上用場的無謂複雜度,無需擔心推拉時打錯字等問題,這都是 Mercurial 比 Git 更便於同享的優點。(註4)
注意,push 的處理範圍只是預設。無論 Mercurial 與 Git,進階用戶都可以變更這些預設行為。
另外,雖然 Mercurial 預設讓用戶同享所有變更集,但如果用戶有進行某些本地實驗的需要,Mercurial 也能够簡單將某變更集(和由他長出的後續變更集)設為不會被推送出去的「秘密開發路徑」,這些秘密的開發路徑不但不會被推送,也不會被其他人拉取或 clone 走,私下實驗時相當方便。這方面請參閱 Mercurial 的 Phases 概念。
以下是1些常常能在國內外討論中看到的,關於 Mercurial 的誤解。我將其整理了1下。
3.1. Mercurial 並未缺少功能
有人認為 Mercurial 相比 Git 的功能較少,對版本庫的控制能力較低,這是誤解。Mercurial 只是沒有將所有功能都整合進程式核心而已。
Mercurial 的設計邏輯,在於提供1個簡單強固的核心,與優秀的擴充介面。在 Mercurial 中,更多更強更危險的功能,和某些正在開發的實驗性功能,都是由擴充套件來提供的。
請將官方發佈的擴充套件視為 Mercurial 的1部分,您就不會覺得 Mercurial 功能不足了--不论是 rebase、strip、stage、合併多個變更集等等--沒啥是 Git 能做而 Mercurial 做不到的。
比方說 Git 經常提及的 Stage 概念,在 Mercurial 中就是由 mq 擴充套件提供的。在 mq 的支持下,您不只像 Git 那樣可以暫存1個未完成的變更集,只要您想,您乃至可以暫存整串變更序列;更進1步地說,您不只可以 stage 1整串變更序列,您還可以分頭建立「好幾串」不同的序列,並行開發。再更進1步,如果您想,您乃至可以替您的暫存區做版本管理,乃至將暫存區版本庫推送到遠端……反過來說,如果您不需要 stage 的概念(我相信大部分的人都不需要),您也不會像 Git 那樣在提交時,3天兩頭因為忘記 stage (-a) 而得到「您啥也沒提交」的惱人訊息。
啟用擴充套件也非常輕鬆,畢竟重要的擴充套件都是由官方製作並直接合併在 Release 中發佈的。雖說名字叫作「擴充套件」,卻無需大費周章抓取安裝,取而代之,只要在設定檔中多加1行就可以立刻啟用。
與 Mercurial 核心1併發佈的擴充套件,其品質都有著徹底的保證,其功能變愈甚至會1併寫入 Mercurial 的 Release Changelog 中,強固性、相容性與被官方重視的程度,都是絲绝不遜於核心功能的。
3.2. Mercurial 1點也不慢
許多人認為 Mercurial 相比 Git 的問題在於它速度很慢,這是誤解。
雖然 Git 是用 C 寫的,Mercurial 是用 Python 寫的,看似會有速度差異,但事實上版本管理系統的速度瓶頸在於網路與 I/O,而 CPU 運算速度則影響不大。實際運用時我也從沒感覺 Mercurial 速度慢過 Git,更別說許多世界上最大型的專案都用他。 Firefox 與 Facebook 這些專案都是用 Mercurial 在管理他們的程式碼,我們的程式會比他們更龐大複雜嗎?
最少絕大多數狀況下,速度實在不是值得擔心的重點來著。
註:本節假設 Git 確實比 Mercurial 快,但事實上在 Facebook 近期的幫助下,如今的 Mercurial 在某些條件下乃至有 Git 的數倍快,雙方的速度差距正在縮小乃至逆轉。
另外還有1部分操作,本来就是 Mercurial 壓倒性的快。比方說在整個版本庫中搜尋特定字串時,hg grep --all “TEXT” 遠比 git grep “TEXT” $(git rev-list --all) 快,有時速度可差到 100 倍以上。(在1個約 1600 次提交,大小 300 M 的倉庫中,以上指令 Mercurial 需時 40 秒,Git 需時 45 分鐘)
3.3. 進行危險操作時的安全性
有人提到,Git 在進行如 rebase 這種破壞性操作時,其實是在版本庫中新增1個新的節點,並保存未來可用來回退這次破壞性操作的訊息;這些訊息會成為版本庫的1部分,因此用戶可以美好地回退危險行為。
而另外一方面, Mercurial 在處理破壞性操作時,只會自動匯出1個單獨的 bundle 檔案作為備份,而不是加在版本庫中,這使得回退很難被管理。
技術上來說,他們說得沒錯:Git 確實是把破壞性操作的復原訊息保存在版本庫的 reflog 中,而 Mercurial 則是將被刪除的資料匯出到版本庫以外。不過事實上,對那些東西進行操作的難度,卻完全顛倒了過來。
就從 Mercurial 開始吧。在 Mercurial 中,您要恢復1些被破壞性操作(如 rebase)毀滅的紀錄,只需要下這樣的指令:
hg strip rebase_result_changeset # 先刪掉現有的 rebase 成果
hg unbundle bundle.hg # 將本来的分支取回來
就好了。
在實際取回分支前,您乃至可以先看看您究竟會匯入些什麼:
hg incoming --graph bunble.hg # 加上 --graph 後可用線圖顯示
另外一方面,Git 要怎麼做?這邊有1篇教學可以參考……總而言之您需要发挥1點 shell 魔法。
雖然原作者堅持這很簡單,事實上也確實不算太難(好吧,我是說對1個有能力編寫 shell 腳本的程式設計師而言,另外這個程式設計師還得對 Git 檔案系統與維護方式有著1定程度的理解,再者他還要有寫 unittest)……但是,和 Mercurial 的版本相比那又如何?
另外,基於容量與效力考量,Git 的復原紀錄會被 Git 不定時刪除(!)。至於 Mercurial 的復原記錄則只是1個普通的檔案,用戶想留就留,想扔就扔,想改名存到別的地方那就儘管去做,1切都看自己,不會成心外狀況發生。
Git 不會比 Mercurial 更安全。
Mercurial 的開發10分穩定有節奏。
它每个月1號都會推出1個 bugfix 版本,每隔3個月會推出1個包括新特徵的版本,除 hotfix 之外,兩年下來累計的誤差連10天都沒有。
除穩定地修 bug、將操作介面改得更好用之外,它也持續在發展各種各樣新特徵。
【1.9 版】fileset:讓 Mercurial 用戶能輕鬆地指定檔案。
例→
hg locate "set: binary() and modified() and not **.png"
列出所有 png 之外,於目前工作目錄中被變更過的2進位檔案
【2.1 版】pha