大家好!今天我和一個(gè)朋友討論 Git 的工作原理,我們感到奇怪,Git 是如何存儲(chǔ)你的文件的?我們知道它存儲(chǔ)在.git目錄中,但具體到.git中的哪個(gè)位置,各個(gè)版本的歷史文件又被存儲(chǔ)在哪里呢?
以這個(gè)博客為例,其文件存儲(chǔ)在一個(gè) Git 倉(cāng)庫(kù)中,其中有一個(gè)文件名為content/post/2019-06-28-brag-doc.markdown。這個(gè)文件在我的.git文件夾中具體的位置在哪里?過(guò)去的文件版本又被存儲(chǔ)在哪里?那么,就讓我們通過(guò)編寫一些簡(jiǎn)短的 Python 代碼來(lái)探尋答案吧。
Git 把文件存儲(chǔ)在 .git/objects 之中
你的倉(cāng)庫(kù)中,每一個(gè)文件的歷史版本都被儲(chǔ)存在.git/objects中。比如,對(duì)于這個(gè)博客,.git/objects包含了 2700 多個(gè)文件。
$ find .git/objects/ -type f | wc -l
2761
注意:.git/objects包含的信息,不僅僅是 “倉(cāng)庫(kù)中每一個(gè)文件的所有先前版本”,但我們暫不詳細(xì)討論這一內(nèi)容。
這里是一個(gè)簡(jiǎn)短的 Python 程序(find-git-object.py gist.github.com),它可以幫助我們定位在.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()))
此程序的主要操作如下:
?讀取文件內(nèi)容 ?計(jì)算一個(gè)頭部(blob 16673),并將其與文件內(nèi)容合并 ?計(jì)算出文件的 sha1 校驗(yàn)和(此處為e33121a9af82dd99d6d706d037204251d41d54) ?將這個(gè) sha1 校驗(yàn)和轉(zhuǎn)換為路徑(如.git/objects/e3/3121a9af82dd99d6d706d037204251d41d54)
運(yùn)行的方法如下:
$ python3 find-git-object.py content/post/2019-06-28-brag-doc.markdown
.git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
術(shù)語(yǔ)解釋:“內(nèi)容尋址存儲(chǔ)”
這種存儲(chǔ)策略的術(shù)語(yǔ)為“內(nèi)容尋址存儲(chǔ)(content addressed storage)”,它指的是對(duì)象在數(shù)據(jù)庫(kù)中的文件名與文件內(nèi)容的哈希值相同。
內(nèi)容尋址存儲(chǔ)的有趣之處就是,假設(shè)我有兩份或許多份內(nèi)容完全相同的文件,在 Git 的數(shù)據(jù)庫(kù)中,并不會(huì)因此占用額外空間。如果內(nèi)容的哈希值是aabbbbbbbbbbbbbbbbbbbbbbbbb,它們都會(huì)被存儲(chǔ)在.git/objects/aa/bbbbbbbbbbbbbbbbbbbbb中。
這些對(duì)象是如何進(jìn)行編碼的?
如果我嘗試在.git/objects目錄下查看這個(gè)文件,顯示的內(nèi)容似乎有一些奇怪:
$ cat .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
x^A<8D><9B>}s
這是怎么回事呢?讓我們來(lái)運(yùn)行file命令檢查一下:
$ file .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
.git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54: zlib compressed data
原來(lái),它是壓縮的!我們可以編寫一個(gè)小巧的 Python 程序——decompress.py,然后用zlib模塊去解壓這些數(shù)據(jù):
import zlib
import sys
with open(sys.argv[1], "rb") as f:
content = f.read()
print(zlib.decompress(content).decode())
讓我們來(lái)解壓一下看看結(jié)果:
$ python3 decompress.py .git/objects/8a/e33121a9af82dd99d6d706d037204251d41d54
blob 16673---
title: "Get your work recognized: write a brag document"
date: 2019-06-28T18:46:02Z
url: /blog/brag-documents/
categories: []
---
... the entire blog post ...
結(jié)果顯示,這些數(shù)據(jù)的編碼方式非常簡(jiǎn)單:首先有blob 16673標(biāo)識(shí),其后就是文件的全部?jī)?nèi)容。
這里并沒(méi)有差異性數(shù)據(jù)(diff)
這里有一件我第一次知道時(shí)讓我感到驚訝的事:這里并沒(méi)有任何差異性數(shù)據(jù)!那個(gè)文件是該篇博客文章的第 9 個(gè)版本,但 Git 在.git/objects目錄中存儲(chǔ)的版本是完整文件內(nèi)容,而并非與前一版本的差異。
盡管 Git 實(shí)際上有時(shí)候會(huì)以差異性數(shù)據(jù)存儲(chǔ)文件(例如,當(dāng)你運(yùn)行g(shù)it gc時(shí),為了提升效率,它可能會(huì)將多個(gè)不同的文件封裝成 “打包文件”),但在我個(gè)人經(jīng)驗(yàn)中,我從未需要關(guān)注這個(gè)細(xì)節(jié),所以我們不在此深入討論。然而,關(guān)于這種格式如何工作,Aditya Mukerjee 有篇優(yōu)秀的文章 《拆解 Git 的打包文件 codewords.recurse.com》。
博客文章的舊版本在哪?
你可能會(huì)好奇:如果在我修復(fù)了一些錯(cuò)別字之前,這篇博文已經(jīng)存在了 8 個(gè)版本,那它們?cè)?git/objects目錄中的位置是哪里?我們?nèi)绾握业剿鼈兡兀?/p>
首先,我們來(lái)使用git log命令來(lái)查找改動(dòng)過(guò)這個(gè)文件的每一個(gè)提交:
$ git log --oneline content/post/2019-06-28-brag-doc.markdown
c6d4db2d
423cd76a
7e91d7d0
f105905a
b6d23643
998a46dd
67a26b04
d9999f17
026c0f52
72442b67
然后,我們選擇一個(gè)之前的提交,比如026c0f52。提交也被存儲(chǔ)在.git/objects中,我們可以嘗試在那里找到它。但是失敗了!因?yàn)閘s .git/objects/02/6c*沒(méi)有顯示任何內(nèi)容!如果有人告訴你,“我們知道有時(shí) Git 會(huì)打包對(duì)象來(lái)節(jié)省空間,我們并不需過(guò)多關(guān)心它”,但現(xiàn)在,我們需要去面對(duì)這個(gè)問(wèn)題了。
那就讓我們?nèi)ソ鉀Q它吧。
讓我們開(kāi)始解包一些對(duì)象
現(xiàn)在我們需要從打包文件中解包出一些對(duì)象。我在 Stack Overflow 上查找了一下,看起來(lái)我們可以這樣進(jìn)行操作:
$ mv .git/objects/pack/pack-adeb3c14576443e593a3161e7e1b202faba73f54.pack .
$ git unpack-objects < pack-adeb3c14576443e593a3161e7e1b202faba73f54.pack
這種直接對(duì)庫(kù)進(jìn)行手術(shù)式的做法讓人有些緊張,但如果我誤操作了,我還可以從 Github 上重新克隆這個(gè)庫(kù),所以我并不太擔(dān)心。
解包所有的對(duì)象文件后,我們得到了更多的對(duì)象:大約有 20000 個(gè),而不是原來(lái)的大約 2700 個(gè)??雌饋?lái)很酷。
find .git/objects/ -type f | wc -l
20138
我們回頭再看看提交
現(xiàn)在我們可以繼續(xù)看看我們的提交026c0f52。我們之前說(shuō)過(guò).git/objects中并不都是文件,其中一部分是提交!為了弄清楚我們的舊文章content/post/2019-06-28-brag-doc.markdown是在哪里被保存的,我們需要深入查看這個(gè)提交。
首先,我們需要在.git/objects中查看這個(gè)提交。
查看提交的第一步:找到提交
經(jīng)過(guò)解包后,我們現(xiàn)在可以在.git/objects/02/6c0f5208c5ea10608afc9252c4a56c1ac1d7e4中找到提交026c0f52,我們可以用下面的方法去查看它:
$ python3 decompress.py .git/objects/02/6c0f5208c5ea10608afc9252c4a56c1ac1d7e4
commit 211tree 01832a9109ab738dac78ee4e95024c74b9b71c27
parent 72442b67590ae1fcbfe05883a351d822454e3826
author Julia Evans
committer Julia Evans
brag doc
我們也可以用git cat-file -p 026c0f52命令來(lái)獲取相同的信息,這個(gè)命令能起到相同的作用,但是它在格式化數(shù)據(jù)時(shí)做得更好一些。(-p選項(xiàng)意味著它能夠以更友好的方式進(jìn)行格式化)
查看提交的第二步:找到樹(shù)
這個(gè)提交包含一個(gè)樹(shù)。樹(shù)是什么呢?讓我們看一下。樹(shù)的 ID 是01832a9109ab738dac78ee4e95024c74b9b71c27,我們可以使用先前的decompress.py腳本查看這個(gè) Git 對(duì)象,盡管我不得不移除.decode()才能避免腳本崩潰。
$ python3 decompress.py .git/objects/01/832a9109ab738dac78ee4e95024c74b9b71c27
這個(gè)輸出的格式有些難以閱讀。主要的問(wèn)題在于,該提交的哈希(xc3xf7$8x9bx8dOx19/x18xb7}|xc7xcex8e…)是原始字節(jié),而沒(méi)有進(jìn)行十六進(jìn)制的編碼,因此我們看到xc3xf7$8x9bx8d而非c3f76024389b8d。我打算切換至git cat-file -p命令,它能以更友好的方式顯示數(shù)據(jù),我不想自己編寫一個(gè)解析器。
$ git cat-file -p 01832a9109ab738dac78ee4e95024c74b9b71c27
100644 blob c3f76024389b8d4f192f18b77d7cc7ce8e3a68ad.gitignore
100644 blob 7ebaecb311a05e1ca9a43f1eb90f1c6647960bc1README.md
100644 blob 0f21dc9bf1a73afc89634bac586271384e24b2c9Rakefile
100644 blob 00b9d54abd71119737d33ee5d29d81ebdcea5a37config.yaml
040000 tree 61ad34108a327a163cdd66fa1a86342dcef4518econtent <-- 這是我們接下來(lái)的目標(biāo)
040000 tree 6d8543e9eeba67748ded7b5f88b781016200db6flayouts
100644 blob 22a321a88157293c81e4ddcfef4844c6c698c26fmystery.rb
040000 tree 8157dc84a37fca4cb13e1257f37a7dd35cfe391escripts
040000 tree 84fe9c4cb9cef83e78e90a7fbf33a9a799d7be60static
040000 tree 34fd3aa2625ba784bced4a95db6154806ae1d9eethemes
這是我在這次提交時(shí)庫(kù)的根目錄中所有的文件。看起來(lái)我曾經(jīng)不小心提交了一個(gè)名為mystery.rb的文件,后來(lái)我刪除了它。
我們的文件在content目錄中,接下來(lái)讓我們看看那個(gè)樹(shù):61ad34108a327a163cdd66fa1a86342dcef4518e
查看提交的第三步:又一棵樹(shù)
$ git cat-file -p 61ad34108a327a163cdd66fa1a86342dcef4518e
040000 tree 1168078878f9d500ea4e7462a9cd29cbdf4f9a56 about
100644 blob e06d03f28d58982a5b8282a61c4d3cd5ca793005 newsletter.markdown
040000 tree 1f94b8103ca9b6714614614ed79254feb1d9676c post <-- 我們接下來(lái)的目標(biāo)!
100644 blob 2d7d22581e64ef9077455d834d18c209a8f05302 profiler-project.markdown
040000 tree 06bd3cee1ed46cf403d9d5a201232af5697527bb projects
040000 tree 65e9357973f0cc60bedaa511489a9c2eeab73c29 talks
040000 tree 8a9d561d536b955209def58f5255fc7fe9523efd zines
還未結(jié)束……
查看提交的第四步:更多的樹(shù)……
我們要尋找的文件位于post/目錄,因此我們需要進(jìn)一步探索:
$ 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之所以位于列表最后,是因?yàn)樵诎l(fā)布時(shí)它是最新的博文。
查看提交的第五步:我們終于找到它!
經(jīng)過(guò)努力,我們找到了博文歷史版本所在的對(duì)象文件!太棒了!它的哈希值是3105bdd067f7db16436d2ea85463755c8a772046,因此它位于git/objects/31/05bdd067f7db16436d2ea85463755c8a772046。
我們可以使用decompress.py來(lái)查看它:
$ python3 decompress.py .git/objects/31/05bdd067f7db16436d2ea85463755c8a772046 | head
blob 15924---
title: "Get your work recognized: write a brag document"
date: 2019-06-28T18:46:02Z
url: /blog/brag-documents/
categories: []
---
... 文件的剩余部分在此 ...
這就是博文的舊版本!如果我執(zhí)行命令git checkout 026c0f52 content/post/2019-06-28-brag-doc.markdown或者git restore --source 026c0f52 content/post/2019-06-28-brag-doc.markdown,我就會(huì)獲取到這個(gè)版本。
這樣遍歷樹(shù)就是 git log 的運(yùn)行機(jī)制
我們剛剛經(jīng)歷的整個(gè)過(guò)程(找到提交、逐層遍歷目錄樹(shù)、搜索所需文件名)看似繁瑣,但實(shí)際上當(dāng)我們執(zhí)行g(shù)it log content/post/2019-06-28-brag-doc.markdown時(shí),背后就是這樣在運(yùn)行。它需要逐個(gè)檢查你歷史記錄中的每一個(gè)提交,在每個(gè)提交中核查content/post/2019-06-28-brag-doc.markdown的版本(例如在這個(gè)案例中為3105bdd067f7db16436d2ea85463755c8a772046),并查看它是否自上一提交以來(lái)有所改變。
這就是為什么有時(shí)git log FILENAME會(huì)執(zhí)行的有些緩慢 —— 我的這個(gè)倉(cāng)庫(kù)中有 3000 個(gè)提交,它需要對(duì)每個(gè)提交做大量的工作,來(lái)判斷該文件是否在該提交中發(fā)生過(guò)變化。
我有多少個(gè)歷史版本的文件?
目前,我在我的博客倉(cāng)庫(kù)中跟蹤了 1530 個(gè)文件:
$ git ls-files | wc -l
1530
但歷史文件有多少呢?我們可以列出.git/objects中所有的內(nèi)容,看看有多少對(duì)象文件:
$ find .git/objects/ -type f | grep -v pack | awk -F/ '{print $3 $4}' | wc -l
20135
但并不是所有這些都代表過(guò)去版本的文件 —— 正如我們之前所見(jiàn),許多都是提交和目錄樹(shù)。不過(guò),我們可以編寫一個(gè)小小的 Python 腳本find-blobs.py,遍歷所有對(duì)象并檢查是否以blob開(kāi)頭:
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
于是,看起來(lái)在我的 Git 倉(cāng)庫(kù)中存放的舊文件版本有6713 - 1530 = 5183個(gè),Git 會(huì)為我保存這些文件,以備我想著要恢復(fù)它們時(shí)使用。太好了!
就這些啦!
在這個(gè) gist gist.github.com中附上了全部的此篇文章所用代碼,其實(shí)沒(méi)多少。
我以為我已經(jīng)對(duì) Git 的工作方式了如指掌,但我以前從未真正涉及過(guò)打包文件,所以這次探索很有趣。我也很少思考當(dāng)我讓git log跟蹤一個(gè)文件的歷史時(shí),它實(shí)際上有多大的工作量,因此也很開(kāi)心能深入研究這個(gè)。
作為一個(gè)有趣的后續(xù):我提交這篇博文后,Git 就警告我倉(cāng)庫(kù)中的對(duì)象太多(我猜 20,000 太多了!),并運(yùn)行g(shù)it gc將它們?nèi)繅嚎s成打包文件。所以現(xiàn)在我的.git/objects目錄已經(jīng)被壓縮得十分小了:
$ find .git/objects/ -type f | wc -l
14
編輯:黃飛
-
數(shù)據(jù)存儲(chǔ)
+關(guān)注
關(guān)注
5文章
970瀏覽量
50894 -
倉(cāng)庫(kù)
+關(guān)注
文章
20瀏覽量
13539 -
Git
+關(guān)注
關(guān)注
0文章
198瀏覽量
15755
原文標(biāo)題:在 Git 倉(cāng)庫(kù)中,文件究竟被存儲(chǔ)在哪里?
文章出處:【微信號(hào):良許Linux,微信公眾號(hào):良許Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論