溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

總結(jié)從基本Git指令到背后原理

發(fā)布時(shí)間:2021-10-21 09:31:38 來(lái)源:億速云 閱讀:136 作者:iii 欄目:編程語(yǔ)言

這篇文章主要講解了“總結(jié)從基本Git指令到背后原理”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“總結(jié)從基本Git指令到背后原理”吧!

1. init

在學(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目錄后我們能看到infopack兩個(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í)管它叫啥都可以(⊙?⊙)。

2.object

接下來(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ò)了。

總結(jié)從基本Git指令到背后原理

我一開(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)容是這樣的:

  1. 首先要構(gòu)造頭部信息,頭部信息由對(duì)象類(lèi)型,一個(gè)空格,數(shù)據(jù)內(nèi)容的字節(jié)數(shù),一個(gè)空字節(jié)拼接而成,格式是這樣:

blob 9\u0000
  1. 然后把頭部信息和原始數(shù)據(jù)拼接起來(lái),格式是這樣:

blob 9\u0000version1
  1. 接著用 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)。

3. tree object

在上一章中,細(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)上述的三條指令:

  1. 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)行模擬ψ(._. )>。

  1. 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)輸出。

  1. 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)了(? ?_?)?。

4.commit object

雖然我們已經(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)了。

一共是兩條指令

  1. 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ě)配置文件的操作。

  1. log

根據(jù)傳入的 commit object 的哈希值向上找它的父節(jié)點(diǎn)并打印信息,通過(guò)遞歸能快速實(shí)現(xiàn)。

這里是我的實(shí)現(xiàn):commit-tree, log

5. references

在前面的四章我們鋪墊了很多 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)在背后的原理我似乎也懂:

  1. 首先是通過(guò)命令 write-tree 把暫存區(qū)的記錄寫(xiě)到一個(gè)樹(shù)對(duì)象里,得到樹(shù)對(duì)象的 SHA1 值。

  2. 然后通過(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ě)操作。

  1. update-ref

把提交對(duì)象的哈希值寫(xiě)到.git/refs/heads下指定的文件中。由于之前 log 指令實(shí)現(xiàn)的不夠完善,這里要重構(gòu)一下,支持對(duì) ref 名字的查找。

  1. symbolic-ref

用于修改 ref,我們就簡(jiǎn)單實(shí)現(xiàn)吧,對(duì)HEAD文件進(jìn)行修改。

  1. 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)啦(? ?_?)?。

6. tag

完成了上面這些功能,估計(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)啦,哎(→_→)。

7. more

這篇文章,我是邊看官方文檔,一邊實(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 的其他功能(紙上談兵(→_→)):

  1. add 指令:其實(shí)就是對(duì)我們 update-index 指令的封裝,我們平常都是直接add .把所有修改過(guò)的文件添加進(jìn)緩存區(qū)。想要實(shí)現(xiàn)這樣的功能可以遞歸遍歷目錄,使用 diff 工具對(duì)修改過(guò)的文件執(zhí)行一次 update-index。

  2. merge 指令:這個(gè)我感覺(jué)比較難實(shí)現(xiàn),目前思路是這樣的:通過(guò)遞歸,借助 diff 工具,把 merge 項(xiàng)目中多出來(lái)的部分追加到被 merge 項(xiàng)目中,如果 diff 指示出現(xiàn)沖突,就讓用戶(hù)解決沖突。

  3. rebase 指令:其實(shí)就是修改提交對(duì)象的順序,具體實(shí)現(xiàn)就是修改它們的 parent 值。類(lèi)似往鏈表中間插入一個(gè)節(jié)點(diǎn)或一個(gè)鏈表這樣的問(wèn)題,就是調(diào)整鏈表。

  4. ······

除了這些,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)的地方:

  1. 沒(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)一的入口。

  2. 對(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
}
  1. 目錄分隔符的問(wèn)題,由于我用 windows 開(kāi)發(fā),在 git bash 上測(cè)試,所有把分隔符寫(xiě)死成了/,這不太好。

  2. 目前可以不停 commit,commit 的時(shí)候應(yīng)該檢查一下暫..存區(qū)是否有更新,沒(méi)有更新就不讓 commit 了。

  3. 對(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)注!

向AI問(wèn)一下細(xì)節(jié)

免責(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)容。

AI