如果我在存储库中移动文件,例如从一个文件夹移动到另一个文件夹,git会足够聪明地知道这些文件是相同的文件,只是更新它对存储库中这些文件的引用,或者实际上是新的提交创建这些文件的副本?
我问,因为我想知道git对于存储二进制文件有多么有用。如果它将移动的文件视为副本,那么即使您实际上没有添加任何新文件,也可以让repo轻松变大。
答案 0 :(得分:26)
要了解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。 (" big"文件是一个谎言,它非常小,但如果它是很多兆字节它将会起作用。)
现在,如果您将文件复制到第二个版本并提交:
$ 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)。如果您随机播放文件,但" new"的内容文件 - 无论名称是什么 - 新目录与以前的内容相同,这里是内部发生的事情:
$ 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
中的内容,你可以通过重命名{{1}来一步完成转到bigcopy
。
Git 总是进行动态重命名检测。假设我们没有进行重命名,而是在某个时刻完全删除了文件,并将其提交。稍后,假设返回相同的数据(以便我们获得相同的底层对象ID),然后针对新的版本区分足够旧的版本。在这里git会说,直接从旧版本到最新版本,您可以重命名文件,即使这不是我们如何到达那里。
换句话说,差异总是按提交对方式完成:"在过去的某个时间,我们有A.现在我们有Z.我如何直接从A到Z?&# 34;那时,git会检查重命名的可能性,并根据需要在diff输出中生成它们。
B/two
的{{1}}或-M
参数:--find-renames
表示如果文件至少是#34,则将更改显示为重命名和编辑; 80%相似"。
Git还会使用git diff
或git diff -M80
标记来查找"复制然后更改"。 (您可以添加-C
对所有文件执行更加计算成本更高的搜索;请参阅documentation。)
这(间接地)与git如何防止存储库随着时间的推移而大大增加。
如果您有一个大文件(甚至是一个小文件)并对其进行少量更改,git将使用这些对象ID存储该文件的两个完整副本。你可以在--find-copies
找到这些东西;例如,ID为--find-copies-harder
的文件位于.git/objects
。它们被压缩以节省空间,但即使压缩,一个大文件仍然可以很大。因此,如果底层对象不是非常活跃并且出现在很多提交中,并且偶尔只有一些小的更改,那么git可以进一步压缩修改。它把它们放入" pack"文件。
在包文件中,通过将对象与存储库中的其他对象进行比较来进一步压缩对象。 1 对于文本文件,可以很容易地解释它是如何工作的(尽管增量压缩)算法是不同的):如果你有一个长文件并删除第75行,你可以说"使用我们在那里的其他副本,但删除第75行。"如果您添加了新行,则可以说"使用其他副本,但添加此新行。"您可以将大文件表示为指令序列,使用其他大文件作为基础。
Git对所有对象(不仅仅是文件)进行这种压缩,因此它可以压缩针对另一个提交的提交,或者相互之间的树。它真的很有效率,但有一个问题。
某些(并非所有)二进制文件delta-compress相互之间非常糟糕。特别是,对于使用bzip2,gzip或zip等压缩文件,在任何地方进行一次小改动往往会改变文件的其余部分。图像(jpg' s等)经常被压缩并受到这种影响。 (我不知道很多未压缩的图像格式.PBM文件完全没有压缩,但这是我所知道的唯一仍在使用中的文件。)
如果你根本没有对进行更改二进制文件,git会因为不变的低级别对象ID而超级高效地压缩它们。如果你做了一些小改动,git的压缩算法可以(不一定"会")失败,这样你就可以得到多个二进制文件的副本。我知道大型gzip的cpio和tar档案非常糟糕:对这样一个文件和2 GB repo的一个小改动就变成了4 GB的回购。
您的特定二进制文件是否能够很好地压缩是您必须要进行试验的内容。如果您只是重命名文件,那么您应该没有任何问题。如果您经常更换大型JPG图像,我希望这会表现不佳(但值得尝试)。
1 In" normal"打包文件,对象只能对同一包文件中的其他对象进行增量压缩。这样就可以保持包文件的独立性。 A"瘦" pack可以使用不在pack-file本身的对象;这些用于网络上的增量更新,例如,与12f00e90b6ef79117ce6e650416b8cf517099b78
一样。
答案 1 :(得分:2)
git存储库通过校验和来区分文件,而不是按名称或位置来区分文件。如果您提交,然后将文件移动到其他位置并提交,则位于之前位置的文件和位于之后位置的文件具有相同的校验和(因为它们具有相同的内容)。因此,存储库不存储文件的新“副本”;它只是记录了这个校验和的文件现在有第二个位置的事实。