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