从git/GitHub的历史记录中删除文件夹及其内容

492
我正在处理我GitHub账户上的一个库,遇到了这个问题。
  • Node.js项目中安装了几个npm包的文件夹
  • 这些包位于node_modules文件夹中
  • 将该文件夹添加到git仓库并将代码推送到GitHub(当时没有考虑npm部分)
  • 后来发现你实际上不需要把那个文件夹作为代码一部分
  • 删除该文件夹并推送

此时,总的git repo大小约为6MB,而实际上的代码(除了那个文件夹外)只有300 KB

现在我最终想要的是,从git的历史记录中摆脱那个包文件夹的细节,这样如果有人克隆它,他们就不必下载价值6MB的历史记录,因为他们所得到的实际上只是最后提交的300KB的文件。

我查阅了可能的解决方案,并尝试了以下两种方法:

这个Gist似乎起作用了,运行脚本后,它显示已经摆脱了那个文件夹,并且之后显示修改了50个提交。但是它不允许我推送该代码。当我尝试推送时,它会显示Branch up to date,但在git status上却显示修改了50个提交。另外两种方法也没有帮助。

即使它显示已经删除了该文件夹的历史记录,但当我检查本地主机上该仓库的大小时,它仍然在6MB左右。(我也删除了refs/original文件夹,但是没有看到仓库大小的变化)。

我想要澄清的是,是否有一种方法可以除去不仅是提交历史记录(这是我认为发生的唯一事情),而且还包括那些git假定需要回滚的文件。

假设解决此问题的方法被应用于我的本地主机,但无法复制到GitHub仓库,是否可以克隆该仓库,回滚到第一个提交,执行这个技巧并将其推送(还是这意味着git仍将拥有所有这些提交的历史记录?-也就是说6MB)。

我的最终目标是找到最好的方法从git中删除文件夹内容,以便用户不必下载6MB的内容,仍可能具有从未触及模块文件夹(几乎全部)的其他提交在git的历史记录中。

我该如何做到这一点?


4
如果以下任何一个答案解决了你的问题,或许你应该考虑接受其中一个作为对你的问题的答案。https://meta.stackexchange.com/questions/5234/how-does-accepting-an-answer-work - starbeamrainbowlabs
最佳答案是:https://dev59.com/GWkw5IYBdhLWcg3wMHum#32886427 - Kuzeko
9个回答

690

警告:git filter-branch 不再被官方推荐使用。官方推荐使用git-filter-repo;详见André Anjos的回答


如果您在这里复制粘贴代码: 这是一个示例,它从历史记录中删除了“node_modules”。
git filter-branch --tree-filter "rm -rf node_modules" --prune-empty HEAD
git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
echo node_modules/ >> .gitignore
git add .gitignore
git commit -m 'Removing node_modules from git history'
git gc
git push origin master --force

Git实际上是做什么的:

第一行遍历与HEAD(您当前的分支)相同树(--tree-filter)上的所有引用,运行命令rm -rf node_modules。此命令删除node_modules文件夹(-r,没有-rrm不会删除文件夹),用户无需提示(-f)。添加的--prune-empty递归地删除无用(不改变任何内容)的提交。

第二行删除对那个旧分支的引用。

其余的命令相对简单明了。


4
小注:我使用git count-objects -v检查文件是否已被删除,但是仓库的大小直到我重新克隆仓库才变小。我认为Git会保留所有原始文件的副本。 - Davide Icardi
5
如果使用的是非古老的 Git 版本,这句话应该写成 --force-with-lease 而不是 --force - Griwes
6
这些命令在Windows上都不能使用,或者至少在Windows 10上不行。请告知"剪切和粘贴"能够正常工作的操作系统。 - David
8
对于 Windows 10 用户而言,在 Bash for Windows 下(我使用的是 Ubuntu)这个方法非常有效。 - Andrej Kyselica
4
我尝试在 Windows Shell 和 Git Bash 中运行它,但失败了。第一个命令执行成功,第二个命令执行失败! - Mohy Eldeen
显示剩余19条评论

331

我发现其他答案中使用的--tree-filter选项可能非常缓慢,特别是在具有大量提交的较大存储库上。

这是我使用的方法,使用--index-filter选项完全从git历史记录中删除目录,速度更快:

# Make a fresh clone of YOUR_REPO
git clone YOUR_REPO
cd YOUR_REPO

# Create tracking branches of all branches
for remote in `git branch -r | grep -v /HEAD`; do git checkout --track $remote ; done

# Remove DIRECTORY_NAME from all commits, then remove the refs to the old commits
# (repeat these two commands for as many directories that you want to remove)
git filter-branch --index-filter 'git rm -rf --cached --ignore-unmatch DIRECTORY_NAME/' --prune-empty --tag-name-filter cat -- --all
git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d

# Ensure all old refs are fully removed
rm -Rf .git/logs .git/refs/original

# Perform a garbage collection to remove commits with no refs
git gc --prune=all --aggressive

# Force push all branches to overwrite their history
# (use with caution!)
git push origin --all --force
git push origin --tags --force

您可以在gc之前和之后通过以下方式检查存储库的大小:

git count-objects -vH

4
你能解释一下为什么这个更快吗? - knocte
28
为什么这个答案没有被采纳?它非常详尽。 - Mad Physicist
3
如果在Windows系统中操作,需要使用双引号而不是单引号。 - Kris Morness
16
在上面的git rm命令中加入--quiet选项,可以将我的重写速度至少提升4倍。 - ctusch
显示剩余14条评论

193
似乎最新的答案是不要直接使用filter-branch(至少git本身不再推荐),而是将该工作推迟到外部工具中进行。特别是,目前推荐使用git-filter-repo。该工具的作者在此处提供了关于为什么直接使用filter-branch会导致问题的论据。

大多数上面用于从历史记录中删除dir的多行脚本可以重写为:

git-filter-repo --path dir --invert-paths

显然这个工具比它看起来的更加强大。您可以通过作者、电子邮件、refname等应用过滤器(完整的手册在此处)。此外,它非常快速。安装很容易 - 它以各种格式分发


14
好工具!在Ubuntu 20.04上运行良好,您只需使用pip3 install git-filter-repo进行安装,因为它只使用标准库,不会安装任何依赖项。在Ubuntu 18上,由于分发版本的git版本不兼容,需要一个带有“--combined-all-paths”选项的diff-tree命令版本的git才能使用,但是您可以轻松地在docker run -ti ubuntu:20.04上运行它。 - kubanczyk
3
git: 'filter-repo' is not a git command. See 'git --help'. - alper
3
谢谢,这很快就完成了!关于使用的一些注意事项:1)您可能需要安装更新版本的git。如果您使用的是ubuntu,则可能需要设置新的apt存储库,因为Xenial存储库仍在使用git 2.7.4,这太旧了。2)这确实会在本地删除文件夹。如果需要,请备份它。3)您需要重新添加远程URL并进行强制推送(像往常一样,小心!)。4)您可以使用pip3轻松安装该工具(如上所述)。5)如果您不想克隆全新的repo,则可能需要使用--force运行。对我来说似乎没问题。 - rococo
2
示例应该读作 git-filter-repo.py,而不是 git filter-repo。它不是 Git 的本地命令。 - Hugh Guiney
2
在OS X系统中,Homebrew链接提供支持。 brew install git-filter-repo - markgo2k
显示剩余8条评论

58

除了流行的答案上面之外,我想为Windows系统添加一些注意事项。该命令

git filter-branch --tree-filter 'rm -rf node_modules' --prune-empty HEAD
  • 完美运行没有任何修改!因此,您不能使用Remove-Itemdel或其他任何东西,而不是使用rm -rf

  • 如果您需要指定文件或目录的路径,请使用斜线,如./path/to/node_modules


如果目录名称中包含 .(点号),则此方法在 Windows 上无法正常工作。 - Corneliu Serediuc
5
我已经找到解决方法。使用双引号来执行rm命令,像这样:"rm -rf node.modules"。 - Corneliu Serediuc
@CorneliuSerediuc 兄弟,就说引号 - undefined

30

我发现最好、最准确的方法是下载bfg.jar文件: https://rtyley.github.io/bfg-repo-cleaner/

然后运行以下命令:

git clone --bare https://project/repository project-repository
cd project-repository
java -jar bfg.jar --delete-folders DIRECTORY_NAME
git reflog expire --expire=now --all && git gc --prune=now --aggressive
git push --mirror https://project/new-repository

如果你想要删除文件,那么请使用 delete-files 选项:

java -jar bfg.jar --delete-files *.pyc

1
非常简单 :) 如果您想确保仅删除特定文件夹,则可以使用以下方法:https://dev59.com/bmEi5IYBdhLWcg3wldFu - emjay
2
但是使用BFG时,如果有几个文件夹与您想要删除的特定文件夹具有相同的名称,则可能会遇到麻烦,即BFG无法接受“--delete-folders”的路径名。 - gzh

8

完整的复制粘贴配方,只需在注释中添加命令(用于复制粘贴解决方案),在测试后:

git filter-branch --tree-filter 'rm -rf node_modules' --prune-empty HEAD
echo node_modules/ >> .gitignore
git add .gitignore
git commit -m 'Removing node_modules from git history'
git gc
git push origin master --force

在此之后,您可以从.gitignore中删除“node_modules/”这一行。

第二个问题是:“在此之后,您可以从 .gitignore 中删除“node_modules/”行。” 答案中的这一行(答案……不是 git 提交消息)说您可以删除 node_modules/ ……但为什么要这样做呢? - WernerCD

8

对于Windows用户,请注意使用"而不是'。 如果已经有另一个备份存在,请添加-f以强制执行该命令。

git filter-branch -f --tree-filter "rm -rf FOLDERNAME" --prune-empty HEAD
git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d
echo FOLDERNAME/ >> .gitignore
git add .gitignore
git commit -m "Removing FOLDERNAME from git history"
git gc
git push origin master --force

2
我使用 Git 在 Windows 上移除了旧的 C# 项目中的 bin 和 obj 文件夹。请注意:
git filter-branch --tree-filter "rm -rf bin" --prune-empty HEAD

它通过删除git安装文件夹中的usr/bin文件夹来破坏git安装的完整性。

1

对于复制粘贴者(从这里):

git filter-repo --invert-paths --path PATH-TO-YOUR-FILE-WITH-SENSITIVE-DATA
echo "YOUR-FILE-WITH-SENSITIVE-DATA" >> .gitignore
git add .gitignore
git commit -m "Add YOUR-FILE-WITH-SENSITIVE-DATA to .gitignore"
git push origin --force --all

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