版本管理三国志 (CVS, Subversion, git)
作者:Vamei 出處:http://www.cnblogs.com/vamei 歡迎轉(zhuǎn)載,也請保留這段聲明。謝謝!
?
最近有兩則和git有關(guān)的新聞很火:
12306的搶票插件拖垮了GitHub?(GitHub基于git)
陳皓建議阿里共享平臺改用Linux+git的解決方案
git是一款版本控制軟件(VCS,Version Control System)。VCS通常用于管理開發(fā)過程中的源代碼文件。VCS是軟件開發(fā)的好幫手。當(dāng)軟件本身在發(fā)布時(shí)獲取大量關(guān)注時(shí),VCS躲在幕后默默管理和記錄軟件的開發(fā)和發(fā)布進(jìn)程。git頗有戲劇性的借春運(yùn)搶票火了一把,也讓許多人好奇什么是git,什么是VCS。我復(fù)習(xí)了一下VCS的歷史,忽然有些讀三國時(shí)的你方唱罷我登場的感覺,就想寫一個(gè)VCS版本的三國志。
現(xiàn)在最常見的VCS軟件(同時(shí)也是開源的VCS軟件)有CVS, Subversion和git。CVS曾經(jīng)雄霸一時(shí),至今還管理著大量的開發(fā)項(xiàng)目。Subversion青出于藍(lán),對CVS進(jìn)行改進(jìn),大有取而代之的勢頭。git另辟蹊徑,依仗Linux的名號,并借GitHub的推廣攻城略地。VCS領(lǐng)域激烈的爭斗正反映了軟件開發(fā)項(xiàng)目的紅火勢頭。
?
斬白蛇而起
早期(1970年到1980年代)的軟件開發(fā)大部分是愉快的個(gè)人創(chuàng)作。比如UNIX下的sed是L. E. McMahon寫的,Python的第一個(gè)編譯器是Guido寫的,Linux最初的內(nèi)核是Linus寫的 (好吧,awk是個(gè)例外,它的名字是三位作者的首字母,但也只是三個(gè)人)。這些程序員可以用手工的方式進(jìn)行備份,并以注釋或者新建文本文件來記錄變動。
正如現(xiàn)在普通用戶常做的,當(dāng)時(shí)的程序員常用cp備份:
$cp dev.c dev.bak
更有條理一些的程序員會加上一個(gè)時(shí)間標(biāo)記,比如:
$cp dev.c dev.bak.19890908
程序員很可能會用vi創(chuàng)建一個(gè)LOG文件來做日志:
1989-09-08 02:00:00 Old input method is stupid Add command-line input function在一個(gè)版本發(fā)布的時(shí)候,程序員可能做一個(gè)tar歸檔,將所有的文件歸為同一個(gè).tar文件。
$tar -cf project_v1.0.tar project
上面的工具構(gòu)成了一套人工VCS。上面的這套組合也非常符合UNIX的模塊化理念:讓每個(gè)應(yīng)用專注于一個(gè)小的功能,使用者根據(jù)需要,將這些功能連接起來。你還可以寫一個(gè)shell腳本,將上面的功能都寫在里面。當(dāng)需要的時(shí)候,調(diào)用該腳本就可以了。
(這樣一個(gè)shell腳本并不復(fù)雜,而且挺有用的,可以作為學(xué)習(xí)shell編程的小練習(xí))
?
再說一下早期的合作開發(fā)模式。如在Python簡史中看到的,Guido通過電子郵件接收補(bǔ)丁(patch),并將補(bǔ)丁應(yīng)用到原來的代碼文件。實(shí)際上,一個(gè)補(bǔ)丁(patch)的主要功能是描述兩個(gè)文件的改變(change, or file delta)。 假設(shè)我們有兩個(gè)文件a.c和b.c內(nèi)容分別為:
a.c (有bug的代碼)
b.c (修正后的代碼)
int sum(int a, int b) {int c;c = a + b;return c; }?
在UNIX系統(tǒng)下,運(yùn)行
$diff a b > iss01.patch
iss01.patch就是一個(gè)補(bǔ)丁文件,它看起來如下:
4c4 < c = a + 1; --- > c = a + b;這個(gè)補(bǔ)丁表示,更改原文件第四行的c = a + 1;,改為c = a + b;,更改后的這一行位于新的文件的第四行。
?
使用patch命令將iss01.patch應(yīng)用到a.c上,相當(dāng)于將 b.c-a.c 的改變作用在a上,a.c將和b.c有一樣的內(nèi)容:
$patch a.c < iss01.patch
當(dāng)我發(fā)現(xiàn)a.c的代碼有錯誤時(shí),可以將我修改后的b.c與原來的a.c做diff獲得補(bǔ)丁文件,并將補(bǔ)丁發(fā)給Guido,并告訴他該補(bǔ)丁是為了修正a.c代碼中的加法錯誤。Guido確認(rèn)之后,就可以使用patch應(yīng)用該補(bǔ)丁了。在后面我們將看到,這種diff-patch的工作方式被VCS不同程度的采用。
?
東漢末年
早在70年代末80年代初,VCS的概念已經(jīng)存在,比如UNIX平臺的RCS (Revision Control System)。RCS是由Walter F. Tichy使用C開發(fā)。RCS對文件進(jìn)行集中式管理,主要目的是避免多人合作情況下可能出現(xiàn)的沖突。如果多用戶同時(shí)寫入同一個(gè)文件,其寫入結(jié)果可能相互混合和覆蓋,從而造成結(jié)果的混亂。你可以將文件交給RCS管理。RCS允許多個(gè)用戶同時(shí)讀取文件,但只允許一個(gè)用戶鎖定(locking)并寫入文件 (類似于多線程的mutex)。這樣,當(dāng)一個(gè)程序員登出(check-out,見RCS的co命令)某個(gè)文件并對文件進(jìn)行修改的時(shí)候。只有在這個(gè)程序完成修改,并登入(check-in,見RCS的ci命令)文件時(shí),其他程序員才能登出文件。基本上RCS用戶所需要的,就是co和ci兩個(gè)命令。在co和ci之間,用戶可以對原文件進(jìn)行許多改變(change, or file delta)。一旦重新登入文件,這些改變將保存到RCS系統(tǒng)中。通過check-in將改變永久化的過程叫做提交(commit)。
RCS互斥寫入
RCS的互斥寫入機(jī)制避免了多人同時(shí)修改同一個(gè)文件的可能,但代價(jià)是程序員長時(shí)間的等待,給團(tuán)隊(duì)合作帶來不便。如果某個(gè)程序員登出了某個(gè)文件,而忘記登入,那他就要面對隊(duì)友的怒火了。(從這個(gè)角度上來說,RCS造成的問題甚至大于它所解決的問題……)
文件每次commit都會創(chuàng)造一個(gè)新的版本(revision)。RCS給每個(gè)文件創(chuàng)建了一個(gè)追蹤文檔來記錄版本的歷史。這個(gè)文檔的名字通常是原文件名加后綴,v?(比如main.c的追蹤文檔為main.c,v)。追蹤文檔中包括:最新版本的文件內(nèi)容,每次check-in的發(fā)生時(shí)間和用戶,每次check-in發(fā)生的改變。在最新文檔內(nèi)容的基礎(chǔ)上,減去歷史上發(fā)生的改變,就可以恢復(fù)到之前的歷史版本。這樣,RCS就實(shí)現(xiàn)了備份歷史和記錄改變的功能。
?
RCS歷史版本追蹤
?
相對與后來的版本管理軟件,RCS純粹線性的開發(fā)方式非常不利于團(tuán)隊(duì)合作。但RCS為多用戶寫入沖突提供了一種有效的解決方案。RCS的版本管理功能逐漸被其他軟件(比如CVS)取代,但時(shí)至今日,它依然是常用的系統(tǒng)管理工具。RCS就像是東漢王室,飄搖多年而不倒。
?
挾天子,令諸侯
1986年,Dick Grune寫了一系列的shell腳本用于版本管理,并最終以這些腳本為基礎(chǔ),構(gòu)成了CVS (Concurrent Versions System)。CVS后來用C語言重寫。CVS是開源軟件。在當(dāng)時(shí),Stallman剛剛舉起GNU的大旗,掀起開源允許的序幕。CVS被包含在GNU的軟件包中,并因此得到廣泛的推廣,最終擊敗諸多商業(yè)版本的VCS,呈一統(tǒng)天下之勢。
CVS繼承了RCS的集中管理的理念。在CVS管理下的文件構(gòu)成一個(gè)庫(repository)。與RCS的鎖定文件模式不同,CVS采用復(fù)制-修改-合并(copy-modify-merge)的模式,來實(shí)現(xiàn)多線開發(fā)。CVS引進(jìn)了分支(branch)的概念。多個(gè)用戶可以從主干(也就是中心庫)創(chuàng)建分支。分支是主干文件在本地復(fù)制的副本。用戶對本地副本進(jìn)行修改。用戶可以在分支提交(commit)多次修改。用戶在分支的工作結(jié)束之后,需要將分支合并到主干中,以便讓其他人看到自己的改動。所謂的合并,就是CVS將分支上發(fā)生的變化應(yīng)用到主干的原文件上。比如下面的過程中,我們從r1.1分支出rb1.1.2.*,并最終合并回主干,構(gòu)成r1.2
?copy-modify-merge
?
CVS與RCS類似,使用,v文件記錄改變,以便追蹤歷史。在合并的過程中,CVS將兩個(gè)change應(yīng)用于r1.1,就得到了r1.2:
r1.2 = r1.1 + change(rb1.1.2.2 - rb1.1.2.1) + change(rb1.1.2.1-r1.1)
上面的兩個(gè)改變都記錄在,v文件中,所以很容易提取。
?
在多用戶情況下,可以創(chuàng)建多個(gè)分支進(jìn)行開發(fā),比如:
在這樣的多分支合并的情況下,有可能出現(xiàn)沖突(colliding)。比如上圖中,第一次合并和第二次合并都對r1.1文件的同一行進(jìn)行了修改,那么r1.3將不知道如何去修改這一行 (第二次合并比圖示的要更復(fù)雜一些,分支需要先將主干拉到本地,合并過之后傳回主干,但這一細(xì)節(jié)并不影響我們這里的討論)。CVS要求沖突發(fā)生時(shí)的用戶手動解決沖突。用戶可以調(diào)用編輯器,對文件發(fā)生合并沖突的地方進(jìn)行修改,以決定最終版本(r1.3)的內(nèi)容。
?
CVS管理下的每個(gè)文件都有一系列獨(dú)立的版本號(比如上面的r1.1,r1.2,r1.3)。但每個(gè)項(xiàng)目中往往包含有許多文件。CVS用標(biāo)簽(tag)來記錄一個(gè)集合,這個(gè)集合中的元素是一對(文件名:版本號)。比如我們的項(xiàng)目中有三個(gè)文件(file1, file2, file3),我們創(chuàng)建一個(gè)v1.0的標(biāo)簽:
tag v1.0?(file1:r1.3) (file2:r1.1) (file3:r1.5)
v1.0的tag中包括了r1.3版本的文件file1,r1.1版本的file2……?一個(gè)項(xiàng)目在發(fā)布(release)的時(shí)候,往往要發(fā)布多個(gè)文件。標(biāo)簽可以用來記錄該次發(fā)布的時(shí)候,是哪些版本的文件被發(fā)布。
?
CVS應(yīng)用在許多重要的開源項(xiàng)目上。在90年代和00年代初,CVS在開源世界幾乎不二選擇 (RCS也是開源的,但正如我們已經(jīng)提到的,RCS無法與CVS媲美)。CVS就像是官渡之戰(zhàn)后的曹魏,挾開源運(yùn)動,號令天下。時(shí)至今天,盡管CVS已經(jīng)長達(dá)數(shù)年沒有發(fā)布新版本,我們依然可以在許多項(xiàng)目中看到CVS的身影。
?
青出于藍(lán)
正如曹操的統(tǒng)治富有爭議一樣(比如非漢祚,以臣欺君等等),CVS也有許多常常被人詬病的地方,比如下面幾條:
- 合并不是原子操作(atomic operation):如果有兩個(gè)用戶同時(shí)合并,那么合并結(jié)果將是某種錯亂的混合體。如果合并的過程中取消合并,不能撤銷已經(jīng)應(yīng)用的改變。
- 文件的附加信息沒有被追蹤:一旦納入CVS的管理,文件的附加信息(比如上次讀取時(shí)間)就被固定了。CVS不追蹤它所管理文件的附加信息的變化。
- 主要用于管理ASCII文件:不能方便的管理Binary文件和Unicode文件
- 分支與合并需要耗費(fèi)大量的時(shí)間:CVS的分支和合并非常昂貴。分支需要復(fù)制,合并需要計(jì)算所有的改變并應(yīng)用到主干。因此,CVS鼓勵盡早合并分支。
CVS還有其它一些富有爭議的地方。隨著時(shí)間,人們對CVS的一些問題越來越感到不滿 (而且程序員喜歡新鮮的東西),Subversion應(yīng)運(yùn)而生。Subversion的開發(fā)者Karl Fogel和Jim Blandy是長期的CVS用戶。贊助開發(fā)的CollabNet, Inc.希望他們寫一個(gè)CVS的替代VCS。這個(gè)VCS應(yīng)該有類似于CVS的工作方式,但對CVS的缺陷進(jìn)行改進(jìn),并提供一些CVS缺失的功能。這就好像劉備從曹營拉出來單干的劉備一樣。
總體上說,Subversion在許多方面沿襲CVS,也是集中管理庫,通過記錄改變來追蹤歷史,允許分支和合并,但并不鼓勵過多分支。Subversion在一些方面得到改善。Subversion的合并是原子操作。它可以追蹤文件的附加信息,并能夠同樣的管理Binary和Unicode文件。但CVS和Subversion又有許多不同:
- 與CVS的,v文件存儲模式不同,Subversion采用關(guān)系型數(shù)據(jù)庫來存儲改變集。VCS相關(guān)數(shù)據(jù)變得不透明。
- CVS中的版本是針對某個(gè)文件的,CVS中每次commit生成一個(gè)文件的新版本。Subversion中的版本是針對整個(gè)文件系統(tǒng)的(包含多個(gè)文件以及文件組織方式),每次commit生成一個(gè)整個(gè)項(xiàng)目文件系統(tǒng)樹的新版本。
Subversion依賴類似于硬連接(hard link)的方式來提高效率,避免過多的復(fù)制文件本身。Subversion不會從庫下載整個(gè)主干到本地,而只是下載主干的最新版本。
?
在Subversion剛剛誕生的時(shí)候,來自CVS用戶的抱怨不斷。他們覺得在Subversion中有太多的改動,有些改動甚至是相對于CVS的倒退。比如CVS中的tag,在Subversion中被改為直接復(fù)制版本的文件系統(tǒng)樹到一個(gè)特殊的文件夾。然而,隨著時(shí)間的推移,Subversion逐漸推廣 (Subversion已經(jīng)是Apache中自帶的一個(gè)模塊了,Subversion應(yīng)用于GCC、SourceForge,新浪APP Engine等項(xiàng)目),并依然有活躍的開發(fā),而CVS則逐漸沉寂。事實(shí)上,許多UNIX的參考書的新版本中,都縮減甚至刪除了CVS的內(nèi)容。
?
別開生面
CVS和Subversion有很多不同的地方。但如果將這兩者和git比較,那么git看起來就像孫權(quán)的碧眼,有一些怪異。
git的作者是Linus Torvald。對,就是寫Linux Kernel的那個(gè)Linus Torvald。Linus在貢獻(xiàn)了最初的Linux Kernel源代碼之后,一直領(lǐng)導(dǎo)著Linux Kernel的開發(fā)。Linus Torvald本人相當(dāng)厭惡CVS(以及Subversion)。然而,操作系統(tǒng)內(nèi)核是復(fù)雜而龐大的代碼“怪獸” (2012年的Linux Kernel有1500萬行代碼,Windows的代碼不公開,估計(jì)遠(yuǎn)遠(yuǎn)超過這一數(shù)目)。Linux內(nèi)核小組最初使用.tar文件來管理內(nèi)核代碼,但這遠(yuǎn)遠(yuǎn)無法匹配Linux內(nèi)核代碼的增長速度。Linus轉(zhuǎn)而使用BitKeeper作為開發(fā)的VCS工具。BitKeeper是一款分布式的VCS工具,它可以快速的進(jìn)行分支和合并。然而由于使用證書方面的爭議(BitKeeper是閉源軟件,但給Linux內(nèi)核開發(fā)人員發(fā)放免費(fèi)的使用證書),Linus最終決定寫一款開源的分布式VCS軟件:git。
git在英文中比喻一個(gè)愚蠢或者不愉快的人(a stupid or unpleasant person)。Linus說這個(gè)比喻是在說自己:
I'm an egotistical bastard, and I name all my projects after myself. First "Linux", now "git".
(這里,Linus似乎并不是在貶低自己,見Linus和Eric S. Raymond的爭論:?The curse of the gifted)
?
對于一個(gè)開發(fā)項(xiàng)目,git會保存blob, tree, commit和tag四種對象。
- 文件被保存為blob對象。
- 文件夾被保存為tree對象。tree對象保存有指向文件或者其他tree對象指針。
上面兩個(gè)對象類似于一個(gè)UNIX的文件系統(tǒng),構(gòu)成了一個(gè)文件系統(tǒng)樹。
- 一個(gè)commit對象代表了某次提交,它保存有修改人,修改時(shí)間和附加信息,并指向一個(gè)文件樹。這一點(diǎn)與Subversion類似,即每次提交為一個(gè)文件系統(tǒng)樹。
- 一個(gè)tag對象包含有tag的名字,并指向一個(gè)commit對象。
虛線下面的對象構(gòu)成了一個(gè)文件系統(tǒng)樹。在git中,一次commit實(shí)際上就是一次對文件系統(tǒng)樹的快照(snapshot)。
?
每個(gè)對象的內(nèi)容的checksum校驗(yàn)(checksum校驗(yàn)可參閱IP頭部的checksum)都經(jīng)過SHA1算法的HASH轉(zhuǎn)換。每個(gè)對象都對應(yīng)一個(gè)40個(gè)字符的HASH值。每個(gè)對象對應(yīng)一個(gè)HASH值。兩個(gè)內(nèi)容不同的對象不會有相同的HASH值(SHA1有可能發(fā)生碰撞,但概率非常非常非常低)。這樣,git可以隨時(shí)識別各個(gè)對象。通過HASH值,我們可以知道這個(gè)對象是否發(fā)生改變。
比如一個(gè)文件LOG,它包含一下內(nèi)容:
aaa
這個(gè)文件的HASH碼為72943a16fb2c8f38f9dde202b7a70ccc19c52f34
如果我們修改這個(gè)文件,成為
aaa
bbb
這個(gè)文件的HASH碼變成dbee0265d31298531773537e6e37e4fd1ee71d62
所以,git只需看對象的HASH碼,就可以知道該對象是否發(fā)生改變。
?
在整個(gè)開發(fā)過程中,可能會有許多次提交(commit)。每次commit的時(shí)候,git并不總是復(fù)制所有的對象。git會檢驗(yàn)所有對象的HASH值。如果該對象的HASH值已經(jīng)存在,說明該對象已經(jīng)保存過,并且沒有發(fā)生改變,所以git只需要調(diào)整新建tree或者commit中的指針,讓它們指向已經(jīng)保存過的對象就可以了。git在commit的時(shí)候,只會新建HASH值發(fā)生改變的對象。如下圖所示,我們創(chuàng)建新的commit的時(shí)候,只需要新建一個(gè)commit對象,一個(gè)tree對象和一個(gè)blob對象就足夠了,而不需要新建整個(gè)文件系統(tǒng)樹。
?
可以看到,與CVS,Subversion保存改變(file delta)的方式形成對照,git保存的不是改變,而是此時(shí)的文件本身。由于不需要遵循改變路徑來計(jì)算歷史版本,所以git可以快速的查閱歷史版本。git可以直接提取兩個(gè)commit所保存的文件系統(tǒng)樹,并迅速的算出兩個(gè)commit之間的改變。
?
同樣由于上面的數(shù)據(jù)結(jié)構(gòu),git可以很方便的創(chuàng)建分支(branch)。實(shí)際上,git的一個(gè)分支是一個(gè)指向某個(gè)commit的指針。合并時(shí),git檢查兩個(gè)分支所指的兩個(gè)commit,并找到它們共同的祖先commit。git會分別計(jì)算每個(gè)commit與祖先發(fā)生的改變,然后將兩個(gè)改變合并(同樣,針對同一行的兩個(gè)改變可能發(fā)生沖突,需要手工解決沖突)。整個(gè)過程中,不需要復(fù)制和遵循路徑計(jì)算總的改變,所以效率提高很多。
比如下面的圖1中有兩個(gè)分支,一個(gè)master和一個(gè)develop。我們先沿著develop分支工作,并進(jìn)行了兩次提交(比如修正bug1),而master分支保持不變。隨后沿著master分支,進(jìn)行了兩次提交(比如增加輸入功能),develop保持不變。在最終進(jìn)行圖4中的合并時(shí),我們只需要將C4-C2和C6-C2的兩個(gè)改變合并,并作用在C2上,就可以得到合并后的C7。合并之后,兩個(gè)分支都指向C7。我們此時(shí)可以刪除不需要的分支develop。
由于git創(chuàng)建、合并和刪除分支的成本極為低廉,所以git鼓勵根據(jù)需要創(chuàng)建多個(gè)分支。實(shí)際上,如果分支位于不同的站點(diǎn)(site),屬于不同的開發(fā)者,那么就構(gòu)成了分布式的多分支開發(fā)模式。每個(gè)開發(fā)者都在本地復(fù)制有自己的庫,并可以基于本地庫創(chuàng)建多個(gè)本地分支工作。開發(fā)者可以在需要的時(shí)候,選取某個(gè)本地分支與遠(yuǎn)程分支合并。git可以方便的建立一個(gè)分布式的小型開發(fā)團(tuán)隊(duì)。比如我和朋友兩人各有一個(gè)庫,各自開發(fā),并相互拉對方的庫到本地庫合并(如果上面master,develop代表了兩個(gè)屬于不同用戶的分支,就代表了這一情況)。當(dāng)然,git也允許集中式的公共倉庫存在,或者多層的公共倉庫,每個(gè)倉庫享有不同的優(yōu)先級。git的優(yōu)勢不在于引進(jìn)了某種開發(fā)模式,而是給了你設(shè)計(jì)開發(fā)模式的自由。
正如東吳門閥合作的政治模式,git非集中式的開發(fā)模式讓git成為了后起之秀。生子當(dāng)如孫仲謀,生子當(dāng)如Git Torvald。
(需要注意的是,GitHub盡管以git為核心,但并不是Linus創(chuàng)建的。事實(shí)上,Linus不接收來自GitHub的Pull Request。Linus本人將此歸罪于GitHub糟糕的Web UI。但有些搞笑的是,正是GitHub的Web頁面讓許多新手熟悉并開始使用git。好吧,Linus大嬸是在鞭策GitHub。)
?
總結(jié)
和三國志不同,VCS的三國還沒有決出最終勝負(fù)?;蛟SSubversion會繼續(xù)在一些重要項(xiàng)目上發(fā)揮作用,或許git會最終一統(tǒng)江山,或許CVS可以有新的發(fā)展并最終逆襲;又或許,一款新的VCS將取代所有的前輩。VCS激烈的競爭對于程序員來說是好事。一款優(yōu)秀的VCS可以提高了我們管理項(xiàng)目的能力,降低我們犯錯所可能支付的代價(jià)。隨著開發(fā)項(xiàng)目越來越龐大和復(fù)雜,這一能力變得越來越不可缺少。花一點(diǎn)時(shí)間學(xué)習(xí)VCS,并習(xí)慣在工作中使用VCS,將會有意想不到的回報(bào)。
(我平時(shí)只用git,經(jīng)驗(yàn)有限,如果有錯漏,謝謝你的指正)
作者:Vamei 出處:http://www.cnblogs.com/vamei 歡迎轉(zhuǎn)載,也請保留這段聲明。謝謝!總結(jié)
以上是生活随笔為你收集整理的版本管理三国志 (CVS, Subversion, git)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 利用钥匙串,在应用里保存用户密码的方法
- 下一篇: java中hashcode()和equa