在 Git 倉庫中,文件究竟被存儲在哪裡?
大家好!今天我和一個朋友討論 Git 的工作原理,我們感到奇怪,Git 是如何存儲你的文件的?我們知道它存儲在 .git
目錄中,但具體到 .git
中的哪個位置,各個版本的歷史文件又被存儲在哪裡呢?
以這個博客為例,其文件存儲在一個 Git 倉庫中,其中有一個文件名為 content/post/2019-06-28-brag-doc.markdown
。這個文件在我的 .git
文件夾中具體的位置在哪裡?過去的文件版本又被存儲在哪裡?那麼,就讓我們通過編寫一些簡短的 Python 代碼來探尋答案吧。
Git 把文件存儲在 .git/objects 之中
你的倉庫中,每一個文件的歷史版本都被儲存在 .git/objects
中。比如,對於這個博客,.git/objects
包含了 2700 多個文件。
$ find .git/objects/ -type f | wc -l
2761
注意:
.git/objects
包含的信息,不僅僅是 「倉庫中每一個文件的所有先前版本」,但我們暫不詳細討論這一內容。
這裡是一個簡短的 Python 程序(find-git-object.py),它可以幫助我們定位在 .git/objects
中的特定文件的具體位置。
import hashlib
import sys
def object_path(content):
header = f"blob {len(content)} "
data = header.encode() + content
sha1 = hashlib.sha1()
sha1.update(data)
digest = sha1.hexdigest()
return f".git/objects/{digest[:2]}/{digest[2:]}"
with open(sys.argv[1], "rb") as f:
print(object_path(f.read()))
此程序的主要操作如下:
- 讀取文件內容
- 計算一個頭部(
blob 16673
),並將其與文件內容合併 - 計算出文件的 sha1 校驗和(此處為
e33121a9af82dd99d6d706d037204251d41d54
) - 將這個 sha1 校驗和轉換為路徑(如
.git/objects/e3/3121a9af82dd99d6d706d037204251d41d54
)
運行的方法如下:
$ python3 find-git-object.py content/post/2019-06-28-brag-doc.markdown
.git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
術語解釋:「內容定址存儲」
這種存儲策略的術語為「 內容定址存儲 」,它指的是對象在資料庫中的文件名與文件內容的哈希值相同。
內容定址存儲的有趣之處就是,假設我有兩份或許多份內容完全相同的文件,在 Git 的資料庫中,並不會因此佔用額外空間。如果內容的哈希值是 aabbbbbbbbbbbbbbbbbbbbbbbbb
,它們都會被存儲在 .git/objects/aa/bbbbbbbbbbbbbbbbbbbbb
中。
這些對象是如何進行編碼的?
如果我嘗試在 .git/objects
目錄下查看這個文件,顯示的內容似乎有一些奇怪:
$ cat .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
x^A<8D><9B>}s<E3>Ƒ<C6><EF>o|<8A>^Q<9D><EC>ju<92><E8><DD><9C><9C>*<89>j<FD>^...
這是怎麼回事呢?讓我們來運行 file
命令檢查一下:
$ file .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
.git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54: zlib compressed data
原來,它是壓縮的!我們可以編寫一個小巧的 Python 程序—— decompress.py
,然後用 zlib
模塊去解壓這些數據:
import zlib
import sys
with open(sys.argv[1], "rb") as f:
content = f.read()
print(zlib.decompress(content).decode())
讓我們來解壓一下看看結果:
$ python3 decompress.py .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
blob 16673title: "Get your work recognized: write a brag document"
date: 2019-06-28T18:46:02Z
url: /blog/brag-documents/
categories: []
... the entire blog post ...
結果顯示,這些數據的編碼方式非常簡單:首先有 blob 16673
標識,其後就是文件的全部內容。
這裡並沒有差異性數據(diff)
這裡有一件我第一次知道時讓我感到驚訝的事:這裡並沒有任何差異性數據!那個文件是該篇博客文章的第 9 個版本,但 Git 在 .git/objects
目錄中存儲的版本是完整文件內容,而並非與前一版本的差異。
儘管 Git 實際上有時候會以差異性數據存儲文件(例如,當你運行 git gc
時,為了提升效率,它可能會將多個不同的文件封裝成 「打包文件」),但在我個人經驗中,我從未需要關注這個細節,所以我們不在此深入討論。然而,關於這種格式如何工作,Aditya Mukerjee 有篇優秀的文章 《拆解 Git 的打包文件》。
博客文章的舊版本在哪?
你可能會好奇:如果在我修復了一些錯別字之前,這篇博文已經存在了 8 個版本,那它們在 .git/objects
目錄中的位置是哪裡?我們如何找到它們呢?
首先,我們來使用 git log
命令來查找改動過這個文件的每一個提交:
$ git log --oneline content/post/2019-06-28-brag-doc.markdown
c6d4db2d
423cd76a
7e91d7d0
f105905a
b6d23643
998a46dd
67a26b04
d9999f17
026c0f52
72442b67
然後,我們選擇一個之前的提交,比如 026c0f52
。提交也被存儲在 .git/objects
中,我們可以嘗試在那裡找到它。但是失敗了!因為 ls .git/objects/02/6c*
沒有顯示任何內容!如果有人告訴你,「我們知道有時 Git 會打包對象來節省空間,我們並不需過多關心它」,但現在,我們需要去面對這個問題了。
那就讓我們去解決它吧。
讓我們開始解包一些對象
現在我們需要從打包文件中解包出一些對象。我在 Stack Overflow 上查找了一下,看起來我們可以這樣進行操作:
$ mv .git/objects/pack/pack-adeb3c14576443e593a3161e7e1b202faba73f54.pack .
$ git unpack-objects < pack-adeb3c14576443e593a3161e7e1b202faba73f54.pack
這種直接對庫進行手術式的做法讓人有些緊張,但如果我誤操作了,我還可以從 Github 上重新克隆這個庫,所以我並不太擔心。
解包所有的對象文件後,我們得到了更多的對象:大約有 20000 個,而不是原來的大約 2700 個。看起來很酷。
find .git/objects/ -type f | wc -l
20138
我們回頭再看看提交
現在我們可以繼續看看我們的提交 026c0f52
。我們之前說過 .git/objects
中並不都是文件,其中一部分是提交!為了弄清楚我們的舊文章 content/post/2019-06-28-brag-doc.markdown
是在哪裡被保存的,我們需要深入查看這個提交。
首先,我們需要在 .git/objects
中查看這個提交。
查看提交的第一步:找到提交
經過解包後,我們現在可以在 .git/objects/02/6c0f5208c5ea10608afc9252c4a56c1ac1d7e4
中找到提交 026c0f52
,我們可以用下面的方法去查看它:
$ python3 decompress.py .git/objects/02/6c0f5208c5ea10608afc9252c4a56c1ac1d7e4
commit 211tree 01832a9109ab738dac78ee4e95024c74b9b71c27
parent 72442b67590ae1fcbfe05883a351d822454e3826
author Julia Evans <julia@jvns.ca> 1561998673 -0400
committer Julia Evans <julia@jvns.ca> 1561998673 -0400
brag doc
我們也可以用 git cat-file -p 026c0f52
命令來獲取相同的信息,這個命令能起到相同的作用,但是它在格式化數據時做得更好一些。(-p
選項意味著它能夠以更友好的方式進行格式化)
查看提交的第二步:找到樹
這個提交包含一個樹。樹是什麼呢?讓我們看一下。樹的 ID 是 01832a9109ab738dac78ee4e95024c74b9b71c27
,我們可以使用先前的 decompress.py
腳本查看這個 Git 對象,儘管我不得不移除 .decode()
才能避免腳本崩潰。
$ python3 decompress.py .git/objects/01/832a9109ab738dac78ee4e95024c74b9b71c27
這個輸出的格式有些難以閱讀。主要的問題在於,該提交的哈希(xc3xf7$8x9bx8dOx19/x18xb7}|xc7xcex8e…
)是原始位元組,而沒有進行十六進位的編碼,因此我們看到 xc3xf7$8x9bx8d
而非 c3f76024389b8d
。我打算切換至 git cat-file -p
命令,它能以更友好的方式顯示數據,我不想自己編寫一個解析器。
$ git cat-file -p 01832a9109ab738dac78ee4e95024c74b9b71c27
100644 blob c3f76024389b8d4f192f18b77d7cc7ce8e3a68ad .gitignore
100644 blob 7ebaecb311a05e1ca9a43f1eb90f1c6647960bc1 README.md
100644 blob 0f21dc9bf1a73afc89634bac586271384e24b2c9 Rakefile
100644 blob 00b9d54abd71119737d33ee5d29d81ebdcea5a37 config.yaml
040000 tree 61ad34108a327a163cdd66fa1a86342dcef4518e content <-- 這是我們接下來的目標
040000 tree 6d8543e9eeba67748ded7b5f88b781016200db6f layouts
100644 blob 22a321a88157293c81e4ddcfef4844c6c698c26f mystery.rb
040000 tree 8157dc84a37fca4cb13e1257f37a7dd35cfe391e scripts
040000 tree 84fe9c4cb9cef83e78e90a7fbf33a9a799d7be60 static
040000 tree 34fd3aa2625ba784bced4a95db6154806ae1d9ee themes
這是我在這次提交時庫的根目錄中所有的文件。看起來我曾經不小心提交了一個名為 mystery.rb
的文件,後來我刪除了它。
我們的文件在 content
目錄中,接下來讓我們看看那個樹:61ad34108a327a163cdd66fa1a86342dcef4518e
查看提交的第三步:又一棵樹
$ git cat-file -p 61ad34108a327a163cdd66fa1a86342dcef4518e
040000 tree 1168078878f9d500ea4e7462a9cd29cbdf4f9a56 about
100644 blob e06d03f28d58982a5b8282a61c4d3cd5ca793005 newsletter.markdown
040000 tree 1f94b8103ca9b6714614614ed79254feb1d9676c post <-- 我們接下來的目標!
100644 blob 2d7d22581e64ef9077455d834d18c209a8f05302 profiler-project.markdown
040000 tree 06bd3cee1ed46cf403d9d5a201232af5697527bb projects
040000 tree 65e9357973f0cc60bedaa511489a9c2eeab73c29 talks
040000 tree 8a9d561d536b955209def58f5255fc7fe9523efd zines
還未結束……
查看提交的第四步:更多的樹……
我們要尋找的文件位於 post/
目錄,因此我們需要進一步探索:
$ git cat-file -p 1f94b8103ca9b6714614614ed79254feb1d9676c
.... 省略了大量行 ...
100644 blob 170da7b0e607c4fd6fb4e921d76307397ab89c1e 2019-02-17-organizing-this-blog-into-categories.markdown
100644 blob 7d4f27e9804e3dc80ab3a3912b4f1c890c4d2432 2019-03-15-new-zine--bite-size-networking-.markdown
100644 blob 0d1b9fbc7896e47da6166e9386347f9ff58856aa 2019-03-26-what-are-monoidal-categories.markdown
100644 blob d6949755c3dadbc6fcbdd20cc0d919809d754e56 2019-06-23-a-few-debugging-resources.markdown
100644 blob 3105bdd067f7db16436d2ea85463755c8a772046 2019-06-28-brag-doc.markdown <-- 我們找到了!!!
在此,2019-06-28-brag-doc.markdown
之所以位於列表最後,是因為在發布時它是最新的博文。
查看提交的第五步:我們終於找到它!
經過努力,我們找到了博文歷史版本所在的對象文件!太棒了!它的哈希值是 3105bdd067f7db16436d2ea85463755c8a772046
,因此它位於 git/objects/31/05bdd067f7db16436d2ea85463755c8a772046
。
我們可以使用 decompress.py
來查看它:
$ python3 decompress.py .git/objects/31/05bdd067f7db16436d2ea85463755c8a772046 | head
blob 15924title: "Get your work recognized: write a brag document"
date: 2019-06-28T18:46:02Z
url: /blog/brag-documents/
categories: []
... 文件的剩餘部分在此 ...
這就是博文的舊版本!如果我執行命令 git checkout 026c0f52 content/post/2019-06-28-brag-doc.markdown
或者 git restore --source 026c0f52 content/post/2019-06-28-brag-doc.markdown
,我就會獲取到這個版本。
這樣遍歷樹就是 git log 的運行機制
我們剛剛經歷的整個過程(找到提交、逐層遍歷目錄樹、搜索所需文件名)看似繁瑣,但實際上當我們執行 git log content/post/2019-06-28-brag-doc.markdown
時,背後就是這樣在運行。它需要逐個檢查你歷史記錄中的每一個提交,在每個提交中核查 content/post/2019-06-28-brag-doc.markdown
的版本(例如在這個案例中為 3105bdd067f7db16436d2ea85463755c8a772046
),並查看它是否自上一提交以來有所改變。
這就是為什麼有時 git log FILENAME
會執行的有些緩慢 —— 我的這個倉庫中有 3000 個提交,它需要對每個提交做大量的工作,來判斷該文件是否在該提交中發生過變化。
我有多少個歷史版本的文件?
目前,我在我的博客倉庫中跟蹤了 1530 個文件:
$ git ls-files | wc -l
1530
但歷史文件有多少呢?我們可以列出 .git/objects
中所有的內容,看看有多少對象文件:
$ find .git/objects/ -type f | grep -v pack | awk -F/ '{print $3 $4}' | wc -l
20135
但並不是所有這些都代表過去版本的文件 —— 正如我們之前所見,許多都是提交和目錄樹。不過,我們可以編寫一個小小的 Python 腳本 find-blobs.py
,遍歷所有對象並檢查是否以 blob
開頭:
import zlib
import sys
for line in sys.stdin:
line = line.strip()
filename = f".git/objects/{line[0:2]}/{line[2:]}"
with open(filename, "rb") as f:
contents = zlib.decompress(f.read())
if contents.startswith(b"blob"):
print(line)
$ find .git/objects/ -type f | grep -v pack | awk -F/ '{print $3 $4}' | python3 find-blobs.py | wc -l
6713
於是,看起來在我的 Git 倉庫中存放的舊文件版本有 6713 - 1530 = 5183
個,Git 會為我保存這些文件,以備我想著要恢復它們時使用。太好了!
就這些啦!
在 這個 gist 中附上了全部的此篇文章所用代碼,其實沒多少。
我以為我已經對 Git 的工作方式了如指掌,但我以前從未真正涉及過打包文件,所以這次探索很有趣。我也很少思考當我讓 git log
跟蹤一個文件的歷史時,它實際上有多大的工作量,因此也很開心能深入研究這個。
作為一個有趣的後續:我提交這篇博文後,Git 就警告我倉庫中的對象太多(我猜 20,000 太多了!),並運行 git gc
將它們全部壓縮成打包文件。所以現在我的 .git/objects
目錄已經被壓縮得十分小了:
$ find .git/objects/ -type f | wc -l
14
(題圖:MJ/319a396c-6f3f-4891-b051-261312c8ea9a)
via: https://jvns.ca/blog/2023/09/14/in-a-git-repository--where-do-your-files-live-/
作者:Julia Evans 選題:lujun9972 譯者:ChatGPT 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive