Найдите файлы в Git repo более x мегабайт, которые не существуют в HEAD


у меня есть репозиторий Git, в котором я храню случайные вещи. В основном случайные скрипты, текстовые файлы, веб-сайты, которые я разработал и так далее.

есть некоторые большие двоичные файлы, которые я удалил с течением времени (как правило, 1-5MB), которые сидят вокруг увеличения размера репозитория, который мне не нужен в истории ревизий.

в основном я хочу быть в состоянии сделать..

me@host:~$ [magic command or script]
aad29819a908cc1c05c3b1102862746ba29bafc0 : example/blah.psd : 3.8MB : 130 days old
6e73ca29c379b71b4ff8c6b6a5df9c7f0f1f5627 : another/big.file : 1.12MB : 214 days old

..затем сможете пройти через каждый результат, проверяя, не требуется ли он больше удаление его (вероятно, с помощью filter-branch)

10 56

10 ответов:

Это адаптация the git-find-blob скрипт, который я опубликовал ранее:

#!/usr/bin/perl
use 5.008;
use strict;
use Memoize;

sub usage { die "usage: git-large-blob <size[b|k|m]> [<git-log arguments ...>]\n" }

@ARGV or usage();
my ( $max_size, $unit ) = ( shift =~ /^(\d+)([bkm]?)\z/ ) ? ( ,  ) : usage();

my $exp = 10 * ( $unit eq 'b' ? 0 : $unit eq 'k' ? 1 : 2 );
my $cutoff = $max_size * 2**$exp; 

sub walk_tree {
    my ( $tree, @path ) = @_;
    my @subtree;
    my @r;

    {
        open my $ls_tree, '-|', git => 'ls-tree' => -l => $tree
            or die "Couldn't open pipe to git-ls-tree: $!\n";

        while ( <$ls_tree> ) {
            my ( $type, $sha1, $size, $name ) = /\A[0-7]{6} (\S+) (\S+) +(\S+)\t(.*)/;
            if ( $type eq 'tree' ) {
                push @subtree, [ $sha1, $name ];
            }
            elsif ( $type eq 'blob' and $size >= $cutoff ) {
                push @r, [ $size, @path, $name ];
            }
        }
    }

    push @r, walk_tree( $_->[0], @path, $_->[1] )
        for @subtree;

    return @r;
}

memoize 'walk_tree';

open my $log, '-|', git => log => @ARGV, '--pretty=format:%T %h %cr'
    or die "Couldn't open pipe to git-log: $!\n";

my %seen;
while ( <$log> ) {
    chomp;
    my ( $tree, $commit, $age ) = split " ", $_, 3;
    my $is_header_printed;
    for ( walk_tree( $tree ) ) {
        my ( $size, @path ) = @$_;
        my $path = join '/', @path;
        next if $seen{ $path }++;
        print "$commit $age\n" if not $is_header_printed++;
        print "\t$size\t$path\n";
    }
}

более компактный Руби скрипт:

#!/usr/bin/env ruby -w
head, treshold = ARGV
head ||= 'HEAD'
Megabyte = 1000 ** 2
treshold = (treshold || 0.1).to_f * Megabyte

big_files = {}

IO.popen("git rev-list #{head}", 'r') do |rev_list|
  rev_list.each_line do |commit|
    commit.chomp!
    for object in `git ls-tree -zrl #{commit}`.split("")
      bits, type, sha, size, path = object.split(/\s+/, 5)
      size = size.to_i
      big_files[sha] = [path, size, commit] if size >= treshold
    end
  end
end

big_files.each do |sha, (path, size, commit)|
  where = `git show -s #{commit} --format='%h: %cr'`.chomp
  puts "%4.1fM\t%s\t(%s)" % [size.to_f / Megabyte, path, where]
end

использование:

ruby big_file.rb [rev] [size in MB]
$ ruby big_file.rb master 0.3
3.8M  example/blah.psd  (aad2981: 4 months ago)
1.1M  another/big.file  (6e73ca2: 2 weeks ago)

скрипт на Python, чтобы сделать то же самое (на основе этот пост):

#!/usr/bin/env python

import os, sys

def getOutput(cmd):
    return os.popen(cmd).read()

if (len(sys.argv) <> 2):
    print "usage: %s size_in_bytes" % sys.argv[0]
else:
    maxSize = int(sys.argv[1])

    revisions = getOutput("git rev-list HEAD").split()

    bigfiles = set()
    for revision in revisions:
        files = getOutput("git ls-tree -zrl %s" % revision).split('')
        for file in files:
            if file == "":
                continue
            splitdata = file.split()
            commit = splitdata[2]
            if splitdata[3] == "-":
                continue
            size = int(splitdata[3])
            path = splitdata[4]
            if (size > maxSize):
                bigfiles.add("%10d %s %s" % (size, commit, path))

    bigfiles = sorted(bigfiles, reverse=True)

    for f in bigfiles:
        print f

Оуч... этот первый сценарий (Аристотель) довольно медленный. На мерзавца.git repo, ища файлы > 100k, он жует процессор около 6 минут.

Он также, похоже, имеет несколько неправильных shas-часто будет напечатан SHA, который не имеет ничего общего с именем файла, упомянутым в следующей строке.

вот и более быстрая версия. Формат вывода отличается, но он очень быстрый, и он также-насколько я могу судить-правильный.

в программа и немного дольше, но это словоблудие.

#!/usr/bin/perl
use 5.10.0;
use strict;
use warnings;

use File::Temp qw(tempdir);
END { chdir( $ENV{HOME} ); }
my $tempdir = tempdir( "git-files_tempdir.XXXXXXXXXX", TMPDIR => 1, CLEANUP => 1 );

my $min = shift;
$min =~ /^\d+$/ or die "need a number";

# ----------------------------------------------------------------------

my @refs =qw(HEAD);
@refs = @ARGV if @ARGV;

# first, find blob SHAs and names (no sizes here)
open( my $objects, "-|", "git", "rev-list", "--objects", @refs) or die "rev-list: $!";
open( my $blobfile, ">", "$tempdir/blobs" ) or die "blobs out: $!";

my ( $blob, $name );
my %name;
my %size;
while (<$objects>) {
    next unless / ./;    # no commits or top level trees
    ( $blob, $name ) = split;
    $name{$blob} = $name;
    say $blobfile $blob;
}
close($blobfile);

# next, use cat-file --batch-check on the blob SHAs to get sizes
open( my $sizes, "-|", "< $tempdir/blobs git cat-file --batch-check | grep blob" ) or die "cat-file: $!";

my ( $dummy, $size );
while (<$sizes>) {
    ( $blob, $dummy, $size ) = split;
    next if $size < $min;
    $size{ $name{$blob} } = $size if ( $size{ $name{$blob} } || 0 ) < $size;
}

my @names_by_size = sort { $size{$b} <=> $size{$a} } keys %size;

say "
The size shown is the largest that file has ever attained.  But note
that it may not be that big at the commit shown, which is merely the
most recent commit affecting that file.
";

# finally, for each name being printed, find when it was last updated on each
# branch that we're concerned about and print stuff out
for my $name (@names_by_size) {
    say "$size{$name}\t$name";

    for my $r (@refs) {
        system("git --no-pager log -1 --format='%x09%h%x09%x09%ar%x09$r' $r -- $name");
    }
    print "\n";
}
print "\n";

вы хотите использовать BFG Repo-Cleaner, более быстрая и простая альтернатива git-filter-branch специально разработан для удаления большие файлы из репозиториев Git.

скачать BFG jar (требуется Java 6 или выше) и выполните следующую команду:

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

любые файлы размером более 1 м (которые не находятся в вашем последний commit) будет удален из истории вашего репозитория Git. Затем вы можете использовать git gc чтобы убрать мертвых данные:

$ git gc --prune=now --aggressive

BFG является, как правило,10-50x быстрее, чем работает git-filter-branch и параметры адаптированы вокруг этих двух общих случаев использования:

  • удаление Сумасшедшие Большие Файлы
  • удаление Пароли, Учетные Данные и другие личные данные

полное раскрытие информации: я автор BFG Repo-Cleaner.

сценарий Аристота покажет вам, что вы хотите. Вы также должны знать, что удаленные файлы все еще занимают место в репо.

по умолчанию Git сохраняет изменения в течение 30 дней, прежде чем они могут быть собраны в мусор. Если вы хотите удалить их сейчас:

$ git reflog expire --expire=1.minute refs/heads/master
     # all deletions up to 1 minute  ago available to be garbage-collected
$ git fsck --unreachable 
     # lists all the blobs(file contents) that will be garbage-collected 
$ git prune 
$ git gc

боковой комментарий: хотя я большой поклонник Git, Git не приносит никаких преимуществ для хранения вашей коллекции "случайных скриптов, текстовых файлов, веб-сайтов" и двоичных файлов. Git отслеживает изменения в содержание, в частности, история скоординированных изменений среди многих текстовых файлов, и делает это очень эффективно и эффективно. Как показывает ваш вопрос, Git не имеет хороших инструментов для отслеживания отдельных изменений файлов. И он не отслеживает изменения в двоичных файлах, поэтому любая ревизия хранит еще одну полную копию в репо.

конечно, это использование Git-отличный способ ознакомиться с тем, как он работает.

#!/bin/bash
if [ "$#" != 1 ]
then
  echo 'git large.sh [size]'
  exit
fi

declare -A big_files
big_files=()
echo printing results

while read commit
do
  while read bits type sha size path
  do
    if [ "$size" -gt "" ]
    then
      big_files[$sha]="$sha $size $path"
    fi
  done < <(git ls-tree --abbrev -rl $commit)
done < <(git rev-list HEAD)

for file in "${big_files[@]}"
do
  read sha size path <<< "$file"
  if git ls-tree -r HEAD | grep -q $sha
  then
    echo $file
  fi
done

источник

мой питон упрощение https://stackoverflow.com/a/10099633/131881

#!/usr/bin/env python
import os, sys

bigfiles = []
for revision in os.popen('git rev-list HEAD'):
    for f in os.popen('git ls-tree -zrl %s' % revision).read().split(''):
        if f:
            mode, type, commit, size, path = f.split(None, 4)
            if int(size) > int(sys.argv[1]):
                bigfiles.append((int(size), commit, path))

for f in sorted(set(bigfiles)):
    print f

этот bash "однострочный" отображает все объекты blob в репозитории, которые больше 10 MiB и не присутствуют в HEAD сортировка от самого маленького до самого большого.

Это очень быстро, легко копировать и вставлять и требует только стандартных утилит GNU.

git rev-list --objects --all \
| git cat-file --batch-check='%(objecttype) %(objectname) %(objectsize) %(rest)' \
| awk -v min_mb=10 '/^blob/ &&  >= min_mb*2^20 {print substr(,6)}' \
| grep -vF "$(git ls-tree -r HEAD | awk '{print }')" \
| sort --numeric-sort --key=2 \
| cut --complement --characters=13-40 \
| numfmt --field=2 --to=iec-i --suffix=B --padding=7 --round=nearest

Это будет генерировать вывод такой:

2ba44098e28f   12MiB path/to/hires-image.png
bd1741ddce0d   63MiB path/to/some-video-1080p.mp4

для получения дополнительной информации, включая формат вывода, более подходящий для дальнейшей обработки сценария, см. my оригинал ответ по аналогичному вопросу.

немного опоздал на вечеринку, но git-fat имеет эту встроенную функциональность.

просто установите его с помощью pip и запустите git fat -a find 100000 где число в конце-это в байтах.