git原理學習記錄:從基本指令到背後原理,實現一個簡單的git

抑菌發表於2020-12-28

好傢伙~

一開始我還擔心 git 的原理會不會很難懂,但在閱讀了官方文件後我發現其實並不難懂,似乎可以動手實現一個簡單的 git,於是就有了下面這篇學習記錄。

本文的敘述思路參照了官方文件Book的原理介紹部分,在一些節點上探討程式碼實現,官方文件連結

看完本文你能:1. 瞭解 git 的設計思想。2. 收穫一點快樂?

程式語言選擇了 go,因為剛學不太熟悉想多使用一下。

這是我的倉庫地址,但如果你和我一樣是初學,直接看程式碼可能不能快速上手,推薦順著文章看。

迷你git實現--連結

如果文章看得吃力可以跟著官方文件的原理部分操作一次再回頭看,可能更易懂?

1. init

在學習 git 原理之前,我們先忘掉平時用的 commit,branch,tag 這些炫酷的 git 指令,後面我們會摸清楚它們的本質的。

要知道,git 是 Linus 在寫 Linux 的時候順便寫出來的,用於對 Linux 進行版本管理,所以,記錄檔案專案在不同版本的變更資訊是 git 最核心的功能。

大牛們在設計軟體的時候總是會做相應的抽象,想要理解他們的設計思路,我們就得在他們的抽象下進行思考。雖然說的有點玄乎,但是這些抽象最終都會落實到程式碼上的,所以不必擔心,很好理解的。

首先,我們要奠定一個 ojbect 的概念,這是 git 最底層的抽象,你可以把 git 理解成一個 object 資料庫。

廢話不多說,跟著指令操作,你會對 git 有一個全新的認識。首先我們在任意目錄下建立一個 git 倉庫:

我的操作環境是 win10 + git bash

$ git init git-test
Initialized empty Git repository in C:/git-test/.git/

可以看到 git 為我們建立了一個空的 git 倉庫,裡面有一個.git目錄,目錄結構如下:

$ ls
config  description  HEAD  hooks/  info/  objects/  refs/

.git目錄下我們先重點關注 .git/objects這個目錄,我們一開始說 git 是一個 object 資料庫,這個目錄就是 git 存放 object 的地方。

進入.git/objects目錄後我們能看到infopack兩個目錄,不過這和核心功能無關,我們只需要知道現在.git/objects目錄下除了兩個空目錄其他啥都沒有就行了。

到這裡我們停停,先把這部分實現了吧,邏輯很簡單,我們只需要編寫一個入口函式,解析命令列的引數,在得到 init 指令後在指定目錄下建立相應的目錄與檔案即可。

這裡是我的實現:init

為了易讀暫時沒有對建立檔案/目錄進行錯誤處理。

我給它取了個土一點的名字,叫 jun,呃,其實管它叫啥都可以(⊙ˍ⊙)

2.object

接下來我們進入 git 倉庫目錄並新增一個檔案:

$ echo "version1" > file.txt

然後我們把對這個檔案的記錄新增進 git 系統。要注意的是,我們暫不使用add指令新增,儘管我們平時很可能這麼做,但這是一篇揭示原理的文章,這裡我們要引入一條平時大家可能沒有聽到過的 git 指令git hash-object

$ git hash-object -w file.txt
5bdcfc19f119febc749eef9a9551bc335cb965e2

指令執行後返回了一個雜湊值,實際上這條指令已經把對 file.txt 的內容以一個 object 的形式新增進 object 資料庫中了,而這個雜湊值就對應著這個 object。

為了驗證 git 把這個 object 寫入了資料庫(以檔案的形式儲存下來),我們檢視一下.git/objects目錄:

$ find .git/objects/ -type f    #-type用於制定型別,f表示檔案
.git/objects/5b/dcfc19f119febc749eef9a9551bc335cb965e2

發現多了一個資料夾5b,該資料夾下有一個名為dcfc19f119febc749eef9a9551bc335cb965e2的檔案,也就是說 git 把該 object 雜湊值的前2個字元作為目錄名,後38個字元作為檔名,存放到了 object 資料庫中。

關於 git hash-object 指令的官方介紹,這條指令用於計算一個 ojbect 的 ID 值。-w 是可選引數,表示把 object 寫入到 object 資料庫中;還有一個引數是 -t,用於指定 object 的型別,如果不指定型別,預設是 blob 型別。

現在你可能好奇 object 裡面儲存了什麼資訊,我們使用git cat-file指令去檢視一下:

$ git cat-file -p 5bdc  # -p:檢視 object 的內容,我們可以只給出雜湊值的字首
version1

$ git cat-file -t 5bdc  # -t:檢視 object 的型別
blob

有了上面的鋪墊之後,接下來我們就揭開 git 實現版本控制的祕密!

我們改變 file.txt 的內容,並重新寫入 object 資料庫中:

$ echo "version2" > file.txt
$ git hash-object -w file.txt
df7af2c382e49245443687973ceb711b2b74cb4a

控制檯返回了一個新的雜湊值,我們再檢視一下 object 資料庫:

$ find .git/objects -type f
.git/objects/5b/dcfc19f119febc749eef9a9551bc335cb965e2
.git/objects/df/7af2c382e49245443687973ceb711b2b74cb4a

(゚Д゚)發現多了一個 object!我們檢視一下新 object 的內容:

$ git cat-file -p df7a
version2

$ git cat-file -t df7a
blob

看到這裡,你可能對 git 是一個 object 資料庫的概念有了進一步的認識:git 把檔案每個版本的內容都儲存到了一個 object 裡面。

如果你想把 file.txt 恢復到第一個版本的狀態,只需要這樣做:

$ git cat-file -p 5bdc > file.txt

然後檢視 file.txt 的內容:

$ cat file.txt
version1

至此,一個能記錄檔案版本,並能把檔案恢復到任何版本狀態的版本控制系統完成(ง •_•)ง

是不是感覺還行,不是那麼難?你可以把 git 理解成一個 key - value 資料庫,一個雜湊值對應一個 object。

到這裡我們停停,把這部分實現了吧。

我一開始有點好奇,為啥檢視 object 不直接用 cat 指令,而是自己編了一條 git cat-file 指令呢?後來想了一下,git 肯定不會把檔案的內容原封不動儲存進 object ,應該是做了壓縮,所以我們還要專門的指令去解壓讀取。

這兩條指令我們參照官方的思路進行實現,先說 git hash-object,一個 object 儲存的內容是這樣的:

  1. 首先要構造頭部資訊,頭部資訊由物件型別,一個空格,資料內容的位元組數,一個空位元組拼接而成,格式是這樣:
blob 9\u0000
  1. 然後把頭部資訊和原始資料拼接起來,格式是這樣:
blob 9\u0000version1
  1. 接著用 zlib 把上面拼接好的資訊進行壓縮,然後存進 object 檔案中。

git cat-file 指令的實現則是相反,先把 object 檔案裡存放的資料用 zlib 進行解壓,根據空格和空位元組對解壓後的資料進行劃分,然後根據引數 -t 或 -p 返回 object 的內容或者型別。

這裡是我的實現:hash-object and cat-file

採用了簡單粗暴的程式導向實現,但是我已經隱隱約約感到後面會用很多重用的功能,所以先把單元測試寫上,方便後面重構。

3. tree object

在上一章中,細心的小夥伴可能會發現,git 會把我們的檔案內容以 blob 型別的 object 進行儲存。這些 blob 型別的 object 似乎只儲存了檔案的內容,沒有儲存檔名。

而且當我們在開發專案的時候,不可能只有一個檔案,通常情況下我們是需要對一個專案進行版本管理的,一個專案會包含多個檔案和資料夾。

所以最基礎的 blob object 已經滿足不了我們使用了,我們需要引入一種新的 object,叫 tree object,它不僅能儲存檔名,還能將多個檔案組織到一起。

但是問題來了,引入概念很容易,但是具體落實到程式碼上怎麼寫呢?(T_T),我腦袋裡的第一個想法是先在記憶體裡建立一個 tree objct,然後我們往這個指定的 tree object 裡面去新增內容。但這樣似乎很麻煩,每次新增東西都要給出 tree object 的雜湊值。而且這樣的話 tree object 就是可變的了,一個可變的 object 已經違背了儲存固定版本資訊的初衷。

我們還是看 git 是怎麼思考這個問題的吧,git 在建立 tree object 的時候引入了一個叫暫存區概念,這是個不錯的主意!你想,我們的 tree object 是要儲存整個專案的版本資訊的,專案有很多個檔案,於是我們把檔案都放進緩衝區裡,git 根據緩衝區裡的內容一次性建立一個 tree object,這樣不就能記錄版本資訊了嗎!

我們先操作一下 git 的緩衝區加深一下理解,首先引入一條新的指令 git update-index,它可以人為地把一個檔案加入到一個新的緩衝區中,而且要加上一個 --add 的引數,因為這個檔案之前還不存在於緩衝區中。

$ git update-index --add file.txt

然後我們觀察一下.git目錄的變化

$ ls
config  description  HEAD  hooks/  index  info/  objects/  refs/

$ find .git/objects/ -type f
objects/5b/dcfc19f119febc749eef9a9551bc335cb965e2
objects/df/7af2c382e49245443687973ceb711b2b74cb4a

發現.git目錄下多了一個名為index的檔案,這估計就是我們的緩衝區了。而objects目錄下的 object 倒沒什麼變化。

我們檢視一下緩衝區的內容,這裡用到一條指令:git ls-files --stage

$ git ls-files --stage
100644 df7af2c382e49245443687973ceb711b2b74cb4a 0       file.txt

我們發現緩衝區是這樣來儲存我們的新增記錄的:一個檔案模式的代號,檔案內容的 blob object,一個數字和檔案的名字。

然後我們把當前緩衝區的內容以一個 tree object 的形式進行儲存。引入一條新的指令:git write-tree

$ git write-tree
907aa76a1e4644e31ae63ad932c99411d0dd9417

輸入指令後,我們得到了新生成的 tree object 的雜湊值,我們去驗證一下它是否存在,並看看它的內容:

$ find .git/objects/ -type f
.git/objects/5b/dcfc19f119febc749eef9a9551bc335cb965e2 #檔案內容為 version1 的 blob object
.git/objects/90/7aa76a1e4644e31ae63ad932c99411d0dd9417 #新的 tree object
.git/objects/df/7af2c382e49245443687973ceb711b2b74cb4a #檔案內容為 version2 的 blob object

$ git cat-file -p 907a
100644 blob df7af2c382e49245443687973ceb711b2b74cb4a    file.txt

估計看到這裡,大家對暫存區與 tree object 的關係就有了初步的瞭解。

現在我們進一步瞭解兩點:一個內容未被 git 記錄的檔案會被怎樣記錄,一個資料夾又會被怎樣記錄。

下面我們一步步來,建立一個新的檔案,並加入暫存區:

$ echo abc > new.txt

$ git update-index --add new.txt

$ git ls-files --stage
100644 df7af2c382e49245443687973ceb711b2b74cb4a 0       file.txt
100644 8baef1b4abc478178b004d62031cf7fe6db6f903 0       new.txt

檢視緩衝區後,我們發現新檔案的記錄已追加的方式加入了暫存區,而且也對應了一個雜湊值。我們檢視一下雜湊值的內容:

$ find .git/objects/ -type f
.git/objects/5b/dcfc19f119febc749eef9a9551bc335cb965e2 #新的 object
.git/objects/8b/aef1b4abc478178b004d62031cf7fe6db6f903 #檔案內容為 version1 的 blob object
.git/objects/90/7aa76a1e4644e31ae63ad932c99411d0dd9417 #tree object
.git/objects/df/7af2c382e49245443687973ceb711b2b74cb4a #檔案內容為 version2 的 blob object

$ git cat-file -p 8bae
abc

$ git cat-file -t 8bae
blob

我們發現,在把 new.txt 加入到暫存區時,git 自動給 new.txt 的內容建立了一個 blob object。

我們再嘗試一下建立一個資料夾,並新增到暫存區中:

$ mkdir dir

$ git update-index --add dir
error: dir: is a directory - add files inside instead
fatal: Unable to process path dir

結果 git 告訴我們不能新增一個空資料夾,需要在資料夾中新增檔案,那麼我們就往資料夾中加一個檔案,然後再次新增到暫存區:

$ echo 123 > dir/dirFile.txt

$ git update-index --add dir/dirFile.txt

成功了~然後檢視暫存區的內容:

$ 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

和之前的演示一樣,自動幫我們為檔案內容建立了一個 blob object。

接下來我們把當前的暫存區儲存成為一個 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 儲存了暫存區的當前版本資訊,值得注意的是,暫存區是以 blob object 的形式記錄dir/dirFile.txt的,而在儲存樹物件的過程中,git 為目錄 dir 建立了一個樹物件,我們驗證一下:

$ git cat-file -p 374e
100644 blob 190a18037c64c43e6b11489df4bf0b9eb6d2c9bf    dirFile.txt

$ git cat-file -t 374e
tree

發現這個為 dir 目錄而創的樹物件儲存了 difFile.txt 的資訊,是不是感覺似曾相似!這個 tree object 就是對檔案目錄的模擬呀!

我們停停!開始動手!

這次我們需要實現上述的三條指令:

  1. git update-index --add

git update-index更新暫存區,官方的這條指令是帶有很多引數的,我們只實現 --add,也就是新增檔案到暫存區。總體的流程是這樣的:如果是第一次新增檔案進緩衝區,我們需要建立一個 index 檔案,如果 index 檔案已經存在則直接把暫存區的內容讀取出來,注意要有個解壓的過程。然後把新的檔案資訊新增到暫存區中,把暫存區的內容壓縮後存入 index 檔案。

這裡涉及到一個序列化和反序列的操作,請允許我偷懶通過 json 進行模擬ψ(._. )>

  1. git ls-files --stage

git ls-files 用來檢視暫存區和工作區的檔案資訊,同樣有很多引數,我們只實現 --stage,檢視暫存區的內容(不帶引數的 ls-files 指令是列出當前目錄包括子目錄下的所有檔案)。實現流程:從 index 檔案中讀取暫存區的內容,解壓後按照一定的格式列印到標準輸出。

  1. git write-tree

git write-tree 用於把暫存區的內容轉換成一個 tree object,根據我們之前演示的例子,對於資料夾我們需要遞迴下降解析 tree object,這應該是本章最難實現的地方了。

程式碼如下:update-index --add, ls-files --stage, write-tree

感覺可以把 object 抽象一下,於是重構了一下和 object 相關的程式碼:refactor object part

當這部分完成後,我們已經擁有一個能夠對資料夾進行版本管理的系統了(ง •_•)ง

4.commit object

雖然我們已經可以用一個 tree object 來表示整個專案的版本資訊了,但是似乎還是有些不足的地方:

tree object 只記錄了檔案的版本資訊,這個版本是誰修改的?是因什麼而修改的?它的上一個版本是誰?這些資訊沒有被儲存下來。

這個時候,就該 commit object 出場了!怎麼樣,從底層一路向上摸索的感覺是不是很爽!?

我們先用 git 操作一遍,然後再考慮如何實現。下面我們使用 commit-tree 指令來建立一個 commit object,這個 commit object 指向第三章最後生成的 tree object。

$ git commit-tree dee1 -m 'first commit'
893fba19d63b401ae458c1fc140f1a48c23e4873

由於生成時間和作者不同,你得到的雜湊值會不一樣,我們檢視一下這個新生成的 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

可以看到,這個commit ojbect 指向一個 tree object,第二第三行是作者和提交者的資訊,空一行後是提交資訊。

下面我們修改我們的專案,模擬版本的變更:

$ echo version3 > file.txt

$ git update-index --add file.txt

$ git write-tree
ff998d076c02acaf1551e35d76368f10e78af140

然後我們建立一個新的提交物件,把它的父物件指向第一個提交物件:

$ git commit-tree ff99 -m 'second commit' -p 893f
b05c65b6fdd7e13a51aaf1abb8ff3e795835bfb0

我們再修改我們的專案,然後建立第三個提交物件:

$ echo version4 >file.txt

$ git update-index --add file.txt

$ git write-tree
1403e859154aee76360e0082c4b272e5d145e13e

$ git commit-tree 1403 -m 'third commit' -p b05c
fe2544fb26a26f0412ce32f7418515a66b31b22d

然後我們執行 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

怎麼樣?是不是有種豁然開朗的感覺!

下面我們停停,把這一部分給實現了。

一共是兩條指令

  1. commit-tree

建立一個 commit object,讓它指向一個 tree object,新增作者資訊,提交者資訊,提交資訊,再增加一個父節點即可(父節點可以不指定)。作者資訊和提交者資訊我們暫時寫死,這個可以通過 git config 指令設定,你可以檢視一下.git/config,其實就是一個讀寫配置檔案的操作。

  1. log

根據傳入的 commit object 的雜湊值向上找它的父節點並列印資訊,通過遞迴能快速實現。

這裡是我的實現:commit-tree, log

5. references

在前面的四章我們鋪墊了很多 git 的底層指令,從這章開始,我們將對 git 的常用功能進行講解,這絕對會有一種勢如破竹的感覺。

雖然我們的 commit object 已經能夠很完整地記錄版本資訊了,但是還有一個致命的缺點:我們需要通過一個很長的SHA1雜湊值來定位這個版本,如果在開發的過程中你和同事說:

嘿!能幫我 review 一下 32h52342 這個版本的程式碼嗎?

那他肯定會回你:哪。。。哪個版本來著?(+_+)?

所以我們要得考慮給我們的 commit object 起名字,比如起名叫 master。

我們實際操作一下 git,給我們最新的提交物件起名叫 master:

$ git update-ref refs/heads/master fe25

然後通過新的名字檢視提交記錄:

$ 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

好傢伙(→_→),要不我們給這個功能起個牛逼的名字,就叫分支吧!

這個時候你可能會想,平時我們在 master 分支上進行提交,都是一個 git commit -m 指令就搞定的,現在背後的原理我似乎也懂:

  1. 首先是通過命令 write-tree 把暫存區的記錄寫到一個樹物件裡,得到樹物件的 SHA1 值。
  2. 然後通過命令 commit-tree 建立一個新的提交物件。

問題是:commit-tree 指令所用到的的樹物件 SHA1 值,-m 提交資訊都有了,但是 -p 父提交物件的 SHA1 值我們怎麼獲得呢?

這就要提到我們的 HEAD 引用了!你會發現我們的.git目錄中有一個HEAD檔案,我們檢視一下它的內容:

$ ls
config  description  HEAD  hooks/  index  info/  logs/  objects/  refs/

$ cat HEAD
ref: refs/heads/master

所以當我們進行 commit 操作的時候,git 會到 HEAD 檔案中取出當前的引用,也就是當前的提交物件的 SHA1 值作為新提交物件的父物件,這樣整個提交歷史就能串聯起來啦!

看到這裡,你是不是對 git branch 建立分支,git checkout 切換分支也有點感覺了呢?!

現在我們有三個提交物件,我們嘗試在第二個提交物件上建立分支,同樣先用底層指令完成,我們使用 git update-ref 指令對第二個提交建立一個 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

然後我們改變我們當前所處的分支,也就是修改 .git/HEAD檔案的值,我們用到 git symbolic-ref 指令:

git symbolic-ref HEAD refs/heads/bugfix

我們再次通過 log 指令檢視日誌,如果不加引數的話,預設就是檢視當前分支:

$ 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

當前分支就切換到 bugfix 啦!

我們停停,把這部分給實現了,基本都是簡單的檔案讀寫操作。

  1. update-ref

把提交物件的雜湊值寫到.git/refs/heads下指定的檔案中。由於之前 log 指令實現的不夠完善,這裡要重構一下,支援對 ref 名字的查詢。

  1. symbolic-ref

用於修改 ref,我們就簡單實現吧,對HEAD檔案進行修改。

  1. commit

有了上面兩條指令打下的基礎,我們就可以把 commit 命令給實現了。再重複一遍流程:首先是通過命令 write-tree 把暫存區的記錄寫到一個樹物件裡,得到樹物件的 SHA1 值。然後通過命令 commit-tree 建立一個新的提交物件,新提交物件的父物件從HEAD檔案中獲取。最後更新對應分支的提交物件資訊。

這個是我的實現:update-ref, symbolic-ref, commit

實現到這裡,估計你已經對 checkout,branch 等命令沒啥興趣了,checkout 就是封裝一下 symbolic-ref,branch 就是封裝一下 update-ref。

git 為了增加指令的靈活性,為指令提供了不少可選引數,但實際上都是這幾個底層指令的呼叫。而且有了這些底層指令,你會發現其他擴充套件功能很輕鬆地實現,這裡就不展開啦(ง •_•)ง

6. tag

完成了上面這些功能,估計大家會對 git 有個較為深刻的認識了,但不知道大家有沒發現一個小問題:

當我們開發出了分支功能後,我們會基於分支做版本管理。但隨著分支有了新的提交,分支又會指向新的提交物件,也就是說我們的分支是變動的。但是我們總會有一些比較重要的版本需要記錄,我們需要一些不變的東西來記錄某個提交版本。

又由於記錄某個提交版本的 SHA1 值不是很好,所以我們給這些重要的提交版本取個名字,以 tag 的形式進行儲存。估計大家在實現 references 的時候也有留意到.git/refs/下除了heads還有一個tags目錄,其實原理和 reference 一樣,也是記錄一個提交物件的雜湊值。我們用 git 實際操作一下,給當前分支的第一個提交物件打一個 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

然後檢視一下這個 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

······

這樣我們就能通過 v1.0 這個 tag 定位到某個版本了。

這個我就不實現啦,哎(→_→)

7. more

這篇文章,我是邊看官方文件,一邊實現一邊寫的,其實寫到這裡整個 git 的輪廓已經很清晰了。因為 git 本身已經足夠優秀了,我們也沒有必要重寫一個,本文這種造小輪子的方式意在學習 git 的核心思想,也就是如何搭建一個用於版本管理的 object 資料庫。

其實我們可以展望一下 git 的其他功能(紙上談兵(→_→)):

  1. add 指令:其實就是對我們 update-index 指令的封裝,我們平常都是直接add .把所有修改過的檔案新增進快取區。想要實現這樣的功能可以遞迴遍歷目錄,使用 diff 工具對修改過的檔案執行一次 update-index。
  2. merge 指令:這個我感覺比較難實現,目前思路是這樣的:通過遞迴,藉助 diff 工具,把 merge 專案中多出來的部分追加到被 merge 專案中,如果 diff 指示出現衝突,就讓使用者解決衝突。
  3. rebase 指令:其實就是修改提交物件的順序,具體實現就是修改它們的 parent 值。類似往連結串列中間插入一個節點或一個連結串列這樣的問題,就是調整連結串列。
  4. ······

除了這些,git 還有遠端倉庫的概念,而遠端倉庫和本地倉庫的本質是一樣的,不過裡面涉及了很多同步協作的問題。感覺現在繼續學 git 的其他功能輕鬆了一些,更加自信了!

最後是關於自己這個迷你 git 的一些回顧

最後要對自己已經實現的部分作一些總結,和開原始碼比起來有啥要可以提高改進的地方:

  1. 沒有實現一個定址的函式。git 可以在倉庫的任何目錄下工作,而我的只能工作在倉庫根目錄下。應該實現一個查詢當前倉庫下.git目錄的函式,這樣整個系統在檔案目錄定址的時候可以有統一的入口。
  2. 對 object 的抽象不夠完善。迷你專案只是實現了把版本新增進物件資料庫,不能從物件資料庫中恢復版本,想要實現恢復版本,需要給每個物件制定相應的反序列化方法,也就是說,object應該實現這樣一套介面:
type obj interface {
	serialize(Object) []byte
	deserialize([]byte) Object
}
  1. 目錄分隔符的問題,由於我用 windows 開發,在 git bash 上測試,所有把分隔符寫死成了/,這不太好。
  2. 目前可以不停 commit,commit 的時候應該檢查一下暫存區是否有更新,沒有更新就不讓 commit 了。
  3. 對命令列引數的判斷有點醜,暫時還沒找到好辦法······

8. end

最後!

感謝閱讀到這裡,有幫助的話不妨點個贊吶!

也歡迎各位關注我的公眾號:抑菌。月更選手永不言棄!(ง •_•)ง

相關文章