确定性追溯 .gitignore(如何让 Git 完全/追溯地忘记现在在 .gitignore 中的文件)

Posted

技术标签:

【中文标题】确定性追溯 .gitignore(如何让 Git 完全/追溯地忘记现在在 .gitignore 中的文件)【英文标题】:Definitive retroactive .gitignore (how to make Git completely/retroactively forget about a file now in .gitignore) 【发布时间】:2019-12-16 12:42:10 【问题描述】:

前言

这个问题试图消除关于追溯应用 .gitignore 的困惑,而不仅仅是现在/未来。1

基本原理

我一直在寻找一种方法来追溯执行当前的 .gitignore,就好像我在第一次提交中创建了 .gitignore

我正在寻求的解决方案:

不需要需要手动指定文件 不会需要提交 将追溯适用于所有分支的所有提交 将忽略工作目录中的 .gitignore 指定文件,不删除它们(就像最初根提交的 .gitignore 文件一样) 将使用 git,而不是 BFG 将适用于 .gitignore 异常,例如:
 *.ext
 !*special.ext

不是解决方案

git rm --cached *.ext
git commit

这需要 1. 手动指定文件和 2. 额外的提交,这将导致在被其他开发人员拉取时新忽略的文件删除。 (它实际上只是一个 git rm - 这是 git 跟踪中的 删除 - 但它将文件单独留在本地(您的)工作目录中。之后git pull的其他人会收到文件删除提交)

git filter-branch --index-filter 'git rm --cached *.ext'

虽然此确实会追溯地清除文件,但它 1. 需要手动指定文件和 2. deletes the specified files from the local working directory 就像普通的 git rm 一样(对于其他 git pull 也是如此)!


脚注

1这里有很多关于 SO 的类似帖子,其中的问题定义不明确,甚至答案也不准确。请参阅this question with 23 answers,其中根据“忘记”as noted by one mostly-correct answer 的标准定义,accepted answer with ~4k votes 不正确,only 2 answers 包括 必需 em> git filter-branch 命令。

This question with 21 answers is 被标记为与前一个重复,但问题的定义不同(忽略与忘记),因此虽然答案可能是适当的,但它是 不是 重复。

This question 是我找到的最接近我正在寻找的东西,但答案并非在所有情况下都有效(带有空格的路径......),并且可能比创建所需的更复杂一个外部到存储库的 .gitignore 文件并将其复制到每个提交中。

【问题讨论】:

有时最好写一个脚本来为你做手动的事情。 如果有问题的文件从未提交,您的目标是重写存储库的外观吗(这会使所有现有的提交 ID 无效,并且可能会破坏每个repo 的现有克隆/签出),或者配置您的本地工作目录,以便 Git 假装当您签出旧提交时,这些文件不存在于旧提交中? 目标是前者,“好像我一开始就创建了.gitignore”。我理解后果,但我的回购是本地/私人的,我不介意强制推送。虽然如果您回答,请随意指定如何处理后者 - 似乎这将是有用的信息。 这将是一个五行的filter-branch,顶部。将您的排除项放入 .git/info/exclude,执行 git ls-files --exclude-standard -ci 并 rm --cached 它们。 谢谢。我同意忘记=追溯,并且不需要明确指定它,如果不是针对其他令人难以置信的“完全忘记”问题,并且接受的答案仅适用于现在/未来。也许这个问题也应该编辑得更明确(仅限现在/未来)? 【参考方案1】:

编辑:我最近发现了git-filter-repo。这可能是一个更好的选择。自己调查 rationale 和 filter-branch gotchas 或许是个好主意,但它们不会影响我下面的用例。


这种方法使 Git 完全忘记被忽略的文件(过去/现在/未来),但不会从工作目录中删除任何内容(甚至从远程重新拉出时)。

此方法需要在所有有文件的提交中使用/.git/info/exclude(首选) 预先存在的 .gitignore被忽略/忘记。 1

此方法避免在下一个git pull 2

上从其他开发人员机器上删除新忽略的文件

所有 强制 Git 的方法会忽略事后行为,有效地重写历史记录,因此在此过程之后可能会拉取任何公共/共享/协作存储库的 significant ramifications。 3

一般建议:从一个干净的 repo 开始 - 一切都已提交,工作目录或索引中没有任何待处理的内容,并进行备份

此外,this answer 的 cmets/revision history(and revision history 的 this question)可能有用/启发性。

#commit up-to-date .gitignore (if not already existing)
#these commands must be run on each branch
#these commands are not strictly necessary if you don't want/need a .gitignore file.  .git/info/exclude can be used instead

git add .gitignore
git commit -m "Create .gitignore"

#apply standard git ignore behavior only to current index, not working directory (--cached)
#if this command returns nothing, ensure /.git/info/exclude AND/OR .gitignore exist
#this command must be run on each branch
#if using .git/info/exclude, it will need to be modified per branch run, if the branches have differing (per-branch) .gitignore requirements.

git ls-files -z --ignored --exclude-standard | xargs -r0 git rm --cached

#Commit to prevent working directory data loss!
#this commit will be automatically deleted by the --prune-empty flag in the following command
#this command must be run on each branch
#optionally use the --amend flag to merge this commit with the previous one instead of creating 2 commits.

git commit -m "ignored index"

#Apply standard git ignore behavior RETROACTIVELY to all commits from all branches (--all)
#This step WILL delete ignored files from working directory UNLESS they have been dereferenced from the index by the commit above
#This step will also delete any "empty" commits.  If deliberate "empty" commits should be kept, remove --prune-empty and instead run git reset HEAD^ immediately after this command

git filter-branch --tree-filter 'git ls-files -z --ignored --exclude-standard | xargs -r0 git rm -f --ignore-unmatch' --prune-empty --tag-name-filter cat -- --all

#List all still-existing files that are now ignored properly
#if this command returns nothing, it's time to restore from backup and start over
#this command must be run on each branch

git ls-files --other --ignored --exclude-standard

最后,关注this GitHub guide 的其余部分(从第 6 步开始)其中包括有关以下命令的重要警告/信息

git push origin --force --all
git push origin --force --tags
git for-each-ref --format="delete %(refname)" refs/original | git update-ref --stdin
git reflog expire --expire=now --all
git gc --prune=now

从现在修改的远程仓库中提取的其他开发人员应该进行备份,然后:

#fetch modified remote

git fetch --all

#"Pull" changes WITHOUT deleting newly-ignored files from working directory
#This will overwrite local tracked files with remote - ensure any local modifications are backed-up/stashed

git reset FETCH_HEAD

脚注

1 因为/.git/info/exclude 可以使用上述说明应用于所有历史提交,也许是有关将.gitignore 文件放入历史提交的详细信息需要它超出了这个答案的范围。我想要一个正确的.gitignore 在根提交中,就好像这是我做的第一件事一样。其他人可能不在乎,因为/.git/info/exclude 可以完成同样的事情,无论.gitignore 存在于提交历史中的哪个位置,并且显然重写历史是一个非常敏感的主题,即使知道@ 987654330@.

FWIW,潜在的方法可能包括 git rebasegit filter-branch,它们将 external .gitignore 复制到每个提交中,例如 this question 的答案

2 通过提交独立 git rm --cached 命令的结果来强制执行 git 事后忽略行为可能会导致新忽略的文件 删除 将来从强制推送的遥控器。 git filter-branch 命令中的 --prune-empty 标志(or git reset HEAD^ 之后)通过自动删除先前的“删除所有忽略的文件”仅索引提交来避免此问题。

3 重写 git 历史记录也会更改提交哈希,这将在未来从公共/共享/协作存储库中拉取时 wreak havoc。在对这样的回购执行此操作之前,请充分了解ramifications。 This GitHub guide 指定以下内容:

告诉你的合作者rebase,不要合并他们从你旧的(受污染的)存储库历史创建的任何分支。一次合并提交可能会重新引入部分或全部您刚刚费力清除的受污染历史。

影响远程仓库的替代解决方案是git update-index --assume-unchanged </path/file>git update-index --skip-worktree <file>,可以在here 找到示例。

【讨论】:

我原本希望使用git filter-branch --index-filter 'git ls-files -z --ignored --exclude-from=.gitignore | xargs -0 git rm --cached --ignore-unmatch' --prune-empty --tag-name-filter cat -- --all,但得到了fatal: cannot use .gitignore as an exclude file 潜在的 .gitignore “注入”解决方案 - git rebase...,或将外部 .gitignore 复制到每个提交中的 git-filter-branch,就像this question的答案一样 不幸的是,由于xargs,这在 Windows 中不起作用。 这是在 Windows Git 上开发/测试的 - 在 Cygwin (Git Bash) 中使用它,而不是 CMD。

以上是关于确定性追溯 .gitignore(如何让 Git 完全/追溯地忘记现在在 .gitignore 中的文件)的主要内容,如果未能解决你的问题,请参考以下文章

如何让git忽略指定的文件

如何告诉 git 忽略个别行,即特定代码行的 gitignore [重复]

git如何忽略文件

git 使用详解—— 最基本命令 + .gitignore 文件

.gitignore设置不生效

如何在 git 中使用 gitignore 命令