作者:Dirkjan Ochtman
譯者:謝路云
狀態:完成
原文鏈接:[http://www.aosabook.org/en/mercurial.html](http://www.aosabook.org/en/mercurial.html)
Mercurial是一個現代分布式版本控制系統(VCS),主要由Python語言編寫,以及一小部分C代碼,以提高性能。在本章中,我會討論Mercurial設計上的一些關于算法和數據結構的決策。首先,請允許我簡短的回顧一下版本控制系統的歷史,介紹一些必要的背景知識。
**12.1.版本控制簡史**
雖然這一章的主要內容關于Mercurial的體系結構,但其中的許多思想和其他版本控制系統是共通的。為了更好的討論,我想先說明一些存在于不同版本控制系統中的概念和行為。為了恰當的說明這一切,我還將簡單的介紹一下這個領域的歷史。
版本控制系統的發明是為了幫助軟件系統的開發人員并行的工作,而不必相互傳遞文件的拷貝并人工的記錄文件的修改歷史。我們可以將軟件的源代碼文件擴展到任意文件樹。版本控制的主要功能之一就是傳遞樹的變化。工作流程的基本循環是這樣的:
1. 從其他人處獲取最新的文件樹
2. 對這個版本的樹進行一系列修改
3. 發布并使其他人可以獲得這些修改
第一個動作,也就是獲取一份本地的文件樹,被稱為“檢出”(checkout)。我們獲取和發布所有修改的地方叫做“版本庫”(repository),而檢出得到的目錄則被稱為“工作目錄”、“工作樹”或是“工作拷貝”。用版本庫中的最新文件更新工作拷貝的動作就叫做“更新”(update)。有時候這還會涉及到“合并”(merge),也就是組合不同用戶對同一個文件作出的修改。diff命令使我們能夠查看樹或文件在兩個版本之間的變化,它最常見的用途是檢查你的工作目錄中的本地(未發布的)修改。修改的發布是通過一個“提交”(commit)命令完成的,它會將工作目錄的改變保存到版本庫中去。
**12.1.1.集中式版本控制**
史上第一個版本控制系統叫做“源代碼控制系統”(Source Code Control System, SCCS),出現于1975年。它的主要功能是將差異保存在文件中,這比保存一個文件的多個版本的完整拷貝更經濟,但它無法將這些差異傳播給其他人。它的繼任是1982年出現的“修訂控制系統”(Revsion Control System, RCS)。RCS是SCCS更加先進并且免費的替代品(它至今仍在GNU項目的維護之下)。
RCS之后是“并行版本系統”(Concurrent Versioning System, CVS),發布于1986年。它最初是一組批量處理RCS修訂文件的腳本。CVS最大的創新是實現了多個用戶同時編輯相同的文件并在最后合并所有的修改(并行編輯)的模式。這也產生了“編輯沖突”的概念。開發者提交的新版本文件必須基于版本庫的最新版本之上。如果版本庫和我的工作目錄都對文件作出了修改,那么我必須解決這些修改所產生的所有沖突(修改了同一行的情況)。
CVS也開創了“分支”(branch)以及“標簽”(tag)的概念。分支使得開發者能夠并行的工作在不同的任務之上,標簽則可以用來標記版本庫的一個快照以便引用。雖然CVS一開始是通過將版本庫放在共享文件系統上來傳遞差異的,但隨后CVS實現了C/S架構以適應大型網絡中的應用(例如因特網)。
在2000年,三位開發者為了糾正CVS中的設計缺陷一同完成了一個新的版本控制系統,叫做Subversion。Subversion最主要的特點將工作樹作為一個整體對待。也就是說,每個修訂所作出的變更都應該具有原子性、一致性、隔離性和持久性。Subversion能夠在工作目錄中記錄工作拷貝的檢出版本,這樣常用的diff操作(比較本地的工作樹和檢出的版本)就能更快的在本地進行。
Subversion的一個有趣的概念是標簽和分支都是項目樹的一部分。一個Subversion項目通常分為三個部分:`tags`、`branches`、`trunk`。事實證明這種設計對于不熟悉版本控制系統的用戶非常直觀,盡管這種設計的天然靈活性也為各種版本庫轉換工具帶來了大量問題,大部分是因為在其他版本控制系統中,`tags`和`branches`有更加結構化的表示方法。
以上提到的都是“集中式”的版本控制系統。也就是說,盡管它們知道如何交換變更(從CVS開始),但是它們仍然依賴于另外一臺計算機來記錄版本庫的歷史。而“分布式”的版本控制系統則會在存在工作拷貝的每臺計算機上都保存版本庫的完整或是部分歷史。
**12.1.2.分布式版本控制**
盡管Subversion相比CVS有了明顯的進步,但它仍有許多缺點。首先,在所有的集中式版本控制系統中,由于版本庫的歷史集中在同一個地方,變更集的提交和發布實際上是一回事,這也意味著在沒有網絡的情況下是無法提交變更的。其次,在集中式版本控制系統中,訪問版本庫總是需要一次或者多次的網絡請求,比分布式版本控制系統中的本地訪問要慢的多。再次,以上提到的所有版本控制系統都不擅長于記錄合并(隨著系統的改進,有些能夠支持)。在有許多人并行工作的大型團隊之中,版本控制系統必須能夠記錄每個新的修訂版本都包含了哪些變更,這樣才能保證不丟失任何工作,并且后續的合并也能使用這些信息。第四,傳統版本控制系統的集中特性有時非常別扭,它強迫你只能在一個地方進行集成。分布式版本控制的提倡者認為,使用分布式系統的團隊的組織更加自然,開發者們能夠根據項目的需要在任何推送和集成變更。
為了滿足這些需求,已經出現了許多新的工具。我(從開源世界的角度)認為,2011年中最重要的三個當屬Git、Mercurial和Bazaar。Git和Mercurial都始于2005年,當時Linux內核的開發者們決定不再繼續使用專有系統BitKeeper。兩者都是由Linux內核開發者發起的(分別是Linus Torvalds和Matt Mackall),以滿足對能夠處理上萬文件的成百上千個變更集進行管理的版本控制系統的需求。Matt和Linus都深受另一個版本控制系統Monotone的影響。同一時期的Bazaar的開發則相對獨立,但在被Canonical采納為所有項目的版本控制系統之后也得到了很廣泛的使用。
構建一個分布式的版本控制系統顯然會遇到許多挑戰,其中一部分是所有分布式系統所固有的。例如,在集中式系統中源代碼控制服務器總是保存著一份統一的版本歷史,但在分布式系統中是不存在統一的版本歷史的。分布式系統允許并行的提交變更,這使得在任何版本庫中按照時間將修訂歷史排序都是不可能的。
幾乎所有的系統都采用了有向無環圖(DAG)而非線性的變更集方式組織來解決這個問題( 圖12.1 )。也就是說,新提交的變更是其基礎版本的子版本,且不可能有任何版本的基礎是自己或是自己的子嗣。在這個方案中,我們有三種特殊類型的修訂版本: 沒有父版本的“根版本”(root revision),一個版本庫可以有多個根版本),有一個以上父版本的“合并版本”(merge revision),和沒有子版本“頭版本”(head revision)。每個版本庫都是從一個空的根版本開始不斷產生一系列變更集,最后得到一個或者多個頭版本。當兩個用戶分別獨立的提交了變更且其中一個人希望從另一個人那里拉取(pull)變更集時,他必須明確的將另一個人的變更合并從而得到一個新的版本,這個版本的提交將得到一個合并版本。

圖12.1:修訂版本的有向無環圖
需要注意的是,DAG模型有助于解決一些在集中式版本控制系統中很難解決的問題:合并修訂的作用是記錄DAG中被合并的分支的信息。合并的結果圖也能很好的展示出大量并行的分支是如何通過合并縮減并最終得到一根特殊的“主干”的。
這種設計要求系統記錄變更集之間的譜系關系。為了簡化變更集數據的交換,這一般通過由每個變更集記錄它們的父變更集完成。要做到這一點,每個變更集顯然還需要某種標識符。部分版本控制系統使用UUID或是某種類似的機制,Git和Mercurial使用的則是變更集的內容的SHA1哈希值。這樣做的額外收獲是能夠用變更集的ID和內容相互驗證。事實上,由于父節點的信息也被包含在哈希值中,任意版本的所有歷史信息都能夠由它的哈希值驗證。作者姓名、提交信息、時間戳和其他變更集的元數據也和每個新版本的實際文件變更內容一樣進行了哈希運算,所以它們也是可被驗證的。而且,由于時間戳是在提交時記錄的,所以它們在版本庫中完全可以是非線性的。
所有這一切對于只有過集中式版本控制系統經驗的人來說都難以適應:只有一個40個字符的十六進制字符串而非一個友好的整數來全局標識一個修訂版本。此外,所有人之間的版本不再是統一有序的了,只有本地的版本是有序的。統一有序的表示只存在于一張有向無環圖,而不再是一條直線。如果你已經習慣了集中式系統在當你向一個已經擁有子版本的版本進行提交時給出警告,那么在分布式系統中這么做所意外產生的一個新的頭版本可能會讓你感到困惑。
幸運的是,有一些工具可以幫助我們將版本樹的次序可視化,Mercurial可以將變更集的哈希值變為一個無歧義的簡短版本并提供一個本地的線性數字版本號來幫助用戶識別版本信息。整數版本號是單調遞增的,它標識的是本地版本庫中變更集的產生順序。由于這個順序在不同的本地版本庫之間是不同的,因此不能依賴它進行非本地的操作。
## 12.2.數據結構
現在,在對DAG的概念有了一定的了解后,我們來看看Mercurial是如何存儲DAG的。DAG模型是Mercurial內部的核心。實際上我們在版本庫的磁盤存儲(以及運行在內存中的代碼結構)中使用了多個不同的DAG。本節將說明它們分別起什么作用以及它們是如何組合在一起的。
**12.2.1.面臨的挑戰**
在我們深入實際的數據結構之前,我想先介紹一下Mercurial成長的大環境。Matt Mackall在2005年4月20日發送到Linux內核郵件列表的一封郵件中第一次提到了Mercurial。這是在大家決定不再使用BitKeeper進行內核開發之后不久的事情。Matt在他的郵件中描述了Mercurial的幾大目標:簡單、高效和可擴展。
在[[Mac06](http://www.aosabook.org/en/bib1.html#bib%3amackall%3abetter)]中,Matt認為一個現代的版本控制系統必須能夠處理百萬級別的文件和版本,并能夠容納數千用戶在幾十年中不斷的提交新的版本。在明確了目標之后,他評估了一些可能成為瓶頸的技術因素:
* 速度:CPU
* 容量:磁盤和內存
* 帶寬:內存,局域網,磁盤和廣域網
* 磁盤定位操作的頻率(seek操作)
磁盤定位操作的頻率和廣域網帶寬在今天仍然是性能的限制因素,應該為它們進行優化。文章繼續說明了一些用于在文件層面評估系統性能的常見的場景或標準:
* 存儲壓縮:最適合將文件歷史保存在磁盤上的壓縮方法是什么?或者說,哪種算法在不占滿CPU的前提下能夠將I/O性能最大化?
* 獲取任意文件版本:許多版本控制系統保存修訂方式使得它們必須要讀取大量舊的版本信息方能重現一個新版本(也就是僅保存差異)。我們希望這種情況得到控制并保證快速獲取舊的修訂版本。
* 新增文件版本:每天都會有新的版本產生。我們不希望在添加新版本時需要重寫舊版本,因為在有很多版本時這將非常慢。
* 顯示文件的版本歷史:我們希望能夠檢查和任意文件相關的所有變更集的歷史。這也使得我們可以實現“注釋”(annotate)功能(它在CVS中被稱為`blame`,但在其他一些版本控制系統中被更名為`annotate`以消除其暗示的負面含義):檢查當前文件中的每一行來自于哪個變更集。
文章隨后繼續在項目層面解釋了一些類似的場景。在這個層面,基本的操作包括檢出某個版本,提交一個新版本,以及檢查工作目錄中的所有修改。特別是最后這個操作,在文件樹很大時會比較慢(例如Mozilla和NetBeans的項目,它們都使用了Mercurial進行版本控制)。
**12.2.2.版本的快速保存:revlog**
Matt為Mercurial想出的解決方案叫做revlog(版本記錄(revision log)的簡寫)。revlog是一種保存多個版本的文件內容的高效方法。基于上一節所描述的場景,它需要同時保證訪問時間(為磁盤讀寫做優化)和存儲空間的效率。要做到這一點,revlog實際上由磁盤上的兩個文件組成:一個索引文件和相應的數據文件。
| 6個字節 | 塊偏移量 |
| 2個字節 | 標志 |
| 4個字節 | 塊長度 |
| 4個字節 | 未壓縮的長度 |
| 4個字節 | 基準版本 |
| 4個字節 | 鏈接版本 |
| 4個字節 | 第一父節點版本 |
| 4個字節 | 第二父節點版本 |
| 32字節 | 哈希值 |
表12.1:Mercurial的記錄格式
索引文件的內容由定長的記錄組成,格式見表12.1 。定長記錄意味著可以用本地版本號直接(常數時間內)訪問到該版本:只需要直接讀取索引文件的正確位置(索引文件總長度 - 記錄長度 * 版本號)就可以獲取記錄數據。將數據和索引分離也意味著我們無需遍歷磁盤上的數據文件就能夠快速的讀取索引數據。
“塊偏移量”和“塊長度”指定了數據文件中對應于該版本的壓縮數據。要獲得原始數據,我們需要首先讀取基準版本,然后根據多個差異得到該版本。這里的困難在于應該在何時存儲一個基準版本。這取決于多個差異的總大小和該版本在壓縮之前的數據大小之比(為了節省磁盤空間數據會用zlib進行壓縮)。通過用這種方式限制差異鏈的長度,我們可以確保重建指定版本的數據無需讀取和使用大量的差異數據。
鏈接版本的作用是指向該revlog的所依賴的最高層級的revlog(我們之后會詳細說明這一點(譯者注:作者后來真的忘記說了……)),父版本字段保存的是本地的數字版本號。同樣,這使得在相關revlog中查找它們的數據很簡單。哈希值字段保存的是該變更集的唯一標識符。我們為它分配了32個而非SHA1所需的20個字節,以滿足未來的擴展需求。
**12.2.3.三種revlog**
以通用的revlog結構保存歷史數據作為基礎,我們可以構造文件樹的數據模型。它由三種revlog組成:“變更記錄”(changelog)、“聲明記錄”(manifest)和“文件記錄”(filelog)。變更記錄含有每個版本的元數據以及一個指向聲明記錄(即聲明記錄中的相應版本的節點id)的指針。相應的,聲明記錄文件含有一列文件名以及每個文件的節點id,這個id指向該文件在文件記錄中的相應版本。在代碼中,變更記錄、聲明記錄和文件記錄都是通用的revlog類的子類,這樣可以清晰而有層次的表現這些概念。

圖12.2:各種記錄的結構
一條變更記錄看起來是這樣的:
~~~
0a773e3480fe58d62dcc67bd9f7380d6403e26fa
Dirkjan Ochtman <dirkjan@ochtman.nl>
1276097267 -7200
mercurial/discovery.py
discovery: fix description line
~~~
這就是你從revlog層得到的內容,變更記錄層會將它變為一列值。第一行是聲明記錄的哈希,然后是作者名、時間和日期(按Unix時間戳格式,加上時區偏移量)、一列涉及到的文件,最后是描述信息。這里沒有說明的是,變更記錄中的元數據可以是任何東西,為了保持向后兼容性這些內容可以添加在時間戳之后。
下面是聲明記錄:
~~~
.hgignore\x006d2dc16e96ab48b2fcca44f7e9f4b8c3289cb701
.hgsigs\x00de81f258b33189c609d299fd605e6c72182d7359
.hgtags\x00b174a4a4813ddd89c1d2f88878e05acc58263efa
CONTRIBUTORS\x007c8afb9501740a450c549b4b1f002c803c45193a
COPYING\x005ac863e17c7035f1d11828d848fb2ca450d89794
...
~~~
這是變更集0a773e指向的版本聲明記錄(Mercurial的命令允許我們將版本標識符縮短為任意長度的無歧義前綴字符串)它是樹中所有文件的一張簡單列表,每行一個文件,文件名后面是一個NULL字符,然后是一個十六進制編碼的的節點id,指向該文件的文件記錄。樹中的目錄不會被單獨列出,而是通過文件路徑中的斜杠推測出來。請記住,存儲的聲明記錄和其他revlog一樣會被比較差異,因此這個結構使得revlog層能夠方便的僅保存任意版本中被修改過的文件和它們的新哈希值。聲明記錄在Mercurial的Python代碼中一般用類似哈希表的結構表示,其中鍵是文件名,值是節點。
第三種類型的revlog是文件記錄。文件記錄保存在Mercurial內部的`store`目錄中,記錄名和它們所追蹤的文件名幾乎一樣,只是經過了一些編碼以確保能夠工作在所有主流的操作系統上。例如,我們需要處理Windows和Mac OS X的文件系統的大小寫沖突、Windows下的非法文件名,以及各種文件系統所使用的不同字符編碼。你可以想像,保證跨操作系統的可靠性是相當痛苦的。相比之下,文件記錄中的每個版本的內容就沒那么有意思了,它們只是文件的內容加上一些非必要的元數據前綴而已。
這個數據模型使我們能夠訪問Mercurial版本庫中的所有數據,但它并不總是那么好用。盡管模型是垂直的(每個文件記錄對應一個文件),但Mercurial的開發者們經常希望能夠由變更記錄中的一個變更集直接獲取一個版本的所有信息,包括聲明記錄和文件記錄。后來,他們在各種revlog之上新增了一組類來做到了這一點。它們叫做`contexts`。
使用多鐘獨立的revlog的協同工作的好處是之一它們的次序。在添加一個版本時,按照次序首先添加的是文件記錄,其次是聲明記錄,最后是變更記錄,這樣版本庫的狀態能夠總是保持一致。任何進程在讀取變更記錄的時候都無需擔心指向其他revlog的指針是無效的,這能夠預防許多問題。盡管如此,Mercurial仍然會用鎖來確保不會有兩個進程同時添加revlog記錄。
**12.2.4.工作目錄**
我們要講的最后一個重要的數據結構叫做“目錄狀態”(dirstate)。“目錄狀態”表示的是工作目錄在任意時刻的內容。最重要的是,它記錄的是被檢出的版本。這個版本是`status`和`diff`命令比較的基準,也是下一個提交的變更集的父版本。在使用`merge`命令之后,目錄狀態將擁有兩個父版本,并會將一個變更集中的差異合并到另一個中去。
由于`status`和`diff`是很常見的操作(它們能夠幫助你查看最近作出的改動),Mercurial會用一個緩存保存上次遍歷工作目錄所得到的狀態。在狀態中記錄文件的最近修改時間和大小可以加快遍歷目錄樹的速度。我們還需要記錄文件的狀態,即文件在工作目錄中是被添加、刪除還是合并了。這也可以加快遍歷工作目錄的速度,這些信息還可以供提交命令使用。
## 12.3.版本策略
現在我們已經熟悉了Mercurial底層的數據模型和代碼的結構,可以在高一些的層次來學習Mercurial是如何在上一節所描述的基礎設施之上實現版本控制的概念了。
**12.3.1.分支**
分支通常用于隔離不同的開發任務,并會在之后被合并。使用分支可能是因為人們希望在嘗試新的實現方式的同時保證開發主線總是可發布的狀態(即功能分支),或是為了快速發布老版本的修正(即維護分支)。這兩種方式都很常用,所有的現代版本控制系統也都支持它們。在基于DAG的版本控制系統中,隱式分支很常見,但命名分支(分支名稱保存在變更集的元數據中)卻有所不同。
最初,Mercurial不支持對分支命名。分支的方式就是克隆(clone)并分別發布它們。這種方式既有效又簡單易懂,因為代價很小,所以特別適合于功能分支。但是,在大型項目中,克隆的成本是很高的。盡管在大多數文件系統中可以使用硬鏈接復制版本庫,但創建一棵單獨的工作樹既緩慢又消耗空間。
鑒于這些缺點,Mercurial添加了第二種分支的方式:在變更集的元數據中保存一個分支的名稱。我們添加了一個`branch`命令用來設置當前工作目錄的分支名稱,這個分支名會被用于下一次提交。`update`命令可以用來更新一個分支的名稱,而在一個分支上提交的變更集將永遠屬于這個分支。這種方式被稱為命名分支 。但是Mercurial在好幾個版本之后才添加了關閉分支的功能(關閉分支意味著在分支列表中不再可見)。關閉分支的實現方式是在變更集的元數據中新增一個域來標記該變更集關閉了這個分支。如果一個分支包含多個頭版本,那么只有在它們全部關閉之后分支才會從版本庫的分支列表中消失。
當然,實現命名分支的方法很多。Git命名分支的方式就不一樣,它使用的是“引用”(reference)。引用是指向Git版本歷史中的一個對象的名字,這個對象通常是變更集。這意味著Git的分支是臨時的。只要你刪去了引用,就沒法再證明這個分支曾經存在過。這和在Mercurial中使用克隆然后再將修改合并回來的效果是類似的。它使得在本地的分支操作非常簡單輕便,也避免了分支列表的混亂。
事實證明這種分支方式非常受歡迎,比Mercurial中的命名空間或克隆式分支都要流行的多。于是產生了Mercurial的`bookmarks`(書簽)擴展,它在未來可能會被合并進入Mercurial。它簡單的使用了一個無版本的文件來記錄所有引用。Mercurial使用的傳輸協議(wire protocol)也相應的進行了擴展,使之能夠傳遞bookmark。
**12.3.2.標簽**
乍一看,Mercurial實現標簽的方式有些令人費解。當你第一次添加一個標簽的時候(使用`tag`命令),版本庫中會添加并提交一個叫做`.hgtags`的文件。該文件的每一行都包含了一個變更集的節點id和其對應的標簽名。因此,這個標簽文件和版本庫中的其他文件并沒有區別。
之所以這么做原因有三。首先,標簽應該是可修改的:意外是不可避免的,所以應該有辦法修正或者刪除錯誤的標簽。其次,標簽應該是變更歷史的一部分:標簽的被創造的日期、作者、起因,包括標簽的變更,都是有價值的信息。第三,應該允許為一個過往的變更集打標簽。例如,有些項目會從版本控制系統中導出一個版本,并在發布之前對它進行詳盡的測試。
`.hgtags`的設計具備所有這些性質。盡管有些用戶對工作目錄中存在的`.hgtags`感到不解,但是它使得標簽機制和Mercurial的其他部分(例如和其他克隆版本庫的同步)的集成變得非常簡單。如果標簽存在于代碼樹之外(例如Git的做法),我們就需要其他的方式來檢查標簽的來源并處理(并行添加的)重復標簽所造成的沖突。即便后者很罕見,但能從設計上消滅問題出現的可能性則更好。
為了做到這些,Mercurial只會向`.hgtags`文件追加內容。當標簽是同時創建于不同的克隆版本庫中時,這也有利于合并標簽文件。任何標簽的最新節點id的優先級總是最高的,添加一個空的節點id(表示的是存在于所有版本庫的根版本)的效果是刪除相應的標簽。Mercurial也會考慮版本庫中的所有分支上的標簽,并用新舊程度來計算它們的優先級。
## 12.4.代碼結構
Mercurial幾乎全部是用Python寫成的,只有一小部分是C,因為它們是整個應用程序性能的關鍵。Python更合適的原因是使用這樣的動態語言來表達高層次的概念更簡單。由于大部分代碼的性能并不重要,所以我們不介意在大多數情況下用速度換取編寫代碼的舒適性。
一個Python模塊對應著一個代碼文件。模塊中所包含的代碼沒有限制,所以它是一種組織代碼的重要方式。模塊可以通過導入其他模塊使用各種數據類型和調用各種函數。含有`__init__.py`模塊的目錄叫做包,包中所包含的所有模塊和包都是可以被其他Python代碼引用的。
在默認情況下Mercurial會在Python的系統路徑下安裝兩個包:`mercurial`和`hgext`?。`mercurial`中包含了Mercurial運行所需的核心代碼,而`hgext`則包含了一些我們覺得應該和核心代碼一并交付的有用的擴展。但是,仍然需要手動編輯配置文件才能在需要的時候啟用它們(稍后討論)。
需要明確的是,Mercurial是一個命令行應用程序。這意味著我們的用戶界面很簡單,即調用`hg`腳本和一個子命令。子命令(例如`log`、`diff`或是`commit`)可能接受一些選項和參數,也有一些選項是適用于所有命令的。用戶界面的可能出現三種不同的情況:
* hg會輸出用戶所請求的結果,或者顯示狀態信息
* hg會通過命令行請求更多的輸入
* hg可能會啟動一個外部程序(如一個編寫提交信息的編輯器或是用于幫助合并代碼沖突的程序)

圖12.3:代碼導入圖
這個過程的開始可以通過圖12.3的代碼導入圖中清楚地觀察到。所有的命令行參數都會被傳遞給dispatch(分發)模塊中的一個函數。這個函數首先會實例化一個`ui`對象。這個`ui`對象會先在一些已知的地方(例如你的家目錄下)尋找幾個配置文件,并將配置選項保存在`ui`對象中。這些配置文件可能會包含一些指向擴展的路徑,這些擴展在此時也會被加載。通過命令行傳遞過來的所有全局選項也會在此時被保存在`ui`對象中。
完成這些之后,我們需要判斷是否創建一個repository對象。雖然大多數命令操作的都是本地的版本庫(用`localrepo`模塊的`localrepo`類表示),有些命令可以操作遠程版本庫(可以是HTTP、SSH或是其他實現了的方式),而一些命令則無需操作任何版本庫。最后這類包括比如說`init`命令,它用于初始化一個新的版本庫。
所有的核心命令都表示在`commands`模塊的一個函數中,這樣我們很容易找到任意命令的實現代碼。Commands模塊還包含一張將命令名和函數對應起來的哈希表,它描述了每個命令所接受的選項。這樣,命令之間就可以共享選項(例如,許多命令都有和`log`命令類似的選項)。Dispatch模塊可以使用選項的描述來檢查任意命令所接受的參數,并將接受到的參數值轉換為命令所需的類型。幾乎所有的函數都會用到`ui`對象和`repository`對象。
## 12.5.可擴展性
Mercurial的強大特性之一就是能夠為它編寫擴展。因為Python是一個相對容易上手的語言,而Mercurial的API大部分設計的很好(盡管文檔不全),許多人都是因為想擴展Mercurial而第一次學習Python。
**12.5.1.編寫擴展**
要啟用擴展,必須在Mercurial啟動時所讀取的任意配置文件之一中加上一行,指定一個鍵和擴展Python模塊的路徑。有幾種方法來為Mercurial添磚加瓦:
* 加入新的命令;
* 封裝現有的命令;
* 封裝所使用的版本庫;
* 封裝Mercurial中的任意函數;
* 添加新的版本庫類型。
添加新的命令只需要在擴展模塊中添加一張名為`cmdtable`的哈希表即可。擴展加載器會讀取它,并將新的命令加入分發時的候選命令列表中。同樣,擴展可以定義名為`uisetup`和`reposetup`的函數,分發代碼會在UI和repository對象被實例化之后調用它們。一種常見的方式是在擴展中使用`reposetup`函數自定義一個子類封裝repository。這使得擴展可以修改版本庫的各種基本行為。例如,我編寫的一個擴展使用`uisetup`來根據環境配置中的SSH驗證信息設置`ui.username`這一屬性。
更強大的擴展還可以添加版本庫的種類。例如,?`hgsubversion`項目(沒有被包含進Mercurial)為Subversion的版本庫注冊了一個新的版本庫類型。這使得它幾乎可以像克隆一個Mercurial版本庫一樣克隆一個Subversion的版本庫。它甚至可以向Subversion版本庫推送代碼,不過由于兩個系統的巨大差異,并不是所有情況下都能成功,但用戶界面則是完全透明的。
對于那些希望從根本上改變Mercurial的人,在動態語言的世界里有一種叫做“monkeypatching”的技巧。因為擴展和Mercurial運行在相同的地址空間中,而且Python語言靈活的反射機制非常強大,Mercurial所定義的任何函數或者類都是可以(甚至是很簡單的)被修改的。盡管這么做并不優雅,但它仍然不失為一種非常強力的技巧。例如,?`hgext`中的`highlight`擴展修改了內置的Web服務器,為版本庫的文件內容頁面加上了語法高亮。
還有另一種簡單得多的擴展Mercurial的方法,那就是aliases (別名)。所有配置文件都可以為已知的命令定義別名。這使得我們能夠為其他命令定義較短的簡寫。最新版本的Mercurial還允許為shell命令定義別名,這樣只用shell腳本你就能夠設計出復雜的命令了。
**12.5.2.鉤子**
所有版本控制系統長期以來都通過“鉤子”這種方式在事件中和外界交互。常見的用法包括向持續集成系統發送通知或是更新Web服務器上的工作目錄來發布修改。當然,Mercurial還包括一個調用此類鉤子的子系統。 事實上,它也含有兩個變種。一種和其他版本控制系統中傳統的鉤子類似,調用的是shell腳本。另一種則更有趣,因為它調用的鉤子是用戶指定的Python模塊中的指定函數。由于運行在同一進程中,這種方式不僅更快,而且還能使用repo和ui對象,這意味著和版本控制系統的內部進行更復雜的交互變得更簡單。
Mercurial的鉤子可以分為“pre-command”(命令前執行)、“post-command”(命令后執行)、“controlling”(控制)和“miscellaneous”(其他)類。前兩種鉤子的定義只需要在配置文件的hooks小節中指定一個pre-command或是post-command鍵即可。另外兩類則包含一組預定義的事件。controlling鉤子的不同之處在于它們運行于事件即將發生的時候,并且可以阻止事件的繼續。這常用于在中央服務器上用某些規則驗證變更集的有效性。由于Mercurial的分布性,在提交時是無法進行這種檢查的。例如,Python項目就使用了一個鉤子來確保某些方面的代碼風格一致性——如果一個變更集所添加的代碼不合要求,它會被中央版本庫所拒絕。
鉤子的另一個有趣的用法是Mozilla和其他一些公司所使用的"推送日志"(pushlog)。推送日志會記錄每次推送的內容(因為一次推送中可能含有任意數量的變更集)、發起者和時間,這也是審核版本庫變更的一種方式。
## 12.6.經驗教訓
Matt在開始開發Mercurial時作出的決定之一就是使用Python。Python有強的可擴展性(通過擴展和鉤子),編寫代碼也很容易。Python還節省了大量的跨平臺兼容性工作,使得Mercurial達到在三個主流操作系統上都良好工作的目標相對簡單。另一方面,Python和許多其他(編譯型)的語言相比運行較慢,特別是解釋器的啟動較慢,這對于會被頻繁調用而非長期運行的工具(例如版本控制系統)來說特別糟糕。
早期的一個選擇使得現在Mercurial很難修改提交之后的變更集,因為修改版本號就必然會修改標識這個版本的哈希值。想要“召回”已經發布到互聯網上的變更集是痛苦的,而Mercurial讓這一切變得更困難。但是,修改尚未被發布的版本一般沒有問題,而且Mercurial社區在第一個版本發布之后就一直在努力使之變得更簡單。有一些擴展嘗試著解決這個問題,但使用它們需要一些學習,而且步驟對于已經簡單使用過Mercurial的人來說并不直觀。
三種revlog能夠很好的降低磁盤定位操作的頻率,而且變更記錄、聲明記錄和文件記錄的三層結構工作的非常好。提交速度很快,每個版本所需的磁盤空間也較少。但是有一些用例的效率就不是很高,比如重命名文件,因為每個文件的版本都是分別存儲的。我們會修正這個問題,但可能會打破分層的設計。同樣,每個文件用來輔助文件記錄存儲的DAG圖在實踐中并不常用,所以用于管理這些數據的代碼其實是個負擔。
Mercurial關注的另一個焦點是易用性。我們盡量用較少的一組命令來完成絕大多數功能,且命令之間的選項是一致的。我們希望學習Mercurial的過程是漸進的,特別是對于那些曾經使用過其他版本控制系統的人。這種設計哲學的延伸使得Mercurial可以通過擴展來適應某些特別的用例。因此,開發者們保持了用戶界面和其他版本控制系統的一致性,特別是Subversion。同樣,開發小組也通過應用程序本身提供了優質的文檔,不同的主題和命令之間都可以交叉引用。我們還盡力給出有用的錯誤信息,包括在操作失敗時提示其他嘗試的可能。
對于新用戶來說,一些小地方可能會讓他們很驚訝。例如,很多用戶一開始并不喜歡用工作目錄中的一個單獨的文件來處理標簽(如前文所述),但這種方式有它的優勢(當然也有它的缺陷)。同樣的,其他的版本控制系統在檢出時只會發送指定的變更集和它的所有父版本,但Mercurial則會發送所有遠程版本庫中所沒有的版本。兩種方式都有各自的道理,最佳的選擇只能根據你的開發方式來決定。
和在任何軟件項目一樣,這其中有許多取舍。我認為Mercurial作出了許多正確的選擇,當然在事后從追求完美的角度來說,其他一些選擇可能更好。從歷史角度來說,Mercurial已經是第一代成熟的通用分布式版本控制系統之一了。我期待著下一代版本控制系統的出現。
- 前言(卷一)
- 卷1:第1章 Asterisk
- 卷1:第3章 The Bourne-Again Shell
- 卷1:第5章 CMake
- 卷1:第6章 Eclipse之一
- 卷1:第6章 Eclipse之二
- 卷1:第6章 Eclipse之三
- 卷1:第8章 HDFS——Hadoop分布式文件系統之一
- 卷1:第8章 HDFS——Hadoop分布式文件系統之二
- 卷1:第8章 HDFS——Hadoop分布式文件系統
- 卷1:第12章 Mercurial
- 卷1:第13章 NoSQL生態系統
- 卷1:第14章 Python打包工具
- 卷1:第15章 Riak與Erlang/OTP
- 卷1:第16章 Selenium WebDriver
- 卷1:第18章 SnowFlock
- 卷1:第22章 Violet
- 卷1:第24章 VTK
- 卷1:第25章 韋諾之戰
- 卷2:第1章 可擴展Web架構與分布式系統之一
- 卷2:第1章 可擴展Web架構與分布式系統之二
- 卷2:第2章 Firefox發布工程
- 卷2:第3章 FreeRTOS
- 卷2:第4章 GDB
- 卷2:第5章 Glasgow Haskell編譯器
- 卷2:第6章 Git
- 卷2:第7章 GPSD
- 卷2:第9章 ITK
- 卷2:第11章 matplotlib
- 卷2:第12章 MediaWiki之一
- 卷2:第12章 MediaWiki之二
- 卷2:第13章 Moodle
- 卷2:第14章 NginX
- 卷2:第15章 Open MPI
- 卷2:第18章 Puppet part 1
- 卷2:第18章 Puppet part 2
- 卷2:第19章 PyPy
- 卷2:第20章 SQLAlchemy
- 卷2:第21章 Twisted
- 卷2:第22章 Yesod
- 卷2:第24章 ZeroMQ