如何在git历史记录中查找/识别大型提交?

633

我有一个300MB的git仓库。当前检出文件的总大小为2MB,剩余git仓库的总大小为298MB。这基本上是一个仅包含代码的仓库,不应该超过几MB。

我怀疑有人意外提交了一些大文件(视频、图片等),然后将它们删除……但没有从git中删除,因此历史记录仍包含无用的大文件。如何在git历史中找到这些大文件?由于有400多个提交,逐个查找不太实际。

注意:我的问题不涉及如何删除文件,而是如何首先找到它。


6
@raphinesse 的那个一行代码的回答应该被标记为答案,因为它非常快。 - soloturn
14个回答

1381

一个极快的一行命令行脚本

这个命令行脚本会显示仓库中的所有 blob 对象,并按照从最小到最大的顺序进行排序。

对于我的示例仓库来说,它的运行速度比这里找到的其他脚本快了100倍
在我的可靠的 Athlon II X4 系统上,它可以在一分钟多一点的时间内处理包含560万个对象Linux 内核仓库

基本脚本

git rev-list --objects --all |
  git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' |
  sed -n 's/^blob //p' |
  sort --numeric-sort --key=2 |
  cut -c 1-12,41- |
  $(command -v gnumfmt || echo numfmt) --field=2 --to=iec-i --suffix=B --padding=7 --round=nearest

当你运行上面的代码时,你会得到一个漂亮的人类可读的输出,就像这样:
...
0d99bb931299  530KiB path/to/some-image.jpg
2ba44098e28f   12MiB path/to/hires-image.png
bd1741ddce0d   63MiB path/to/some-video-1080p.mp4

macOS用户:由于macOS上没有可用的numfmt命令,您可以选择省略最后一行并处理原始字节大小,或者安装brew install coreutils。
过滤
为了实现进一步的过滤,在sort行之前插入以下任意行之一。
为了排除在HEAD中存在的文件,请插入以下行。
grep -vF --file=<(git ls-tree -r HEAD | awk '{print $3}') |

为了只显示超过给定大小的文件(例如 1 MiB = 2^20 B),请插入以下行:
awk '$2 >= 2^20' |

计算机输出

为了生成适合计算机进一步处理的输出,可以省略基本脚本的最后两行。这些行负责格式化。这样你就会得到类似下面的内容:

...
0d99bb93129939b72069df14af0d0dbda7eb6dba 542455 path/to/some-image.jpg
2ba44098e28f8f66bac5e21210c2774085d2319b 12446815 path/to/hires-image.png
bd1741ddce0d07b72ccf69ed281e09bf8a2d0b2f 65183843 path/to/some-video-1080p.mp4

附录

文件删除

关于实际的文件删除,请查看这个关于该主题的Stack Overflow问题

理解显示文件大小的含义

这个脚本显示的是每个文件在工作目录中的大小。如果你想查看一个文件在未签出状态下占用多少空间,可以使用%(objectsize:disk)代替%(objectsize)。然而,请注意这个度量指标也有其局限性,正如文档中所提到的。

更复杂的大小统计

有时候仅仅列出大文件的列表还不足以找出问题所在。例如,你可能无法发现包含大量小文件的目录或分支。

所以,如果这里的脚本对你来说不够好(而且你有一个相当新的git版本),可以尝试一下git-filter-repo --analyze或者git rev-list --disk-usage示例)。

62
在 Mac 上使用此命令需要运行 brew install coreutils,然后将 cut 替换为 gcut,将 numfmt 替换为 gnumfmt - Nick Sweeting
3
建议使用 objectsize:disk 而不是 objectsize - Victor Yarema
2
非常感谢。在我的MacOS上有效(使用homebrew的“coreutils”包,在“cut”和“numfmt”的情况下使用“gcut”和“gnumfmt”)。 - beefeather
4
这个答案似乎打印了对象ID和文件名,而不是添加它们的提交记录,对吗?我该如何找到需要移除的提交记录,就像问题所问的那样? - oarfish
2
我想知道Git LFS是如何管理文件的。因此,我创建了一个包含两个大文件的存储库(https://github.com/brandizzi/big),在启用Git LFS之前添加/提交了“wrong.iso”,并使用Git LFS添加和提交了“xubuntu-18.04.2-desktop-amd64.iso”(顺便说一下,它们是相同的文件)。对于在LFS之前添加的文件,脚本显示:177485aecd84 1.4GiB wrong.iso。对于在LFS之后添加的文件,结果如下:c381232ed0de 135B xubuntu-18.04.2-desktop-amd64.iso。因此,LFS文件不会列出完整的大小(这正是我想要的行为)。 - brandizzi
显示剩余29条评论

197

我在ETH Zurich物理系维基页面上找到了一种单行解决方案(页面接近末尾)。只需运行git gc 即可删除过时的垃圾,然后……

git rev-list --objects --all \
  | grep "$(git verify-pack -v .git/objects/pack/*.idx \
           | sort -k 3 -n \
           | tail -10 \
           | awk '{print$1}')"

将会给你列出代码库中最大的10个文件。

现在还有一种更懒惰的解决方案,GitExtensions现在有一个插件可以在UI中完成这项操作(并且也处理历史重写)。

GitExtensions 'Find large files' dialog


8
如果你只想获取最大的单个文件(即使用tail -1),那么这个单行命令才有效。对于任何更大的文件,换行符会成为干扰。你可以使用sed将换行符转换为grep所需的格式:git rev-list --objects --all | grep -E `git verify-pack -v .git/objects/pack/*.idx | sort -k 3 -n | tail -10 | awk '{print$1}' | sed ':a;N;$!ba;s/\n/|/g'` - Throctukes
10
grep: a70783fca9bfbec1ade1519a41b6cc4ee36faea0: 没有那个文件或目录。 - Jonathan Allard
1
维基链接已迁移到:https://readme.phys.ethz.ch/documentation/git_advanced_hints/ - outsmartin
18
找到GitExtensions就像找到彩虹尽头的金罐一样,非常感谢! - ckapilla
3
还有一个可以打印文件大小的扩展程序吗? - Michael
显示剩余3条评论

174

我曾经发现这个脚本非常有用,可以用来查找git仓库中的大文件(包括不明显的):


#!/bin/bash
#set -x 
 
# Shows you the largest objects in your repo's pack file.
# Written for osx.
#
# @see https://stubbisms.wordpress.com/2009/07/10/git-script-to-show-largest-pack-objects-and-trim-your-waist-line/
# @author Antony Stubbs
 
# set the internal field separator to line break, so that we can iterate easily over the verify-pack output
IFS=$'\n';
 
# list all objects including their size, sort by size, take top 10
objects=`git verify-pack -v .git/objects/pack/pack-*.idx | grep -v chain | sort -k3nr | head`
 
echo "All sizes are in kB's. The pack column is the size of the object, compressed, inside the pack file."
 
output="size,pack,SHA,location"
allObjects=`git rev-list --all --objects`
for y in $objects
do
    # extract the size in bytes
    size=$((`echo $y | cut -f 5 -d ' '`/1024))
    # extract the compressed size in bytes
    compressedSize=$((`echo $y | cut -f 6 -d ' '`/1024))
    # extract the SHA
    sha=`echo $y | cut -f 1 -d ' '`
    # find the objects location in the repository tree
    other=`echo "${allObjects}" | grep $sha`
    #lineBreak=`echo -e "\n"`
    output="${output}\n${size},${compressedSize},${other}"
done
 
echo -e $output | column -t -s ', '

这将给你blob的对象名称(SHA1sum),然后您可以使用像这样的脚本:

... 来查找指向每个blob的提交。


37
这个回答真的很有用,因为它把我发送到了上面的帖子。虽然帖子的脚本可以运行,但我发现它运行得非常慢。所以我重新编写了它,现在在大型代码库中速度显著提高。请看这里:https://gist.github.com/nk9/b150542ef72abc7974cb - Nick K9
11
请在你的回答中包含完整的说明,而不仅仅是离题的链接;当 stubbisms.wordpress.com 不可避免地关闭时,我们该怎么办呢? - ThorSummoner
@NickK9,你对UpAndAdam在脚本缺少一些文件的经验有什么见解吗?Antony的脚本没有产生任何输出,而你的脚本有,但我想确保它没有漏掉任何东西。 - indigo
这里有一个好的方法和灵活的方法:http://blog.jessitron.com/2013/08/finding-and-removing-large-files-in-git.html - herve
1
这些注释让人觉得我们报告的是字节大小,但我得到的是千字节。 - Kat
显示剩余7条评论

38

第一步:将所有文件的SHA1值写入文本文件:

git rev-list --objects --all | sort -k 2 > allfileshas.txt

步骤二:将 blobs 从大到小排序,并将结果写入文本文件:

git gc && git verify-pack -v .git/objects/pack/pack-*.idx | egrep "^\w+ blob\W+[0-9]+ [0-9]+ [0-9]+$" | sort -k 3 -n -r > bigobjects.txt

第3步a:将两个文本文件合并,以获取文件名/ sha1/大小信息:

for SHA in `cut -f 1 -d\  < bigobjects.txt`; do
echo $(grep $SHA bigobjects.txt) $(grep $SHA allfileshas.txt) | awk '{print $1,$3,$7}' >> bigtosmall.txt
done;

步骤3b 如果您的文件名或路径名包含空格,请尝试Step 3a的这个变体。 它使用cut而不是awk来从第7列到行末获取所需的包含空格的列:

for SHA in `cut -f 1 -d\  < bigobjects.txt`; do
echo $(grep $SHA bigobjects.txt) $(grep $SHA allfileshas.txt) | cut -d ' ' -f'1,3,7-' >> bigtosmall.txt
done;

现在,您可以查看bigtosmall.txt文件,以决定从Git历史记录中删除哪些文件。

步骤4执行删除操作(请注意,此部分速度较慢,因为它将检查历史记录中的每个提交,以获取有关您确定的文件的数据):

git filter-branch --tree-filter 'rm -f myLargeFile.log' HEAD

来源

步骤1-3a是从从Git历史记录中查找和清除大文件中复制的。

编辑

该文章在2017年下半年被删除,但可以使用Wayback机器访问其存档副本


6
一行代码实现相同功能:git gc && join -e ERROR -a 2 -j 1 -o 2.1,2.3,1.2 --check-order <( git rev-list --objects --all | sort -k 1 ) <( git verify-pack -v .git/objects/pack/pack-*.idx | gawk '( NF == 5 && $2 == "blob" ){print}' | sort -k1 ) | sort -k2gr - Iwan Aucamp
1
@Iwan,感谢你的一行代码!它无法处理文件名中带有空格的情况,但这个命令可以:join -t' ' -e ERROR -a 2 -j 1 -o 2.1,2.3,1.2 --check-order <( git rev-list --objects --all | sed 's/[[:space:]]/\t/' | sort -k 1 ) <( git verify-pack -v .git/objects/pack/pack-*.idx | gawk '( NF == 5 && $2 == "blob" ){print}' | sort -k1 | sed 's/[[:space:]]\+/\t/g' ) | sort -k2gr | less。请注意,在 join -t' 后必须使用 CTRL+V <TAB> 输入实际的 TAB 字符,参见 http://geekbraindump.blogspot.ru/2009/04/unix-join-with-tabs.html。 - Nickolay
2
@Nickolay 在 bash 中使用 $'\t' 应该会给你一个制表符。echo -n $'\t' | xxd -ps -> 09 - Iwan Aucamp
1
@IwanAucamp:更好了,谢谢你的提示!(太遗憾我不能编辑之前的评论...嗯,算了吧。) - Nickolay
1
@Sridhar-Sarnobat 这篇文章被Wayback Machine保存了! :) https://web.archive.org/web/20170621125743/http://www.naleid.com/blog/2012/01/17/finding-and-purging-big-files-from-git-history - friederbluemle
显示剩余3条评论

16

您应该使用BFG Repo-Cleaner

根据该网站:

BFG是一个更简单,更快速的选项,可用于清除Git存储库历史记录中的不良数据:

  • 删除超大文件
  • 删除密码、凭据和其他私有数据

减小存储库大小的传统过程是:

git clone --mirror git://example.com/some-big-repo.git
java -jar bfg.jar --strip-biggest-blobs 500 some-big-repo.git
cd some-big-repo.git
git reflog expire --expire=now --all
git gc --prune=now --aggressive
git push

4
BFG Repo-Cleaner非常好用。它操作速度极快且非常可靠。 - fschmitt
34
然而,这并没有告诉你如何列出所有最大的文件。 - Andi Jay
6
问题在于你不能仅仅通过查看就知道哪些是大文件,必须先删除它们才能确定。在没有进行试运行的情况下,我不太愿意这样做,我希望只是列出大文件。 - Sridhar Sarnobat
--strip-biggest-blobs 500 是什么意思? - 2540625
最终使用git push并没有清理远程仓库。我仍然能够下载之前的巨大git .pack文件。 - Sambit Swain
4
截至2020年,我会避免使用bfg。它只接受文件基本名称(“foo.out”),而不是路径,因此您无法有意义地限制它。它没有-dryrun选项。最后一次提交是在2015年。实质上,它已经死了。被投票降级(抱歉)。 - chrisinmtown

15
如果您只想要一个大文件的列表,那么我想为您提供以下一行代码:
join -o "1.1 1.2 2.3" <(git rev-list --objects --all | sort) <(git verify-pack -v objects/pack/*.idx | sort -k3 -n | tail -5 | sort) | sort -k3 -n

输出将为:

commit       file name                                  size in bytes

72e1e6d20... db/players.sql 818314
ea20b964a... app/assets/images/background_final2.png 6739212
f8344b9b5... data_test/pg_xlog/000000010000000000000001 1625545
1ecc2395c... data_development/pg_xlog/000000010000000000000001 16777216
bc83d216d... app/assets/images/background_1forfinal.psd 95533848

列表中的最后一项指向您的git历史记录中最大的文件。

您可以使用此输出来确保在使用BFG时,不会删除您在历史记录中需要的内容。

请注意,您需要使用--mirror克隆存储库才能使其正常工作。


2
太棒了!不过请注意,在运行此命令之前,您需要使用--mirror选项克隆repo。 - Andi Jay
我很好奇,1.1、1.2、2.3 这些数字是用来做什么的? - ympostor
这些数字是一个列表,格式为“<filenumber>.<field>”,指定了组合的顺序。详见http://man.cx/join获取更多信息。 - schmijos
这对于路径中带有空格的文件无法正常工作;原样的 join 命令只取文件路径中由空格分隔的第一个“单词”。 - villapx

8
如果您使用的是Windows操作系统,这里有一个PowerShell脚本,可以列出您代码库中最大的10个文件:
$revision_objects = git rev-list --objects --all;
$files = $revision_objects.Split() | Where-Object {$_.Length -gt 0 -and $(Test-Path -Path $_ -PathType Leaf) };
$files | Get-Item -Force | select fullname, length | sort -Descending -Property Length | select -First 10

1
这会得出一个与 @raphinesse 不同的答案,忽略了我的存储库中许多最大的文件。而且当一个大文件有很多修改时,只报告最大的大小。 - kristianp
这个脚本对我来说失败了,报错为:“您无法在空值表达式上调用方法。在第2行字符1处”。然而,这个答案有效:https://dev59.com/_2ox5IYBdhLWcg3wDgTR#57793716(而且更短)。 - Venryx

7

对于Windows,我编写了这个答案的PowerShell版本:

function Get-BiggestBlobs {
  param ([Parameter(Mandatory)][String]$RepoFolder, [int]$Count = 10)
  Write-Host ("{0} biggest files:" -f $Count)
  git -C $RepoFolder rev-list --objects --all | git -C $RepoFolder cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' | ForEach-Object {
    $Element = $_.Trim() -Split '\s+'
    $ItemType = $Element[0]
    if ($ItemType -eq 'blob') {
      New-Object -TypeName PSCustomObject -Property @{
          ObjectName = $Element[1]
          Size = [int]([int]$Element[2] / 1kB)
          Path = $Element[3]
      }
    }
  } | Sort-Object Size | Select-Object -last $Count | Format-Table ObjectName, @{L='Size [kB]';E={$_.Size}}, Path -AutoSize
}

你可能希望对显示的单位进行微调,根据自己的情况选择显示 kB 或 MB 或 Bytes。

如果性能优化是一个问题,那么可以随意尝试进行实验。

要获取所有更改,请省略 | Select-Object -last $Count
要获取更具机器可读性的版本,请省略 | Format-Table @{L='Size [kB]';E={$_.Size}}, Path -AutoSize


1
很有趣看到我的脚本有PowerShell版本!我还没有尝试过,但从代码来看,似乎你没有输出“objectname”字段。我真的认为你应该这样做,因为路径:对象名称的关系是n:m而不是1:1。 - raphinesse
1
@raphinesse 嗯,我的使用情况是创建一个忽略正则表达式,以便从TFVC迁移到git而不会有太多大文件,因此我只对需要忽略的文件路径感兴趣 ;) 但你是对的,我会添加它的。 顺便感谢您的编辑 :) - SvenS

5

尝试使用git ls-files | xargs du -hs --threshold=1M命令。

我们在CI流水线中使用以下命令,如果在git存储库中发现任何大文件,则会停止运行:

test $(git ls-files | xargs du -hs --threshold=1M 2>/dev/null | tee /dev/stderr | wc -l) -gt 0 && { echo; echo "Aborting due to big files in the git repository."; exit 1; } || true

5

针对 Windows Git 的 Powershell 解决方案,查找最大的文件:

git ls-tree -r -t -l --full-name HEAD | Where-Object {
 $_ -match '(.+)\s+(.+)\s+(.+)\s+(\d+)\s+(.*)'
 } | ForEach-Object {
 New-Object -Type PSObject -Property @{
     'col1'        = $matches[1]
     'col2'      = $matches[2]
     'col3' = $matches[3]
     'Size'      = [int]$matches[4]
     'path'     = $matches[5]
 }
 } | sort -Property Size -Top 10 -Descending

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