Git-Git库管理

对象和引用哪里去了?

从GitHub上克隆一个示例版本库,这个版本库在“历史穿梭”一章就已经克隆过一次了,如今要从新克隆一份。为了和原来的克隆相区别,克隆到另外的目录。执行下面的命令。git

$ cd /path/to/my/workspace/
$ git clone git://github.com/ossxp-com/gitdemo-commit-tree.git i-am-admin
Cloning into i-am-admin...
remote: Counting objects: 65, done.
remote: Compressing objects: 100% (53/53), done.
remote: Total 65 (delta 8), reused 0 (delta 0)
Receiving objects: 100% (65/65), 78.14 KiB | 42 KiB/s, done.
Resolving deltas: 100% (8/8), done.

进入克隆的版本库,使用git show-ref命令看看所含的引用。github

$ cd /path/to/my/workspace/i-am-admin
$ git show-ref
6652a0dce6a5067732c00ef0a220810a7230655e refs/heads/master
6652a0dce6a5067732c00ef0a220810a7230655e refs/remotes/origin/HEAD
6652a0dce6a5067732c00ef0a220810a7230655e refs/remotes/origin/master
c9b03a208288aebdbfe8d84aeb984952a16da3f2 refs/tags/A
1a87782f8853c6e11aacba463af04b4fa8565713 refs/tags/B
9f8b51bc7dd98f7501ade526dd78c55ee4abb75f refs/tags/C
887113dc095238a0f4661400d33ea570e5edc37c refs/tags/D
6decd0ad3201ddb3f5b37c201387511059ac120c refs/tags/E
70cab20f099e0af3f870956a3fbbbda50a17864f refs/tags/F
96793e37c7f1c7b2ddf69b4c1e252763c11a711f refs/tags/G
476e74549047e2c5fbd616287a499cc6f07ebde0 refs/tags/H
76945a15543c49735634d58169b349301d65524d refs/tags/I
f199c10c3f1a54fa3f9542902b25b49d58efb35b refs/tags/J

其中以refs/heads/开头的是分支;以refs/remotes/开头的是远程版本库分支在本地的映射,会在后面章节介绍;以refs/tags/开头的是里程碑。按照以前的经验,在.git/refs目录下应该有这些引用所对应的文件才是。看看都在么?安全

$ find .git/refs/ -type f
.git/refs/remotes/origin/HEAD
.git/refs/heads/master

为何才有两个文件?实际上当运行下面的命令后,引用目录下的文件会更少:服务器

$ git pack-refs --all
$ find .git/refs/ -type f
.git/refs/remotes/origin/HEAD

那么本应该出如今.git/refs/目录下的引用文件都到哪里去了呢?答案是这些文件被打包了,放到一个文本文件.git/packed-refs中了。查看一下这个文件中的内容。性能

$ head -5 .git/packed-refs
# pack-refs with: peeled
6652a0dce6a5067732c00ef0a220810a7230655e refs/heads/master
6652a0dce6a5067732c00ef0a220810a7230655e refs/remotes/origin/master
c9b03a208288aebdbfe8d84aeb984952a16da3f2 refs/tags/A
^81993234fc12a325d303eccea20f6fd629412712

再来看看Git的对象(commit、blob、tree、tag)在对象库中的存储。经过下面的命令,会发现对象库也不是原来熟悉的模样了。优化

$ find .git/objects/ -type f
.git/objects/pack/pack-969329578b95057b7ea1208379a22c250c3b992a.idx
.git/objects/pack/pack-969329578b95057b7ea1208379a22c250c3b992a.pack

对象库中只有两个文件,本应该一个一个独立保存的对象都不见了。读者应该可以猜到,全部的对象文件都被打包到这两个文件中了,其中以.pack结尾的文件是打包文件,以.idx结尾的是索引文件。打包文件和对应的索引文件只是扩展名不一样,都保存于.git/objects/pack/目录下。Git对于以SHA1哈希值做为目录名和文件名保存的对象有一个术语,称为松散对象。松散对象打包后会提升访问效率,并且不一样的对象能够经过增量存储节省磁盘空间。spa

能够经过Git一个底层命令能够查看索引中包含的对象:3d

$ git show-index < .git/objects/pack/pack-*.idx | head -5
661 0cd7f2ea245d90d414e502467ac749f36aa32cc4 (0793420b)
63020 1026d9416d6fc8d34e1edfb2bc58adb8aa5a6763 (ed77ff72)
3936 15328fc6961390b4b10895f39bb042021edd07ea (13fb79ef)
3768 1a588ca36e25f58fbeae421c36d2c39e38e991ef (86e3b0bd)
2022 1a87782f8853c6e11aacba463af04b4fa8565713 (e269ed74)

为何克隆远程版本库就能够产生对象库打包以及引用打包的效果呢?这是由于克隆远程版本库时,使用了“智能”的通信协议,远程Git服务器将对象打包后传输给本地,造成本地版本库的对象库中的一个包含全部对象的包以及索引文件。无疑这样的传输方式——按需传输、打包传输,效率最高。code

克隆以后的版本库在平常的提交中,产生的新的对象仍旧以松散对象存在,而不是以打包的形式,日积月累会在本地版本库的对象库中造成大量的松散文件。松散对象只是进行了压缩,而没有(打包文件才有的)增量存储的功能,会浪费磁盘空间,也会下降访问效率。更为严重的是一些非正式的临时对象(暂存区操做中产生的临时对象)也以松散对象的形式保存在对象库中,形成磁盘空间的浪费。下一节就着手处理临时对象的问题。对象

暂存区操做引入的临时对象

暂存区操做有可能在对象库中产生临时对象,例如文件反复的修改和反复的向暂存区添加,或者添加到暂存区后不提交甚至直接撤销,就会产生垃圾数据占用磁盘空间。为了说明临时对象的问题,须要准备一个大的压缩文件,10MB便可。

在Linux上与内核匹配的initrd文件(内核启动加载的内存盘)就是一个大的压缩文件,能够用于此节的示例。将大的压缩文件放在版本库外的一个位置上,由于这个文件会屡次用到。

$ cp /boot/initrd.img-2.6.32-5-amd64 /tmp/bigfile
$ du -sh bigfile
11M     bigfile

将这个大的压缩文件复制到工做区中,拷贝两份。

$ cd /path/to/my/workspace/i-am-admin
$ cp /tmp/bigfile bigfile
$ cp /tmp/bigfile bigfile.dup

查看一下磁盘空间占用:

  • 工做区连同版本库共占用33MB。
$ du -sh .
33M     .
  • 其中版本库只占用了11MB。版本库空间占用是工做区的一半。

若是再有谁说版本库空间占用必定比工做区大,能够用这个例子回击他。

$ du -sh .git/
11M     .git/

看看版本库中对象库内的文件,会发现多出了一个松散对象。之因此添加两个文件而只有一个松散对象,是由于Git对于文件的保存是将内容保存为blob对象中,和文件名无关,相同内容的不一样文件会共享同一个blob对象。

$ find .git/objects/ -type f
.git/objects/2e/bcd92d0dda2bad50c775dc662c6cb700477aff
.git/objects/pack/pack-969329578b95057b7ea1208379a22c250c3b992a.idx
.git/objects/pack/pack-969329578b95057b7ea1208379a22c250c3b992a.pack

若是不想提交,想将文件撤出暂存区,则进行以下操做。

  • 当前暂存区的状态。
$ git status -s
A  bigfile
A  bigfile.dup
  • 将添加的文件撤出暂存区。
$ git reset HEAD
  • 经过查看状态,看到文件被撤出暂存区了。
$ git status -s
?? bigfile
?? bigfile.dup

文件撤出暂存区后,在对象库中产生的blob松散对象仍然存在,经过查看版本库的磁盘占用就能够看出来。

$ du -sh .git/
11M     .git/

Git提供了git fsck命令,能够查看到版本库中包含的没有被任何引用关联松散对象。

$ git fsck
dangling blob 2ebcd92d0dda2bad50c775dc662c6cb700477aff

标识为dangling的对象就是没有被任何引用直接或者间接关联到的对象。这个对象就是前面经过暂存区操做引入的大文件的内容。如何将这个文件从版本库中完全删除呢?Git提供了一个清理的命令:

用git prune清理以后,会发现:

  • 用git fsck查看,没有未被关联到的松散对象。
$ git fsck
  • 版本库的空间占用也小了10MB,证实大的临时对象文件已经从版本库中删除了。
$ du -sh .git/
236K    .git/

重置操做引入的对象

上一节用git prune命令清除暂存区操做时引入的临时对象,可是若是是用重置命令抛弃的提交和文件就不会轻易的被清除。下面用一样的大文件提交到版本库中试验一下。

$ cd /path/to/my/workspace/i-am-admin
$ cp /tmp/bigfile bigfile
$ cp /tmp/bigfile bigfile.dup

将这两个大文件提交到版本库中。

  • 添加到暂存区。
$ git add bigfile bigfile.dup
  • 提交到版本库。
$ git commit -m "add bigfiles."
[master 51519c7] add bigfiles.
 2 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 bigfile
 create mode 100644 bigfile.dup
  • 查看版本库的空间占用。
$ du -sh .git/
11M     .git/

作一个重置操做,抛弃刚刚针对两个大文件作的提交。

$ git reset --hard HEAD^

重置以后,看看版本库的变化。

  • 版本库的空间占用没有变化,仍是那么“庞大”。
$ du -sh .git/
11M     .git/
  • 查看对象库,看到三个松散对象。
$ find .git/objects/ -type f
.git/objects/info/packs
.git/objects/2e/bcd92d0dda2bad50c775dc662c6cb700477aff
.git/objects/d9/38dee8fde4e5053b12406c66a19183a24238e1
.git/objects/51/519c7d8d60e0f958e135e8b989a78e84122591
.git/objects/pack/pack-969329578b95057b7ea1208379a22c250c3b992a.idx
.git/objects/pack/pack-969329578b95057b7ea1208379a22c250c3b992a.pack
  • 这三个松散对象分别对应于撤销的提交,目录树,以及大文件对应的blob对象。
$ git cat-file -t 51519c7
commit
$ git cat-file -t d938dee
tree
$ git cat-file -t 2ebcd92
blob

向上一节同样,执行git prune命令,期待版本库空间占用会变小。但是:

  • 版本库空间占用没有变化!
$ git prune
$ du -sh .git/
11M     .git/
  • 执行git fsck也看不到未被关联到的对象。
$ git fsck
  • 除非像下面这样执行。
$ git fsck --no-reflogs
dangling commit 51519c7d8d60e0f958e135e8b989a78e84122591

还记得reflog么?reflog是防止误操做的最后一道闸门。

$ git reflog
6652a0d HEAD@{0}: HEAD^: updating HEAD
51519c7 HEAD@{1}: commit: add bigfiles.

能够看到撤销的操做仍然记录在reflog中,正因如此Git认为撤销的提交和大文件都还被能够被追踪到,还在使用着,因此没法用git prune命令删除。

若是确认真的要丢弃不想要的对象,须要对版本库的reflog作过时操做,至关于将.git/logs/下的文件清空。

  • 使用下面的reflog过时命令作不到让刚刚撤销的提交过时,由于reflog的过时操做缺省只会让90天前的数据过时。
$ git reflog expire --all
$ git reflog
6652a0d HEAD@{0}: HEAD^: updating HEAD
51519c7 HEAD@{1}: commit: add bigfiles.
  • 须要要为git reflog命令提供--expire= 参数,强制 以前的记录所有过时。
$ git reflog expire --expire=now --all
$ git reflog

使用now做为时间参数,让 reflog 的所有记录都过时。没有了 reflog,即回滚的添加大文件的提交从 reflog 中看不到后,该提交对应的 commit 对象、tree 对象和 blob 对象就会成为未被关联的 dangling 对象,能够用git prune命令清理。下面能够看到清理后,版本库变小了。

$ git prune
$ du -sh .git/
244K    .git/

Git管家:git gc

前面两节介绍的是比较极端的状况,实际操做中会不多用到git prune命令来清理版本库,而是会使用一个更为经常使用的命令git gc。命令git gc就比如Git版本库的管家,会对版本库进行一系列的优化动做。

  • 对分散在.git/refs下的文件进行打包,打包到文件.git/packed-refs中。

若是没有将配置gc.packrefs关闭,就会执行命令:git pack-refs –all –prune实现对引用的打包。

  • 丢弃90天前的reflog记录。

会运行使reflog过时命令:git reflog expire –all。由于采用了缺省参数调用,所以只会清空reflog中90天前的记录。

  • 对松散对象进行打包。

运行git repack命令,凡有引用关联的对象都被打在包里,未被关联的对象仍旧以松散对象形式保存。

  • 清除未被关联的对象。缺省只清除2周之前的未被关联的对象。

能够向git gc提供--prune=<date>参数,其中的时间参数传递给git prune –expire <date>,实现对指定日期以前的未被关联的松散对象进行清理。

  • 其余清理。

如运行git rerere gc对合并冲突的历史记录进行过时操做。

从上面的描述中可见命令git gc完成了至关多的优化和清理工做,而且最大限度照顾了安全性的须要。例如像暂存区操做引入的没有关联的临时对象会最少保留2个星期,而由于重置而丢弃的提交和文件则会保留最少3个月。

下面就把前面的例子用git gc再执行一遍,不过这一次添加的两个大文件要稍有不一样,以便看到git gc打包所实现的对象增量存储的效果。

复制两个大文件到工做区。

$ cp /tmp/bigfile bigfile
$ cp /tmp/bigfile bigfile.dup

在文件bigfile.dup后面追加些内容,形成bigfile和bigfile.dup内容不一样。

$ echo "hello world" >> bigfile.dup

将这两个稍有不一样的文件提交到版本库。

$ git add bigfile bigfile.dup
$ git commit -m "add bigfiles."
[master c62fa4d] add bigfiles.
 2 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 bigfile
 create mode 100644 bigfile.dup

能够看到版本库中提交进来的两个不一样的大文件是不一样的对象。

$ git ls-tree HEAD | grep bigfile
100644 blob 2ebcd92d0dda2bad50c775dc662c6cb700477aff    bigfile
100644 blob 9e35f946a30c11c47ba1df351ca22866bc351e7b    bigfile.dup

作版本库重置,抛弃最新的提交,即抛弃添加两个大文件的提交。

$ git reset --hard HEAD^
HEAD is now at 6652a0d Add Images for git treeview.

此时的版本库有多大呢,仍是像以前添加两个相同的大文件时占用11MB空间么?

$ du -sh .git/
22M     .git/

版本库空间占用竟然扩大了一倍!这显然是由于两个大文件分开存储形成的。能够用下面的命令在对象库中查看对象的大小。

$ find .git/objects -type f -printf "%-20p\t%s\n"
.git/objects/0c/844d2a072fd69e71638558216b69ebc57ddb64  233
.git/objects/2e/bcd92d0dda2bad50c775dc662c6cb700477aff  11184682
.git/objects/9e/35f946a30c11c47ba1df351ca22866bc351e7b  11184694
.git/objects/c6/2fa4d6cb4c082fadfa45920b5149a23fd7272e  162
.git/objects/info/packs 54
.git/objects/pack/pack-969329578b95057b7ea1208379a22c250c3b992a.idx   2892
.git/objects/pack/pack-969329578b95057b7ea1208379a22c250c3b992a.pack  80015

输出的每一行用空白分隔,前面是文件名,后面是以字节为单位的文件大小。从上面的输出能够看出来,打包文件很小,可是有两个大的文件各自占用了11MB左右的空间。

执行git gc并不会删除任何对象,由于这些对象都尚未过时。可是会发现版本库的占用变小了。

  • 执行git gc对版本库进行整理。
$ git gc
Counting objects: 69, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (49/49), done.
Writing objects: 100% (69/69), done.
Total 69 (delta 11), reused 63 (delta 8)
  • 版本库空间占用小了一半!
$ du -sh .git/
11M     .git/
  • 原来是由于对象库从新打包,两个大文件采用了增量存储使得版本库变小。
$ find .git/objects -type f -printf "%-20p\t%s\n" | sort
.git/objects/info/packs 54
.git/objects/pack/pack-7cae010c1b064406cd6c16d5a6ab2f446de4076c.idx 3004
.git/objects/pack/pack-7cae010c1b064406cd6c16d5a6ab2f446de4076c.pack 11263033

若是想将抛弃的历史数据完全丢弃,以下操做。

  • 再也不保留90天的reflog,而是将全部reflog所有即时过时。
$ git reflog expire --expire=now --all
  • 经过git fsck能够看到有提交成为了未被关联的提交。
$ git fsck
dangling commit c62fa4d6cb4c082fadfa45920b5149a23fd7272e
  • 这个未被关联的提交就是删除大文件的提交。
$ git show c62fa4d6cb4c082fadfa45920b5149a23fd7272e
commit c62fa4d6cb4c082fadfa45920b5149a23fd7272e
Author: Jiang Xin <jiangxin@ossxp.com>
Date:   Thu Dec 16 20:18:38 2010 +0800

    add bigfiles.

diff --git a/bigfile b/bigfile
new file mode 100644
index 0000000..2ebcd92
Binary files /dev/null and b/bigfile differ
diff --git a/bigfile.dup b/bigfile.dup
new file mode 100644
index 0000000..9e35f94
Binary files /dev/null and b/bigfile.dup differ
  • 不带参数调用git gc虽然不会清除还没有过时(未到2周)的大文件,可是会将未被关联的对象从打包文件中移出,成为松散文件。
$ git gc
Counting objects: 65, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (45/45), done.
Writing objects: 100% (65/65), done.
Total 65 (delta 8), reused 63 (delta 8)
  • 未被关联的对象从新成为松散文件,因此.git版本库的空间占用又反弹了。
$ du -sh .git/
22M     .git/
$ find .git/objects -type f -printf "%-20p\t%s\n" | sort
.git/objects/0c/844d2a072fd69e71638558216b69ebc57ddb64  233
.git/objects/2e/bcd92d0dda2bad50c775dc662c6cb700477aff  11184682
.git/objects/9e/35f946a30c11c47ba1df351ca22866bc351e7b  11184694
.git/objects/c6/2fa4d6cb4c082fadfa45920b5149a23fd7272e  162
.git/objects/info/packs 54
.git/objects/pack/pack-969329578b95057b7ea1208379a22c250c3b992a.idx 2892
.git/objects/pack/pack-969329578b95057b7ea1208379a22c250c3b992a.pack 80015
  • 实际上若是使用当即过时参数--prune=now调用git gc,就不用再等2周了,直接就能够完成对未关联的对象的清理。
$ git gc --prune=now
Counting objects: 65, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (45/45), done.
Writing objects: 100% (65/65), done.
Total 65 (delta 8), reused 65 (delta 8)
  • 清理事后,版本库的空间占用降了下来。
$ du -sh .git/
240K    .git/

Git管家的自动执行

对于老版本库的Git,会看到帮助手册中建议用户对版本库进行周期性的整理,以便得到更好的性能,尤为是对于规模比较大的项目,可是对于整理的周期都语焉不详。

实际上对于1.6.6及之后版本的Git已经基本上不须要手动执行git gc命令了,由于部分Git命令会自动调用git gc –auto命令,在版本库确实须要整理的状况下自动开始整理操做。

目前有以下Git命令会自动执行git gc –auto命令,实现对版本库的按需整理。

  • 执行命令git merge进行合并操做后,对版本库进行按需整理。

  • 执行命令git receive-pack,即版本库接收其余版本库推送(push)的提交后,版本库会作按需整理操做。

当版本库接收到其余版本库的推送(push)请求时,会调用git receive-pack命令以接收请求。在接收到推送的提交后,对版本库进行按需整理。

  • 执行命令git rebase -i进行交互式变基操做后,会对版本库进行按需整理。

  • 执行命令git am对mbox邮箱中经过邮件提交的补丁在版本库中进行应用的操做后,会对版本库作按需整理操做。

对于提供共享式“写操做”的Git版本库,能够免维护。所谓的共享式写操做,就是版本库做为一个裸版本库放在服务器上,团队成员能够经过推送(push)操做将提交推送到共享的裸版本中。每一次推送操做都会触发git gc –auto命令,对版本库进行按需整理。

对于非独立工做的本地工做区,也能够免维护。由于和他人协同工做的本地工做区会常常执行git pull操做从他人版本库或者从共享的版本库拉回新提交,执行git pull操做会,会触发git merge操做,所以也会对本地版本库进行按需整理。

Git管家命令使用--auto参数调用,会进行按需整理。由于版本库整理操做对于大的项目可能会很是费时,所以实际的整理并不会常常被触发,即有着很是苛刻的触发条件。想要观察到触发版本库整理操做是很是不容易的事情。

主要的触发条件是:松散对象只有超过必定的数量时才会执行。并且在统计松散对象数量时,为了下降在.git/objects/目录下搜索松散对象对系统形成的负担,实际采起了取样搜索,即只会对对象库下一个子目录.git/objects/17进行文件搜索。在缺省的配置下,只有该目录中对象数目超过27个,才会触发版本库的整理。至于为何只在对象库下选择了一个子目录进行松散对象的搜索,这是由于SHA1哈希值是彻底随机的,文件在由前两位哈希值组成的目录中差很少是平均分布的。至于为何选择17,不知道对于做者Junio C Hamano有什么特殊意义,也许是向Linus Torvalds被评选为二十世纪最有影响力的100人中排名第17位而进行致敬。

能够经过配置gc.auto的值,调整Git管家自动运行时触发版本库整理操做的频率,可是注意不要将gc.auto设置为0,不然git gc –auto命令永远不会触发版本库的整理。

相关文章
相关标签/搜索