为什么git使用不同的文本属性给出不同的diff结果?

问题描述 投票:0回答:1

我发现当我更改.gitattributes中的text属性时,git给了我一个不同的diff结果。有人可以向我解释一下吗?

这是我做的:

  1. 添加内容为* -text的.gitattributes文件
  2. 添加包含某些内容的其他文本文件
  3. 承诺

然后我添加了一行“ddd”和git diff,结果如预期的那样

diff --git a/abc.txt b/abc.txt
index aa3b7ba..911ddef 100644
--- a/abc.txt
+++ b/abc.txt
@@ -2,3 +2,5 @@ aaa
 bbb
 ccc
+ddd^M
+

但当我将.gitattributes更改为* text并再次进行差异时,git给了我这个:

diff --git a/abc.txt b/abc.txt
index aa3b7ba..9a3ed4f 100644
--- a/abc.txt
+++ b/abc.txt
@@ -1,4 +1,6 @@
-aaa
-bbb
-ccc
+aaa
+bbb
+ccc
+ddd
+

据我所知,text属性仅适用于eol规范化。为什么它会影响差异结果?

git
1个回答
1
投票

这一切都有点复杂,因为这里有很多活动部件。首先,我们来谈谈git diff和“树木”。那么让我们来看看Git可以做什么样的行尾修改,以及Git何时做。然后,让我们看看* -text* text.gitattributes中的意思。最后,让我们与git diff一起考虑所有这些。

The git diff command compares two "trees"

默认情况下 - 有比较文件的特定模式,但我们不打算进入这些 - 运行git diff比较两个Git调用树。树是文件的集合,其中每个文件都有一个名称:a.txtabc.txtdir/c.txtdir/sub/d.txt等等(但我们将在此处停止此示例)。这棵树的顶层是目录/文件夹(使用你喜欢的任何术语),包含a.txtabc.txt和子目录/文件夹dir,Git称之为子树。名为dir的子树包含c.txt和另一个子树sub,最后的子树包含d.txt

不过,Git想要其中的两棵树。一个通常是提交,另一个通常也是第二个(可能是不同的)提交。这种git diff比较了两个提交树的内容。

但是,默认情况下,git diff以您的索引作为第一个树开始。您的索引(Git调用索引,或者有时是临时区域或缓存)是Git主要用于构建您将进行的下一次提交的特殊实体。索引也有一堆子任务,这就是为什么它有这三个不同的名称。 (我们将在这个答案的末尾看到一个额外的任务。)索引开始时是当前提交中所有内容的副本:你运行git checkout的提交。所以,至少在起初,索引与当前提交匹配。

你也有一个工作树。工作树非常简单:它就是你工作的地方。 Git需要你有一个工作树,因为Git在提交或索引中存储的所有文件都是一种特殊的,高度压缩的,仅Git格式。 (从技术上讲,这些是Git blob对象。)计算机上的大多数程序,包括您自己的文本编辑器和编译器等,都无法处理仅限Git的文件。这些程序需要文件具有正常的每日文件格式,因此Git在工作树中将Git-only文件提取为普通格式。

每当你git addabc.txt这样的文件时,Git会将你的工作树中的文件复制到特殊的Git-only格式,并将特殊的blob哈希ID填入索引。因此,如果您更改工作树中的文件,然后git add更改的文件,Git将更改复制到存储库(作为blob对象)并将新的哈希ID放入索引中,将以前的索引版本替换为复制的 - 从工作树版本。请注意,索引连续有一些版本的abc.txt。首先,它具有当前提交的版本。然后,在git add abc.txt之后,它具有工作树的版本(尽管现在采用特殊的Git格式)。

无论如何,这是我们需要了解的关于索引的大部分内容:它包含作为Git“树”的变体,将包含进入下一次提交的所有内容。最初,这与我们刚刚签出的提交中的所有内容相同。

我们已经提到了工作树,它是一个正常的,不那么好的Git形式。尽管如此,各种Git命令也可以作为树使用,而git diff就是其中之一。 Git会将每个目录/文件夹视为子树,工作树本身就是顶级树。树中的每个文件都像Git blob对象一样,但每个文件都是自己的,通常在计算机上的形式,而不是特殊的Git形式。

因此:运行没有参数的git diff会将索引与工作树进行比较。在这两种情况下,Git都使用它们就好像它们是Git的内部“树”对象一样。重要的是要记住Git正在比较的内容:现在它是索引与工作树。这在一瞬间变得更加重要。

End-of-line modifications

Git的特殊内部Git专用格式适用于Git。它也是由Linus Torvalds设计的,正如您所料,它非常适合Linux。因此,你可以说它宁愿文本文件的行以普通换行符\n字符结尾,而不是DOS / Windows样式的CRLF(或\r\n)序列。这有点夸大其词:Git真的不关心这一点。但是很多使用Git的人都会关心,无论出于何种原因,无论你喜欢与否,\n-only在这一点上都是普通的Git内部格式,对于文本文件。你不必使用它,但许多人确实使用它。

同时,你的工作树,使用计算机的首选(“普通”)格式在你的计算机上,可能有文本文件具有CR-LF(我将拼写没有连字符从这里开出)行结束,如果你使用DOS / Windows。所以Git的人们提供了一些内置于Git的翻译软件。在处理文本文件时,它会将CRLF行结尾转换为仅\n结尾,或将\n-only结尾转换为CRLF结尾。要做到这一点,Git需要知道哪些文件是文本。我们马上回过头来看看。它还需要有一些特定的翻译点:如果Git要用CRLF替换\n,它什么时候会这样做呢?如果Git要用\n替换CRLF对,它何时会这样做?

这里另一个重要的问题是:这台计算机上的“正常”或“首选”格式是什么?这就是core.eol的用武之地:在Windows上,core.eol通常设置为CRLF,而在Linux上,core.eol默认只是换行符\n。这是该计算机的“正常”设置。更确切地说,core.eol默认为native,而native在DOS / Windows上表示CRLF,但在Linux / Unix上表示\n

The index <-> work-tree is "when"

请注意,在上面的所有索引操作中,每个文件始终有三个版本:

current commit       index       work-tree
--------------      -------      ---------
       a.txt          a.txt         a.txt
     abc.txt        abc.txt       abc.txt

前两列“当前提交”和“索引”采用内部的Git格式。此外,不能更改任何提交(这是一个基本的Git属性:没有对象可以更改),因此永久保存在当前提交中的版本是只读的。工作树版本采用您的正常格式。因此,内部的提交形式只有\n才有意义,工作树形式就是CRLF。

同时,内部索引文件也是特殊的Git格式。从逻辑上讲,它应该只有\n形式,而且确实如此。而且,Git可以从工作树复制到索引(git add),或者从索引复制到工作树(git checkoutgit reset),所以这是将\n-to-CRLF和CRLF-放在一起的显而易见的地方。 to-\n转换。

就是这样:当从索引复制到工作树时,Git会执行\n-to-CRLF。当从工作树复制到索引时,Git会执行CRLF到\n

嗯,这就是大部分的地方,还有另一种扭曲,正如我们稍后会看到的那样。

What .gitattributes does

每个.gitattributes条目都为您提供了一种控制Git将要应用的转换的方法。我们在上面提到Git需要在文本文件上执行此CR-LF。但哪些文件是文本文件?文件abc.txt可能是一个文本文件。但是README怎么样?如果它被命名为README.md怎么办?如果.md不是指“降价”而是意味着“魔术数据库”这是一个二进制文件怎么办?

Git在DOS / Windows上的默认设置是根据文件内容猜测文件是文本还是二进制文件。这对某些情况来说效果很好。你可以告诉它:*.txt text说所有名为*.txt的文件都是文本文件,而*.jpg -text说所有名为*.jpg的文件都不是文本文件(是二进制文件)。

文本文件应用了行尾转换。您可以在每个文件名的基础上,在.gitattributes中选择Git应该执行哪些行尾转换,但是简单的text意味着此文件是文本,而不是二进制,所以正常的转换,无论那些是什么。同样,它们依赖于core.eol,默认为native,默认为DOS / Windows上的CRLF。

Now we have the hard/tricky part

Git通常在git add上进行所有CRLF转换(从工作树复制到索引:从本机转换为Git内部)和git checkout(从索引复制到工作树:从Git内部转换为本机)。但是如果git diff要将索引与工作树进行比较,我们就会遇到一个问题:索引采用Git内部格式,而工作树采用本机格式。我们如何区分这些?

Git的答案是,它至少会暂时将工作树文件转换为内部格式,以便对它们进行区分。这两个方向在理论上都是足够的,但这是Git程序员选择的方向。所以有一个额外的点,Git进行这种转换,那就是你运行git diff

或者是吗?嗯,答案是否定和是。情况很复杂!

The index is also a cache

在我的(Linux / Unix)系统上,我创建了一个Git存储库并创建了一个文件foo.txt,其中包含两行,这些行有意地以CRLF结尾结束:

$ vis foo.txt
this file has\^M
crlf line endings\^M

vis程序以这种方式显示CRLF结尾,并在文件未以CRLF结束时省略\^M)。

我以这种形式将这个文件作为二进制文件提交,以便它以这种方式进入Git(这在Unix-ish系统上更容易,“没有.gitattributes意味着文件都不被Git修改”)。

$ git status
On branch master
nothing to commit, working tree clean

然后我创建了一个.gitattributes来强制Git将文件视为文本:

$ echo '* text' > .gitattributes

奇怪的是,还没有发生任何事情:

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

        .gitattributes

nothing added to commit but untracked files present (use "git add" to track)

现在我使用touch命令更改文件的时间戳,而不更改其内容,然后再次运行git status

$ touch foo.txt
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   foo.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)

        .gitattributes

no changes added to commit (use "git add" and/or "git commit -a")

发生了什么? touch命令没有更改文件,但确实更改了文件的时间戳。并且,git status运行git diff,现在git diff“看到”文件与索引版本不同,即使两个文件都相同。

原因是Git使用索引作为保存已清理的Git-ized文件的位置,以及一种知道清理的Git-ized文件是否与工作树文件匹配的方法。这是告诉Git后者的时间戳。通过touching文件,我更改了时间戳,以便Git不再相信索引文件与工作树文件匹配。现在Git意识到它必须Git-ize工作树版本的foo.txt,所以当它这样做时,我们看到了一个区别(虽然我们需要vis来看它):

$ git diff | vis
warning: CRLF will be replaced by LF in foo.txt.
The file will have its original line endings in your working directory.
diff --git a/foo.txt b/foo.txt
index 257cbae..6bf00d0 100644
--- a/foo.txt
+++ b/foo.txt
@@ -1,2 +1,2 @@
-this file has\^M
-crlf line endings\^M
+this file has
+crlf line endings

如果我现在要运行git add foo.txt,Git会“清理”CRLF,用\n结尾替换它们。当前索引版本的git diff和清理后的工作树版本显示了一个更改以删除回车(\r\^M,因为vis在这里显示它们)。

如果我现在删除.gitattributes文件,或将其更改为* -text,状态会更改并且git diff输出会消失:

$ echo '* -text' > .gitattributes
$ git diff
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

        .gitattributes

nothing added to commit but untracked files present (use "git add" to track)

这是因为文件将不再使用LF替换CRLF,因此现在工作树中的文件确实与索引中的文件匹配,即使在“清理”(不更改任何数据)之后也是如此。

Note some special conditions

为了进入这种情况,我必须创建一个文件并提交它,文件中的工作树形式和内部的Git-ized形式都有CRLF结尾。这在Linux / Unix系统上更容易,因为这里的Git默认是:不要乱用我的任何数据。这实际上是莱纳斯对Git的最初渴望;对于那些需要使用Windows的人来说,后来添加了所有CRLF内容。

* text中的.gitattributes属性不仅告诉Git该文件肯定是文本(即,在复制进出索引期间弄乱它),而且该文件肯定是用于git diff目的的文本。读取* -text的行告诉Git文件肯定不是文本:在复制进出索引时不要弄乱它的数据。

有一些单独的控件告诉Git,默认情况下,git diff甚至不应该尝试对文件进行区分,因为它不是文本。默认情况下,如果没有明确标记文件,并且在DOS / Windows上,Git会执行相同类型的“是文本”自动检测。除此之外,有一个设置,core.bigFileThreshold,这使得Git跳过差异。由于你的文件确实是文本,并且不是太大,你得到git diff输出。看起来所有线路都发生了变化,他们可能将CRLF结尾换成纯线换线,反之亦然。奇特的是显示的一个^M。这可能来自一条CR CR LF作为其最后三个字节的行,1但是你究竟如何得到它是一个谜。


1 less寻呼机知道显示CR-LF结尾而不显示控制-M,但会在以两个^Ms结束的行的末尾显示^M,即CR-CR-LF。

© www.soinside.com 2019 - 2024. All rights reserved.