关于版本控制:如何从Git存储库的提交历史记录中删除大型文件?

How to remove/delete a large file from commit history in Git repository?

偶尔我会在一个网站项目中插入一个DVD翻录,然后不小心地删除了git commit -a -m ...,然后,zap,回购被2.2 gigs放大了。下一次我做了一些编辑,删除了视频文件,并提交了所有内容,但是压缩文件仍然在存储库中,在历史上。

我知道我可以从这些提交中启动分支,并将一个分支重新设置为另一个分支。但是,我应该怎么做才能将2个提交合并在一起,从而使大文件不显示在历史记录中,并在垃圾收集过程中被清除?


如果您已经向其他开发人员发布了历史记录,那么您想要做的是高度破坏性的工作。有关修复历史记录后的必要步骤,请参阅git rebase文档中的"从上游重新平衡恢复"。

您至少有两个选项:git filter-branch和一个交互式的rebase,这两个选项都在下面解释。

使用git filter-branch

我有一个类似的问题,从Subversion导入大量的二进制测试数据,并写了关于从Git存储库中删除数据的内容。

假设你的Git历史是:

1
2
3
4
5
6
7
8
9
10
11
12
$ 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 Index
  A     index.html

注意,git lola是一个非标准但非常有用的别名。使用--name-status开关,我们可以看到与每个提交相关的树修改。

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

1
2
3
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发行版,那么在/dev/shm中指定树将导致更快的执行速度。
  • --index-filter是历史上最主要的事件,在历史的每一步都与指数背道而驰。您希望在找到oops.iso的地方删除它,但它并不存在于所有提交中。当DVD翻录出现时,命令git rm --cached -f --ignore-unmatch oops.iso将删除该翻录,否则不会失败。
  • --tag-name-filter描述了如何重写标记名。cat的过滤器是身份操作。与上面的示例一样,您的存储库可能没有任何标记,但出于完全通用性考虑,我包含了这个选项。
  • --规定了git filter-branch期权的终止。
  • --后面的--all是所有refs的简写。像上面的示例一样,您的存储库可能只有一个引用(master),但出于完全通用性考虑,我包含了这个选项。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ 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 Index
  A     index.html

注意,新的"粗心"提交只添加了other.html,并且"删除DVD翻录"提交不再在主分支上。标记为refs/original/refs/heads/master的分支机构包含您的原始承诺,以防您出错。要删除它,请按照"缩小存储库检查表"中的步骤操作。

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

为了更简单的选择,克隆存储库以丢弃不需要的位。

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

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

现在你的历史是:

1
2
3
4
5
6
7
8
9
$ git lola --name-status
* 8e0a11c (HEAD, master) Login page
| A     login.html
* e45ac59 Careless
| A     other.html
* 5af4522 Admin page
| A     admin.html
* e738b63 Index
  A     index.html

前两次提交的sha1对象名("index"和"admin page")保持不变,因为筛选操作没有修改这些提交。"粗心"丢失了oops.iso和"登录页面"有了新的家长,所以他们的sha1确实改变了。

交互式钢筋网

有以下历史:

1
2
3
4
5
6
7
8
9
10
11
12
$ 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 Index
  A     index.html

你想从"粗心"中删除oops.iso,就好像你从未添加过一样,然后"删除DVD翻录"对你来说是无用的。因此,我们的计划进入一个互动的重新平衡是保持"管理页",编辑"粗心",并放弃"删除DVD翻录"。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pick ce36c98 Careless
pick cb14efd Remove DVD-rip
pick 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.
#

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

1
2
3
4
5
edit ce36c98 Careless
pick f772d66 Login page

# Rebase 5af4522..f772d66 onto 5af4522
# ...

也就是说,我们删除了带有"删除DVD翻录"的行,将"粗心"的操作改为edit,而不是pick

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

1
2
3
4
5
6
7
8
Stopped at ce36c98... Careless
You can amend the commit now, with

        git commit --amend

Once you are satisfied with your changes, run

        git rebase --continue

正如消息告诉我们的,我们正在"粗心"提交我们想要编辑的内容,所以我们运行两个命令。

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

第一个从索引中删除有问题的文件。第二个修改或修改"粗心"为更新的索引,-C HEAD指示git重用旧的提交消息。最后,git rebase --continue继续进行剩余的再平衡操作。

这提供了以下历史:

1
2
3
4
5
6
7
8
9
$ git lola --name-status
* 93174be (HEAD, master) Login page
| A     login.html
* a570198 Careless
| A     other.html
* 5af4522 Admin page
| A     admin.html
* e738b63 Index
  A     index.html

这就是你想要的。


使用bfg repo cleaner,这是一种比git-filter-branch更简单、更快的替代方案,专门设计用于从git历史中删除不需要的文件。

仔细遵循使用说明,核心部分如下:

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

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

1
$ git gc --prune=now --aggressive

bfg通常比运行git-filter-branch快至少10-50倍,并且通常更容易使用。

完全公开:我是bfg repo cleaner的作者。


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

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

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

请参见此链接。


(我看到的这个问题的最佳答案是:https://stackoverflow.com/a/42544963/714112,因为这个线程在谷歌搜索排名中显示得很高,但另一个没有。)

?一个极快的壳牌一号班轮??

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

对于我的样本回购,它比这里发现的其他回购快100倍。在我信任的AthlonIIX4系统上,它只需一分钟多的时间就可以处理带有5622155个对象的Linux内核存储库。

基础脚本

1
2
3
4
5
6
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

当您运行上面的代码时,您将得到很好的可读输出,如下所示:

1
2
3
4
...
0d99bb931299  530KiB path/to/some-image.jpg
2ba44098e28f   12MiB path/to/hires-image.png
bd1741ddce0d   63MiB path/to/some-video-1080p.mp4

??快速删除文件??

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

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


这些命令适用于我的情况:

1
2
3
4
5
git filter-branch --force --index-filter 'git rm --cached -r --ignore-unmatch oops.iso' --prune-empty --tag-name-filter cat -- --all
rm -rf .git/refs/original/
git reflog expire --expire=now --all
git gc --prune=now
git gc --aggressive --prune=now

它与上述版本稍有不同。

对于那些需要把这个推到GitHub/BitBucket的用户(我只用BitBucket测试过这个):

1
2
3
4
5
6
7
8
# 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


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

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

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

将foldername替换为要从给定Git存储库中删除的文件或文件夹。

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

1
2
3
4
rm -rf .git/refs/original/
git reflog expire --expire=now --all
git gc --prune=now
git gc --aggressive --prune=now

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

1
git push --all --force

这将清理远程存储库。


git filter-branch --tree-filter 'rm -f path/to/file' HEAD虽然我遇到了和这里描述的相同的问题,但我的工作非常好,我按照这个建议解决了这个问题。

pro-git这本书有一整章都是关于重写历史的——看看filter-branch/从每个commit部分删除一个文件。


请注意,这个命令可能非常具有破坏性。如果更多的人在回购业务上工作,他们都将不得不拔出新的树。如果您的目标不是缩小大小,则不需要使用三个中间命令。因为过滤器分支会创建一个已删除文件的备份,并且它可以在那里停留很长时间。

1
2
3
4
5
$ 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 LARGE_FILE.zip' HEAD~10..HEAD


我在一个bitback帐户中遇到了这个问题,在那里我意外地存储了大量的*.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与相关文件夹的关系,以完全重写您的历史记录(包括标签)。

来源:http://naleid.com/blog/2012/01/17/finding-and-purging-big-files-from-git-history


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

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


我基本上是这样回答的:https://stackoverflow.com/a/11032521/1286423

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

1
2
3
4
5
$ 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对象中引用了这些文件。我最终的解决办法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 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 repository
git checkout -b newinit <sha1 of first commit>
# Create a parallel initial commit
git 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'ed
git gc --prune --aggressive

我的repo(.git)从32MB变为388KB,甚至过滤分支都无法清除。


当你遇到这个问题时,git rm是不够的,因为git记得这个文件在我们的历史上曾经存在过,因此会保留对它的引用。

更糟的是,重新定位也不容易,因为对blob的任何引用都会阻止git垃圾收集器清理空间。这包括远程引用和重新记录引用。

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

一旦你的blob完全没有引用,git gc就会去掉它。

使用非常简单的git forget-blob file-to-forget。你可以在这里得到更多的信息

Completely remove a file from a git repository with git forget-blob

多亏了堆栈溢出和一些博客条目的回答,我把这些放在一起。他们的功劳!


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

在使用此工具之前不要使用"git filter branch",因为它将找不到"filter branch"删除的文件(尽管"filter branch"不会从存储库包文件中完全删除文件)。


git filter-branch是一个强大的命令,您可以使用它从提交历史中删除一个巨大的文件。该文件将保留一段时间,Git将在下一次垃圾收集中删除它。下面是从提交历史记录中删除文件的完整过程。为了安全起见,它首先在新分支上运行命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 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 -rm test

# Push it with force
$ git push --force origin master