當今IT界,VCS版本控制系統的的使用已經成了一種日常不可或缺部分。不光是程序員、IT界甚至在其他領域也是逐漸了有了共識。通過Github寫書也不再是天方夜譚或者技術段子,而是實實在在天天發生的事情。本文,我們回顧下歷史上主要版本控制系統(VCS)的發展過程,涉及了內容有:SCSS、RCS、CVS、SVN,Git和Mercurial。
概述
回顧VCS的發展歷程,總體上可以劃分為三個階段。
第一代VCS,包括SCSS和RCS。立足于對單個文件變化的跟蹤,檢出的文件一次只能由一個用戶在本地進行編輯,用戶通過自己的帳戶登錄到同一共享Unix主機方式實現。
第二代VCS,包括CVS和SVN。通過引入網絡,從而形成了包含正式意義上的項目版本的集中式版本存儲庫。相比第一代VSC,有了實質性的發展,可以供多個用戶同時檢出并使用代碼,但是他們都需要重新提交到同一中央存儲庫。存在的問題是嚴重依賴于中央存儲庫,對網絡和實時性同步要求很大。
第三代VSC,包括Git和Mercurial。V到現在發展成為了分布式VCS。在分布式VCS中,創建存儲庫的所有副本都是相同的,無需一個集中的中央存儲庫。無需通過網絡實時同步內容,只需本地創建提交,分支和合并打開了路徑,在合適時候再推送到遠端庫。
VCS重要軟件發展歷史時間表:
SCCS第一代版本管理系統
SCCS(Source Code Control System)是最早創建的VCS工具。它由貝爾實驗室的Marc Rochkind于1972年用C開發。SCCS旨在解決源文件修訂跟蹤的問題。此外,它還解決程序bug錯誤定位的問題。SCCS是現代VCS的鼻祖,至此后VCS發展30年到現在茁壯發展。
功能
和大多數現代的VCS一樣,SCCS支持一組命令,供開發人員做文件版本控制。主要實現的功能有:
檢入文件以使用SCCS跟蹤其歷史記錄;
檢出特定的文件修訂以供審核或編譯;
檢出特定的文件修訂以進行編輯;
檢入新文件修訂以及說明更改的注釋;
還原檢出文件中所做的更改;
基本分支和變更合并;
提供文件修訂歷史記錄。
技術實現
當添加文件到SCCS進行跟蹤時,會創建一種稱為s文件或歷史文件特殊類型的文件。該文件使用以s開頭的原始文件名來命名,存儲在名為SCCS的子目錄中。比如,一個名為test.txt的源文件將在./SCCS/目錄中創建一個名為s.test.txt的歷史文件。創建后,歷史記錄文件將包含原始文件的初始內容以及一些元數據以幫助進行版本跟蹤。文件校驗和存儲在歷史記錄文件中,以驗證內容是否遭到篡改。歷史記錄文件的內容未經過壓縮或編碼。由于原始文件的內容存儲在歷史記錄文件中,因此可以將檢出到到工作目錄進行查看,編譯或編輯。可以將對文件所做的進一步更改(例如,行添加,修改和刪除)檢入到歷史文件中,并增加修訂號。
SCCS檢入僅存儲增量或文件更改,而不是每次存儲整個文件內容。這樣可以減小歷史記錄文件的大小。每次檢入時,增量都存儲在歷史記錄文件內部的稱為增量表的結構中。如前所述,實際文件內容或多或少是逐字復制的,帶有特殊的控制序列,用于標記已添加和已刪除內容的各個部分的開頭和結尾。由于SCCS歷史記錄文件不使用壓縮,因此它們通常比要跟蹤的實際文件大。
SCCS使用一種稱為交錯增量的增量方法,支持恒定時間檢出,而不管檢出的修訂版有多老。即較舊的修訂版檢出所花費的時間不會比新的修訂版更長。
需要注意的重要一件事是,所有文件都在SCCS中被單獨跟蹤和檢入。沒有辦法將更改作為一個原子單位的一部分檢入多個文件(和Git提交一樣)。每個跟蹤的文件都有一個對應的歷史文件,用于存儲其修訂歷史。
當檢出文件以在SCCS中進行編輯時,為了防止更改被其他用戶覆蓋,文件上將放置一個鎖,但是這會限制多用戶的編輯從而開發效率。
SCCS支持可以在特定文件內存儲更改序列的分支。分支可以與原始版本合并回去,也可以與同一父級的其他分支版本合并。
基本命令
以下是最常見的SCCS命令的列表。
sccs create <filename.ext>:將新文件檢入SCCS并為其創建新的歷史記錄文件(默認在./SCCS/目錄中)。
sccs get <filename.ext>:從相應的歷史文件中檢出文件,并以只讀模式將其放置在工作目錄中。
sccs edit <filename.ext>:從相應的歷史文件中檢出文件進行編輯。鎖定歷史記錄文件,以便其他用戶無法修改它。
sccs delta <filename.ext>:檢入對指定文件的修改。將提示填寫commit,將更改存儲在歷史記錄文件中,然后刪除鎖。
sccs prt <filename.ext>:顯示跟蹤文件的修訂日志。
sccs diffs <filename.ext>:顯示文件的當前工作副本與檢出時文件狀態之間的差異。
版本文件
一個SCCS歷史記錄文件示例:
RCS版本管理系統
RCS(Revision Control System)由Walter Tichy于1982年用C編寫,用以替代SCCS,SCCS當時還不是開源的。
功能
RCS與它的前任SCCS有許多共同之處,包括:
逐個文件處理修訂;
跨多個文件的更改不能一起歸類為原子提交;
跟蹤文件旨在一次由一個用戶修改;
沒有網絡功能;
每個跟蹤文件的修訂都存儲在相應的歷史文件中;
基本分支和修訂在單個文件中的合并。
技術實現
當將文件檢入RCS時,會在當前目錄下的./RCS/目錄中創建相應的歷史文件。該文件后綴有.v,比如,test.txt的文件將將創建test.txt.v的跟蹤文件。
RCS使用反向增量方案來存儲文件更改。檢入文件后,文件內容的完整快照將存儲在歷史記錄文件中。修改文件并再次檢入后,將根據現有歷史文件內容計算增量。舊的快照將被丟棄,新的快照將被保存,并與增量一起恢復到較早的狀態。之所以稱為反向增量,是因為要檢出舊版本,RCS需要從文件的最新版本開始并應用連續的增量,直到達到舊版本為止。由于始終可以使用當前修訂的完整快照,因此該方法可以非常快速地檢出當前修改。但是,檢出版本越老,檢出花費的時間越長,因為需要針對當前快照計算越來越多的增量。
相比較SCCS花費相同的時間來獲取任何修改。RCS歷史記錄文件中沒有存儲校驗和,因此無法確保文件完整性。
基本命令
以下是最常見的RCS命令的列表:
ci <filename.ext>:將一個新文件檢入RCS并為其創建一個新的歷史記錄文件(默認在./RCS/目錄中)。
co <filename.ext>:從相應的歷史文件中檢出文件,并以只讀模式將其放置在工作目錄中。
co -l <??filename.ext>:從相應的歷史文件中檢出文件以進行編輯。鎖定歷史記錄文件,以便其他用戶無法修改它。
ci <filename.ext>:檢入文件更改并在其對應的歷史文件中為其創建一個新修訂。
merge <file-to-merge-into.ext> <parent.ext> <file-to-merge-from.ext>:合并更改來自同一父文件的兩個修改后的子對象。
rcsdiff <filename.ext>:顯示文件的當前工作副本與檢出時文件狀態之間的差異。
rcsclean:刪除沒有鎖的工作文件。
RCS更多操作介紹,詳見GNU RCS說明手冊。
版本文件
RCS .v歷史記錄文件樣例
CVS第二代版本管理系統
CVS(Concurrent Versions System)由Dick Grune于1986年創建,主要是在第一代單機版本控制工具基礎上添加了網絡,使其網絡化協作化。CVS也是用C語言編寫的。CVS開啟了VCS發展的第二個里程碑,開啟了第二代VCS工具的發展。CVS的網絡化使地理上分散的開發團隊可以協同開發。
功能
CVS采用C/S架構體系,其代碼都都存儲在服務器端,開發者需從服務器上獲得一份代碼復制到本機,然后開發。開發者可隨時將新代碼提交給服務器,也可以通過更新操作獲得最新的代碼,保持與其他開發者的一致。CVS提供了一組用于與項目中的文件進行交互的命令,但是使用RCS歷史文件格式和后臺命令。
VCS歷史上,CVS首次允許多個開發人員檢出并同時處理相同的文件。CVS在處理多人同時修改頁面時,采用"先允許修改,再處理沖突"。
檢出:用來下載文件和建立服務器和本機目錄之間的對應關系。不會修改本機已有文件的"只讀"屬性;
Export輸出:用來下載服務器文件到本機,從而進行軟件的編譯發布;
更新:用于獲取當前最新版本,也可以用于獲取某個特定版本;
Edit編輯:僅用來通知服務器,要編輯某個文件。
unedit:僅用來通知服務器,完成某個文件的編輯了,同時將本機文件置為"只讀";
Watch:實現監視協作者edit文件狀態的情況,要實現這種監控,要求所有人在自己本機修改文件之前,都edit一下,并在commit之后,unedit一下,別人才能收到通知;
Commit:類似于檢入,不同的是,其不修改本機文件的"只讀"屬性,commit后你仍然可以繼續修改本機文件,必須unedit后文件屬性才改完"只讀"。
技術實現
CVS通過使用集中式存儲庫模型,第一步是使用CVS在遠程服務器上建立集中式存儲庫。然后就可以將項目導入到存儲庫中,將項目導入CVS后,每個文件都被轉化為.v歷史文件,并存儲在被稱為模塊的中央目錄中。該存儲庫通常位于可通過本地網絡或Internet訪問的遠程服務器上。
開發人員通過檢出該模塊的副本,并復制到本地工作目錄中。在此過程中不會文件被鎖定,因此可以同時無限制的進行文件檢出。開發人員可以修改檢出的文件并根據需要提交更改。如果開發人員要提交更改,則其他開發人員將需要在提交更改之前先通過自動合并過程更新其工作副本。必要時候,需要先解決合并沖突。 CVS還提供了創建和合并分支的功能。
基本命令
export CVSROOT=<path/to/repository>:設置CVS存儲庫根目錄,無需在每個命令中都指定它。
cvs import -m 'Import module' <module-name> <vendor-tag> <release-tag>:將文件目錄導入CVS模塊。
cvs checkout <module-name>:將模塊復制到工作目錄。
cvs commit <filename.ext>:將更改的文件提交回模塊。
cvs add <filename.txt>:添加一個新文件以跟蹤修訂。
cvs update:通過合并遠程存儲庫中存在的已提交更改而不是工作副本來更新工作目錄。
cvs status:顯示有關已檢出模塊工作副本的常規信息。
cvs tag <tag-name> <files>:將識別標記添加到單個文件或一組文件中。
cvs tag -b <new-branch-name>:在存儲庫中創建一個新分支(必須先檢出,然后在本地進行操作)。
cvs checkout -r <branch-name>:將現有分支檢出到工作目錄。
cvs update -j <branch-to-merge>:將現有分支合并到本地工作副本中。
更多CVS操作,詳見GNU CVS手冊。
版本文件
CVS歷史記錄文件示例:
SVN,集大成者
Subversion由Collabnet公司在2000年創建,后交由Apache 軟件基金會維護。SVN也是用C編寫的,用于改善CVS,實現更強大的集中式解決方案。時至當下,仍有大量的公司依賴于SVN實現其項目管理。
技術實現
和CVS一樣,SVN也使用集中式存儲庫模型。遠程用戶必須依賴網絡來實現連接才能將其更改提交到中央存儲庫。
Subversion引入了原子提交的功能,確保提交將完全成功,或者在發生問題時被完全放棄。在CVS中,如果提交操作中途失敗(例如,由于網絡中斷),則存儲庫可能損壞和不一致的狀態。
Subversion中的提交或修訂可以包含多個文件和目錄。這樣可以允許用戶以項目為單位的跟蹤相關更改集,而無需分別跟蹤每個文件的更改。
Subversion用于跟蹤文件的存儲模型稱為FSFS(File System atop the File System),使用與運行的操作系統文件系統相匹配的文件和目錄結構來創建其數據庫結構。Subversion文件系統的獨特之處在于,它不僅可以跟蹤其包含的文件和目錄,還可以跟蹤這些文件和目錄的不同版本,并且它們會隨著時間變化。它是一個具有附加時間維度的文件系統。
Subversion以文件夾為基本管理單位。可以在Subversion中提交空文件夾,而在其它(甚至是Git)VCS中,無法管理空文件夾。
創建Subversion存儲庫后,將創建一個空的文件和文件夾數據庫作為其一部分。將創建一個名為db/revs的目錄,其中存儲了已檢入(已提交)文件的所有修訂跟蹤信息。每次提交(可以包括對多個文件的更改)都存儲在revs目錄中的新文件中,并以從1開始的順序數字標識符命名。當首次提交文件時,將存儲其全部內容。為了節省空間,同一文件的在次提交時候將僅存儲變化部分,也稱為diffs或deltas。
另外,SVN還使用lz4或zlib壓縮算法壓縮增量,以進一步減小其大小。
盡管每次都存儲文件增量而不是整個文件確實節省了存儲空間,但由于需要將所有增量捆綁在一起以重新創建文件的當前狀態,因此增加了檢出和提交操作的時間。默認情況下,Subversion在存儲文件的新完整副本之前,每個文件最多可以存儲1023個增量。這樣可以實現存儲和速度之間的良好平衡。
SVN不使用常規的分支和標記系統。一般的Subversion存儲庫布局是在根目錄中包含三個文件夾:
trunk/
brances/
tags/
trunk文件夾用于應用程序的生產版本。brances文件夾用于保存與各個分支相對應的子文件夾。tags文件夾用于保存特定(通常是重要的)項目修訂的標簽。
基本命令
svn create <path-to-repository>:在指定目錄中創建一個新的空存儲庫。
svn import <path-to-project> <svn-url>:將文件目錄導入指定的Subversion存儲庫路徑。
svn checkout <svn-path> <path-to-checkout>:將存儲的存儲庫路徑復制到所需的工作目錄。
svn commit -m 'Commit message':提交一組更改的文件和文件夾以及描述性的提交消息。
svn add <filename.txt>:添加一個新文件以跟蹤修訂。
svn update:通過合并svn服務器中存在的已提交更改而不是工作副本來更新工作副本。
svn status:顯示工作目錄中已更改的跟蹤文件的列表。
svn info:顯示有關已檢出副本的常規詳細信息列表。
svn copy <branch-to-copy> <new-branch-path-and-name>:通過復制現有分支來創建一個新分支。
svn switch <existing-branch>:將工作目錄切換到現有分支。這將檢出指定的分支。
svn merge <existing-branch>:將指定的分支合并到工作目錄中檢出的當前分支中。請注意,這需要在以后提交。
svn log:顯示活動分支的提交歷史記錄和相關的描述性消息。
版本文件
SVN修訂文件示例如下
第三代版本管理系統Git
Git由Linux之父Linus Torvalds于2005年創建用來替代Linux內核開發用的商業的版本管理軟件BitKeeper。主要是用C結合一些Shell腳本編寫的。由于其功能,靈活性和速度,分布式版本管理、協作性,使得Git成長為一個最出色的VCS軟件。由于GitHub,Gitlab等添加的協作性,社交性功能使得Git風靡于世,被廣泛使用。
技術實現
Git是分布式VCS,無需集中式的中央存儲卡,就可以正常工作。在Git中所有副本都創建為相等,即便是遠程Git服務上也是是等價的副本。這是與第二代VCS最明顯得差異,第二代集中式的版本管理,必須要依靠中央服務器來提供用戶檢入和檢出。而在Git中開發人員可以本地任意開發,即使連不到遠程庫也不受任何影響,只需在網絡通暢時候,再將變化推送服務器即可。開發人員可以脫機在本地工作,直到準備與他人共享他們的工作為止。此時,可以將更改推送到其他存儲庫以進行檢查,測試或部署。
Blob對象
添加文件以使用Git進行跟蹤時,Git使用zlib壓縮算法對其進行壓縮。使用SHA-1哈希函數對結果進行哈希處理。這將產生一個唯一的哈希值,該值對應于該文件中的內容。Git將其存儲在位于隱藏的.git/objects文件夾中的對象數據庫中。文件的名稱為生成的哈希值,內容為壓縮的內容。這些對象文件稱為Blob,每次將新文件添加到存儲庫時會創建Blob對象。
Git實現了一個staging索引,該索引被設計為在提交的中間區域。在準備提交新更改時,它們的壓縮內容在特殊的索引文件中被引用,該文件采用樹對象的形式。
樹對象是另一種Git對象,對應于文件目錄,它將blob對象連接到它們的真實文件名,文件許可權和到其他樹的鏈接,并以此方式表示特定文件和目錄集的狀態。一旦所有相關更改都準備好提交,索引樹就可以提交到存儲庫,該存儲庫在Git對象數據庫中創建一個commit對象。
commit對象保存特定修訂的標題樹以及提交作者,電子郵件地址,日期和描述性提交消息。每個commit對象還保存對其父提交的引用,因此隨著時間的推移,將建立項目開發的歷史記錄。
如前所述,所有Git對象(Blob,tree和commit)都根據其哈希值進行壓縮,哈希處理并存儲在對象數據庫中。這些被稱為松散對象。Git實現中沒有通過差異來節省空間,而都是全部內容哈希鍵索引和壓縮鏡像,所以,Git非常快,因為每個文件修訂版的全部內容都可以作為一個松散的對象來訪問。但是,某些操作(例如,將提交推送到遠程存儲庫,存儲太多對象或手動運行Git的垃圾收集命令)可能會導致Git將對象重新打包為打包文件,在打包過程中,采用反向差異并進行壓縮以消除多余的內容并減小尺寸。該過程將生成包含對象內容的.pack文件,每個文件都有一個對應的.idx索引文件,其中包含對打包對象及其在打包文件中位置的引用。
當將分支推送到遠程存儲庫或從遠程存儲庫拉出分支時,這些打包文件將通過網絡傳輸。提取或獲取分支時,將打包文件解壓縮以在對象存儲庫中創建松散對象。
基本命令
git init:將當前目錄初始化為Git存儲庫(創建隱藏的.git文件夾及其內容)。
git clone <git-url>:在指定的URL下載Git存儲庫的副本。
git add <filename.ext>:將未跟蹤的文件或更改的文件添加到暫存區(在對象數據庫中創建相應的條目)。
git commit -m '提交消息':提交一組更改的文件和文件夾以及描述性提交消息。
git status:顯示與工作目錄,當前分支,未跟蹤的文件,已修改的文件等狀態有關的信息。
git branch <new-branch>:基于當前檢出的分支創建一個新分支。
git checkout <branch>:將指定的分支檢出到工作目錄中。
git merge <branch>:將指定的分支合并到工作目錄中檢出的當前分支中。
git pull:更新工作c通過合并遠程存儲庫中存在的已提交更改而不是工作副本來進行操作。
git push:將本地活動分支提交的松散對象打包到打包文件中,并傳輸到遠程存儲庫。
git log:顯示活動分支的提交歷史記錄和相關的描述性消息。
git stash:將工作目錄中所有未提交的更改保存到緩存中,以便以后可以檢索。
有關Git內部的更多信息,請參閱Pro Git書籍中有關Git內部的章節
版本文件
Git對象文件示例:
Blob對象
.git/objects/a8/420ef73065a9e3e57fe8fd2d32dad28e332bd0
表示哈希值a8420ef73065a9e3e57fe8fd2d32dad28e332bd0
的Blob對象
可以使用:
git cat-file -t a8420ef73065a9e3e57fe8fd2d32dad28e332bd0
查看器對象類型為blob
使用git cat-file -p a8420ef73065a9e3e57fe8fd2d32dad28e332bd0
查看器內容為hello,Chongchong!
Tree對象
.git/objects/ba/0ea2e2c2f9fc822ca16046f8d3f1f24660014c
表示哈希值為ba0ea2e2c2f9fc822ca16046f8d3f1f24660014c
的tree對象,其類型為tree,值為:
040000 tree 887ad439b842a19be9a1922253872427763b0376 hello
Commit對象
.git/objects/da/7a3c9c71eb9da8022018fb5ce02a4625b753d5
表示哈希值為da7a3c9c71eb9da8022018fb5ce02a4625b753d5
的commit對象,其類型為commit,值為
該次commit關聯到da7a3c9c71eb9da8022018fb5ce02a4625b753d5
的tree對象,commit消息為init
。
Mercurial 即將死去的另外一個選擇
Mercurial由Matt Mackall于2005年創建,采用 Python編寫。它也是從托管Linux代碼庫的目標開始的,是目前次于Git的第二受歡迎的分布式VCS,但使用頻率不多。隨著最近BitBucket宣布將停止對Mercurial的支持,表示著Mercurial即將死去,除了Git后沒有了另外的選擇。
技術實現
和Git一樣,Mercurial是一個分布式版本控制系統,它允許任何數量的開發人員獨立于其他人使用他們自己的項目副本。 Mercurial利用了許多與Git相同的技術,例如壓縮和SHA-1哈希,但是采用了不同的方式。
當提交新文件以在Mercurial中進行跟蹤時,將在隱藏目錄.hg/store/data/中為其創建相應的revlog文件。可以將revlog(或修訂日志)文件看作是較舊的VCS(如CVS,RCS和SCCS)使用的歷史記錄文件的現代化版本。與Git為每個暫存文件的每個版本創建一個新的Blob不同,Mercurial只是在該文件的revlog中創建一個新條目。為了節省空間,每個新條目僅包含先前版本的增量(更改)。一旦達到閾值數量的增量,將再次存儲文件的完整快照。在應用許多增量來重建特定文件修訂版時,這減少了查找時間。
這些文件修訂日志的名稱與它們跟蹤的文件匹配,但是后綴為.i和.d擴展名。.d文件包含壓縮的增量內容。.i文件用作索引,以快速跟蹤.d文件中的不同修訂版本。
Mercurial使用了另一種類型的修訂日志,稱為變更日志。更改日志包含條目列表,這些條目將每個提交與以下信息相關聯:
Manifest nodeid:標識特定時間存在的完整文件修訂集;
父提交節點ID:這使Mercurial可以建立時間表或項目歷史記錄的分支。根據提交的類型(正常vs合并),存儲一個或兩個父ID;
提交人;
提交日期;
提交信息;
每個變更日志條目還會生成一個稱為其節點ID的哈希。
基本命令
hg init:將當前目錄初始化為Mercurial存儲庫(創建隱藏的.hg文件夾及其內容)。
hg clone <hg-url>:在指定的URL下載Mercurial存儲庫的副本。
hg add <filename.ext>:添加新文件以進行修訂跟蹤。
hg commit -m '提交消息':提交一組更改的文件和文件夾以及描述性提交消息。
hg status:顯示與工作目錄,未跟蹤文件,已修改文件等狀態有關的信息。
hg update <revision>:將指定的分支檢出到工作目錄中。
hg merge <branch>:將指定的分支合并到工作目錄中檢出的當前分支中。
hg pull:從遠程存儲庫下載新修訂,但不要將其合并到工作目錄中。
hg push:將新修訂版本傳輸到遠程存儲庫。
hg log:顯示活動分支的提交歷史記錄和相關的描述性消息。
版本文件
Mercurial 版本文件示例:
Manifest revlog
hey.txt208b6e0998e8099b16ad0e43f036ec745d58ec04
hi.txt74568dc1a5b9047c8041edd99dd6f566e78d3a42hi.txt74568dc1a5b9047c8041edd99dd6f566e78d3a42
revlog
總結
本文中,我們回顧了VCS版本控制的發展歷史,介紹了VCS版本控制系統技術演進過程,對歷史上曾經出現的主要VCS軟件進行了介紹和比較、還有各自的技術實現以及常見操作命令。