git如何处理文件系统中的移动文件?

git如何处理文件系统中的移动文件?,git,Git,如果我在存储库中移动文件,例如从一个文件夹移动到另一个文件夹,git是否会足够聪明,知道这些文件是相同的,并且只更新存储库中对这些文件的引用,还是新提交实际上创建了这些文件的副本 我这样问是因为我想知道git对于存储二进制文件有多有用。如果它将移动的文件视为副本,那么即使您实际上没有添加任何新文件,您也可以很容易地获得非常大的repo。git存储库通过校验和而不是名称或位置来区分文件。如果提交,然后将文件移动到其他位置并提交,则位于before位置的文件和位于after位置的文件具有相同的校验和

如果我在存储库中移动文件,例如从一个文件夹移动到另一个文件夹,git是否会足够聪明,知道这些文件是相同的,并且只更新存储库中对这些文件的引用,还是新提交实际上创建了这些文件的副本


我这样问是因为我想知道git对于存储二进制文件有多有用。如果它将移动的文件视为副本,那么即使您实际上没有添加任何新文件,您也可以很容易地获得非常大的repo。

git存储库通过校验和而不是名称或位置来区分文件。如果提交,然后将文件移动到其他位置并提交,则位于before位置的文件和位于after位置的文件具有相同的校验和(因为它们具有相同的内容)。因此,存储库不存储文件的新“副本”;它只记录了这样一个事实,即具有此校验和的文件现在具有第二个位置。

要了解git如何处理这些位置,首先需要知道两件事:

  • 每个单独的文件(在任何目录中,在任何提交中)总是单独存储的
  • 但它是由对象ID存储的,对象ID对于文件中的任何数据都是唯一的
如何储存物品 假设您有一个新的回购协议,其中包含一个巨大的文件:

$ 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现在有一个commit,它有一个树(顶层目录),它有一个文件,它有一些唯一的object-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%)
现在,让我们来询问
头2
(在任何重命名之前)和
(在重命名之后)之间的差异:

即使它是分两步完成的,git也能分辨出从
HEAD~2
中的内容到现在的
HEAD
中的内容,您可以通过将
bigcopy
重命名为
B/two
一步完成

Git总是执行动态重命名检测。假设我们没有进行重命名,而是在某个时候完全删除了文件,并提交了该文件。稍后,假设将相同的数据放回(这样我们就得到了相同的底层对象ID),然后将足够旧的版本与新的版本区别开来。在这里git会说,要直接从旧版本转换到最新版本,您可以重命名文件,即使我们不是这样做到的

换句话说,diff总是成对提交的:“在过去的某个时候,我们有A。现在我们有Z。我如何直接从A到Z?”当时,git检查重命名的可能性,并根据需要在diff输出中生成它们

零钱呢? Git仍然(有时)显示重命名,即使文件内容有一些小的更改。在本例中,您将获得一个“相似性索引”。基本上,您可以告诉git,给定“在rev A中删除了一些文件,在rev Z中添加了一些不同名称的文件”(在区分rev A和rev Z时),它应该尝试区分这两个文件,看看它们是否“足够接近”。如果是,您将得到一个“文件重命名然后更改”diff。此控件是
-M
-find renames
参数,用于
git diff
git diff-M80
表示如果文件至少“80%相似”,则将更改显示为重命名和编辑

Git还将使用
-C
-find copies
标志查找“复制然后更改”。(您可以添加
——更难查找副本
,以对所有文件执行计算成本更高的搜索;请参阅。)

这(间接地)与git如何防止存储库随着时间的推移而膨胀有关

增量压缩 如果您有一个大文件(甚至是一个小文件)并对其进行了一点更改,git将使用这些对象ID存储文件的两个完整副本。您可以在
.git/objects
中找到这些东西;例如,ID为
12f00e90b6ef79117ce6e650416b8cf517099b78
的文件位于
.git/objects/12/f00e90b6ef79117ce6e650416b8cf517099b78
中。它们被压缩以节省空间,但即使被压缩,一个大文件仍然可以相当大。因此,如果底层对象不是很活跃,并且经常出现在提交中,偶尔只做一些小的更改,那么git有办法进一步压缩修改。它将它们放入“打包”文件中

在包文件中,通过将对象与
$ 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