添加一些大文件到仓库中,以此展示 Git 的一个很有趣的功能。 为了便于演示,我们要把之前在 Grit 库中用到过的 repo.rb 文件添加进来——这是一个大小约为 22K 的源代码文件:

    1. $ curl https://raw.githubusercontent.com/mojombo/grit/master/lib/grit/repo.rb > repo.rb
    2. $ git add repo.rb
    3. $ git commit -m 'added repo.rb'

    如果查看生成的树对象,可以看到 repo.rb 文件对应的数据对象的 SHA-1 值:

    1. $ git cat-file -p main^{tree}
    2. 040000 tree a376c417225045e853f785ef73ecd0b10291e1b9 bak
    3. 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
    4. 100644 blob 5a503b609ab094999685d0c7d7dd2b7420530482 repo.rb
    5. 100644 blob 180cf8328022becee9aaa2577a8f84ea2b9f3827 test.txt

    接下来使用 git cat-file 命令查看这个对象有多大:

    1. $ git cat-file -s 5a503b609ab
    2. 22043

    现在,稍微修改这个文件,然后看看会发生什么:

    1. $ echo '# testing' >> repo.rb
    2. $ git commit -am 'modifued repo.rb a bit'

    查看这个最新的提交生成的树对象,你会看到一些有趣的东西:

    1. $ git cat-file -p main^{tree}
    2. 040000 tree a376c417225045e853f785ef73ecd0b10291e1b9 bak
    3. 100644 blob fa49b077972391ad58037050f2a75f74e3671e92 new.txt
    4. 100644 blob 2db57377a8df9cd8e8e9278afb23dc7421b2d4fc repo.rb
    5. 100644 blob 180cf8328022becee9aaa2577a8f84ea2b9f3827 test.txt

    repo.rb 对应一个与之前完全不同的数据对象,这意味着,虽然你只是在一个 400 行的文件后面加入一行新内容,Git 也会用一个全新的对象来存储新的文件内容。

    1. $ git cat-file -s 2db57377
    2. 22053

    你的磁盘上现在有两个几乎完全相同、大小均为 22K 的对象(每个都被压缩到大约 7K)。 如果 Git 只完整保存其中一个,再保存另一个对象与之前版本的差异内容,岂不更好?
    事实上 Git 可以那样做。 Git 最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对象格式。 但是,Git 会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制文件,以节省空间和提高效率。 当版本库中有太多的松散对象,或者你手动执行 git gc 命令,或者你向远程服务器执行推送时,Git 都会这样做。 要看到打包过程,你可以手动执行 git gc 命令让 Git 对对象进行打包:

    1. $ git gc
    2. Enumerating objects: 16, done.
    3. Counting objects: 100% (16/16), done.
    4. Delta compression using up to 8 threads
    5. Compressing objects: 100% (12/12), done.
    6. Writing objects: 100% (16/16), done.
    7. Total 16 (delta 2), reused 0 (delta 0), pack-reused 0

    这个时候再查看 objects 目录,你会发现大部分的对象都不见了,与此同时出现了一对新文件:

    1. $ find .git/objects/ -type f
    2. .git/objects/bd/9dbf5aae1a3862dd1526723246b20206e5fc37
    3. .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
    4. .git/objects/info/commit-graph
    5. .git/objects/info/packs
    6. .git/objects/pack/pack-f1b445ef2d293214cc8d64a8b3bb89cafd115d08.idx
    7. .git/objects/pack/pack-f1b445ef2d293214cc8d64a8b3bb89cafd115d08.pack

    仍保留着的几个对象是未被任何提交记录引用的数据对象——在此例中是你之前创建的 “what is up, doc?” 和 “test content” 这两个示例数据对象。 因为你从没将它们添加至任何提交记录中,所以 Git 认为它们是悬空(dangling)的,不会将它们打包进新生成的包文件中。
    剩下的文件是新创建的包文件和一个索引。 包文件包含了刚才从文件系统中移除的所有对象的内容。 索引文件包含了包文件的偏移信息,我们通过索引文件就可以快速定位任意一个指定对象。 有意思的是运行 gc 命令前磁盘上的对象大小约为 15K,而这个新生成的包文件大小仅有 7K。 通过打包对象减少了一半的磁盘占用空间。
    Git 是如何做到这点的? Git 打包对象时,会查找命名及大小相近的文件,并只保存文件不同版本之间的差异内容。 你可以查看包文件,观察它是如何节省空间的。 git verify-pack 这个底层命令可以让你查看已打包的内容:

    1. $ git verify-pack -v .git/objects/pack/pack-f1b445ef2d293214cc8d64a8b3bb89cafd115d08.idx
    2. caee76e64ffe9cd7bc3a41ea5347eb4bf647eb03 commit 219 153 12
    3. 4eab614c1161e9424721baa8365b129a133965bd commit 210 146 165
    4. 8156ac50b36237aa71db17f4d9e07e243fa1e412 commit 209 146 311
    5. c5f9689205729d1713ec9cf6a4e97fa7b5d9dfda commit 210 145 457
    6. 76328ef53b2ecef2463943332f2b65f5ed925eb6 commit 161 116 602
    7. 22958f74c92ee3d5281002141288eca069c75b86 tree 71 76 718
    8. 574fc795bf72965f78162feed3306a1f4e7175c7 tag 126 120 794
    9. 41f71ab0cbea6ab17fcb88daa151f6d153f26ac9 tree 136 136 914
    10. 3387740527358bcad3ab519369794c8cc2c15673 tree 8 19 1050 1 41f71ab0cbea6ab17fcb88daa151f6d153f26ac9
    11. a376c417225045e853f785ef73ecd0b10291e1b9 tree 36 47 1069
    12. aa97b0662b9713e5d95d8285a82078f7a8395405 tree 136 136 1116
    13. fa49b077972391ad58037050f2a75f74e3671e92 blob 9 18 1252
    14. 180cf8328022becee9aaa2577a8f84ea2b9f3827 blob 6 15 1270
    15. a5bce3fd2565d8f458555a0c6f42d0504a848bd5 blob 6 15 1285
    16. 2db57377a8df9cd8e8e9278afb23dc7421b2d4fc blob 22053 5797 1300
    17. 5a503b609ab094999685d0c7d7dd2b7420530482 blob 9 20 7097 1 2db57377a8df9cd8e8e9278afb23dc7421b2d4fc
    18. non delta: 14 objects
    19. chain length = 1: 2 objects
    20. .git/objects/pack/pack-f1b445ef2d293214cc8d64a8b3bb89cafd115d08.pack: ok

    此处,5a503b6 这个数据对象(即 repo.rb 文件的第一个版本,如果你还记得的话)引用了数据对象 2db573,即该文件的第二个版本。 命令输出内容的第三列显示的是各个对象在包文件中的大小,可以看到2db573 占用了 22K 空间,而 5a503b6仅占用 9 字节。 同样有趣的地方在于,第二个版本完整保存了文件内容,而原始的版本反而是以差异方式保存的——这是因为大部分情况下需要快速访问文件的最新版本。
    最妙之处是你可以随时重新打包。 Git 时常会自动对仓库进行重新打包以节省空间。当然你也可以随时手动执行 git gc 命令来这么做。