从 Git 中删除一个文件,但不要为远程用户删除,只是忽略它

Remove a file from Git, but don't delete for remote users, merely ignore it

我和其他几个人都可以访问一个存储库,其中包含一个由 IDE 自动生成的文件。该文件是特定于 PC 的,因此不应该在源代码管理中,但目前是。我想删除它并将其添加到 .gitignore,但我不希望在其他协作者提取我的更改时删除它。关于删除文件但保留我的本地副本有很多问题;但它们不覆盖其他用户,所以当他们拉动时,尽管我保留了我的副本,但他们仍然会丢失副本:

Remove a file from a Git repository without deleting it from the local filesystem

How do I git rm a file without deleting it from disk?

也有拉取时不丢失本地文件的问题和解决方案,所以他们可以保留文件,但这需要拉取的人明确操作,我不想去告诉大家具体如何只拉一次。我确实发现了两个重复的问题。那里的答案是无法完成,但它们都是 5 年前的事 - 在此期间有什么变化吗?

这很重要,因为该文件是在您首次导入整个项目时自动生成的,并且包含有关本地 compiler/library 版本的信息。所以删除它需要重新导入。如果有什么不同,那就是 .idea/scala_compiler.xml.idea/scala_settings.xml(实际上应该忽略整个 .idea 目录)。基本上我想让 Git 将文件设置为不再被跟踪,但不会为任何人删除它。

你不能。

嗯,让我再试一次:不能,但是他们可以。好吧,你可以,但只对你,他们也可以,但对他们。您或他们必须在恰当的时间 运行 git rm --cached 。当然,这是你不想使用的解决方案。

为了更有用(冒着重复前面问题的风险):就 Git 提交而言,你对这些文件唯一能做的就是从未来 [=369] 中省略它们=] 提交。由于不在提交中,它们也不会被推送和获取操作传送运行。

请记住,每个提交都包含 Git 知道的所有文件的完整快照。(稍后我们将对此进行进一步细化.) 如果 Git 知道 .idea/*,Git 将把它们放入新的提交中,当你推送这些提交时——你不能推送 files,只有 提交 — 那些提交,完成那些文件,将四处走动。当您获取新的提交时 — 同样,您将获得整个 提交 ,而不是文件 — 这些提交将与这些文件一起出现。

那么根本问题就变成了:

  • 您或他们正在进行 Git 知道 .idea/* 的提交。您当前的提交包含文件。
  • 您或他们获取了一些新的提交。这些新提交 包含这些 .idea/* 文件。
  • 如果您(或他们)现在要求您的(或他们的)Git将您切换到当前提交, 缺少 文件的提交,你的(或他们的)Git 看到你(或他们)明确告诉你的(他们的)Git 删除 文件。所以它会这样做。

这个问题的解决方法是:

  • 你(他们)必须告诉你的(他们)Git现在忘记这些文件,这样work-tree副本其中的文件 未跟踪:

     git rm -r --cached .idea      # note the --cached
    
  • 现在你(他们)告诉你的Git:切换到新提交。未跟踪的文件根本不在 Git 的视图中,也不在新提交中,因此 Git 不会 删除 work-tree 这些文件的副本。

请注意,如果您曾经将 back 切换到 old 提交,那么 does 包含这些文件,您的 Git 将使用提交的文件 覆盖 您的 work-tree 文件。 (他们的 Git 将在相同条件下对他们的 work-tree 文件执行相同的操作。)因此在返回包含这些文件的历史提交时要非常小心。有关详细信息,请参阅下面的详细说明。

Long:这是怎么回事

正如我们刚才提到的,每个提交都有每个文件的完整快照。这些快照以特殊的 read-only、Git-only 格式保存。我喜欢称这种格式为freeze-dried。这种形式的文件是自动 de-duplicated,所以大多数提交的事实主要是 re-use 来自以前提交的大多数文件意味着新提交几乎不占用任何磁盘 space.

Git到re-use这些freeze-dried文件是安全的,因为任何现有提交的任何部分都没有,包括保存的文件, 永远无法改变。您可以进行不同于现有提交的新提交,但不能更改现有提交。甚至 Git 本身也做不到。

因为您实际上不能使用这些文件做任何实际工作,Git 必须提取 提交。这就是 git checkout(或者,自 Git 2.23,git switch)所做的:它从某个提交中提取 freeze-dried 文件,并将其转换为您可以实际使用的形式(并且改变)。您选择提取然后使用 and/or 的提交是您的 当前提交 .

这意味着从当前提交中获取的每个文件实际上都有两个副本:freeze-dried 一个与提交本身一起存储,另一个 regular-format,你正在使用的再水化的真正的工作。

要进行 new 提交,任何使用这种方案的版本控制系统——大多数都这样做,尽管内部细节差异很大——必须采用你当前的 work-tree 版本并将它们转回适当的提交版本。在大型存储库中,这可能需要相当长的时间。为了让自己更容易,Git 实际上根本没有这样做。

相反,Git 保留了一个 third 副本——好吧,不是真正的 副本,确切地说,因为它使用freeze-dried、de-duplicated 格式——Git 称之为 index,或 staging area,或 (这些天很少)缓存。此缓存的 freeze-dried 格式的 de-duplicated 文件副本已准备好进入您将进行的下一次提交。

让我们以粗体重复一遍,因为它是这里的关键:Git's inde包含将以 freeze-dried 格式进入 next commit 的文件,准备就绪。 git checkoutgit switch 操作从提交中填充 Git 的索引和您的 work-tree,现在是 current 提交。所有三个副本现在都匹配,除了 work-tree 副本实际上可用,而不是 freeze-dried.

如果您更改 work-tree 副本,您必须运行 git add 就可以了。 git add 命令告诉 Git:使你的索引副本与我的 work-tree 副本匹配。 Git 现在将读取 work-tree 复制并压缩 de-duplicate 到 freeze-dried 格式 ,准备进入下一次提交。因此索引中的文件不再匹配当前提交中的文件。换句话说,indexcommit 之间的主要区别在于您 can 更改索引内容,通过像这样批量替换文件。

从字面上看,这些索引副本是Git知道的文件。它们是将在 next 提交中的文件。为确保下一次提交 没有 某些文件,您只需将其从 Git 的索引中删除即可。

git rm命令

git rm 命令从 Git 的索引中删除文件。如果没有 --cached,它 也会 从您的 work-tree 中删除这些文件。你想保留你的 work-tree 副本,所以你需要告诉 Git: keep my work-tree copy 添加 --cached git rm:仅从索引(“缓存”)中删除。

既然文件 不在 Git 的索引中,它们将不会在 下一个 提交。因此,一旦您删除了这些文件,您就可以进行新的提交,没有 有这些文件:

git rm -r --cached .idea && git commit

例如。

切换提交

当你使用 git checkoutgit switch 从一个提交切换到另一个提交时——例如,通过改变你所在的 b运行ch——你是在告诉 Git: 删除与 当前 提交相关的所有内容并切换到另一个提交。 这 Git 清空了它的索引, 删除每个对应文件的 work-tree 副本——Git 知道的文件。然后 Git 可以 re-fill 它的索引和 re-populate 你的 work-tree 以及你想要工作的提交中的文件副本 on/with:你的新当前提交.

如果 Git 知道 .idea/*,这就是 .idea/* 文件被删除的原因。如果他们不在 新提交中,则他们不会从 新提交中返回。

.gitignore 有一个陷阱

.gitignore 文件的命名有些错误。 .gitignore 中列出的文件不一定 未跟踪 ,如果它们被跟踪——如果 Git 知道它们,因为它们在 Git 的索引中—他们根本没有被忽视。

请注意,未跟踪的文件 是您 work-tree 现在 中的文件,但 [=212= Git 的索引 现在 不。这意味着如果 .idea/* 被跟踪——例如来自当前提交——但你只是 运行 git rm --cached .idea/*git rm -r --cached .idea,那些 work-tree 副本是现在未追踪。它们是否在当前 commit: 中并不重要,重要的是它们是否在 Git 的 index 现在.

.gitignore 所做的是告诉 Git 三件事。前两个通常是重要的两个。最后一个是陷阱

  1. 如果未跟踪文件的名称或模式出现在 .gitignore 中,git status 命令将不会 抱怨文件未被跟踪。

  2. 如果未跟踪文件的名称或模式出现在 .gitignore 中,git add 将不会 添加 文件 Git的索引(如果你愿意,你可以强制git add覆盖它)。这意味着该文件将在正常的每天 git add 秒内保持未跟踪状态。

  3. 如果 .gitignore 中列出了未跟踪文件的名称或模式,Git 有时会随意 破坏 文件。

当您切换提交时,Git 尽量不破坏未保存的工作

您可能对这个问题很熟悉:您开始处理某个文件 - work-tree 中的副本 - 然后意识到:哎呀,我想做这个工作在不同的 b运行ch. 你 运行 git checkout <em>b运行ch</em> git 切换 <em>b运行ch</em>,并且 Git 在其有点神秘的方式:我不能那样做。 Git 告诉你你有未保存的更改会被破坏。

(有时 Git 会让你切换 b运行 的位置。这又与 Git 的索引有关。有关详细信息,请参阅 Checkout another branch when there are uncommitted changes on the current branch)

如果此未保存的作品在跟踪文件中,或者在未跟踪文件中 列在 .gitignore 中,此安全检查将防止您丢失数据。但是在 .gitignore 中列出文件 有时 允许 Git 覆盖或删除 work-tree 副本。发生这种情况的确切时间并不明显 - 有时即使有这种情况,Git 也会告诉你先保存你的文件 - 但它 一个问题。

唯一彻底的解决办法是痛苦的

不幸的是,这个问题唯一真正的解决方案与问题本身一样痛苦,或者比问题本身更痛苦:你可以获取包含提交的存储库,并使用它来构建一个新的、不兼容的edited-history 仅包含根本没有文件的提交的存储库。

为此,请使用 git filter-branch,或 git filter-repo(相对较新,尚未与 Git 一起分发),或 BFG,或任何此类 Git-commit-history-editing 系统。这些所有工作的方式,必然是,它们 copy 旧提交——那些有文件的提交——到新提交,具有不同的哈希 ID,这些文件永远不会出现。然后,此更改会“随着时间的推移”波及所有后续提交。这就是新存储库与旧存储库不兼容的原因。

如果您曾经让旧存储库和新存储库相遇,并且有任何相关历史没有改变,1两个 Git 将连接新旧历史记录,您实际上会将存储库的大小增加一倍,同时添加回您 认为 已删除的所有提交。


1这将是在不需要的文件存在之前的历史提交。例如,如果您使用 GitHub 的技巧,即从 README.mdLICENSE 文件开始,则该提交将不需要重写,并且将保持不变并在旧文件之间建立共同的提交历史记录和新的存储库。

除此之外,如果您使用可以追溯到 --allow-unrelated-histories 标志之前的旧 Git,或者将 --allow-unrelated-histories 提供给 git merge,那也可以融合旧历史回到新历史。