0

我编写的构建脚本在 ci/cd 管道(在 linux 中运行)上失败,因为 build.sh 脚本以某种方式转换/保存为 CRLF 格式(基于我在线收集的内容),导致此错误:

/bin/sh^M: bad interpreter: No such file or directory

脚本本身非常基本:

#!/bin/sh
mvn clean install

由于我在运行 git config 时看到的内容,我想确认原因是由 git 引起的。下面详细介绍我采取的补救措施:

  1. 专门在我的 IDE 上保存 LF(Intellij 显示的选定行结束, build.sh 打开):

文件 k

  1. 专门配置 git 不弄乱文件行结尾并转换为 CLRF(我之前收到此警告),所以我运行了以下命令git config --global core.autocrlf falsegit config --global core.eol lf并重新克隆了存储库。

这是我的 git 配置(本地、全局和整个系统)

本地配置:

        core.bare=false
        core.logallrefupdates=true
        core.symlinks=false
        core.ignorecase=true
        core.autocrlf=false

全局配置:

http.sslverify=false
core.autocrlf=false
core.eol=lf

运行git config --list --show-origin

file:"C:\\ProgramData/Git/config"       core.symlinks=false
file:"C:\\ProgramData/Git/config"       core.autocrlf=true
file:"C:\\ProgramData/Git/config"       core.fscache=true
file:C:/Users/testUser/.gitconfig    http.sslverify=false
file:C:/Users/testUser/.gitconfig    core.autocrlf=false
file:C:/Users/testUser/.gitconfig    core.eol=lf
file:.git/config        core.logallrefupdates=true
file:.git/config        core.symlinks=false
file:.git/config        core.ignorecase=true
file:.git/config        core.autocrlf=false
file:.git/config        core.eol=lf

我已删除与此问题无关的行。正如您在整体配置输出中看到的那样,输出显示配置中存在差异。这是否会导致我的 shell 脚本在其他环境中无法正常运行?

4

1 回答 1

1

这里有一些简单的规则,虽然其中一些是意见:

  • core.eol不需要;不要打扰它。
  • core.autocrlf应该永远是false
  • 如果您有天真的 Windows 用户,他们将*.sh在 Windows 系统上编辑文件并因此在其中插入 CRLF 行结尾,请使用.gitattributes来更正此问题。

.gitattributes文件中,列出有.sh问题的文件,或者*.sh,连同指令一起列出text eol=lf。列出任何其他需要特别考虑的文件,当您使用它时:*.jpg可以有一个binary指令,如果您在存储库中有 JPG 图像;*.bat可以标记text eol=crlf;等等。

这不会解决您现有的问题;为此,克隆存储库,检查当前分支顶端的错误提交,修改.sh文件以将现有的 CRLF 行结尾替换为仅 LF 行结尾,然后添加并提交这些文件。(您可以在创建.gitattributes文件的同一提交中执行此操作。)如果您有一个相当现代的 Git,那么创建.gitattributes文件然后运行git add --renormalize build.sh应该完成所有这些(当然除了“创建新提交”步骤)一举(或膨胀,如果你喜欢Spoonerisms)。

这里发生了什么?

Git 中的换行是一个无穷无尽的混乱来源。部分问题源于人们试图通过检查工作树中的文件来观察正在发生的事情。这类似于试图找出为什么冰箱里的制冰机不工作的原因,方法是把托盘拿出来,放在极热和明亮的灯光下,这样塑料托盘就会融化。如果你这样做,你是:

  • 在错误的地方寻找,并且
  • 使用一种工具会破坏您可能首先要寻找的信息。

也就是说,问题出在其他地方,当你四处寻找它时,它早已不复存在。

要了解正在发生的事情,以及解决问题的方法和原因,您必须首先了解 Git 的三个可以找到文件的位置:

  • 文件以一种特殊的、只读的、仅限 Git 的、压缩的和去重复的形式存储在提交中,永久1且不可变。每个提交都充当每个文件的存档(有点像 tar 或 zip 存档),根据您提交时该文件的状态。

    由于这些文件的特殊属性,它们实际上不能被您的计算机使用,除了 Git 本身。因此必须将它们提取出来tar -x,例如使用或取消归档存档unzip

  • 文件以可用的形式存储在您的工作树中,作为日常文件。这是提取(解压缩或其他)文件结束的地方。这些文件实际上根本不在 Git 中。它们可供您用作输入和/或输出,而您的工作树只是一组普通的文件夹(或目录,无论您喜欢哪个术语)和文件,以您特定计算机的普通方式存储。2

这包括两个地方:那么我所说的这个“第三地方”在哪里?这就是 Git 所称的index,或staging area,或者——现在很少见—— cache。Git 的索引包含每个文件的第三个“副本”。我在这里将“复制”一词放在引号中,因为索引中的内容实际上是一种参考,使用了重复数据删除技巧。

最初,当您第一次使用git checkoutgit switch从刚刚克隆的存储库中提取特定提交时,Git 所做的是:

  • 将每个文件“复制”到自己的索引中:这个“副本”是只读的 Git-only 压缩和去重形式;然后
  • 将文件扩展为可用的形式并将其放入您的工作树中。

请注意,在这一步之前,Git 的索引是空的:它根本没有文件。现在 Git 的索引包含来自当前提交的每个文件。这些不占用空间,因为它们是重复数据删除的,并且 - 来自提交- 它们都已经在存储库中,因此它们是重复的,因此这些副本不使用空间来保存数据。3

那么:这个索引/暂存区/缓存有什么意义?好吧,有一点是它使 Git 运行得更快。另一个是它允许您部分暂存文件(尽管我不会在这里介绍这意味着什么)。但事实上,这并不是绝对必要的:其他版本控制系统没有一个就可以逃脱。只是 Git 不仅拥有它,Git还强迫你使用它。因此,您需要了解它,只要知道它位于您和您的文件(在您的工作树中)和存储库中的提交之间。

通过省略一些最终重要但还不重要的细节,我们可以很好地描述索引作为您提议的下一次提交。也就是说,索引包含将进入下一次提交的每个文件。这些文件采用 Git 自己的格式(压缩和去重),但与commit中的文件不同,您可以 替换它们。你不能修改它们(它们是只读格式,并且预先去重),但你可以运行git add.

git add命令读取某个文件的工作树副本。此工作树副本是您看到和使用的版本。如果您更改了它,请git add阅读更改后的版本。4add命令将此数据压缩为 Git 的特殊内部格式,并检查它是否重复。如果它重复的,Git 会丢弃它的压缩结果并重新使用现有数据,并使用重新使用的文件更新索引。如果它不是重复的,Git 会保存压缩和去重(但现在是第一次)的文件数据,并使用.

无论哪种方式,现在索引中的内容都是更新的文件。 因此,索引现在包含您提议的下一次提交。它也保留了您提议的下一次提交git add现在您提议的下一次提交已更新。从我们的角度来看,这告诉我们索引的用途:索引保存您提议的下一次提交。您不会提交工作树中的内容。相反,您提交 Git 索引中的内容。这就是您需要了解索引的原因:它是 Git 进行新提交的方式。


1提交本身只有在您或 Git 删除它们之前是永久的,但在很多情况下“永远不会”。出于多种原因,实际上很难摆脱 Git 提交。但是,存储提交中的文件数据经过重复数据删除后仍保留在存储库中,直到删除包含该文件的每个提交为止。

2计算机内部的实际文件存储格式本身就非常复杂多样。例如,一些系统在文件名中保留大小写但大小写折叠,因此README.mdReadMe.md是“同一个文件”,而另一些系统则说这是两个不同的文件。Git 持有后一种观点,当提交存档同时包含 aREADME.md aReadMe.md并且您将该提交提取到您的工作树时,其中一个文件会从您的工作树中丢失,因为它在物理上无法同时保存两者(因为它们具有就您的计算机而言,“同名” )。因为 Git 的归档文件是特殊的 Git-only格式,这对Git 本身来说不是问题。但这对来说可能是一个巨大的头痛。

3存储在索引中的其他属性(例如有助于 Git 快速运行的缓存方面)确实会占用一些空间。平均每个文件接近 100 字节,所以除非你有一百万个文件(然后需要大约 100 MB 的索引),否则这在现代系统中完全是微不足道的,因为你指甲大小的芯片提供 256 GB 的存储空间.

4如果您还没有更改它,请git add尝试跳过阅读它,以使 Git 运行得更快。这很快就会给我们带来麻烦。所以有时你会发现让 Git认为你已经改变了它很有用。例如,您可以通过就地重写文件或使用touch命令(如果有)来执行此操作。--renormalize标志 togit add应该解决这个问题,但我看到人们说它没有。


这与行尾有何关系

现在让我们快速回顾一下:

  • 每个提交都包含文件即快照,采用冻结(只读)、压缩、重复数据删除格式。没有任何东西,甚至 Git 本身,都无法更改任何提交的任何部分。

  • Git从 Git 索引中的任何内容进行新的提交。当您签出提交时,Git提交中填充索引,并在您运行时从其索引中的任何内容构建git commit提交。

  • 您的工作树可以让您查看提交的结果:文件来自提交,进入 Git 的索引,然后被复制并扩展为工作树中的普通文件。您的工作树让您可以控制新提交的内容:您运行git add,数据被压缩、重复数据删除,通常是 Git 化并放入索引中,准备好提交。

请注意,这里有一些步骤 Git 为 Git 做一些非常简单的事情:将提交复制到索引中根本不会更改任何文件,因为它们仍然是特殊的只读、仅 Git 格式。进行新的提交根本不会更改任何文件:它们只是从索引中的(可替换但仍然是只读的)“副本”打包成一个(只读)提交。但是有两个步骤让 Git 做一些更难的事情:

  • 当一个文件从索引中复制到您的工作树时,它会被扩展和转换。Git 必须从压缩字节更改为未压缩字节。 这是将 LF-only 更改为 CRLF 的理想时机,如果Git 确实这样做的话,这也是 Git 将这样做的时候。

  • 当一个文件从工作树中复制出来进行压缩和 Git 化并检查它是否重复时,Git 必须从未压缩的字节更改为压缩的字节。 这是将 CRLF 更改为 LF-only 的理想时机,如果Git 确实这样做的话,这也是 Git 将这样做的时候。

因此,它是 Git 进行 CRLF 行结尾修改的索引进出的副本。此外,例如在 期间发生的“索引 -> 工作树”步骤git checkout只能添加CR。它无法删除它们。例如,在 期间发生的“工作树 -> 索引”步骤git add只能删除CR,不能添加它们。

这反过来意味着,如果您选择开始进行行尾转换,随着时间的推移,存储库中的已提交文件最终将以仅 LF 行结尾结束。如果某些已提交的文件现在具有 CRLF 行结尾,那么它们将在这些提交中永远具有这些结尾,因为无法更改现有的提交。

阻碍的优化

现在我们进行一些优化:

  • 在签出提交时,Git会尽量触碰工作树。这很慢!如果没有必要,我们就不要这样做。

  • 使用 时git add,Git尽量触碰索引。太慢了!

假设您检查了一些提交,例如deadbeef. 它有5923个文件。这些文件被“复制”到索引中,这非常快,因为这些不是真正的副本。但是以前索引中有文件吗?假设您dadc0ffee在切换到deadbeef. 提交已将 5752 个文件放入索引中,然后您所做的就是查看工作树副本。

显然这些文件并不完全相同,但如果 5519 个文件相同,只剩下 233 个文件需要更改和 171 个新文件需要创建。无论出于何种原因,没有不在其中的文件只有dadc0ffee新文件。或者也许一个文件确实消失了,Git 将不得不从工作树中删除那个文件并创建 172 个文件。但无论哪种方式,Git 只需要处理工作树中404 或 405 个文件,不超过 5500 个。这样运行速度会快十倍左右。deadbeef

所以,Git做到了。如果 Git 可以,它不会触及文件。它假定如果 commit 中的索引中的文件path/to/file.ext与 commit 中的索引中的文件dadc0ffee具有相同的原始哈希 ID ,则它不必对工作树副本做任何事情。path/to/file.extdeadbeef

这个假设在 CRLF 行结束技巧的存在下失效。如果 Git应该在退出时对 CRLF 进行 LF 修改,但没有 for dadc0ffee,那么 Git 也可能会跳过这样做deadbeef

这意味着每当您更改CRLF 行尾设置时,您的工作树中可能会出现“错误”的行尾。您可以通过删除工作树副本然后再次签出文件来解决此问题(例如,使用git restoreor ,但请记住这会丢失未提交的工作!)。git reset --hardgit reset --hard

同时,如果你git add在某个文件上运行,并且 Git认为缓存的索引副本是最新的——例如,因为你没有编辑工作树副本——Git 将默默地什么都不做。但是,如果工作树副本具有 CRLF 行结尾,而索引(以及因此未来的提交)副本不应该,这是错误的。应该使用 usinggit add --renormalize来解决它,或者您可以“触摸”该文件,以便 Git 看到更新的工作树时间戳并重做副本。或者,您甚至可以git rm --cached在文件上运行,然后确实必须复制它,因为索引中git add根本没有该文件的副本。

总结:上面“简单规则”的原因

使用.gitattributes文件条目让 Git 有最大的机会把事情做好:Git 可以判断.gitattributes文件条目是否影响某个特定文件。例如,这让 Git 有机会进行更好的缓存检查。(我认为,Git 目前没有正确利用这个机会,但至少它提供了这种可能性。)

当你使用.gitattributes条目时,它们会告诉 Git 多件事:

  • 这个文件肯定不是文本:做或不要弄乱它;
  • 如果您弄乱行尾,请执行以下操作。

这让您可以说*.bat文件需要在工作树中以 CRLF 结尾,即使在 Linux 系统上也是如此,并且*.sh文件需要在工作树中以 LF 结尾,即使在 Windows 系统上也是如此。

Git 愿意为您提供尽可能多的控制权:

  • 您可以将工作树中的 CRLF 转换为索引中的 LF-only,因此在未来的提交中。
  • 您可以在将来提取提交时将已提交文件副本中的LF-only转换为工作树中的 CRLF 。

你失去的一件事是 and 的简单和全局效果core.eolcore.autocrlf这些影响现有提交,并告诉 Git猜测每个文件是否是文本。只要 Git 猜对了,这往往会正常工作。当 Git猜错时,事情变得非常糟糕。但是因为这些设置会影响实际发生的每个文件提取(索引到工作树)和每个git add(工作树到索引),并且很难知道发生了哪些,所以很难看到发生了什么。

于 2021-10-02T21:17:09.273 回答