如果我在存储库中移动文件,例如从一个文件夹移动到另一个文件夹,git就足够聪明,知道这些文件是相同的文件,只是更新它对存储库中这些文件的引用,或新的提交实际上是否创建了这些文件?
我问,因为我想知道git对于存储二进制文件有多么有用.如果它将移动的文件视为副本,那么即使您实际上没有添加任何新文件,也可以轻松地获得非常大的存储库.
tor*_*rek 29
要了解git如何处理这些内容,您需要了解两件事:
假设你有一个新的仓库,里面有一个巨大的文件:
$ mkdir temp; cd temp; git init
$ echo contents > bigfile; git add bigfile; git commit -m initial
[master (root-commit) d26649e] initial
 1 file changed, 1 insertion(+)
 create mode 100644 bigfile
repo现在有一个提交,它有一个树(顶级目录),它有一个文件,它有一些唯一的对象ID.("大"文件是谎言,它很小,但如果它是很多兆字节它会工作相同.)
现在,如果您将文件复制到第二个版本并提交:
$ cp bigfile bigcopy; git add bigcopy; git commit -m 'make a copy'
[master 971847d] make copy
 1 file changed, 1 insertion(+)
 create mode 100644 bigcopy
存储库现在有两个提交(显然),有两个树(每个版本的顶级目录一个)和一个文件.两个副本的唯一对象ID 相同.为了看到这个,让我们查看最新的树:
$ git cat-file -p HEAD:
100644 blob 12f00e90b6ef79117ce6e650416b8cf517099b78    bigcopy
100644 blob 12f00e90b6ef79117ce6e650416b8cf517099b78    bigfile
那个大的SHA-1 12f00e9...是文件内容的唯一ID.如果文件确实非常庞大,那么git现在将使用大约一半的repo空间作为工作目录,因为repo只有一个文件副本(在名称下12f00e9...),而工作目录有两个.
如果您更改文件内容,即使是一个单独的位,例如将小写字母设置为大写或其他内容,则新内容将具有新的SHA-1对象ID,并且需要在repo中创建新副本.我们稍后会谈到这一点.
现在,假设您有一个更复杂的目录结构(具有更多"树"对象的repo).如果您随机播放文件,但"新"文件的内容 - 无论名称是什么 - 在新目录中的内容与以前的内容相同,这里是内部发生的事情:
$ mkdir A B; mv bigfile A; mv bigcopy B; git add -A .
$ git commit -m 'move stuff'
[master 82a64fe] move stuff
 2 files changed, 0 insertions(+), 0 deletions(-)
 rename bigfile => A/bigfile (100%)
 rename bigcopy => B/bigcopy (100%)
Git检测到(有效)重命名.让我们看看其中一棵新树:
$ git cat-file -p HEAD:A
100644 blob 12f00e90b6ef79117ce6e650416b8cf517099b78    bigfile
该文件仍然在相同的旧对象ID下,因此它仍然只在repo中.git很容易检测到重命名,因为对象ID匹配,即使路径名(存储在这些"树"对象中)可能不匹配.让我们做最后一件事:
$ mv B/bigcopy B/two; git add -A .; git commit -m 'rename again'
[master 78d92d0] rename again
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename B/{bigcopy => two} (100%)
现在让我们要求HEAD~2(在任何重命名之前)和HEAD(在重命名之后)之间的差异:
$ git diff HEAD~2 HEAD
diff --git a/bigfile b/A/bigfile
similarity index 100%
rename from bigfile
rename to A/bigfile
diff --git a/bigcopy b/B/two
similarity index 100%
rename from bigcopy
rename to B/two
即使它分两步完成,git可以告诉你从现在的内容HEAD~2到现在的内容HEAD,你可以通过重命名bigcopy来一步完成B/two.
Git 总是进行动态重命名检测.假设我们没有进行重命名,而是在某个时刻完全删除了文件,并将其提交.稍后,假设返回相同的数据(以便我们获得相同的底层对象ID),然后针对新的版本区分足够旧的版本.在这里git会说直接从旧版本到最新版本,你可以重命名文件,即使这不是我们在那里的方式.
换句话说,差异总是按提交方式完成:"在过去的某个时间,我们有A.现在我们有Z.我如何直接从A到Z?" 那时,git会检查重命名的可能性,并根据需要在diff输出中生成它们.
即使对文件内容进行了一些小的更改,Git仍会(有时)显示重命名.在这种情况下,您将获得"相似性指数".基本上,你可以告诉git给出"在rev A中删除了一些文件,在rev Z中添加了一些不同命名的文件"(当转换转速A和Z时),它应该尝试区分两个文件以查看它们是否"关闭"足够".如果是,你会得到一个"文件重命名,然后改变"差异.对此的控制是-M或--find-renames参数git diff:git diff -M80表示如果文件至少"80%相似",则将更改显示为重命名和编辑.
Git还会使用-C或--find-copies标记来查找"复制然后更改" .(您可以添加--find-copies-harder对所有文件执行更加计算成本更高的搜索;请参阅文档.)
这(间接地)与git如何使存储库随着时间的推移而不断增大.
如果您有一个大文件(甚至是一个小文件)并对其进行少量更改,git将使用这些对象ID存储该文件的两个完整副本.你发现这些东西.git/objects; 例如,ID 12f00e90b6ef79117ce6e650416b8cf517099b78在的文件是.git/objects/12/f00e90b6ef79117ce6e650416b8cf517099b78.它们被压缩以节省空间,但即使压缩,一个大文件仍然可以很大.因此,如果底层对象不是非常活跃并且出现在很多提交中,并且偶尔只有一些小的更改,那么git可以进一步压缩修改.它将它们放入"包"文件中.
在包文件中,通过将对象与存储库中的其他对象进行比较来进一步压缩对象.1 对于文本文件,可以很容易地解释它是如何工作的(尽管增量压缩算法不同):如果你有一个长文件并删除第75行,你可以说"使用我们那边的其他副本,但删除第75行".如果添加了新行,则可以说"使用其他副本,但添加此新行".您可以使用其他大文件作为基础,将大文件表示为指令序列.
Git对所有对象(不仅仅是文件)进行这种压缩,因此它可以压缩针对另一个提交的提交,或者也可以针对彼此压缩树.这真的非常有效,但有一个问题.
一些(不是全部)二进制文件delta-compress非常糟糕.特别是,对于使用bzip2,gzip或zip等压缩文件,在任何地方进行一次小改动往往会改变文件的其余部分.图像(jpg等)经常被压缩并受到这种影响.(我不知道很多未压缩的图像格式.PBM文件是完全未压缩的,但这是我所知道的唯一一个仍在使用中的文件.)
如果你对二进制文件没有任何改变,git会因为不变的低级对象ID而超级高效地压缩它们.如果你做了一些小改动,git的压缩算法可以(不一定"会")失败,这样你就可以获得二进制文件的多个副本.我知道大型的gzip'ed cpio和tar档案非常糟糕:对这样一个文件和2 GB repo的一个小改动就变成了4 GB的回购.
您的特定二进制文件是否能够很好地压缩是您必须要尝试的东西.如果你只是重命名文件,你应该没有任何问题.如果您经常更换大型JPG图像,我希望这会表现不佳(但值得尝试).
1在"普通"包文件中,对象只能对同一包文件中的其他对象进行增量压缩.这样就可以保持包文件的独立性."瘦"包可以使用不在包文件本身中的对象; 这些用于网络上的增量更新,例如,与git fetch.