从Git中移除一个文件,但不要删除远程用户的文件,只需忽略它。

9
我和其他几个人都可以访问一个包含由IDE自动生成的一个文件的代码库。这个文件非常特定于PC,因此不应该被纳入源代码控制,但目前已经被纳入了。我想将其删除并添加到.gitignore中,但是我不希望当其他协作者拉取我的更改时,该文件也被删除。有很多关于删除文件但保留本地副本的问题,但它们没有涉及到其他用户,所以当他们拉取时,他们仍然会失去自己的副本: 从Git存储库中删除文件而不从本地文件系统中删除 如何在不从磁盘中删除文件的情况下git rm文件? 还有一些关于在拉取时不丢失本地文件的问题和解决方案,这样他们就可以保留文件,但这需要那些拉取的人采取明确的行动,我不想去告诉每个人如何仅此一次地拉取。我找到了两个重复的问题。那里的答案是无法完成,但它们都是5年前的 - 中间有什么变化吗? Git删除已跟踪的文件,但保留本地和远程 Git忽略文件,而不删除它 这很重要,因为当您首次导入整个项目并包含有关本地编译器/库版本的信息时,该文件是自动生成的。因此,删除它将需要重新导入。如果有任何区别,它是 .idea / scala_compiler.xml .idea / scala_settings.xml (实际上应该忽略整个 .idea 目录)。基本上,我希望Git将文件设置为不再被跟踪,但不会删除任何人的文件。

1
为什么不将它放在.gitignore中?这样在拉取代码时就不需要删除它了。 - Tomer Shetah
我个人不赞同忽略一个文件,但仍然继续跟踪在你决定停止跟踪时发生的版本。为什么要永久保留一些随机生成的文件版本?如果是自动生成的,则应从git中完全删除并添加到.gitignore中。至于导致人们必须重新导入...如果他们是开发人员,他们需要知道这些东西是如何工作的。这是一种好的不便。发送一封快速电子邮件提醒他们并做正确的事情。 - JoelFan
应该从git中完全删除并添加到.gitignore。是的,那就是我想要的,但可能不是很清楚。我不想跟踪这个文件,我希望它就像从一开始就被忽略一样。我只是不想让每个人失去他们本地未跟踪的、现在未版本化的副本。 - Y_Less
请查看这个权威的问题/答案 - 它是可以做到的!:https://dev59.com/9lMH5IYBdhLWcg3w92It - goofology
1个回答

9
你不能这样做。
嗯,让我再试一次:" 不能",但他们 可以。嗯,你也可以,但只限于你自己,他们也可以,但只是对于他们自己的情况。你或他们必须在 git rm --cached 命令运行的恰当时间运行此命令。当然,这不是您想要使用的解决方法。
更有用的方式是(可能会重复之前的问题):在 Git 提交方面,你唯一能做的就是从未来的 Git 提交中省略这些文件。因为它们没有被包含在提交中,所以它们在推送和拉取操作时也不会被传输。
请记住,每个提交都在Git知道的所有文件上保持了一个完整和全面的快照。(我们稍后会进一步改进。)如果 Git 知道 .idea/* ,Git将把它们放入新的提交中,而当您推送这些提交时,您无法推送文件,只能推送提交,并且带有这些文件的提交将被传播。当您获取新的提交时——同样,您获取整个提交而不是文件——这些提交将带有这些文件。
那么基本问题变成了这个:
  • 你或他们执行的是包含 .idea/* 的 commit。 当前提交中具有这些文件。
  • 你或他们已经拉取了某些新的提交。这些新提交包含这些.idea/*的文件。
  • 如果你(或他们)现在要求你的(或他们的)Git从当前提交 切换到缺少文件的提交,则你(或他们)的Git会看到你(或他们)正在明确告诉自己Git删除这些文件,因此Git将执行此操作。
解决这个问题的方法是:
  • You (they) must tell your (their) Git to forget these files now, so that the work-tree copies of these files are untracked:

     git rm -r --cached .idea      # note the --cached
    
  • Now you (they) tell your Git: switch to the new commit. The untracked files aren't in Git's view at all, and aren't in the new commit either, so Git won't remove the work-tree copies of these files.

请注意,如果你切换回一个包含这些文件的旧提交,你的 Git 将会用提交的文件覆盖你的工作树文件。(在相同情况下,他们的 Git 将对他们的工作树文件执行相同操作。)因此,在返回包含这些文件的历史提交时,一定要非常小心。有关更多详细信息,请参见下面的长说明。
长说明:这里发生了什么
正如我们刚才提到的,每个提交都有每个文件的完整快照。这些快照以一种特殊的、只读的、Git-only格式保存。我喜欢把这种格式称为“freeze-dried”。这些文件以这种形式自动去重,因此大多数提交大多数情况下从前一个提交重新使用大多数文件的事实意味着新提交几乎不占用磁盘空间。
Git 可以安全地重用这些 freeze-dried 文件,因为任何现有提交的任何部分,包括保存的文件,都不能被更改。您可以创建与现有提交不同的新提交,但不能更改现有提交。甚至 Git 本身也不能这样做。
由于您不能使用这些文件来执行任何实际工作,因此 Git 必须提取提交。这就是 git checkout(或 Git 2.23 以后的版本中的 git switch)所做的:它从某个提交中提取了 freeze-dried 文件,以一种您可以实际使用(和更改)的形式。您选择提取并随后处理和/或处理的提交是当前提交。
这意味着从当前提交获取的每个文件都有两个副本:存储在提交本身中的冷冻干燥的副本和您用来做真正工作的常规格式、重新水化的副本。
为了创建一个新的提交,使用这种方案的任何版本控制系统 - 大多数版本控制系统都是这样做的,尽管内部细节有很大不同 - 必须将您当前的工作树版本转换为适当的提交版本。在大型存储库中,这可能需要相当长的时间。为了让自己更容易,Git 实际上根本不这样做。
相反,Git 保留了第三个副本 - 好吧,不完全是一个“副本”,因为它使用了 freeze-dried、去重复的格式 - 在 Git 称之为其“索引”、“暂存区”或(现在很少见)“缓存”的地方。这个缓存的、freeze-dried 格式的、预先去重的文件副本已经准备好进入你将要制作的下一个提交。
让我们用粗体重申一下,因为这是关键:Git 的索引包含将进入下一个提交的文件,以冻干格式准备好了。Git checkout 或 Git switch 操作会从当前提交中填充 Git 的索引和工作树,使它们匹配,除了工作树副本实际可用,而不是冻干的。
如果您更改了工作树副本,则必须在其上运行 git add 命令。git add 命令告诉 Git:“让你的索引副本与我的工作树副本相匹配。”Git 现在会读取工作树副本并将其压缩并去重,以冻干格式准备好进入下一个提交。所以,索引中的文件不再与当前提交中的文件匹配。换句话说,索引和提交之间的一个关键区别是,您可以通过像这样批量替换文件来更改索引内容。
这些索引副本就是 Git 所知道的文件。它们是将在下一个提交中的文件。要确保下一个提交没有某个文件,您只需要将它从 Git 的索引中删除即可。
git rm 命令从 Git 的索引中删除文件。如果不使用 --cached 参数,它还将从您的工作树中删除这些文件。您想要保留工作树副本,因此需要告诉 Git:使用 --cached 将我的工作树副本保留下来,只从索引(“缓存”)中删除。
现在,文件不再在 Git 的索引中了,它们也不会在下一个提交中。因此,一旦您删除了这些文件,就可以创建一个新的提交,该提交将不包含这些文件。
git rm -r --cached .idea && git commit

切换提交记录

当你使用git checkoutgit switch切换一个提交记录到另一个时,比如改变所在的分支,你就是要告诉Git:删除关于当前提交记录的所有内容,并切换到另一个提交记录。 这样Git会清空它的索引,移除你工作目录中每个对应文件的副本——也就是Git知道的那些文件。然后重新填充索引,并从你想要工作/使用的提交记录中复制相应的文件到你的工作目录:这就是你的新的当前提交记录。

如果Git知道.idea/*,那么这就是为什么.idea/*文件会被移除。如果它们不在新的提交记录中,那么它们就不会从新的提交记录中恢复出来。

.gitignore对不注意细节的人有陷阱

.gitignore文件的名字有些误导性。在.gitignore中列出的文件并不一定是未跟踪的,如果它们已经被跟踪——因为它们在Git的索引中——那么它们根本没有被忽略。

在这里需要注意的是未跟踪文件是指当前存在于你的工作目录中,但不在Git索引中的文件。这意味着如果.idea/*已经被跟踪了——例如从当前提交记录中出现——但你刚刚运行了git rm --cached .idea/*git rm -r --cached .idea,那么这些工作目录中的副本就变成了未跟踪状态。它们是否在当前提交记录中并不重要:重要的是它们现在是否在Git的索引中。

.gitignore告诉Git三件事情。前两个通常很重要,第三个则是陷阱。

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

  2. 如果未跟踪的文件名或模式出现在.gitignore中,则git add命令不会将该文件添加到Git索引中(如果需要,可以强制覆盖此行为)。 这意味着该文件将在日常的git add操作中保持未跟踪状态。

  3. 如果未跟踪的文件名或模式列在.gitignore中,则Git有时会随意重写该文件。

当您切换提交时,Git会尽量不覆盖未保存的工作

您可能熟悉这个问题:您开始处理某个文件(即工作树中的副本),然后意识到:“哎呀,我想在另一个分支上完成这项工作。” 您运行git checkout branchgit switch branch,Git以其有些神秘的方式说道:我不能那样做。 Git告诉您有未保存的更改将被覆盖。

(有时Git仍会让您切换分支。这都涉及到Git的索引。有关详细信息,请参见在当前分支上有未提交的更改时切换到另一个分支

如果此未保存的工作在已跟踪的文件中,或者在未列在.gitignore中的未跟踪文件中,则此安全检查将防止您丢失数据。 但是,在.gitignore中列出文件有时会允许Git覆盖或删除工作树副本。不明显的是准确发生这种情况的时间 - 即使采取此措施,Git有时也会告诉您先保存文件 - 但这是问题。

唯一完整的解决方案很痛苦

不幸的是,这个问题的唯一真正的解决方法与问题本身一样痛苦或更加痛苦:您可以使用具有包含永远没有该文件的提交的存储库来构建新的、不兼容的编辑历史记录存储库。

要完成这个任务,可以使用 git filter-branchgit filter-repo(比较新的工具,尚未和 Git 一起发布),或者 The BFG 等 Git 提交历史编辑工具。所有这些工具的工作方式都是必须要 复制 老的提交——那些包含文件的提交——到新的提交中,并使用不同的哈希 ID,在这些新的提交中,这些文件将永远不会出现。这个改变会 "自上而下" 扩散到所有随后的提交中。这就是新版本库与旧版本库不兼容的原因。
如果你让旧版本库和新版本库相遇,并且存在任何未更改的相关历史记录1,那么两个 Git 将连接旧的和新的历史记录,并且你将在加回你 认为 已经移除的所有提交的同时将仓库大小翻倍。

1这些历史提交是早于不需要的文件的存在时间的。例如,如果你使用 GitHub 的技巧从一个 README.mdLICENSE 文件开始,那么该提交就不需要重写,并且将保持不变,并建立旧版本库和新版本库之间的公共提交历史记录。

此外,如果你使用旧版 Git,该版本早于 --allow-unrelated-histories 标志,或提供 --allow-unrelated-historiesgit merge 中,那也可能会将旧的历史记录融合回新的历史记录中。


谢谢您提供如此详尽的解释,即使它非常详细,说明了为什么您不能做我希望您能做的事情。这也突显出一个问题,即使以艰难(手动)的方式完成,当他们切换到其他提交时仍会导致问题。我曾希望可能已经添加了一些“非纯”方法来实现这一点,即使它与逻辑文件/提交模型不完全匹配。我认为它仍然可以以某种方式进行特殊处理,即使现在还没有。但是谢谢。 - Y_Less
1
很不幸,即使Git获得了一些应对方法,也无法修复任何旧的Git版本。因此,我认为Git项目人员并不认为这是高优先级的问题。如果您编写自己的方法并提交它,我不确定会发生什么(请参阅GitHub上Git源代码镜像中的“提交补丁”文档)。 - torek
我想在这里插一句嘴 - 文件可以从git历史记录中删除(被遗忘),并且可以被本地和远程用户忽略(但不是删除!)。当然,任何新的拉取都不会有这个文件,并且git历史记录需要被重写,这可能是一个巨大的问题 - 但它是可以完成的:https://dev59.com/9lMH5IYBdhLWcg3w92It - goofology
@goofology:这一切都是正确的,但请注意,仍然拥有原始克隆版本的其他用户将仍然拥有所有原始提交记录。因此,这些远程用户必须避免 git pull,以免将原始提交记录合并回重写的历史记录中:他们应该丢弃现有的克隆版本并创建新的克隆版本。(在这种情况下,清除旧提交记录的任何方法都可以。) - torek
我需要调查一下。我相信执行 'fetch --all' 和 'git reset FETCH_HEAD' 命令可以避免将原始提交合并回来?我还没有尝试过 git pull。或许我试过但失败了,所以才使用上述方法。 - goofology

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接