您好,登錄后才能下訂單哦!
這篇文章主要講解了“總結(jié)從基本Git指令到背后原理”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“總結(jié)從基本Git指令到背后原理”吧!
在學(xué)習(xí) git 原理之前,我們先忘掉平時(shí)用的 commit,branch,tag 這些炫酷的 git 指令,后面我們會(huì)摸清楚它們的本質(zhì)的。
要知道,git 是 Linus 在寫(xiě) Linux 的時(shí)候順便寫(xiě)出來(lái)的,用于對(duì) Linux 進(jìn)行版本管理,所以,記錄文件項(xiàng)目在不同版本的變更信息是 git 最核心的功能。
大牛們?cè)谠O(shè)計(jì)軟件的時(shí)候總是會(huì)做相應(yīng)的抽象,想要理解他們的設(shè)計(jì)思路,我們就得在他們的抽象下進(jìn)行思考。雖然說(shuō)的有點(diǎn)玄乎,但是這些抽象最終都會(huì)落實(shí)到代碼上的,所以不必?fù)?dān)心,很好理解的。
首先,我們要奠定一個(gè) ojbect 的概念,這是 git 最底層的抽象,你可以把 git 理解成一個(gè) object 數(shù)據(jù)庫(kù)。
廢話(huà)不多說(shuō),跟著指令操作,你會(huì)對(duì) git 有一個(gè)全新的認(rèn)識(shí)。首先我們?cè)谌我饽夸浵聞?chuàng)建一個(gè) git 倉(cāng)庫(kù):
我的操作環(huán)境是 win10 + git bash
$ git init git-test Initialized empty Git repository in C:/git-test/.git/
可以看到 git 為我們創(chuàng)建了一個(gè)空的 git 倉(cāng)庫(kù),里面有一個(gè).git
目錄,目錄結(jié)構(gòu)如下:
$ ls config description HEAD hooks/ info/ objects/ refs/
在.git
目錄下我們先重點(diǎn)關(guān)注 .git/objects
這個(gè)目錄,我們一開(kāi)始說(shuō) git 是一個(gè) object 數(shù)據(jù)庫(kù),這個(gè)目錄就是 git 存放 object 的地方。
進(jìn)入.git/objects
目錄后我們能看到info
和pack
兩個(gè)目錄,不過(guò)這和核心功能無(wú)關(guān),我們只需要知道現(xiàn)在.git/objects
目錄下除了兩個(gè)空目錄其他啥都沒(méi)有就行了。
到這里我們停停,先把這部分實(shí)現(xiàn)了吧,邏輯很簡(jiǎn)單,我們只需要編寫(xiě)一個(gè)入口函數(shù),解析命令行的參數(shù),在得到 init 指令后在指定目錄下創(chuàng)建相應(yīng)的目錄與文件即可。
這里是我的實(shí)現(xiàn):init
為了易讀暫時(shí)沒(méi)有對(duì)創(chuàng)建文件/目錄進(jìn)行錯(cuò)誤處理。
我給它取了個(gè)土一點(diǎn)的名字,叫 jun,呃,其實(shí)管它叫啥都可以(⊙?⊙)
。
接下來(lái)我們進(jìn)入 git 倉(cāng)庫(kù)目錄并添加一個(gè)文件:
$ echo "version1" > file.txt
然后我們把對(duì)這個(gè)文件的記錄添加進(jìn) git 系統(tǒng)。要注意的是,我們暫不使用add
指令添加,盡管我們平時(shí)很可能這么做,但這是一篇揭示原理的文章,這里我們要引入一條平時(shí)大家可能沒(méi)有聽(tīng)到過(guò)的 git 指令git hash-object
。
$ git hash-object -w file.txt 5bdcfc19f119febc749eef9a9551bc335cb965e2
指令執(zhí)行后返回了一個(gè)哈希值,實(shí)際上這條指令已經(jīng)把對(duì) file.txt 的內(nèi)容以一個(gè) object 的形式添加進(jìn) object 數(shù)據(jù)庫(kù)中了,而這個(gè)哈希值就對(duì)應(yīng)著這個(gè) object。
為了驗(yàn)證 git 把這個(gè) object 寫(xiě)入了數(shù)據(jù)庫(kù)(以文件的形式保存下來(lái)),我們查看一下.git/objects
目錄:
$ find .git/objects/ -type f #-type用于制定類(lèi)型,f表示文件 .git/objects/5b/dcfc19f119febc749eef9a9551bc335cb965e2
發(fā)現(xiàn)多了一個(gè)文件夾5b
,該文件夾下有一個(gè)名為dcfc19f119febc749eef9a9551bc335cb965e2
的文件,也就是說(shuō) git 把該 object 哈希值的前2個(gè)字符作為目錄名,后38個(gè)字符作為文件名,存放到了 object 數(shù)據(jù)庫(kù)中。
關(guān)于 git hash-object 指令的官方介紹,這條指令用于計(jì)算一個(gè) ojbect 的 ID 值。-w 是可選參數(shù),表示把 object 寫(xiě)入到 object 數(shù)據(jù)庫(kù)中;還有一個(gè)參數(shù)是 -t,用于指定 object 的類(lèi)型,如果不指定類(lèi)型,默認(rèn)是 blob 類(lèi)型。
現(xiàn)在你可能好奇 object 里面保存了什么信息,我們使用git cat-file
指令去查看一下:
$ git cat-file -p 5bdc # -p:查看 object 的內(nèi)容,我們可以只給出哈希值的前綴 version1 $ git cat-file -t 5bdc # -t:查看 object 的類(lèi)型 blob
有了上面的鋪墊之后,接下來(lái)我們就揭開(kāi) git 實(shí)現(xiàn)版本控制的秘密!
我們改變 file.txt 的內(nèi)容,并重新寫(xiě)入 object 數(shù)據(jù)庫(kù)中:
$ echo "version2" > file.txt $ git hash-object -w file.txt df7af2c382e49245443687973ceb711b2b74cb4a
控制臺(tái)返回了一個(gè)新的哈希值,我們?cè)俨榭匆幌?object 數(shù)據(jù)庫(kù):
$ find .git/objects -type f .git/objects/5b/dcfc19f119febc749eef9a9551bc335cb965e2 .git/objects/df/7af2c382e49245443687973ceb711b2b74cb4a
(?Д?)
發(fā)現(xiàn)多了一個(gè) object!我們查看一下新 object 的內(nèi)容:
$ git cat-file -p df7a version2 $ git cat-file -t df7a blob
看到這里,你可能對(duì) git 是一個(gè) object 數(shù)據(jù)庫(kù)的概念有了進(jìn)一步的認(rèn)識(shí):git 把文件每個(gè)版本的內(nèi)容都保存到了一個(gè) object 里面。
如果你想把 file.txt 恢復(fù)到第一個(gè)版本的狀態(tài),只需要這樣做:
$ git cat-file -p 5bdc > file.txt
然后查看 file.txt 的內(nèi)容:
$ cat file.txt version1
至此,一個(gè)能記錄文件版本,并能把文件恢復(fù)到任何版本狀態(tài)的版本控制系統(tǒng)完成(? ?_?)?
!
是不是感覺(jué)還行,不是那么難?你可以把 git 理解成一個(gè) key - value 數(shù)據(jù)庫(kù),一個(gè)哈希值對(duì)應(yīng)一個(gè) object。
到這里我們停停,把這部分實(shí)現(xiàn)了吧。
推薦一下自己的linux C/C++交流群:973961276!整理了一些個(gè)人覺(jué)得比較好的學(xué)習(xí)書(shū)籍與大廠(chǎng)面試題、有趣的項(xiàng)目和熱門(mén)技術(shù)教學(xué)視頻,感興趣朋友可以進(jìn)群領(lǐng)取哦。正在找工作或者準(zhǔn)備跳槽的朋友那便更不能錯(cuò)過(guò)了。
我一開(kāi)始有點(diǎn)好奇,為啥查看 object 不直接用 cat 指令,而是自己編了一條 git cat-file 指令呢?后來(lái)想了一下,git 肯定不會(huì)把文件的內(nèi)容原封不動(dòng)保存進(jìn) object ,應(yīng)該是做了壓縮,所以我們還要專(zhuān)門(mén)的指令去解壓讀取。
這兩條指令我們參照官方的思路進(jìn)行實(shí)現(xiàn),先說(shuō) git hash-object,一個(gè) object 存儲(chǔ)的內(nèi)容是這樣的:
首先要構(gòu)造頭部信息,頭部信息由對(duì)象類(lèi)型,一個(gè)空格,數(shù)據(jù)內(nèi)容的字節(jié)數(shù),一個(gè)空字節(jié)拼接而成,格式是這樣:
blob 9\u0000
然后把頭部信息和原始數(shù)據(jù)拼接起來(lái),格式是這樣:
blob 9\u0000version1
接著用 zlib 把上面拼接好的信息進(jìn)行壓縮,然后存進(jìn) object 文件中。
git cat-file 指令的實(shí)現(xiàn)則是相反,先把 object 文件里存放的數(shù)據(jù)用 zlib 進(jìn)行解壓,根據(jù)空格和空字節(jié)對(duì)解壓后的數(shù)據(jù)進(jìn)行劃分,然后根據(jù)參數(shù) -t 或 -p 返回 object 的內(nèi)容或者類(lèi)型。
這里是我的實(shí)現(xiàn):hash-object and cat-file
采用了簡(jiǎn)單粗暴的面向過(guò)程實(shí)現(xiàn),但是我已經(jīng)隱隱約約感到后面會(huì)用很多重用的功能,所以先把單元測(cè)試寫(xiě)上,方便后面重構(gòu)。
在上一章中,細(xì)心的小伙伴可能會(huì)發(fā)現(xiàn),git 會(huì)把我們的文件內(nèi)容以 blob 類(lèi)型的 object 進(jìn)行保存。這些 blob 類(lèi)型的 object 似乎只保存了文件的內(nèi)容,沒(méi)有保存文件名。
而且當(dāng)我們?cè)陂_(kāi)發(fā)項(xiàng)目的時(shí)候,不可能只有一個(gè)文件,通常情況下我們是需要對(duì)一個(gè)項(xiàng)目進(jìn)行版本管理的,一個(gè)項(xiàng)目會(huì)包含多個(gè)文件和文件夾。
所以最基礎(chǔ)的 blob object 已經(jīng)滿(mǎn)足不了我們使用了,我們需要引入一種新的 object,叫 tree object,它不僅能保存文件名,還能將多個(gè)文件組織到一起。
但是問(wèn)題來(lái)了,引入概念很容易,但是具體落實(shí)到代碼上怎么寫(xiě)呢?(T_T)
,我腦袋里的第一個(gè)想法是先在內(nèi)存里創(chuàng)建一個(gè) tree objct,然后我們往這個(gè)指定的 tree object 里面去添加內(nèi)容。但這樣似乎很麻煩,每次添加?xùn)|西都要給出 tree object 的哈希值。而且這樣的話(huà) tree object 就是可變的了,一個(gè)可變的 object 已經(jīng)違背了保存固定版本信息的初衷。
我們還是看 git 是怎么思考這個(gè)問(wèn)題的吧,git 在創(chuàng)建 tree object 的時(shí)候引入了一個(gè)叫暫存區(qū)概念,這是個(gè)不錯(cuò)的主意!你想,我們的 tree object 是要保存整個(gè)項(xiàng)目的版本信息的,項(xiàng)目有很多個(gè)文件,于是我們把文件都放進(jìn)緩沖區(qū)里,git 根據(jù)緩沖區(qū)里的內(nèi)容一次性創(chuàng)建一個(gè) tree object,這樣不就能記錄版本信息了嗎!
我們先操作一下 git 的緩沖區(qū)加深一下理解,首先引入一條新的指令 git update-index,它可以人為地把一個(gè)文件加入到一個(gè)新的緩沖區(qū)中,而且要加上一個(gè) --add 的參數(shù),因?yàn)檫@個(gè)文件之前還不存在于緩沖區(qū)中。
$ git update-index --add file.txt
然后我們觀(guān)察一下.git
目錄的變化
$ ls config description HEAD hooks/ index info/ objects/ refs/ $ find .git/objects/ -type f objects/5b/dcfc19f119febc749eef9a9551bc335cb965e2 objects/df/7af2c382e49245443687973ceb711b2b74cb4a
發(fā)現(xiàn).git
目錄下多了一個(gè)名為index
的文件,這估計(jì)就是我們的緩沖區(qū)了。而objects
目錄下的 object 倒沒(méi)什么變化。
我們查看一下緩沖區(qū)的內(nèi)容,這里用到一條指令:git ls-files --stage
$ git ls-files --stage 100644 df7af2c382e49245443687973ceb711b2b74cb4a 0 file.txt
我們發(fā)現(xiàn)緩沖區(qū)是這樣來(lái)存儲(chǔ)我們的添加記錄的:一個(gè)文件模式的代號(hào),文件內(nèi)容的 blob object,一個(gè)數(shù)字和文件的名字。
然后我們把當(dāng)前緩沖區(qū)的內(nèi)容以一個(gè) tree object 的形式進(jìn)行保存。引入一條新的指令:git write-tree
$ git write-tree 907aa76a1e4644e31ae63ad932c99411d0dd9417
輸入指令后,我們得到了新生成的 tree object 的哈希值,我們?nèi)ヲ?yàn)證一下它是否存在,并看看它的內(nèi)容:
$ find .git/objects/ -type f .git/objects/5b/dcfc19f119febc749eef9a9551bc335cb965e2 #文件內(nèi)容為 version1 的 blob object .git/objects/90/7aa76a1e4644e31ae63ad932c99411d0dd9417 #新的 tree object .git/objects/df/7af2c382e49245443687973ceb711b2b74cb4a #文件內(nèi)容為 version2 的 blob object $ git cat-file -p 907a 100644 blob df7af2c382e49245443687973ceb711b2b74cb4a file.txt
估計(jì)看到這里,大家對(duì)暫存區(qū)與 tree object 的關(guān)系就有了初步的了解。
現(xiàn)在我們進(jìn)一步了解兩點(diǎn):一個(gè)內(nèi)容未被 git 記錄的文件會(huì)被怎樣記錄,一個(gè)文件夾又會(huì)被怎樣記錄。
下面我們一步步來(lái),創(chuàng)建一個(gè)新的文件,并加入暫存區(qū):
$ echo abc > new.txt $ git update-index --add new.txt $ git ls-files --stage 100644 df7af2c382e49245443687973ceb711b2b74cb4a 0 file.txt 100644 8baef1b4abc478178b004d62031cf7fe6db6f903 0 new.txt
查看緩沖區(qū)后,我們發(fā)現(xiàn)新文件的記錄已追加的方式加入了暫存區(qū),而且也對(duì)應(yīng)了一個(gè)哈希值。我們查看一下哈希值的內(nèi)容:
$ find .git/objects/ -type f .git/objects/5b/dcfc19f119febc749eef9a9551bc335cb965e2 #新的 object .git/objects/8b/aef1b4abc478178b004d62031cf7fe6db6f903 #文件內(nèi)容為 version1 的 blob object .git/objects/90/7aa76a1e4644e31ae63ad932c99411d0dd9417 #tree object .git/objects/df/7af2c382e49245443687973ceb711b2b74cb4a #文件內(nèi)容為 version2 的 blob object $ git cat-file -p 8bae abc $ git cat-file -t 8bae blob
我們發(fā)現(xiàn),在把 new.txt 加入到暫存區(qū)時(shí),git 自動(dòng)給 new.txt 的內(nèi)容創(chuàng)建了一個(gè) blob object。
我們?cè)賴(lài)L試一下創(chuàng)建一個(gè)文件夾,并添加到暫存區(qū)中:
$ mkdir dir $ git update-index --add dir error: dir: is a directory - add files inside instead fatal: Unable to process path dir
結(jié)果 git 告訴我們不能添加一個(gè)空文件夾,需要在文件夾中添加文件,那么我們就往文件夾中加一個(gè)文件,然后再次添加到暫存區(qū):
$ echo 123 > dir/dirFile.txt $ git update-index --add dir/dirFile.txt
成功了~然后查看暫存區(qū)的內(nèi)容:
$ git ls-files --stage 100644 190a18037c64c43e6b11489df4bf0b9eb6d2c9bf 0 dir/dirFile.txt 100644 df7af2c382e49245443687973ceb711b2b74cb4a 0 file.txt 100644 8baef1b4abc478178b004d62031cf7fe6db6f903 0 new.txt $ git cat-file -t 190a blob
和之前的演示一樣,自動(dòng)幫我們?yōu)槲募?nèi)容創(chuàng)建了一個(gè) blob object。
接下來(lái)我們把當(dāng)前的暫存區(qū)保存成為一個(gè) tree object:
$ git write-tree dee1f9349126a50a52a4fdb01ba6f573fa309e8f $ git cat-file -p dee1 040000 tree 374e190215e27511116812dc3d2be4c69c90dbb0 dir 100644 blob df7af2c382e49245443687973ceb711b2b74cb4a file.txt 100644 blob 8baef1b4abc478178b004d62031cf7fe6db6f903 new.txt
新的 tree object 保存了暫存區(qū)的當(dāng)前版本信息,值得注意的是,暫存區(qū)是以 blob object 的形式記錄dir/dirFile.txt
的,而在保存樹(shù)對(duì)象的過(guò)程中,git 為目錄 dir
創(chuàng)建了一個(gè)樹(shù)對(duì)象,我們驗(yàn)證一下:
$ git cat-file -p 374e 100644 blob 190a18037c64c43e6b11489df4bf0b9eb6d2c9bf dirFile.txt $ git cat-file -t 374e tree
發(fā)現(xiàn)這個(gè)為 dir
目錄而創(chuàng)的樹(shù)對(duì)象保存了 difFile.txt
的信息,是不是感覺(jué)似曾相似!這個(gè) tree object 就是對(duì)文件目錄的模擬呀!
我們停停!開(kāi)始動(dòng)手!
這次我們需要實(shí)現(xiàn)上述的三條指令:
git update-index --add
git update-index更新暫存區(qū),官方的這條指令是帶有很多參數(shù)的,我們只實(shí)現(xiàn) --add,也就是添加文件到暫存區(qū)。總體的流程是這樣的:如果是第一次添加文件進(jìn)緩沖區(qū),我們需要?jiǎng)?chuàng)建一個(gè) index 文件,如果 index 文件已經(jīng)存在則直接把暫存區(qū)的內(nèi)容讀取出來(lái),注意要有個(gè)解壓的過(guò)程。然后把新的文件信息添加到暫存區(qū)中,把暫存區(qū)的內(nèi)容壓縮后存入 index 文件。
這里涉及到一個(gè)序列化和反序列的操作,請(qǐng)?jiān)试S我偷懶通過(guò) json 進(jìn)行模擬ψ(._. )>
。
git ls-files --stage
git ls-files 用來(lái)查看暫存區(qū)和工作區(qū)的文件信息,同樣有很多參數(shù),我們只實(shí)現(xiàn) --stage,查看暫存區(qū)的內(nèi)容(不帶參數(shù)的 ls-files 指令是列出當(dāng)前目錄包括子目錄下的所有文件)。實(shí)現(xiàn)流程:從 index 文件中讀取暫存區(qū)的內(nèi)容,解壓后按照一定的格式打印到標(biāo)準(zhǔn)輸出。
git write-tree
git write-tree 用于把暫存區(qū)的內(nèi)容轉(zhuǎn)換成一個(gè) tree object,根據(jù)我們之前演示的例子,對(duì)于文件夾我們需要遞歸下降解析 tree object,這應(yīng)該是本章最難實(shí)現(xiàn)的地方了。
代碼如下:update-index --add, ls-files --stage, write-tree
感覺(jué)可以把 object 抽象一下,于是重構(gòu)了一下和 object 相關(guān)的代碼:refactor object part
當(dāng)這部分完成后,我們已經(jīng)擁有一個(gè)能夠?qū)ξ募A進(jìn)行版本管理的系統(tǒng)了(? ?_?)?
。
雖然我們已經(jīng)可以用一個(gè) tree object 來(lái)表示整個(gè)項(xiàng)目的版本信息了,但是似乎還是有些不足的地方:
tree object 只記錄了文件的版本信息,這個(gè)版本是誰(shuí)修改的?是因什么而修改的?它的上一個(gè)版本是誰(shuí)?這些信息沒(méi)有被保存下來(lái)。
這個(gè)時(shí)候,就該 commit object 出場(chǎng)了!怎么樣,從底層一路向上摸索的感覺(jué)是不是很爽???
我們先用 git 操作一遍,然后再考慮如何實(shí)現(xiàn)。下面我們使用 commit-tree 指令來(lái)創(chuàng)建一個(gè) commit object,這個(gè) commit object 指向第三章最后生成的 tree object。
$ git commit-tree dee1 -m 'first commit' 893fba19d63b401ae458c1fc140f1a48c23e4873
由于生成時(shí)間和作者不同,你得到的哈希值會(huì)不一樣,我們查看一下這個(gè)新生成的 commit object:
$ git cat-file -p 893f tree dee1f9349126a50a52a4fdb01ba6f573fa309e8f author liuyj24 <liuyijun2017@email.szu.edu.cn> 1608981484 +0800 committer liuyj24 <liuyijun2017@email.szu.edu.cn> 1608981484 +0800 first commit
可以看到,這個(gè)commit ojbect 指向一個(gè) tree object,第二第三行是作者和提交者的信息,空一行后是提交信息。
下面我們修改我們的項(xiàng)目,模擬版本的變更:
$ echo version3 > file.txt $ git update-index --add file.txt $ git write-tree ff998d076c02acaf1551e35d76368f10e78af140
然后我們創(chuàng)建一個(gè)新的提交對(duì)象,把它的父對(duì)象指向第一個(gè)提交對(duì)象:
$ git commit-tree ff99 -m 'second commit' -p 893f b05c65b6fdd7e13a51aaf1abb8ff3e795835bfb0
我們?cè)傩薷奈覀兊捻?xiàng)目,然后創(chuàng)建第三個(gè)提交對(duì)象:
$ echo version4 >file.txt $ git update-index --add file.txt $ git write-tree 1403e859154aee76360e0082c4b272e5d145e13e $ git commit-tree 1403 -m 'third commit' -p b05c fe2544fb26a26f0412ce32f7418515a66b31b22d
然后我們執(zhí)行 git log 指令查看我們的提交歷史:
$ git log fe25 commit fe2544fb26a26f0412ce32f7418515a66b31b22d Author: liuyj24 <liuyijun2017@email.szu.edu.cn> Date: Sat Dec 26 19:36:31 2020 +0800 third commit commit b05c65b6fdd7e13a51aaf1abb8ff3e795835bfb0 Author: liuyj24 <liuyijun2017@email.szu.edu.cn> Date: Sat Dec 26 19:34:25 2020 +0800 second commit commit 893fba19d63b401ae458c1fc140f1a48c23e4873 Author: liuyj24 <liuyijun2017@email.szu.edu.cn> Date: Sat Dec 26 19:18:04 2020 +0800 first commit
怎么樣?是不是有種豁然開(kāi)朗的感覺(jué)!
下面我們停停,把這一部分給實(shí)現(xiàn)了。
一共是兩條指令
commit-tree
創(chuàng)建一個(gè) commit object,讓它指向一個(gè) tree object,添加作者信息,提交者信息,提交信息,再增加一個(gè)父節(jié)點(diǎn)即可(父節(jié)點(diǎn)可以不指定)。作者信息和提交者信息我們暫時(shí)寫(xiě)死,這個(gè)可以通過(guò) git config 指令設(shè)置,你可以查看一下.git/config
,其實(shí)就是一個(gè)讀寫(xiě)配置文件的操作。
log
根據(jù)傳入的 commit object 的哈希值向上找它的父節(jié)點(diǎn)并打印信息,通過(guò)遞歸能快速實(shí)現(xiàn)。
這里是我的實(shí)現(xiàn):commit-tree, log
在前面的四章我們鋪墊了很多 git 的底層指令,從這章開(kāi)始,我們將對(duì) git 的常用功能進(jìn)行講解,這絕對(duì)會(huì)有一種勢(shì)如破竹的感覺(jué)。
雖然我們的 commit object 已經(jīng)能夠很完整地記錄版本信息了,但是還有一個(gè)致命的缺點(diǎn):我們需要通過(guò)一個(gè)很長(zhǎng)的SHA1散列值來(lái)定位這個(gè)版本,如果在開(kāi)發(fā)的過(guò)程中你和同事說(shuō):
嘿!能幫我 review 一下 32h62342 這個(gè)版本的代碼嗎?
那他肯定會(huì)回你:哪。。。哪個(gè)版本來(lái)著?(+_+)?
所以我們要得考慮給我們的 commit object 起名字,比如起名叫 master。
我們實(shí)際操作一下 git,給我們最新的提交對(duì)象起名叫 master:
$ git update-ref refs/heads/master fe25
然后通過(guò)新的名字查看提交記錄:
$ git log master commit fe2544fb26a26f0412ce32f7418515a66b31b22d (HEAD -> master) Author: liuyj24 <liuyijun2017@email.szu.edu.cn> Date: Sat Dec 26 19:36:31 2020 +0800 third commit commit b05c65b6fdd7e13a51aaf1abb8ff3e795835bfb0 Author: liuyj24 <liuyijun2017@email.szu.edu.cn> Date: Sat Dec 26 19:34:25 2020 +0800 second commit commit 893fba19d63b401ae458c1fc140f1a48c23e4873 Author: liuyj24 <liuyijun2017@email.szu.edu.cn> Date: Sat Dec 26 19:18:04 2020 +0800 first commit
好家伙(→_→)
,要不我們給這個(gè)功能起個(gè)牛逼的名字,就叫分支吧!
這個(gè)時(shí)候你可能會(huì)想,平時(shí)我們?cè)?master 分支上進(jìn)行提交,都是一個(gè) git commit -m 指令就搞定的,現(xiàn)在背后的原理我似乎也懂:
首先是通過(guò)命令 write-tree 把暫存區(qū)的記錄寫(xiě)到一個(gè)樹(shù)對(duì)象里,得到樹(shù)對(duì)象的 SHA1 值。
然后通過(guò)命令 commit-tree 創(chuàng)建一個(gè)新的提交對(duì)象。
問(wèn)題是:commit-tree 指令所用到的的樹(shù)對(duì)象 SHA1 值,-m 提交信息都有了,但是 -p 父提交對(duì)象的 SHA1 值我們?cè)趺传@得呢?
這就要提到我們的 HEAD 引用了!你會(huì)發(fā)現(xiàn)我們的.git
目錄中有一個(gè)HEAD
文件,我們查看一下它的內(nèi)容:
$ ls config description HEAD hooks/ index info/ logs/ objects/ refs/ $ cat HEAD ref: refs/heads/master
所以當(dāng)我們進(jìn)行 commit 操作的時(shí)候,git 會(huì)到 HEAD 文件中取出當(dāng)前的引用,也就是當(dāng)前的提交對(duì)象的 SHA1 值作為新提交對(duì)象的父對(duì)象,這樣整個(gè)提交歷史就能串聯(lián)起來(lái)啦!
看到這里,你是不是對(duì) git branch 創(chuàng)建分支,git checkout 切換分支也有點(diǎn)感覺(jué)了呢?!
現(xiàn)在我們有三個(gè)提交對(duì)象,我們嘗試在第二個(gè)提交對(duì)象上創(chuàng)建分支,同樣先用底層指令完成,我們使用 git update-ref 指令對(duì)第二個(gè)提交創(chuàng)建一個(gè) reference:
$ git update-ref refs/heads/bugfix b05c $ git log bugfix commit b05c65b6fdd7e13a51aaf1abb8ff3e795835bfb0 (bugfix) Author: liuyj24 <liuyijun2017@email.szu.edu.cn> Date: Sat Dec 26 19:34:25 2020 +0800 second commit commit 893fba19d63b401ae458c1fc140f1a48c23e4873 Author: liuyj24 <liuyijun2017@email.szu.edu.cn> Date: Sat Dec 26 19:18:04 2020 +0800 first commit
然后我們改變我們當(dāng)前所處的分支,也就是修改 .git/HEAD
文件的值,我們用到 git symbolic-ref 指令:
git symbolic-ref HEAD refs/heads/bugfix
我們?cè)俅瓮ㄟ^(guò) log 指令查看日志,如果不加參數(shù)的話(huà),默認(rèn)就是查看當(dāng)前分支:
$ git log commit b05c65b6fdd7e13a51aaf1abb8ff3e795835bfb0 (HEAD -> bugfix) Author: liuyj24 <liuyijun2017@email.szu.edu.cn> Date: Sat Dec 26 19:34:25 2020 +0800 second commit commit 893fba19d63b401ae458c1fc140f1a48c23e4873 Author: liuyj24 <liuyijun2017@email.szu.edu.cn> Date: Sat Dec 26 19:18:04 2020 +0800 first commit
當(dāng)前分支就切換到 bugfix 啦!
我們停停,把這部分給實(shí)現(xiàn)了,基本都是簡(jiǎn)單的文件讀寫(xiě)操作。
update-ref
把提交對(duì)象的哈希值寫(xiě)到.git/refs/heads
下指定的文件中。由于之前 log 指令實(shí)現(xiàn)的不夠完善,這里要重構(gòu)一下,支持對(duì) ref 名字的查找。
symbolic-ref
用于修改 ref,我們就簡(jiǎn)單實(shí)現(xiàn)吧,對(duì)HEAD
文件進(jìn)行修改。
commit
有了上面兩條指令打下的基礎(chǔ),我們就可以把 commit 命令給實(shí)現(xiàn)了。再重復(fù)一遍流程:首先是通過(guò)命令 write-tree 把暫存區(qū)的記錄寫(xiě)到一個(gè)樹(shù)對(duì)象里,得到樹(shù)對(duì)象的 SHA1 值。然后通過(guò)命令 commit-tree 創(chuàng)建一個(gè)新的提交對(duì)象,新提交對(duì)象的父對(duì)象從HEAD
文件中獲取。最后更新對(duì)應(yīng)分支的提交對(duì)象信息。
這個(gè)是我的實(shí)現(xiàn):update-ref, symbolic-ref, commit
實(shí)現(xiàn)到這里,估計(jì)你已經(jīng)對(duì) checkout,branch 等命令沒(méi)啥興趣了,checkout 就是封裝一下 symbolic-ref,branch 就是封裝一下 update-ref。
git 為了增加指令的靈活性,為指令提供了不少可選參數(shù),但實(shí)際上都是這幾個(gè)底層指令的調(diào)用。而且有了這些底層指令,你會(huì)發(fā)現(xiàn)其他擴(kuò)展功能很輕松地實(shí)現(xiàn),這里就不展開(kāi)啦(? ?_?)?
。
完成了上面這些功能,估計(jì)大家會(huì)對(duì) git 有個(gè)較為深刻的認(rèn)識(shí)了,但不知道大家有沒(méi)發(fā)現(xiàn)一個(gè)小問(wèn)題:
當(dāng)我們開(kāi)發(fā)出了分支功能后,我們會(huì)基于分支做版本管理。但隨著分支有了新的提交,分支又會(huì)指向新的提交對(duì)象,也就是說(shuō)我們的分支是變動(dòng)的。但是我們總會(huì)有一些比較重要的版本需要記錄,我們需要一些不變的東西來(lái)記錄某個(gè)提交版本。
又由于記錄某個(gè)提交版本的 SHA1 值不是很好,所以我們給這些重要的提交版本取個(gè)名字,以 tag 的形式進(jìn)行存儲(chǔ)。估計(jì)大家在實(shí)現(xiàn) references 的時(shí)候也有留意到.git/refs/
下除了heads
還有一個(gè)tags
目錄,其實(shí)原理和 reference 一樣,也是記錄一個(gè)提交對(duì)象的哈希值。我們用 git 實(shí)際操作一下,給當(dāng)前分支的第一個(gè)提交對(duì)象打一個(gè) tag:
$ git log commit b05c65b6fdd7e13a51aaf1abb8ff3e795835bfb0 (HEAD -> bugfix) Author: liuyj24 <liuyijun2017@email.szu.edu.cn> Date: Sat Dec 26 19:34:25 2020 +0800 second commit commit 893fba19d63b401ae458c1fc140f1a48c23e4873 Author: liuyj24 <liuyijun2017@email.szu.edu.cn> Date: Sat Dec 26 19:18:04 2020 +0800 first commit $ git tag v1.0 893f
然后查看一下這個(gè) tag
$ git show v1.0 commit 893fba19d63b401ae458c1fc140f1a48c23e4873 (tag: v1.0) Author: liuyj24 <liuyijun2017@email.szu.edu.cn> Date: Sat Dec 26 19:18:04 2020 +0800 first commit ······
這樣我們就能通過(guò) v1.0 這個(gè) tag 定位到某個(gè)版本了。
這個(gè)我就不實(shí)現(xiàn)啦,哎(→_→)
。
這篇文章,我是邊看官方文檔,一邊實(shí)現(xiàn)一邊寫(xiě)的,其實(shí)寫(xiě)到這里整個(gè) git 的輪廓已經(jīng)很清晰了。因?yàn)?git 本身已經(jīng)足夠優(yōu)秀了,我們也沒(méi)有必要重寫(xiě)一個(gè),本文這種造小輪子的方式意在學(xué)習(xí) git 的核心思想,也就是如何搭建一個(gè)用于版本管理的 object 數(shù)據(jù)庫(kù)。
其實(shí)我們可以展望一下 git 的其他功能(紙上談兵(→_→)
):
add 指令:其實(shí)就是對(duì)我們 update-index 指令的封裝,我們平常都是直接add .
把所有修改過(guò)的文件添加進(jìn)緩存區(qū)。想要實(shí)現(xiàn)這樣的功能可以遞歸遍歷目錄,使用 diff 工具對(duì)修改過(guò)的文件執(zhí)行一次 update-index。
merge 指令:這個(gè)我感覺(jué)比較難實(shí)現(xiàn),目前思路是這樣的:通過(guò)遞歸,借助 diff 工具,把 merge 項(xiàng)目中多出來(lái)的部分追加到被 merge 項(xiàng)目中,如果 diff 指示出現(xiàn)沖突,就讓用戶(hù)解決沖突。
rebase 指令:其實(shí)就是修改提交對(duì)象的順序,具體實(shí)現(xiàn)就是修改它們的 parent 值。類(lèi)似往鏈表中間插入一個(gè)節(jié)點(diǎn)或一個(gè)鏈表這樣的問(wèn)題,就是調(diào)整鏈表。
······
除了這些,git 還有遠(yuǎn)程倉(cāng)庫(kù)的概念,而遠(yuǎn)程倉(cāng)庫(kù)和本地倉(cāng)庫(kù)的本質(zhì)是一樣的,不過(guò)里面涉及了很多同步協(xié)作的問(wèn)題。感覺(jué)現(xiàn)在繼續(xù)學(xué) git 的其他功能輕松了一些,更加自信了!
最后是關(guān)于自己這個(gè)迷你 git 的一些回顧
最后要對(duì)自己已經(jīng)實(shí)現(xiàn)的部分作一些總結(jié),和開(kāi)源代碼比起來(lái)有啥要可以提高改進(jìn)的地方:
沒(méi)有實(shí)現(xiàn)一個(gè)尋址的函數(shù)。git 可以在倉(cāng)庫(kù)的任何目錄下工作,而我的只能工作在倉(cāng)庫(kù)根目錄下。應(yīng)該實(shí)現(xiàn)一個(gè)查找當(dāng)前倉(cāng)庫(kù)下.git
目錄的函數(shù),這樣整個(gè)系統(tǒng)在文件目錄尋址的時(shí)候可以有統(tǒng)一的入口。
對(duì) object 的抽象不夠完善。迷你項(xiàng)目只是實(shí)現(xiàn)了把版本添加進(jìn)對(duì)象數(shù)據(jù)庫(kù),不能從對(duì)象數(shù)據(jù)庫(kù)中恢復(fù)版本,想要實(shí)現(xiàn)恢復(fù)版本,需要給每個(gè)對(duì)象制定相應(yīng)的反序列化方法,也就是說(shuō),object應(yīng)該實(shí)現(xiàn)這樣一套接口:
type obj interface { serialize(Object) []byte deserialize([]byte) Object }
目錄分隔符的問(wèn)題,由于我用 windows 開(kāi)發(fā),在 git bash 上測(cè)試,所有把分隔符寫(xiě)死成了/
,這不太好。
目前可以不停 commit,commit 的時(shí)候應(yīng)該檢查一下暫..存區(qū)是否有更新,沒(méi)有更新就不讓 commit 了。
對(duì)命令行參數(shù)的判斷有點(diǎn)丑,暫時(shí)還沒(méi)找到好辦法···
感謝各位的閱讀,以上就是“總結(jié)從基本Git指令到背后原理”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)總結(jié)從基本Git指令到背后原理這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。