背景
Linux 作為最大也是最成功的開(kāi)源項(xiàng)目,吸引了全球程序員的貢獻(xiàn),到目前為止,共有兩萬(wàn)多名開(kāi)發(fā)者給 Linux Kernel 提交過(guò)代碼。令人驚訝的是,在項(xiàng)目的前十年(1991 ~ 2002)中,Linus 作為項(xiàng)目管理員并沒(méi)有借助任何配置管理工具,而是以手工方式通過(guò) patch 來(lái)合并大家提交的代碼。倒不是說(shuō) Linus 喜歡手工處理,而是因?yàn)樗麑?duì)于軟件配置管理工具(SCM)非常挑剔,無(wú)論是商用的 clearcase 還是開(kāi)源的 cvs、svn 等都不能入他的法眼。在他看來(lái),一個(gè)能夠滿足 Linux 內(nèi)核項(xiàng)目開(kāi)發(fā)使用的版本控制系統(tǒng)需要滿足幾個(gè)條件:1) 快 2)支持多分支場(chǎng)景(幾千個(gè)分支并行開(kāi)發(fā)場(chǎng)景) 3) 分布式 4) 能夠支持大型項(xiàng)目。直到2002年,Linus 終于找到了一款基本滿足他要求的工具——BitKeeper, 而 BitKeeper 是商業(yè)工具,他們?cè)敢饨o Linux 社區(qū)免費(fèi)使用,但是需要保證遵守不得進(jìn)行反編譯等條款。BitKeeper 提供的默認(rèn)接口顯然不能滿足社區(qū)用戶(hù)的全部需要,一位社區(qū)開(kāi)發(fā)者反編譯 BitKeeper 并利用了未公開(kāi)接口,這讓 BitKeeper 公司撤回了免費(fèi)使用的 License。不得已,Linus 利用假期十天時(shí)間,實(shí)現(xiàn)一款 DVCS —— Git,并推送給社區(qū)開(kāi)發(fā)者們使用。
設(shè)計(jì)
Git 已經(jīng)成為全球軟件開(kāi)發(fā)者的標(biāo)配,關(guān)于 Git 的介紹和用法不需多說(shuō),我今天想要談?wù)?Git 的內(nèi)部實(shí)現(xiàn)。不過(guò)在看本文之前,我先給大家提一個(gè)問(wèn)題:如果是你來(lái)設(shè)計(jì) git(或者重新設(shè)計(jì) git),你打算怎么設(shè)計(jì)?第一個(gè)版本發(fā)布準(zhǔn)備實(shí)現(xiàn)哪些功能?看完本文,再對(duì)照自己的想法做個(gè)比較。歡迎留言討論。
學(xué)習(xí) Git 的內(nèi)部實(shí)現(xiàn),最好的辦法是看 Linus 最初的代碼提交,checkout 出 git 項(xiàng)目的第一次提交節(jié)點(diǎn),可以看到代碼庫(kù)中只有幾個(gè)文件:一個(gè) README,一個(gè)構(gòu)建腳本Makefile,剩下幾個(gè) C 源文件。這次 commit 的備注寫(xiě)的也非常特別:Initial revision of "git", the information manager from hell.
commite83c5163316f89bfbde7d9ab23ca2e25604af290 Author:LinusTorvalds
在 README 中,Linus 詳細(xì)描述了 Git 的設(shè)計(jì)思路。看似復(fù)雜的 Git 工作,在 Linus 的設(shè)計(jì)里,只有兩種對(duì)象抽象:1) 對(duì)象數(shù)據(jù)庫(kù)("object database");2) 當(dāng)前目錄緩存("current directory cache")。
Git 的本質(zhì)就是一系列的文件對(duì)象集合,代碼文件是對(duì)象、文件目錄樹(shù)是對(duì)象、commit 也是對(duì)象。這些文件對(duì)象的名稱(chēng)即內(nèi)容的 SHA1 值,SHA1 哈希算法的值為40位。Linus 將前二位作為文件夾、后38位作為文件名。大家可以在 .git 目錄里的 objects 里看到有很多兩位字母/數(shù)字名稱(chēng)的目錄,里面存儲(chǔ)了很多38位hash值名稱(chēng)的文件,這就是 Git 的所有信息。Linus 在設(shè)計(jì)對(duì)象的數(shù)據(jù)結(jié)構(gòu)時(shí)按照 <標(biāo)簽ascii碼表示>(blob/tree/commit) + <空格> + <長(zhǎng)度ascii碼表示> + <> + <二進(jìn)制數(shù)據(jù)內(nèi)容> 來(lái)定義,大家可以用 xxd 命令看下 objects 目錄里的對(duì)象文件(需 zlib 解壓),比如一個(gè) tree 對(duì)象文件內(nèi)容如下:
00000000: 7472 6565 2033 3700 3130 3036 3434 2068 tree 37.100644 h 00000010: 656c 6c6f 2e74 7874 0027 0c61 1ee7 2c56 ello.txt.'.a..,V 00000020: 7bc1 b2ab ec4c bc34 5bab 9f15 ba {....L.4[....
對(duì)象有三種:BLOB、TREE、CHANGESET。
BLOB: 即二進(jìn)制對(duì)象,這就是 Git 存儲(chǔ)的文件,Git 不像某些 VCS (如 SVN)那樣存儲(chǔ)變更 delta 信息,而是存儲(chǔ)文件在每一個(gè)版本的完全信息。比如先提交了一份 hello.c 進(jìn)入了 Git 庫(kù),會(huì)生成一個(gè) BLOB 文件完整記錄 hello.c 的內(nèi)容;對(duì) hello.c 修改后,再提交 commit,會(huì)再生成一個(gè)新的 BLOB 文件記錄修改后的 hello.c 全部?jī)?nèi)容。Linus 在設(shè)計(jì)時(shí),BLOB 中僅記錄文件的內(nèi)容,而不包含文件名、文件屬性等元數(shù)據(jù)信息,這些信息被記錄在第二種對(duì)象 TREE 里。
TREE: 目錄樹(shù)對(duì)象。在 Linus 的設(shè)計(jì)里 TREE 對(duì)象就是一個(gè)時(shí)間切片中的目錄樹(shù)信息抽象,包含了文件名、文件屬性及BLOB對(duì)象的SHA1值信息,但沒(méi)有歷史信息。這樣的設(shè)計(jì)好處是可以快速比較兩個(gè)歷史記錄的 TREE 對(duì)象,不能讀取內(nèi)容,而根據(jù) SHA1 值顯示一致和差異的文件。另外,由于 TREE 上記錄文件名及屬性信息,對(duì)于修改文件屬性或修改文件名、移動(dòng)目錄而不修改文件內(nèi)容的情況,可以復(fù)用 BLOB 對(duì)象,節(jié)省存儲(chǔ)資源。而 Git 在后來(lái)的開(kāi)發(fā)演進(jìn)中又優(yōu)化了 TREE 的設(shè)計(jì),變成了某一時(shí)間點(diǎn)文件夾信息的抽象,TREE 包含其子目錄的 TREE 的對(duì)象信息(SHA1)。這樣,對(duì)于目錄結(jié)構(gòu)很復(fù)雜或?qū)蛹?jí)較深的 Git庫(kù) 可以節(jié)約存儲(chǔ)資源。歷史信息被記錄在第三種對(duì)象 CHANGESET 里。

CHANGESET: 即 Commit 對(duì)象。一個(gè) CHANGESET 對(duì)象中記錄了該次提交的 TREE 對(duì)象信息(SHA1),以及提交者(committer)、提交備注(commit message)等信息。跟其他SCM(軟件配置管理)工具所不同的是,Git 的 CHANGESET 對(duì)象不記錄文件重命名和屬性修改操作,也不會(huì)記錄文件修改的 Delta 信息等,CHANGESET 中會(huì)記錄父節(jié)點(diǎn) CHANGESET 對(duì)象的 SHA1 值,通過(guò)比較本節(jié)點(diǎn)和父節(jié)點(diǎn)的 TREE 信息來(lái)獲取差異。Linus 在設(shè)計(jì) CHANGESET 父節(jié)點(diǎn)時(shí)允許一個(gè)節(jié)點(diǎn)最多有 16 個(gè)父節(jié)點(diǎn),雖然超過(guò)兩個(gè)父節(jié)點(diǎn)的合并是很奇怪的事情,但實(shí)際上,Git 是支持超過(guò)兩個(gè)分支的多頭合并的。
Linus 在三種對(duì)象的設(shè)計(jì)解釋后著重闡述了可信(TRUST):雖然 Git 在設(shè)計(jì)上沒(méi)有涉及可信的范疇,但 Git 作為配置管理工具是可以做到可信的。原因是所有的對(duì)象都以SHA1編碼(Google 實(shí)現(xiàn) SHA1 碰撞攻擊是后話,且 Git 社區(qū)也準(zhǔn)備使用更高可靠性的 SHA256 編碼來(lái)代替),而簽入對(duì)象的過(guò)程可信靠簽名工具保證,如 GPG 工具等。
理解了Git 的三種基本對(duì)象,那么對(duì)于 Linus 對(duì)于 Git 初始設(shè)計(jì)的“對(duì)象數(shù)據(jù)庫(kù)”和“當(dāng)前目錄緩存”這兩層抽象就很好理解了。加上原本的工作目錄,Git 有三層抽象,如下圖示:一個(gè)是當(dāng)前工作區(qū)(Working Directory),也就是我們查看/編寫(xiě)代碼的地方,一個(gè)是 Git 倉(cāng)庫(kù)(Repository),即 Linus 說(shuō)的對(duì)象數(shù)據(jù)庫(kù),我們?cè)?Git 倉(cāng)看到的 .git 文件夾中存儲(chǔ)的內(nèi)容,Linus 在第一版設(shè)計(jì)時(shí)命名為 .dircache,在這兩個(gè)存儲(chǔ)抽象中還有一層中間的緩存區(qū)(Staging Area),即 .git/index 里存儲(chǔ)的信息,我們?cè)趫?zhí)行 git add 命令時(shí),便是將當(dāng)前修改加入到了緩存區(qū)。
Linus 解釋了“當(dāng)前目錄緩存”的設(shè)計(jì),該緩存就是一個(gè)二進(jìn)制文件,內(nèi)容結(jié)構(gòu)很像 TREE 對(duì)象,與 TREE 對(duì)象不同的是 index 不會(huì)再包含嵌套 index 對(duì)象,即當(dāng)前修改目錄樹(shù)內(nèi)容都在一個(gè) index 文件里。這樣設(shè)計(jì)有兩個(gè)好處:1. 能夠快速的復(fù)原緩存的完整內(nèi)容,即使不小心把當(dāng)前工作區(qū)的文件刪除了,也可以從緩存中恢復(fù)所有文件;2. 能夠快速找出緩存中和當(dāng)前工作區(qū)內(nèi)容不一致的文件。

實(shí)現(xiàn)
Linus 在 Git 的第一次代碼提交里便完成了 Git 的最基礎(chǔ)功能,并可以編譯使用。代碼極為簡(jiǎn)潔,加上 Makefile 一共只有 848 行。感興趣的同事可以通過(guò)上一段所述方法 checkout Git 最早的 commit 上手編譯玩玩,只要有 Linux 環(huán)境即可。因?yàn)橐蕾?lài)庫(kù)版本的問(wèn)題,需要對(duì)原始 Makefile 腳本做些小修改。Git 第一個(gè)版本依賴(lài) openssl 和 zlib 兩個(gè)庫(kù),需要手工安裝這兩個(gè)開(kāi)發(fā)庫(kù)。在 ubuntu 上執(zhí)行:sudo apt install libssl-dev libz-dev ;然后修改 makefile 在 LIBS= -lssl 行 中的 -lssl 改成 -lcrypto 并增加 -lz ;最后執(zhí)行 make,忽略編譯告警,會(huì)發(fā)現(xiàn)編出了7個(gè)可執(zhí)行程序文件:init-db, update-cache, write-tree, commit-tree, cat-file, show-diff 和 read-tree.
下面分別簡(jiǎn)要介紹下這些可執(zhí)行程序的實(shí)現(xiàn):
init-db: 初始化一個(gè) git 本地倉(cāng)庫(kù),這也就是我們現(xiàn)在每次初始化建立 git 庫(kù)式敲擊的 git init 命令。只不過(guò)一開(kāi)始 Linus 建立的 倉(cāng)庫(kù)及 cache 文件夾名稱(chēng)叫 .dircache, 而不是我們現(xiàn)在所熟知的 .git 文件夾。
update-cache: 輸入文件路徑,將該文件(或多個(gè)文件)加入緩沖區(qū)中。具體實(shí)現(xiàn)是:校驗(yàn)路徑合法性,然后將文件計(jì)算 SHA1值,將文件內(nèi)容加上 blob 頭信息進(jìn)行 zlib 壓縮后寫(xiě)入到對(duì)象數(shù)據(jù)庫(kù)(.dircache/objects)中;最后將文件路徑、文件屬性及 blob sha1 值更新到 .dircache/index 緩存文件中。
write-tree: 將緩存的目錄樹(shù)信息生成 TREE 對(duì)象,并寫(xiě)入對(duì)象數(shù)據(jù)庫(kù)中。TREE 對(duì)象的數(shù)據(jù)結(jié)構(gòu)為:'tree ' + 長(zhǎng)度 + + 文件樹(shù)列表。文件樹(shù)列表中按照 文件屬性 + 文件名 + + SHA1 值結(jié)構(gòu)存儲(chǔ)。寫(xiě)入對(duì)象成功后,返回該 TREE 對(duì)象的 SHA1 值。
commit-tree: 將 TREE 對(duì)象信息生成 commit 節(jié)點(diǎn)對(duì)象并提交到版本歷史中。具體實(shí)現(xiàn)是輸入要提交的 TREE 對(duì)象 SHA1 值,并選擇輸入父 commit 節(jié)點(diǎn)(最多 16個(gè)),commit 對(duì)象信息中包含 TREE、父節(jié)點(diǎn)、committer 及作者的 name、email及日期信息,最后寫(xiě)入新的 commit 節(jié)點(diǎn)對(duì)象文件,并返回 commit 節(jié)點(diǎn)的 SHA1 值。
cat-file: 由于所有的對(duì)象文件都經(jīng)過(guò) zlib 壓縮,因此想要查看文件內(nèi)容的話需要使用這個(gè)工具來(lái)解壓生成臨時(shí)文件,以便查看對(duì)象文件的內(nèi)容。
show-diff: 快速比較當(dāng)前緩存與當(dāng)前工作區(qū)的差異,因?yàn)槲募膶傩孕畔ⅲòㄐ薷臅r(shí)間、長(zhǎng)度等)也保存在緩存的數(shù)據(jù)結(jié)構(gòu)中,因此可以快速比較文件是否有修改,并展示差異部分。
read-tree: 根據(jù)輸入的 TREE 對(duì)象 SHA1 值輸出打印 TREE 的內(nèi)容信息。
這就是第一個(gè)可用版本的 Git 的全部七個(gè)子程序,可能用過(guò) Git 的同事會(huì)說(shuō):這怎么跟我常用的 Git 命令不一樣呢?Git add, git commit 呢?是的,在最初的 Git 設(shè)計(jì)中是沒(méi)有我們這些平常所使用的 git 命令的。在 Git 的設(shè)計(jì)中,有兩種命令:分別是底層命令(Plumbing commands)和高層命令(Porcelain commands)。一開(kāi)始,Linus 就設(shè)計(jì)了這些給開(kāi)源社區(qū)黑客使用的符合 Unix KISS 原則的命令,因?yàn)楹诳蛡儽旧砭褪莿?dòng)手高手,水管壞了就擼起袖子去修理,因此這些命令被稱(chēng)為 plumbing commands. 后來(lái)接手 Git 的 Junio Hamano 覺(jué)得這些命令對(duì)于普通的用戶(hù)可不太友好,因此在此之上,封裝了更易于使用、接口更精美的高層命令,也就是我們今天每天使用的 git add, git commit 之類(lèi)。Git add 就是封裝了 update-cache 命令,而 git commit 就是封裝了 write-tree, commit-tree 命令。關(guān)于底層命令的更詳細(xì)介紹,大家有興趣的話可以看 Pro Git 中的 Git Internals 章節(jié)。
具體的代碼實(shí)現(xiàn)在這里就不再細(xì)述,Linus 的代碼風(fēng)格極為簡(jiǎn)潔,能一行完成的絕不寫(xiě)兩行。另外,對(duì)于 Linux API 的使用自然無(wú)人出其右,我印象最深的是有好多處使用 mmap 建立文件與內(nèi)存的映射,省去了內(nèi)存申請(qǐng)、文件讀寫(xiě)等操作,提升了工具性能。正如一位同事說(shuō)的:Linus 的代碼除了不滿足編程規(guī)范,其他好像真挑不出什么毛病。順便說(shuō)一句,Linus 的縮進(jìn)風(fēng)格是 Tab 鍵(典故參見(jiàn)《制表符還是空格符,這是個(gè)問(wèn)題》)。
啟示
Linus 在提交了第一個(gè) git commit 后,并向社區(qū)發(fā)布了 git 工具。當(dāng)時(shí),社區(qū)中有位叫 Junio Hamano 的開(kāi)發(fā)者覺(jué)得這個(gè)工具很有意思,便下載了代碼,結(jié)果發(fā)現(xiàn)一共才 1244 行代碼,這更令他驚奇也引發(fā)了極大的興趣。Junio 在郵件列表與 Linus 交流并幫助增加了 merge 等功能,而后持續(xù)打磨 git,最后 Junio 完全接手了 Git 的維護(hù)工作,Linus 則回去繼續(xù)維護(hù) Linux Kernel 項(xiàng)目。
如果選歷史上最偉大的一次 Git 代碼提交,那一定是這 Git 工具項(xiàng)目本身的第一次代碼提交。這次代碼提交無(wú)疑是開(kāi)創(chuàng)性的,如果說(shuō) Linux 項(xiàng)目促成了開(kāi)源軟件的成功并改寫(xiě)了軟件行業(yè)的格局,那么 Git 則是改變了全世界開(kāi)發(fā)者的工作方式和寫(xiě)作方式。在 Git 誕生后兩年,舊金山的一個(gè)小酒館里坐著三位年輕的程序員,決定要用 Git 做點(diǎn)什么,幾個(gè)月后,GitHub 上線。
回到文中開(kāi)頭提到的問(wèn)題,如果我來(lái)設(shè)計(jì) Git 的話,估計(jì)還是會(huì)從已有工具經(jīng)驗(yàn)(如SVN使用)上來(lái)延伸設(shè)計(jì),甚至在我最早接觸 Git 時(shí)候曾膚淺的認(rèn)為 Git 就是 SVN + 分布式。正是了解了 Git 的內(nèi)部原理乃至閱讀了 Git 的初始代碼后才感嘆其設(shè)計(jì)的精妙,Git 的初始設(shè)計(jì)和實(shí)現(xiàn)大概能給(開(kāi)源)軟件產(chǎn)品如下啟發(fā):
解決痛點(diǎn)問(wèn)題:Git 的緣起便是 Linus 本人及 Linux 社區(qū)的訴求,而這些訴求推而廣之是項(xiàng)目協(xié)作開(kāi)發(fā)(特別是跨地域項(xiàng)目)的共性訴求。Linus 解決了他本人遇到的痛點(diǎn)問(wèn)題,順便達(dá)成了一項(xiàng)偉大的成就。
極簡(jiǎn)設(shè)計(jì):Linus 在設(shè)計(jì) Git 工具時(shí)并沒(méi)有受傳統(tǒng) SCM 工具的束縛,考慮文件差異、版本對(duì)比等,而是抽象了幾種基本對(duì)象就把 git 的設(shè)計(jì)思路給理清楚了。
MVP (minimum viable product, 最小可用產(chǎn)品):這個(gè)概念大家都懂,但實(shí)際操作起來(lái)卻不容易。一個(gè) MVP 的配置管理工具需要哪些功能?一般來(lái)說(shuō)會(huì)想到代碼提交、歷史追溯、版本比較、分支合并等。但 Linus 卻將它拆解開(kāi)來(lái),快速實(shí)現(xiàn)了底層的基本功能,簡(jiǎn)單到只有開(kāi)源社區(qū)黑客才能用。但這就夠了,黑客們因此發(fā)現(xiàn)了它的價(jià)值,繼續(xù)給它添磚加瓦。
快速發(fā)布,快速迭代:這也是源于 Linux Kernel 的開(kāi)發(fā)經(jīng)驗(yàn);Linus 在實(shí)現(xiàn)了 Git MVP 后,便在 Linux 社區(qū)郵件列表中公布,并征求意見(jiàn),迭代完善。
找到合適接班人:《大教堂與集市》中也有類(lèi)似的觀點(diǎn),它說(shuō)的是:“如果你對(duì)一個(gè)項(xiàng)目失去了興趣,你最后的職責(zé)就是把它交給一個(gè)稱(chēng)職的繼承者。”不過(guò) Linus 將 Git 交給 Junio 并不是因?yàn)槭チ伺d趣,而是因?yàn)樗l(fā)現(xiàn)在 Git 基礎(chǔ)架構(gòu)建立好之后,Junio 比他更擅長(zhǎng)于實(shí)現(xiàn)更豐富、對(duì)普通用戶(hù)界面更友好的功能,因此他就放心的將 Git 交給了 Junio. 為開(kāi)源項(xiàng)目找到更合適的接班人,這既需要魄力也需要智慧。
原文標(biāo)題:改變世界的一次代碼提交
文章出處:【微信公眾號(hào):Linuxer】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
責(zé)任編輯:haq
-
Linux
+關(guān)注
關(guān)注
88文章
11775瀏覽量
219156 -
代碼
+關(guān)注
關(guān)注
30文章
4971瀏覽量
74062
原文標(biāo)題:改變世界的一次代碼提交
文章出處:【微信號(hào):LinuxDev,微信公眾號(hào):Linux閱碼場(chǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
發(fā)布元服務(wù)提交審核
網(wǎng)絡(luò)接口:數(shù)字世界的“門(mén)鈴”,你了解多少?
禾賽科技入選財(cái)富雜志2025年改變世界的公司榜單
Flash driver數(shù)據(jù)會(huì)隨著代碼修改而改變,怎么解決?
低代碼開(kāi)發(fā)云平臺(tái)是什么?零編程零成本搭建
聲學(xué)世界模型將如何改變我們的生活
機(jī)器人將改變世界,我們應(yīng)該做些什么
ADS129x設(shè)備如何將ADC輸出代碼轉(zhuǎn)換為電壓
改變世界的代碼提交
評(píng)論