章節 ▾ 第二版

10.4 Git 內部原理 - 打包檔案 (Packfiles)

打包檔案 (Packfiles)

如果你按照上一節示例中的所有說明進行操作,現在應該有一個測試 Git 倉庫,其中包含 11 個物件——四個 blob、三個 tree、三個 commit 和一個 tag。

$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/95/85191f37f7b0fb9444f35a9bf50de191beadc2 # tag
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1

Git 使用 zlib 壓縮這些檔案的內容,而且你儲存的內容不多,所以所有這些檔案加起來總共只佔用 925 位元組。現在你將向倉庫新增一些更大的內容,以演示 Git 的一個有趣特性。為了演示,我們將新增 Grit 庫中的 `repo.rb` 檔案——這是一個大約 22K 的原始碼檔案。

$ curl https://raw.githubusercontent.com/mojombo/grit/master/lib/grit/repo.rb > repo.rb
$ git checkout master
$ git add repo.rb
$ git commit -m 'Create repo.rb'
[master 484a592] Create repo.rb
 3 files changed, 709 insertions(+), 2 deletions(-)
 delete mode 100644 bak/test.txt
 create mode 100644 repo.rb
 rewrite test.txt (100%)

如果你檢視生成的 tree,可以看到為新的 `repo.rb` blob 物件計算的 SHA-1 值。

$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5      repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b      test.txt

然後你可以使用 `git cat-file` 檢視該物件有多大。

$ git cat-file -s 033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5
22044

此時,稍微修改一下該檔案,看看會發生什麼。

$ echo '# testing' >> repo.rb
$ git commit -am 'Modify repo.rb a bit'
[master 2431da6] Modify repo.rb a bit
 1 file changed, 1 insertion(+)

檢查上次提交建立的 tree,你會看到一些有趣的東西。

$ git cat-file -p master^{tree}
100644 blob fa49b077972391ad58037050f2a75f74e3671e92      new.txt
100644 blob b042a60ef7dff760008df33cee372b945b6e884e      repo.rb
100644 blob e3f094f522629ae358806b17daf78246c27c007b      test.txt

這個 blob 現在是一個不同的 blob,這意味著儘管你只是在一個 400 行檔案的末尾添加了一行,Git 仍然將這個新內容儲存為一個全新的物件。

$ git cat-file -s b042a60ef7dff760008df33cee372b945b6e884e
22054

你的磁碟上有兩個幾乎相同的 22K 物件(每個壓縮後大約 7K)。如果 Git 可以完整儲存其中一個,然後將第二個物件只儲存為它與第一個物件之間的差異,那不是很好嗎?

事實證明,它可以。Git 在磁碟上儲存物件的初始格式稱為“鬆散(loose)”物件格式。然而,Git 會偶爾將其中幾個物件打包成一個名為“打包檔案(packfile)”的單一二進位制檔案,以節省空間並提高效率。當鬆散物件過多、手動執行 `git gc` 命令或推送到遠端伺服器時,Git 就會這樣做。要檢視會發生什麼,你可以透過呼叫 `git gc` 命令手動要求 Git 打包物件。

$ git gc
Counting objects: 18, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (14/14), done.
Writing objects: 100% (18/18), done.
Total 18 (delta 3), reused 0 (delta 0)

如果你檢視 `objects` 目錄,你會發現大多數物件都消失了,並出現了一對新檔案。

$ find .git/objects -type f
.git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/info/packs
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack

剩下的物件是沒有任何 commit 指向的 blob——在本例中,是“what is up, doc?”示例和你之前建立的“test content”示例 blob。因為你從未將它們新增到任何 commit 中,所以它們被認為是懸空物件,並且不會被打包到你的新打包檔案中。

其他檔案是你的新打包檔案和索引。打包檔案是一個包含從檔案系統移除的所有物件內容的單一檔案。索引檔案包含打包檔案中的偏移量,因此你可以快速查詢特定物件。很酷的是,在你執行 `gc` 命令之前,磁碟上的物件總共約為 15K,而新的打包檔案只有 7K。透過打包物件,你的磁碟使用量減少了一半。

Git 是如何做到這一點的呢?當 Git 打包物件時,它會尋找名稱和大小相似的檔案,並只儲存檔案從一個版本到下一個版本的差異。你可以檢視打包檔案,看看 Git 為節省空間做了什麼。`git verify-pack` 底層命令允許你檢視打包了什麼內容。

$ git verify-pack -v .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
2431da676938450a4d72e260db3bf7b0f587bbc1 commit 223 155 12
69bcdaff5328278ab1c0812ce0e07fa7d26a96d7 commit 214 152 167
80d02664cb23ed55b226516648c7ad5d0a3deb90 commit 214 145 319
43168a18b7613d1281e5560855a83eb8fde3d687 commit 213 146 464
092917823486a802e94d727c820a9024e14a1fc2 commit 214 146 610
702470739ce72005e2edff522fde85d52a65df9b commit 165 118 756
d368d0ac0678cbe6cce505be58126d3526706e54 tag    130 122 874
fe879577cb8cffcdf25441725141e310dd7d239b tree   136 136 996
d8329fc1cc938780ffdd9f94e0d364e0ea74f579 tree   36 46 1132
deef2e1b793907545e50a2ea2ddb5ba6c58c4506 tree   136 136 1178
d982c7cb2c2a972ee391a85da481fc1f9127a01d tree   6 17 1314 1 \
  deef2e1b793907545e50a2ea2ddb5ba6c58c4506
3c4e9cd789d88d8d89c1073707c3585e41b0e614 tree   8 19 1331 1 \
  deef2e1b793907545e50a2ea2ddb5ba6c58c4506
0155eb4229851634a0f03eb265b69f5a2d56f341 tree   71 76 1350
83baae61804e65cc73a7201a7252750c76066a30 blob   10 19 1426
fa49b077972391ad58037050f2a75f74e3671e92 blob   9 18 1445
b042a60ef7dff760008df33cee372b945b6e884e blob   22054 5799 1463
033b4468fa6b2a9547a70d88d1bbe8bf3f9ed0d5 blob   9 20 7262 1 \
  b042a60ef7dff760008df33cee372b945b6e884e
1f7a7a472abf3dd9643fd615f6da379c4acb3e3a blob   10 19 7282
non delta: 15 objects
chain length = 1: 3 objects
.git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.pack: ok

在這裡,`033b4` blob(如果你還記得,它是 `repo.rb` 檔案的第一個版本)引用了 `b042a` blob(這是檔案的第二個版本)。輸出的第三列是物件在包中的大小,所以你可以看到 `b042a` 佔用檔案 22K,而 `033b4` 只佔用 9 位元組。有趣的是,檔案的第二個版本是完整儲存的,而原始版本是作為增量儲存的——這是因為你很可能需要更快地訪問檔案的最新版本。

這樣做真正的好處是它可以隨時重新打包。Git 會偶爾自動重新打包你的資料庫,始終試圖節省更多空間,但你也可以透過手動執行 `git gc` 隨時重新打包。

scroll-to-top