【編者的話】Git 是目前最流行的版本控制系統,從本地開發到生產部署,我們每天都在使用 Git 進行我們的版本控制,除了日常使用的命令之外,如果想要對 Git 有更深一步的瞭解,那麼研究下 Git 的底層儲存原理將會對理解 Git 及其使用非常有幫助,就算你不是一個 Git 開發者,也推薦你瞭解下 Git 的底層原理,你會對 Git 的強大有一個全新的認識,並且將會在日常的 Git 使用過程中更加得心應手。
這篇文章面向的讀者主要是對 Git 有一定的瞭解的群體,並不會介紹具體 Git 的作用及其使用,也不會介紹與其它版本控制系統如 Subversion 之間的差異,主要是介紹下 Git 的本質以及他的儲存實現的相關原理,旨在幫助 Git 使用者更加清晰的瞭解在使用 Git 進行版本控制的時候其內部實現。
Git 本質是什麼Git 本質上是一個內容定址的 Key-Value 資料庫,我們可以向 Git 倉庫內插入任意型別的內容,Git 會返回給我們一個唯一的鍵值,可以透過這個鍵取出當時我們插入的值,我們可以透過底層命令git hash-object命令來嘗試:
➜ Zoker git:(master) ✗ cat testfileHello Git➜ Zoker git:(master) ✗ git hash-object testfile -w9f4d96d5b00d98959ea9960f069585ce42b1349a
可以看到我們目錄下有一個名為testfile的檔案,內容是Hello Git! 我們使用git hash-object命令將這個檔案的內容寫入到 Git 倉庫,-w 選項告訴 Git 把這個內容寫到 Git 的.git/objects物件資料庫目錄,並且 Git 返回了一個 SHA 值,這個 SHA 值就是後續我們要取出這個檔案的鍵值:
➜ Zoker git:(master) ✗ git cat-file -p 9f4d96d5b00d98959ea9960f069585ce42b1349aHello Git
我們使用了git cat-file命令取回剛剛存入到 Git 倉庫的內容,雖然不像 Redis 的命令 get set 那麼直觀,但是它確實是一個 KV 資料庫,不是嗎?
我們剛剛嘗試插入的這種資料是基礎的blob型別的物件,Git 還有其它如 tree、commit 等物件型別,這些不同的物件型別之間有特定的關聯關係,它們將不同的物件有邏輯的關聯起來,才能夠幫我們進行不同版本的控制和檢出。稍後會展開講解這幾種不同的物件型別,我們先來了解下 Git 的目錄結構,看看在 Git 中資料是如何存放的。
Git 目錄結構透過上一節的介紹,我們知道了 Git 本質就是一個 KV 資料庫,而且還提到了內容都是寫到 .git/objects物件目錄,那麼這個目錄放在哪裡?Git 又是如何儲存這些資料的呢?本節我們重點介紹一下 Git 的儲存目錄結構,瞭解下 Git 是如何存放不同型別的資料的。
更詳細的介紹參見: https://github.com/git/git/blo ... t.txt
透過 git init 我們可以在當前目錄初始化一個空的 Git 倉庫,Git 會自動生成 .git 目錄,這個 .git 目錄就是後續所有的 Git 元資料的儲存中心,我們來看一下它的目錄結構:
➜ Zoker git initInitialized empty Git repository in /Users/zoker/tmp/Zoker/.git/➜ Zoker git:(master) ✗ tree .git.git├── HEAD // 是一個符號引用,指明當前工作目錄的版本引用資訊,我們平時執行 checkout 命令時就會改變 HEAD 的內容├── config // 配置當前儲存庫的一些資訊,如:Proxy、使用者資訊、引用等,此處的配置項相對於全域性配置權重更高├── description // 倉庫描述資訊├── hooks // 鉤子目錄,執行 Git 相關命令後的回撥指令碼,預設會有一些模板│ ├── update.sample│ ├── pre-receive.sample│ └── ...├── info // 儲存一些額外的倉庫資訊如 refs、exclude、attributes 等│ └── exclude├── objects // 元資料儲存中心│ ├── info│ └── pack└── refs // 存放引用資訊,也就是分支、標籤├── heads└── tags
預設初始化生成的 Git 倉庫就只有這些檔案,除此之外還存在一些其它型別的檔案和目錄如packed-refs modules logs等,這些檔案都有特定的用途,都是在特定的操作或者配置後才會出現,這裡我們只關注核心儲存的實現,這些額外檔案或目錄的作用及使用場景再可自行翻閱文件,這裡僅介紹核心的一些檔案。
hooks 目錄hooks 目錄主要儲存的是 Git 鉤子,Git 鉤子可以在很多事件發生後或者發生前觸發,能夠提供給我們非常靈活的使用方式,預設情況下全部都是帶.sample字尾的,需要移除這個字尾並賦予可執行許可權方可生效,下面列舉下常用的一些鉤子及其常見的用途:
客戶端鉤子:
pre-commit:提交前觸發,比如檢查提交資訊是否規範,測試是否執行完畢,程式碼格式是否符合要求post-commit:相反,這個是整個提交完成後觸發,可以用來發通知服務端鉤子:
pre-receive:服務端接收推送請求首先被呼叫的指令碼,可以檢測這些被推送的引用是否符合要求update:與 pre-receive 相似,但是 pre-receive 只會執行一次,而 update 將會為每一個推送的分支分別執行一次post-receive:整個推送過程完成後觸發,可以用來發送通知、觸發構建系統等objects 目錄如上一節我們提到的,Git 將所有接收到的內容生成物件檔案儲存在這個目錄下,我們透過 git hash-object 生成了一個物件並寫入了 Git 倉庫,這個物件的鍵值是 9f4d96d5b00d98959ea9960f069585ce42b1349a,這個時候我們來檢視下 objects 目錄的結構:
➜ Zoker git:(master) ✗ git hash-object testfile -w9f4d96d5b00d98959ea9960f069585ce42b1349a➜ Zoker git:(master) ✗ tree .git/objects.git/objects├── 9f│ └── 4d96d5b00d98959ea9960f069585ce42b1349a├── info└── pack
可以看到 objects 目錄已經有了新的內容,多了一個 9f 的資料夾以及其中的檔案,這個檔案就是插入到 Git 倉庫的內容的物件檔案,Git 取其鍵值的前兩個字母作為資料夾,將後面的字母作為物件檔案的檔名進行儲存,這裡(也就是objects/[0-9a-f][0-9a-f])所儲存的物件我們一般稱為 loose objects 或者 unpacked objects,也就是鬆散物件。
除了物件的儲存資料夾,細心的同學應該已經注意到了 objects/pack 資料夾的存在,這裡對應的是打包後的檔案,為了節省空間和提升效率,當儲存庫中有過多的鬆散物件檔案或者手動執行 git gc 命令時,亦或是推送拉取的傳輸過程中,Git 都會將這些鬆散的物件檔案打包成pack檔案來提升效率,這裡存放的就是這些打包後的檔案:
➜ objects git:(master) git gc...Compressing objects: 100% (75/75), done....➜ objects git:(master) tree.├─ pack├── pack-fe24a22b0313342a6732cff4759bedb25c2ea55d.idx└── pack-fe24a22b0313342a6732cff4759bedb25c2ea55d.pack└── ...
可以看到 objects 目錄已經沒有了鬆散物件,取而代之的是 pack 目錄的兩個檔案,一個是打包後的檔案,另一個是對這個打包的內容進行索引的 idx 檔案,方便查詢某個物件是否在這個對應的 pack 包內。
需要注意的是,如果在剛剛我們手動建立的一個 blob 物件的倉庫進行 GC,將不會產生任何效果,因為這個時候整個 Git 倉庫並沒有任何一個引用指向這個物件,我們說這個物件是遊離的,下面我們來介紹下儲存引用的目錄。
refs 目錄refs 目錄儲存我們的引用(references),引用可以看做是對一個版本號的別名,它儲存的實際就是某一個 Commit 的 SHA 值,上面我們用來測試的倉庫並沒有任何一個提交,所以只有一個空的目錄結構。
└── refs├── heads└── tags
我們隨便找一個包含提交的倉庫檢視他的預設分支 master。
➜ .git git:(master) cat refs/heads/master87e917616712189ecac8c4890fe7d2dc2d554ac6
可以看到這個master的引用只是儲存了一個 Commit 的 SHA 值,好處當然就是我們不需要記著那長長的一串 SHA 值,我們只需要用master這個別名就可以獲取到這個版本。同樣的 tags 目錄下儲存的就是我們的標籤,與分支不同的是,標籤的所記錄的引用值一般是不會變化的,而分支可以我們的版本變化而變化。除此之外,還可能會看到 refs/remotes refs/fetch 等目錄,這些裡面儲存的是特定名稱空間的引用。
還有一種情況,就是上面我們講到的 GC 機制,如果一個倉庫執行了 GC,那麼不僅objects目錄下的鬆散物件會被打包,refs下面的引用同樣也會被打包,只不過它存放在裸倉庫的根目錄下 .git/packed-refs
➜ .git git:(master) cat packed-refs# pack-refs with: peeled fully-peeled sorted87e917616712189ecac8c4890fe7d2dc2d554ac6 refs/heads/master
當我們需要訪問分支 master 的時候,Git 會首先去 refs/heads 裡面進行查詢,如果找不到就會前往 .git/packed-refs 進行查詢,將所有的引用打包到一個檔案無疑提升了不少效率。需要注意的是,如果我們在這個時候往 master 分支上更新了一些提交,這個時候 Git 並不會直接修改 .git/packed-refs檔案,它會直接在refs/heads/下重新建立一個master引用,包含最新的提交的 SHA 值,根據剛剛我們介紹的 Git 的機制,Git 會首先在 refs/heads/ 查詢,找不到才會去 .git/packed-refs 查詢。
那麼引用裡面儲存的 Commit 的這串 SHA 值到底是指向什麼內容呢,我們可以使用之前檢視 blob 物件內容的 cat-file 命令進行檢視:
➜ .git git:(master) git cat-file -p 87e917616712189ecac8c4890fe7d2dc2d554ac6tree aab1a9217aa6896ef46d3e1a90bc64e8178e1662 // 指向的 tree 物件parent 7d000309cb780fa27898b4d103afcfa95a8c04db // 父提交author Zoker <[email protected]> 1607958804 +0800 // 作者資訊committer Zoker <[email protected]> 1607958804 +0800 // 提交者資訊test ssh // 提交資訊
它是一個 commit 型別的物件,主要的屬性是它指向的 tree 物件,它的父提交(如果它是第一個提交,那麼這裡是 0000000...),以及作者和提交資訊。
那麼 commit 物件是什麼?它所指向的 tree 物件又是什麼?與之前我們手工建立的 blob 物件有什麼差別?接下來我們來談談 Git 儲存物件。
Git 儲存物件在 Git 的世界裡,一共有四種類型的儲存物件: 檔案(blob)、樹(tree)、提交(commit)、標籤(tag),這裡我們主要探討頭三種類型,因為這三種是最基礎的 Git 元資料,而標籤物件只是一個包含了額外屬性資訊的 Tag 而已,也就是附註標籤(annotated tag),這裡不再過多的介紹。
輕量標籤(lightweight)與附註標籤(annotated)介紹: https://git-scm.com/book/zh/v2 ... %25BE
Blob 物件在介紹 Git 本質的時候,為了演示 Git 是一個基於內容定址的 KV 資料庫,我們向 Git 倉庫插入了一個檔案的內容:
➜ Zoker git:(master) ✗ cat testfileHello Git➜ Zoker git:(master) ✗ git hash-object testfile -w9f4d96d5b00d98959ea9960f069585ce42b1349a
這個 Key 為 9f4d96d5b00d98959ea9960f069585ce42b1349a 的 Git 物件實際上就是一個 Blob 物件,他儲存了這個 testfile 檔案的值,我們可以使用 cat-file 命令來進行檢視:
➜ Zoker git:(master) ✗ git cat-file -p 9f4d96d5b00d98959ea9960f069585ce42b1349aHello Git
每一次我們修改檔案,Git 都會完整的儲存一份這個檔案的快照而非記錄差異,所以如果我們修改了testfile檔案的內容再次存入到 Git 倉庫中的時候,Git 會基於當前最新的內容來生成它的 Key,需要注意的是當內容不變的時候,它的 Key 值是固定的,畢竟我們前面也說了,Git 是一個基於內容定址的 KV 資料庫。
另外,這裡的 Blob 物件儲存的是文字內容,它還可以是二進位制內容,但是這裡並不建議使用 Git 管理二進位制檔案的版本。我們 Gitee 平臺在日常運營過程中遇到最多的問題就是使用者倉庫過大,這種情況一般都是使用者提交了大的二進位制檔案導致的,因為每次檔案的變更記錄的是快照,所以這個二進位制檔案如果變更頻繁,它佔用的空間是倍增的。而且對於文字內容的 Blob,Git 在 GC 的過程中會只儲存兩次提交之間的檔案差異,是可以達到節省空間的效果的,但是對於二進位制內容的 Blob 是無法像文字內容的 Blob 那樣處理的,所以儘量不要把頻繁變動的二進位制內容儲存到 Git 倉庫,可以使用 LFS 的方式進行儲存。如果已經存在了大量的二進位制檔案,可以使用filter-branch進行瘦身,新加入的同事在首次 Clone 倉庫的時候肯定會感激你的。
LFS 的使用: https://gitee.com/help/articles/4235 ,大倉庫的瘦身: https://gitee.com/help/articles/4232 ,filter-branch: https://github.com/git/git/blo ... h.txt
到了這裡是不是覺得哪裡不對勁?沒錯,這個 Blob 物件只儲存了這個檔案的內容,卻沒有記錄檔名,那我們該怎麼知道這個內容是屬於哪個檔案的啊?答案是 Git 的另外一個重要的物件:Tree 物件。
Tree 物件在 Git 中,Tree 物件主要的作用是將多個 Blob 或者 子 Tree 物件組織到一起,所有的內容都是透過 Tree 和 Blob 型別的物件進行儲存的。一個 Tree 物件包含了一個或者多個 Tree Entry(樹物件記錄),每個樹物件記錄都包含了一個指向 Blob 或者子 Tree SHA 值的指標,還有它們對應的檔名等資訊,其實就可以理解為索引檔案系統中的 inode 和 block 的關係,圖示一個 Tree 物件的話,如下圖:
這個 Tree 物件對應的目錄結構就是下面這樣的:
.├── LICENSE├── readme.md└── src├── libssl.so└── logo.png
透過這種方式,我們可以像組織 Linux 下目錄的方式一樣來結構化的儲存我們倉庫的內容,把 Tree 看作目錄結構,把 Blob 看作具體的檔案內容。
那麼該如何建立一個 Tree 物件呢?在 Git 中是根據暫存區的狀態來建立對應的 Tree 物件的,這裡的暫存區其實就是我們日常在使用 Git 的過程中所理解的暫存區(Staged),一般我們使用 git add 命令將某些檔案新增到暫存區待提交。在沒有任何提交的空倉庫裡,這個暫存區的狀態就是你透過 git add 所新增的那些檔案,如:
➜ Zoker git:(master) ✗ git statusOn branch masterNo commits yetChanges to be committed:(use "git rm --cached <file>..." to unstage)new file: LICENSEnew file: readme.mdUntracked files:(use "git add <file>..." to include in what will be committed)src/
這裡當前的暫存區狀態就是在根目錄有兩個檔案,暫存區的狀態是儲存在 .git/index 檔案的,我們使用 file 命令來看看它是什麼:
➜ Zoker git:(master) ✗ file .git/index.git/index: Git index, version 2, 2 entries
可以發現在 index 檔案中有兩個 entry,也就是根目錄的兩個檔案 LICENSE 和 readme.md。對於已經有提交的倉庫,如果暫存區沒有任何內容,那麼這個 index 表示的就是當前版本的目錄樹狀態,如果修改或者增刪了檔案,並且加入了暫存區,那麼 index 就會發生改變,將相關檔案的指標指向該檔案新的 Blob 物件的 SHA 值。
所以如果想要建立一個 Tree 物件,我們需要往暫存區放點東西,除了使用 git add,我們還可以使用底層命令 update-index 來建立一個暫存區。接下來我們根據上面已經建立好的 testfile 檔案來建立一個樹物件,首先就是將檔案 testfile 加入到暫存區:
➜ Zoker git:(master) ✗ git update-index --add testfile // 與 git add testfile 一樣➜ Zoker git:(master) ✗ git statusOn branch masterNo commits yetChanges to be committed:(use "git rm --cached <file>..." to unstage)new file: testfile
這個過程 Git 主要是先把testfile的內容以 Blob 的形式插入到 Git 倉庫,然後將返回的這個 Blob 的 SHA 值記錄到index中,告訴暫存區目前這個檔案的內容是哪個。
➜ Zoker git:(master) ✗ tree .git/objects.git/objects├── 9f│ └── 4d96d5b00d98959ea9960f069585ce42b1349a├── info└── pack3 directories, 1 file➜ Zoker git:(master) ✗ git cat-file -p 9f4d96d5b00d98959ea9960f069585ce42b1349aHello Git
Git 在執行update-index命令的時候,把指定檔案的內容儲存為 Blob 物件,並且記錄在index檔案狀態內。由於在之前我們已經透過 git hash-object 命令將這個檔案的內容插入過了,並且我們可以發現因為內容不變,所以生成的這個 Blob 物件的 SHA 值也是一致的,如果像我們這樣已經做過插入的動作,下面的命令是等效的:
git update-index --add --cacheinfo 9f4d96d5b00d98959ea9960f069585ce42b1349a testfile
這個命令其實就是把之前已經生成的 Blob 物件放到暫存區,並且指定它的檔名字是 testfile。由於我們的暫存區已經有一個檔案 testfile,所以我接下來我們可以使用 git write-tree 命令來基於當前暫存區的狀態來建立一個 Tree 物件了:
➜ Zoker git:(master) ✗ git write-treeaa406ee8804971cf8edfd8c89ff431b0462e250c➜ Zoker git:(master) ✗ tree .git/objects.git/objects├── 9f│ └── 4d96d5b00d98959ea9960f069585ce42b1349a├── aa│ └── 406ee8804971cf8edfd8c89ff431b0462e250c├── info└── pack
執行完命令後,Git 會基於當前暫存區的狀態生成一個 SHA 值為aa406ee8804971cf8edfd8c89ff431b0462e250c的 Tree 物件,並把這個 Tree 物件像 Blob 物件一樣儲存在.git/objects目錄下。
➜ Zoker git:(master) ✗ git cat-file -p aa406ee8804971cf8edfd8c89ff431b0462e250c100644 blob 9f4d96d5b00d98959ea9960f069585ce42b1349a testfile
使用 cat-file 命令檢視這個 Tree 物件,可以看到這個物件下只有一個檔案,名為testfile。
我們繼續建立第二個 Tree 物件,我們需要第二個 Tree 物件下有修改後的testfile檔案,有新增的 testfile2檔案,並且需要把第一個 Tree 物件作為 第二個 Tree 物件的duplicate目錄。首先我們先把修改後的testfile和新增的testfile2檔案加入到暫存區:
➜ Zoker git:(master) ✗ git update-index testfile➜ Zoker git:(master) ✗ git update-index --add testfile2➜ Zoker git:(master) ✗ git statusOn branch masterNo commits yetChanges to be committed:(use "git rm --cached <file>..." to unstage)new file: testfilenew file: testfile2
緊接著我們需要把第一個 Tree 物件掛到 duplicate 目錄下,我們可以使用 read-tree 命令來實現:
➜ Zoker git:(master) ✗ git read-tree --prefix=duplicate aa406ee8804971cf8edfd8c89ff431b0462e250c ➜ Zoker git:(master) ✗ git statusOn branch masterNo commits yetChanges to be committed:(use "git rm --cached <file>..." to unstage)new file: duplicate/testfilenew file: testfilenew file: testfile2
然後我們執行 write-tree 並透過 cat-file 檢視第二個 Tree 物件:
➜ Zoker git:(master) ✗ git write-tree64d62cef754e6cc995ed8d34f0d0e233e1dfd5d1➜ Zoker git:(master) ✗ git cat-file -p 64d62cef754e6cc995ed8d34f0d0e233e1dfd5d1040000 tree aa406ee8804971cf8edfd8c89ff431b0462e250c duplicate100644 blob 106287c47fd25ad9a0874670a0d5c6eacf1bfe4e testfile100644 blob 098ffe6f84559f4899edf119c25d276dc70607cf testfile2
成功完成了,我們不僅修改了 testfile 的檔案內容,還新增了一個 testfile2 檔案,並且還把第一個 Tree 物件當作第二個 Tree 物件的 duplicate 目錄了,這個時候 Tree 物件看起來應該是這樣的:
至此,我們知道了如何手動建立一個 Tree 物件,但是後面如果我需要這兩個不同的 Tree 的快照該怎麼辦?總不能都記住這三個 Tree 物件的 SHA 值吧?沒錯,記起來費老大勁了,關鍵是還不知道是誰在什麼時間為了什麼而建立的這個快照,而 Commit 物件(提交物件)就能夠幫我們解決這個問題。
Commit 物件Commit 物件主要是為了記錄快照的一些附加資訊,並且維護快照之間的線性關係。我們可以透過git commit-tree命令來建立一個提交,這個命令看字面意思就知道,它是用來將 Tree 物件提交為一個 Commit 物件的命令:
➜ Zoker git:(master) ✗ git commit-tree -husage: git commit-tree [(-p <parent>)...] [-S[<keyid>]] [(-m <message>)...] [(-F <file>)...] <tree>-p <parent> id of a parent commit object-m <message> commit message-F <file> read commit log message from file-S, --gpg-sign[=<key-id>] GPG sign commit
關鍵的兩個引數是 -p 和 -m,-p 是指定這個提交的父提交,如果是初始的第一個提交,那這裡可以忽略;-m 則是指定本次提交的資訊,主要是用來描述提交的原因。我們來把第一個 Tree 物件作為我們的初始提交:
➜ Zoker git:(master) ✗ git commit-tree -m "init commit" aa406ee8804971cf8edfd8c89ff431b0462e250c17ae181bd6c3e703df7851c0f7ea01d9e33a675b
使用cat-file來檢視這個提交:
tree aa406ee8804971cf8edfd8c89ff431b0462e250cauthor Zoker <[email protected]> 1613225370 +0800committer Zoker <[email protected]> 1613225370 +0800init commit
Commit 所儲存的內容是一個 Tree 物件,並且記錄了提交者、提交時間以及提交資訊。我們基於這個 Commit 將第二個 Tree 物件作為引用:
➜ Zoker git:(master) ✗ git commit-tree -p 17ae181bd -m "add dir" 64d62cef754e6cc995ed8d34f0d0e233e1dfd5d1de96a74725dd72c10693c4896cb74e8967859e58➜ Zoker git:(master) ✗ git cat-file -p de96a74725dd72c10693c4896cb74e8967859e58tree 64d62cef754e6cc995ed8d34f0d0e233e1dfd5d1parent 17ae181bd6c3e703df7851c0f7ea01d9e33a675bauthor Zoker <[email protected]> 1613225850 +0800committer Zoker <[email protected]> 1613225850 +0800add dir
我們可以使用 git log 來檢視這兩個提交,這裡新增 --stat 引數檢視檔案變更記錄:
commit de96a74725dd72c10693c4896cb74e8967859e58Author: Zoker <[email protected]>Date: Sun Feb 13 22:17:30 2021 +0800add dirduplicate/testfile | 1 +testfile | 2 +-testfile2 | 1 +3 files changed, 3 insertions(+), 1 deletion(-)commit 17ae181bd6c3e703df7851c0f7ea01d9e33a675bAuthor: Zoker <[email protected]>Date: Sun Feb 13 22:09:30 2021 +0800init committestfile | 1 +1 file changed, 1 insertion(+)
這個時候整個物件的結構如下圖:
練習:使用底層命令建立一個提交僅使用我們上面提到的hash-object write-tree read-tree commit-tree等底層命令來建立一個提交,思考哪些過程是與git add git commit等價的。
物件儲存方式我們透過前面的介紹,知道了 Git 是將資料以不同的物件型別歸納,並且根據內容計算出一個 SHA 值用來作為定址,那麼到底是如何計算的呢?以 Blob 物件為例,Git 主要是做了如下幾步:
識別物件的型別,構造頭部資訊,以型別 + 內容位元組數 + 空位元組作為頭部資訊如 blob 151\u0000將頭部資訊與內容拼接,並且計算 SHA-1 校驗和透過 zlib 壓縮內容透過 SHA 值將其內容放到對應的 objects 目錄整個過程就做了這些事情,Tree 物件和 Commit 物件也差不多,只是頭部型別有所差異而已,這裡不再贅述,《Pro Git 2》在 Git 內部原理章節中有介紹如何使用 Ruby 來實現同等的邏輯,感興趣的可以自行翻閱。
Git-內部原理: https://git-scm.com/book/zh/v2 ... %25A1
Git 引用我們在上面透過 git log --stat 17ae181b 能夠檢視第一個版本的相關資訊,並且可以透過這串 SHA 值拿到這個快照的內容,但是還是挺麻煩的,因為我們要記住一串毫無意義的字串,這個時候 Git 的引用就派上用場了,在 Git 目錄結構章節我們已經介紹了refs目錄,我們知道在引用中儲存的就是 Commit 物件的鍵值,也就是這個物件的 SHA 值,既然如此,我們就給我們當前的版本起一個有意義的名字,一般我們會拿master作為預設分支引用:
➜ Zoker git:(master) ✗ echo "17ae181bd6c3e703df7851c0f7ea01d9e33a675b" >> .git/refs/heads/master➜ Zoker git:(master) ✗ tree .git/refs.git/refs├── heads│ └── master└── tags
這個時候,master 裡面儲存了我們的第一個 Commit 的 SHA 值,我們可以使用 master 來代替 17ae181b 這串毫無意義的字串了。
➜ Zoker git:(master) ✗ git cat-file -p mastertree aa406ee8804971cf8edfd8c89ff431b0462e250cauthor Zoker <[email protected]> 1613916447 +0800committer Zoker <[email protected]> 1613916447 +0800init commit
但是,這個並不是我們最新的版本,我們最新的版本是第二個提交 de96a74725dd72c10693c4896cb74e8967859e58,同樣的,我們可以把refs/heads/master的內容更改為這個提交的 SHA 值,但是這裡我們使用一個底層命令來完成。
➜ Zoker git:(master) ✗ git update-ref refs/heads/master de96a74725dd72c10693c4896cb74e8967859e58➜ Zoker git:(master) ✗ cat .git/refs/heads/masterde96a74725dd72c10693c4896cb74e8967859e58
這個時候,分支 master 就指向了我們最新的版本。
原文連結: https://zoker.io/blog/talk-about-git-internals