/bin/sh^M: 糟糕的解释器:没有这样的文件或目录错误导致不同的 GIT env 配置互相覆盖?

/bin/sh^M: bad interpreter: No such file or directory error caused by different GIT env configs overriding each other?

我编写的构建脚本在 ci/cd 管道(在 linux 中运行)上失败,因为 build.sh 脚本以某种方式获得了 converted/save CRLF 格式(基于根据我在网上收集的信息),导致此错误:

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

脚本本身非常基础:

#!/bin/sh
mvn clean install

我想确认原因是 git 由于我在 运行 git 配置时看到的。下面详细介绍我采取的补救措施:

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

  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 脚本在其他环境中无法 运行 正常运行?

这里有一些简单的规则,尽管其中有些是个人意见:

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

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

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

这是怎么回事?

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

  • 找错地方了,
  • 使用一种工具会破坏您最初可能正在寻找的信息。

也就是说,问题出在别处,等你有空去找它的时候,它早就不见了。

要了解发生了什么,并因此了解解决问题的方法和原因实际上解决了问题,您必须首先了解可以找到文件的 Git 三个位置:

  • 文件被永久存储1并且不可改变地存储在提交中,在特殊的read-only、Git-only、压缩和de-duplicated表格。每次提交都充当 每个 文件的存档(有点像 tar 或 zip 存档),截至您提交时该文件的状态。

    由于这些文件的特殊属性,除了 Git 本身之外,您的计算机实际上无法使用它们。因此,它们必须 提取,例如 un-archiving 具有 tar -xunzip.

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

包括两个地方:那么我说的“第三个地方”在哪里?这就是 Git 所称的 索引 暂存区 ,或者现在很少见的 缓存。 Git 的索引包含每个文件第三个“副本”。我在这里将“复制”一词用引号引起来,因为索引中的内容实际上是一种参考,使用 de-duplication 技巧。

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

  • 将每个文件“复制”到它自己的索引中:此“复制”采用read-only Git-only压缩和de-duplicated形式;然后
  • 将文件扩展为可用形式并将其放入您的工作树中。

请注意,在此步骤之前,Git 的索引是 空的: 它根本没有任何文件。现在 Git 的索引有 来自当前提交 的每个文件。这些没有 space,因为它们是 de-duplicated 并且 - 已经 来自提交 - 它们都已经 在存储库中 所以它们 是重复的 因此这些副本不使用 space 来保存数据。3

那么:这个索引/staging-area/缓存的是什么?好吧,有一点是它让 Git 走得很快。另一个是它允许您 部分暂存 文件(尽管我不会在这里介绍这意味着什么)。但事实上,这并不是绝对必要的:其他版本控制系统没有一个就可以逃脱。只是Git不仅,Git 强制你使用它。所以你需要了解它,只要知道它把自己放在你和你的文件之间——在你的工作树中——以及存储库中的提交。

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

git add命令读取某个文件的工作树副本。此工作树副本是您看到和使用的版本。如果你改变了它,git add读取改变的版本。4add命令将这个数据压缩成Git 的特殊内部格式并检查它是否重复。如果它 重复项,Git 将丢弃其压缩结果和 re-uses 现有数据,并使用 re-used 文件更新索引。如果它 不是 重复项,Git 保存压缩和 de-duplicated(但现在是第一次)文件数据并使用 that.

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


1只有在您或 Git 删除它们之前,提交本身才是永久的,但在很多情况下,这是“永远不会”的。由于很多原因,实际上很难摆脱 Git 提交。存储在 提交 de-duplicated 中的文件数据保留在存储库中,直到 every 保存该文件的提交被删除,虽然.

2计算机内部的实际文件存储格式本身就非常复杂和多样。例如,某些系统在文件 names 中 case-preserving 但 case-folding,因此 README.mdReadMe.md 是“同一个文件” ,而其他人则说这是两个 不同的 文件。 Git 持有后一种意见,并且当提交存档同时包含 README.md ReadMe.md 时,您将该提交提取到您的工作树,其中一个文件从您的工作树中丢失,因为它在物理上无法同时保存这两个文件(因为它们具有“相同的名称” 您的 计算机而言)。因为Git的归档文件是特殊的Git-only格式,所以Git本身。但是.

可能会很头疼

3存储在索引中的其他属性——比如缓存方面,这有助于 Git 走得更快——做一点 space .平均每个文件接近 100 字节,所以除非你有一百万个文件(然后需要大约 100 MB 的索引),否则这在现代系统中是微不足道的,因为一个指甲大小的芯片提供 256 GB 的存储空间.

4如果您还没有更改它,git add会尝试跳过 阅读它,使 Git 走得快。一会儿这会给我们带来麻烦。因此,有时您可能会发现将 Git 欺骗成 认为 您已更改它很有用。例如,您可以通过重写文件或使用 touch 命令(如果有)来完成此操作。 git add--renormalize 标志 应该 也可以解决这个问题,但我看到有人说它没有。


这与行尾有何关系

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

  • 每个提交都包含 files-as-a-snapshot,冻结(read-only),压缩,de-duplicated 格式。任何东西,甚至 Git 本身,都不能 更改 任何提交的任何部分。

  • Git 使 new 从 Git 的索引中的任何内容提交。 Git 在您检出提交时从 来自 的提交填写索引,并从其索引中的任何内容构建 new 提交当时你 运行 git commit.

  • 你的工作树让你看到什么从提交中出来:文件从提交中出来,进入Git的索引,然后复制并扩展成为你的普通文件工作树。您的工作树让您 控制进入新提交的内容: 您 运行 git add 并且数据被压缩,de-duplicated,通常 Git-ified 并将 放入 索引中,准备提交。

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

  • 随着文件从索引中被复制到您的工作树,它会被扩展和转换。 Git 必须从压缩字节更改为未压缩字节。 这是将 LF-only 更改为 CRLF 的理想时机,此时 Git 会这样做,if Git 完全做到了。

  • 随着文件从工作树复制进行压缩并Git-ified检查是否重复,Git必须从未压缩字节更改为压缩字节。 这是将 CRLF 更改为 LF-only 的理想时机,此时 Git 会这样做,if Git 完全做到了。

所以它在 Git 进行 CRLF 行结束修改的索引中进行复制。此外,例如在 git checkout 期间发生的“索引 -> 工作树”步骤只能 添加 CR。它无法 删除 它们。例如,在 git add 期间发生的“工作树 -> 索引”步骤只能 删除 CR,而不能添加它们。

这反过来意味着,如果您选择 start 进行行结束转换,存储库 中提交的文件最终将以 LF-only 行结尾,随着时间的推移。如果一些提交的文件现在有 CRLF 行结尾,它们将在 那些提交 中永远拥有这些结尾,因为没有 existing 提交可以更改。

阻碍的优化

现在我们进行一些优化:

  • 签出提交时,Git 尽可能 尝试触及工作树。这很慢!没必要我们就不做。

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

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

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

所以,Git 做到了。如果 Git 可以,它 不会触及文件 。它 假设 如果提交 dadc0ffee 中索引中的文件 path/to/file.ext 与提交 [=] 中索引中的文件 path/to/file.ext 具有相同的原始哈希 ID 49=],它不必对工作树副本执行任何操作。

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

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

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

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

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

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

  • 这个文件肯定是或者不是 text: do, or don't, mess with it;
  • 如果您要弄乱行尾,请按以下步骤操作。

这让您可以说 *.bat 文件需要在工作树中 CRLF-ended,即使在 Linux 系统上,并且 *.sh 文件需要 LF-ended 在工作树中,甚至在 Windows 系统上。

您可以获得 Git 愿意给您的控制权:

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

你失去的一件事是 core.eolcore.autocrlf 的简单和全局效果:这些影响现有的提交,并告诉 Git guess 每个文件是否是文本。只要 Git 猜对了,就可以正常工作 sort-of-OK。当 Git 猜测 错误 时,事情变得非常糟糕。但是因为这些设置会影响实际发生的每个文件提取 (index-to-work-tree) 和每个 git add (work-tree-to-index) ,而且很难知道是哪些发生,很难看出是怎么回事。

git config --global core.autocrlf false

git rm --cached -r .

git reset --hard

这只适合像我这样懒惰的人...