如何从Git存储库中的提交历史记录中删除/删除大文件?

我不小心将DVD翻录到网站项目中,然后不小心git commit -a -m ...,然后,zap,存储库膨胀了2.2 gigs。下次我做了一些编辑,删除了视频文件,并提交了所有内容,但压缩文件仍然存在于存储库中,在历史中。

我知道我可以从这些提交中启动分支并将一个分支重新定位到另一个分支。但是我应该怎么做才能合并这两个提交,这样大文件就不会显示在历史记录中,而是在垃圾回收机制过程中被清理?

382789 次浏览

如果您已经向其他开发人员发布了历史记录,您想要做的是高度破坏性的。修复历史记录后的必要步骤请参阅#0留档中的“从上游重定向恢复”

您至少有两个选项:git filter-branch交互式数据库,都在下面解释。

使用git filter-branch

我在Subversion导入的庞大二进制测试数据中遇到了类似的问题,并写了大约从git存储库中删除数据

假设你的git历史记录是:

$ git lola --name-status* f772d66 (HEAD, master) Login page| A     login.html* cb14efd Remove DVD-rip| D     oops.iso* ce36c98 Careless| A     oops.iso| A     other.html* 5af4522 Admin page| A     admin.html* e738b63 IndexA     index.html

请注意,git lola是一个非标准但非常有用的别名。(有关详细信息,请参阅本答案末尾的附录。)--name-status切换到git log显示了与每个提交关联的树修改。

在“粗心”提交(其SHA1对象名称为ce36c98)中,文件oops.iso是偶然添加并在下一次提交cb14efd中删除的DVD-rip。使用上述博客文章中描述的技术,要执行的命令是:

git filter-branch --prune-empty -d /dev/shm/scratch \--index-filter "git rm --cached -f --ignore-unmatch oops.iso" \--tag-name-filter cat -- --all

选项:

  • --prune-empty删除由于过滤器操作而变为空的提交(,不更改树)。在典型情况下,此选项会生成更干净的历史记录。
  • -d命名一个尚不存在的临时目录,用于构建过滤的历史记录。如果您在现代Linux发行版上运行,请指定#1中的树将导致更快的执行
  • --index-filter是主事件,在历史记录的每一步都针对索引运行。您希望在找到oops.iso的任何地方删除它,但它并未出现在所有提交中。命令git rm --cached -f --ignore-unmatch oops.iso会在DVD-rip存在时删除它,否则不会失败。
  • --tag-name-filter描述了如何重写标记名称。cat的过滤器是标识操作。您的存储库,就像上面的示例一样,可能没有任何标记,但我包含此选项是为了完全通用。
  • --指定git filter-branch的选项结束
  • --之后的--all是所有引用的简写。您的存储库,就像上面的示例一样,可能只有一个ref(master),但我包含此选项是为了完全通用。

经过一番翻腾,现在的历史是:

$ git lola --name-status* 8e0a11c (HEAD, master) Login page| A     login.html* e45ac59 Careless| A     other.html|| * f772d66 (refs/original/refs/heads/master) Login page| | A   login.html| * cb14efd Remove DVD-rip| | D   oops.iso| * ce36c98 Careless|/  A   oops.iso|   A   other.html|* 5af4522 Admin page| A     admin.html* e738b63 IndexA     index.html

请注意,新的“粗心”提交只添加了other.html,并且“删除DVD-rip”提交不再在master分支上。标记为refs/original/refs/heads/master的分支包含您的原始提交,以防您犯了错误。要删除它,请按照"缩减存储库的清单。"中的步骤操作

$ git update-ref -d refs/original/refs/heads/master$ git reflog expire --expire=now --all$ git gc --prune=now

对于更简单的替代方案,克隆存储库以丢弃不需要的位。

$ cd ~/src$ mv repo repo.old$ git clone file:///home/user/src/repo.old repo

使用file:///...克隆URL复制对象,而不是仅创建硬链接。

现在你的历史是:

$ git lola --name-status* 8e0a11c (HEAD, master) Login page| A     login.html* e45ac59 Careless| A     other.html* 5af4522 Admin page| A     admin.html* e738b63 IndexA     index.html

前两次提交的SHA1对象名称(“Index”和“Admin page”)保持不变,因为过滤器操作没有修改这些提交。“Careless”丢失了oops.iso,“Login page”获得了一个新的父级,因此它们的SHA1s确实发生了变化。

交互式rebase

具有以下历史:

$ git lola --name-status* f772d66 (HEAD, master) Login page| A     login.html* cb14efd Remove DVD-rip| D     oops.iso* ce36c98 Careless| A     oops.iso| A     other.html* 5af4522 Admin page| A     admin.html* e738b63 IndexA     index.html

您想从“Careless”中删除oops.iso,就好像您从未添加过它一样,然后“删除DVD-rip”对您毫无用处。因此,我们进入交互式重新定位的计划是保留“管理页面”,编辑“Careless”,并丢弃“删除DVD-rip”。

运行$ git rebase -i 5af4522会启动一个包含以下内容的编辑器。

pick ce36c98 Carelesspick cb14efd Remove DVD-rippick f772d66 Login page
# Rebase 5af4522..f772d66 onto 5af4522## Commands:#  p, pick = use commit#  r, reword = use commit, but edit the commit message#  e, edit = use commit, but stop for amending#  s, squash = use commit, but meld into previous commit#  f, fixup = like "squash", but discard this commit's log message#  x, exec = run command (the rest of the line) using shell## If you remove a line here THAT COMMIT WILL BE LOST.# However, if you remove everything, the rebase will be aborted.#

执行我们的计划,我们将其修改为

edit ce36c98 Carelesspick f772d66 Login page
# Rebase 5af4522..f772d66 onto 5af4522# ...

也就是说,我们删除带有“删除DVD-rip”的行,并将“粗心”的操作更改为edit而不是pick

保存退出编辑器会在命令提示符处显示以下消息。

Stopped at ce36c98... CarelessYou can amend the commit now, with
git commit --amend
Once you are satisfied with your changes, run
git rebase --continue

正如消息告诉我们的那样,我们正在进行我们想要编辑的“粗心”提交,因此我们运行两个命令。

$ git rm --cached oops.iso$ git commit --amend -C HEAD$ git rebase --continue

第一个从索引中删除了有问题的文件。第二个修改或修改“Careless”作为更新的索引,-C HEAD指示git重用旧的提交消息。最后,git rebase --continue继续执行其余的rebase操作。

这给出了一个历史:

$ git lola --name-status* 93174be (HEAD, master) Login page| A     login.html* a570198 Careless| A     other.html* 5af4522 Admin page| A     admin.html* e738b63 IndexA     index.html

这就是你想要的。

附录:通过~/.gitconfig启用git lola

引用康拉德·帕克的话

我在2010年linux.conf.auScott Chacon的演讲中学到的最好的技巧,Git Wrangling-Advanced Tips and Tricks是这个别名:

lol = log --graph --decorate --pretty=oneline --abbrev-commit

这提供了一个非常好的树图,显示了合并的分支结构等当然有非常好的GUI工具来显示这样的图,但是git lol的优点是它可以在控制台或超过ssh上工作,所以它对于远程开发很有用,或者在嵌入式板上进行本机开发……

因此,只需将以下内容复制到~/.gitconfig中即可进行全彩git lola操作:

[alias]lol = log --graph --decorate --pretty=oneline --abbrev-commitlola = log --graph --decorate --pretty=oneline --abbrev-commit --all[color]branch = autodiff = autointeractive = autostatus = auto

请注意,此命令可能非常具有破坏性。如果更多的人在repo上工作,他们都必须拉取新树。如果您的目标不是减小大小,则不需要中间的三个命令。因为过滤器分支会创建已删除文件的备份,并且它可以在那里停留很长时间。

$ git filter-branch --index-filter "git rm -rf --cached --ignore-unmatch YOURFILENAME" HEAD$ rm -rf .git/refs/original/$ git reflog expire --all$ git gc --aggressive --prune$ git push origin master --force

git filter-branch --tree-filter 'rm -f path/to/file' HEAD对我来说工作得很好,尽管我遇到了与描述这里相同的问题,我通过遵循这一建议解决了这个问题。

pro-git书籍有一整章关于改写历史-看看#0/从每个提交中删除一个文件部分。

这些命令在我的案例中起作用:

git filter-branch --force --index-filter 'git rm --cached -r --ignore-unmatch oops.iso' --prune-empty --tag-name-filter cat -- --allrm -rf .git/refs/original/git reflog expire --expire=now --allgit gc --prune=nowgit gc --aggressive --prune=now

它与上述版本几乎没有区别。

对于那些需要将其推送到github/bit桶的人(我只使用bit桶测试了这个):

# WARNING!!!# this will rewrite completely your bitbucket refs# will delete all branches that you didn't have in your local
git push --all --prune --force
# Once you pushed, all your teammates need to clone repository again# git pull will not work

使用BFG回收清洁剂,这是git-filter-branch的一个更简单、更快的替代方案,专为从Git历史记录中删除不需要的文件而设计。

仔细按照使用说明,核心部分就是这样:

$ java -jar bfg.jar --strip-blobs-bigger-than 100M my-repo.git

任何大小超过100MB的文件(不在您的最新提交中)都将从您的Git存储库的历史记录中删除。然后,您可以使用git gc清除死数据:

$ git reflog expire --expire=now --all && git gc --prune=now --aggressive

剪枝后,我们可以强制推送到远程存储库*

$ git push --force

*注意:无法在GitHub上强制推送保护分支

BFG通常比运行git-filter-branch至少快10-50x,并且通常更易于使用。

完全披露:我是BFG Repo-Cleaner的作者。

我遇到了这个与比特桶帐户,我不小心存储了巨大的*. jpa备份我的网站。

git filter-branch --prune-empty --index-filter 'git rm -rf --cached --ignore-unmatch MY-BIG-DIRECTORY-OR-FILE' --tag-name-filter cat -- --all

使用有问题的文件夹重新设置MY-BIG-DIRECTORY以完全重写您的历史记录(包括标签)。

来源:https://web.archive.org/web/20170727144429/http://naleid.com:80/blog/2012/01/17/finding-and-purging-big-files-from-git-history/

为什么不使用这个简单而强大的命令呢?

git filter-branch --tree-filter 'rm -f DVD-rip' HEAD

--tree-filter选项在项目的每次签出后运行指定的命令,然后重新生成结果。在这种情况下,您可以从每个快照中删除一个名为DVD-rip的文件,无论它是否存在。

如果你知道哪个提交引入了这个大文件(比如35dsa2),你可以将HEAD替换为35dsa2… HEAD以避免重写太多历史记录,从而避免在还没有推送的情况下出现不同的提交。@alpha_989提供的这条评论似乎太重要了,不能在这里省略。

此链接

如果你知道你的提交是最近的,而不是遍历整个树,请执行以下操作:git filter#######git filter####LARGE_FILE.zip###文件夹目录

使用git扩展,它是一个UI工具。它有一个名为“查找大文件”的插件,可以在存储库中查找lage文件并允许永久删除它们。

在使用此工具之前,请不要使用“git filter分支”,因为它将无法找到被“filter分支”删除的文件(Al的“filter分支”不会从存储库包文件中完全删除文件)。

当你遇到这个问题时,git rm是不够的,因为git会记住该文件在我们的历史记录中存在过一次,因此会保留对它的引用。

更糟糕的是,重设基址也不容易,因为对blob的任何引用都会阻止git垃圾收集器清理空间。这包括远程引用和reflg引用。

我把git forget-blob放在一起,这是一个尝试删除所有这些引用的小脚本,然后使用git filter分支重写分支中的每个提交。

一旦你的blob完全没有被引用,git gc将摆脱它

用法非常简单git forget-blob file-to-forget。您可以在此处获取更多信息

https://ownyourbits.com/2017/01/18/completely-remove-a-file-from-a-git-repository-with-git-forget-blob/

多亏了Stack Overflow的回答和一些博客条目,我把这些放在一起。归功于他们!

您可以使用branch filter命令执行此操作:

git filter-branch --tree-filter 'rm -rf path/to/your/file' HEAD

在尝试了SO中的几乎所有答案之后,我终于找到了这个宝石,它可以快速删除和删除存储库中的大文件,并允许我再次同步:http://www.zyxware.com/articles/4027/how-to-delete-files-permanently-from-your-local-and-remote-git-repositories

CD到本地工作文件夹并运行以下命令:

git filter-branch -f --index-filter "git rm -rf --cached --ignore-unmatch FOLDERNAME" -- --all

将FOLDERNAME替换为您希望从给定git存储库中删除的文件或文件夹。

完成后,运行以下命令来清理本地存储库:

rm -rf .git/refs/original/git reflog expire --expire=now --allgit gc --prune=nowgit gc --aggressive --prune=now

现在将所有更改推送到远程存储库:

git push --all --force

这将清理远程存储库。

我基本上做了这个答案:https://stackoverflow.com/a/11032521/1286423

(对于历史,我将在这里复制粘贴)

$ git filter-branch --index-filter "git rm -rf --cached --ignore-unmatch YOURFILENAME" HEAD$ rm -rf .git/refs/original/$ git reflog expire --all$ git gc --aggressive --prune$ git push origin master --force

它不起作用,因为我喜欢重命名和移动很多东西。所以一些大文件在已重命名的文件夹中,我认为gc无法删除对这些文件的引用,因为tree对象中的引用指向这些文件。我真正杀死它的最终解决方案是:

# First, apply what's in the answer linked in the front# and before doing the gc --prune --aggressive, do:
# Go back at the origin of the repositorygit checkout -b newinit <sha1 of first commit># Create a parallel initial commitgit commit --amend# go back on the master branch that has big file# still referenced in history, even though# we thought we removed them.git checkout master# rebase on the newinit created earlier. By reapply patches,# it will really forget about the references to hidden big files.git rebase newinit
# Do the previous part (checkout + rebase) for each branch# still connected to the original initial commit,# so we remove all the references.
# Remove the .git/logs folder, also containing references# to commits that could make git gc not remove them.rm -rf .git/logs/
# Then you can do a garbage collection,# and the hidden files really will get gc'edgit gc --prune --aggressive

我的存储库(.git)从32MB更改为388KB,甚至过滤器分支也无法清理。

(我见过这个问题的最佳答案是:https://stackoverflow.com/a/42544963/714112,复制到这里,因为这个线程在Google搜索排名中排名很高,但另一个没有)

🚀一个速度极快的外壳单线🚀

此外壳脚本显示存储库中的所有blob对象,从最小到最大排序。

对于我的示例存储库,它比这里找到的其他存储库运行了大约快100倍
在我信任的Athlon II X4系统上,它在一分多钟中处理Linux内核存储库及其5,622,155个对象。

的基本脚本

git rev-list --objects --all \| git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \| awk '/^blob/ {print substr($0,6)}' \| sort --numeric-sort --key=2 \| cut --complement --characters=13-40 \| numfmt --field=2 --to=iec-i --suffix=B --padding=7 --round=nearest

当你运行上面的代码时,你会得到像这样的好人类可读输出

...0d99bb931299  530KiB path/to/some-image.jpg2ba44098e28f   12MiB path/to/hires-image.pngbd1741ddce0d   63MiB path/to/some-video-1080p.mp4

🚀快速文件删除🚀

假设您想从HEAD可访问的每个提交中删除文件ab,您可以使用以下命令:

git filter-branch --index-filter 'git rm --cached --ignore-unmatch a b' HEAD

git filter-branch是一个强大的命令,您可以使用它从提交历史记录中删除一个巨大的文件。该文件将保留一段时间,Git将在下一个垃圾回收机制中删除它。下面是从提交历史中删除文件的完整过程。为了安全起见,下面的过程首先在新分支上运行命令。如果结果是你需要的,然后将其重置回你真正想要更改的分支。

# Do it in a new testing branch$ git checkout -b test
# Remove file-name from every commit on the new branch# --index-filter, rewrite index without checking out# --cached, remove it from index but not include working tree# --ignore-unmatch, ignore if files to be removed are absent in a commit# HEAD, execute the specified command for each commit reached from HEAD by parent link$ git filter-branch --index-filter 'git rm --cached --ignore-unmatch file-name' HEAD
# The output is OK, reset it to the prior branch master$ git checkout master$ git reset --soft test
# Remove test branch$ git branch -d test
# Push it with force$ git push --force origin master

除了git filter-branch(缓慢但纯粹的git解决方案)和BFG(更容易且非常高性能)之外,还有另一个工具可以以良好的性能进行过滤:

https://github.com/xoofx/git-rocket-filter

从它的描述:

git-rocch-filter的目的类似于命令git-filter-branch,同时提供以下独特功能:

  • 快速重写提交和树(按x10到x100的顺序)。
  • 内置支持使用--保持(保存文件或目录)的白名单和使用--删除选项的黑名单。
  • 使用. gitignore类似模式进行树过滤
  • 用于提交过滤和树过滤的快速简单的C#脚本
  • 支持每个文件/目录模式的树过滤脚本
  • 自动修剪空/不变的提交,包括合并提交

这将从您的历史中删除它

git filter-branch --force --index-filter 'git rm -r --cached --ignore-unmatch bigfile.txt' --prune-empty --tag-name-filter cat -- --all

比git filter分支快100倍,更简单

这个线程中有非常好的答案,但同时其中许多已经过时了。不再推荐使用git-filter-branch,因为它很难使用并且在大型存储库上非常慢。

git-filter-repo更快,更容易使用。

git-filter-repo是一个Python脚本,可在github:https://github.com/newren/git-filter-repo获得。安装后,它看起来像一个常规的git命令,可以由git filter-repo调用。

您只需要一个文件:Python3脚本git-filter-repo。将其复制到包含在PATH变量中的路径。在Windows上,您可能需要更改脚本的第一行(请参阅INSTALL.md)。您需要在系统上安装Python3,但这没什么大不了的。

首先你可以跑

git filter-repo --analyze

这有助于您确定下一步要做什么。

您可以在任何地方删除您的DVD-rip文件:

git filter-repo --invert-paths --path-match DVD-rip 

Filter-repo真的很快。在我的计算机上通过filter-分支花费了大约9个小时的任务,通过filter-repo在4分钟内完成。您可以使用filter-repo做更多美好的事情。请参阅留档。

警告:在存储库的副本上执行此操作。filter-repo的许多操作无法撤消。filter-repo将更改所有修改的提交(当然)及其所有后代的提交哈希值到最后一次提交!

这对我来说很完美:在git扩展中:

右键单击选定的提交:

将当前分支重置到此处:

硬复位;

令人惊讶的是,没有人能给出这个简单的答案。

重置当前分支到这里

硬重置

根据GitHub需求文档,只需遵循以下步骤:

  1. 摆脱大文件

选项1:您不想保留大文件:

rm path/to/your/large/file        # delete the large file

选项2:您希望将大文件保存在未跟踪的目录中

mkdir large_files                       # create directory large_filestouch .gitignore                        # create .gitignore file if needed'/large_files/' >> .gitignore           # untrack directory large_filesmv path/to/your/large/file large_files/ # move the large file into the untracked directory
  1. 保存您的更改
git add path/to/your/large/file   # add the deletion to the indexgit commit -m 'delete large file' # commit the deletion
  1. 从所有提交中删除大文件
git filter-branch --force --index-filter \"git rm --cached --ignore-unmatch path/to/your/large/file" \--prune-empty --tag-name-filter cat -- --allgit push <remote> <branch>
git reset --soft HEAD~1

它将保留更改,但删除提交,然后您可以重新提交这些更改。

20222年有效的新答案。

不要使用:

git filter-branch

此命令在推送后可能不会更改远程存储库。如果您在使用它后克隆,您将看到什么都没有改变,存储库仍然有很大的大小。此命令现在很旧。例如,如果您使用https://github.com/18F/C2/issues/439中的步骤,这将不起作用。

你需要使用

git filter-repo

步骤:

(1)查找. git中最大的文件:

git rev-list --objects --all | grep -f <(git verify-pack -v  .git/objects/pack/*.idx| sort -k 3 -n | cut -f 1 -d " " | tail -10)

(2)开始过滤这些大文件:

 git filter-repo --path-glob '../../src/../..' --invert-paths --force

 git filter-repo --path-glob '*.zip' --invert-paths --force

 git filter-repo --path-glob '*.a' --invert-paths --force

或第一步,你会发现什么?

(3)

 git remote add origin git@github.com:.../...git

(4)

git push --all --force
git push --tags --force

成交!!!