Найти файлы в git репо за x мегабайтами, которых нет в HEAD

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

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

В основном я хочу иметь возможность делать.

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

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

Ответ 1

Это адаптация git-find-blob script, которую я опубликовал ранее:

#!/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/ ) ? ( $1, $2 ) : 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";
    }
}

Ответ 2

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

#!/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("\0")
      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)

Ответ 3

Python script сделать то же самое (на основе этого сообщения):

#!/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('\0')
        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

Ответ 4

Ouch..., что первый script (по Аристотелю), довольно медленный. В реестре git.git, ища файлы > 100k, он разыгрывает процессор около 6 минут.

Также, как представляется, напечатано несколько неправильных SHA - часто будет напечатана 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";

Ответ 5

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

Загрузите баннер BFG (требуется Java 6 или выше) и запустите эту команду:

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

Любые файлы размером более 1 МБ (которые не находятся в последней фиксации) будут удалены из истории хранилища Git. Затем вы можете использовать git gc для удаления мертвых данных:

$ git gc --prune=now --aggressive

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

  • Удаление Сумасшедших больших файлов
  • Удаление Паролей, учетных данных и других личных данных

Полное раскрытие: я являюсь автором BFG Repo-Cleaner.

Ответ 6

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

По умолчанию 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 - отличный способ узнать, как это работает.

Ответ 7

#!/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 "$1" ]
    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

Источник

Ответ 8

Мое упрощение python fooobar.com/questions/9400/...

#!/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('\0'):
        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

Ответ 9

Этот 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/ && $3 >= min_mb*2^20 {print substr($0,6)}' \
| grep -vF "$(git ls-tree -r HEAD | awk '{print $3}')" \
| 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

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

Ответ 10

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

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