2008-11-18 6 views
56

J'ai un référentiel Git Je stocke des choses aléatoires. Généralement des scripts aléatoires, des fichiers texte, des sites Web que j'ai conçus et ainsi de suite.Trouver des fichiers en git repo sur x mégaoctets, qui n'existent pas dans HEAD

Il ya quelques gros fichiers binaires que j'ai supprimés au fil du temps (généralement 1-5 Mo), qui sont assis autour de l'augmentation de la taille du référentiel, dont je n'ai pas besoin dans l'historique des révisions.

Fondamentalement, je veux être en mesure de le faire ..

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

..then pouvoir aller si chaque résultat, vérifier si elle est plus nécessaire puis de le retirer (probablement à l'aide filter-branch)

Répondre

52

Ceci est une adaptation de the git-find-blob script I posted previously:

#!/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"; 
    } 
} 
+3

J'ai des difficultés à comprendre ce code. Des exemples de comment utiliser votre belle commande? – neoneye

+3

aha. pas d'arguments. Il a juste fallu du temps pour que cela sorte de tout à l'écran. git-large-blob 500k – neoneye

+0

adore ça! une tonne! – alex

4

scénario de vous montrer Aristote ce que vous voulez. Vous devez également savoir que les fichiers supprimés occuperont toujours de l'espace dans le rapport. Par défaut, Git conserve les modifications pendant 30 jours avant de pouvoir être récupérées. Si vous voulez les supprimer maintenant:

$ 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 

Un commentaire latéral: Alors que je suis grand fan de Git, Git n'apporte pas d'avantages à stocker votre collection de « scripts aléatoires, des fichiers texte, des sites Web » et binaire des dossiers. Git suit les changements de contenu, en particulier l'historique des changements coordonnés entre de nombreux fichiers texte, et le fait de manière très efficace et efficiente. Comme votre question l'illustre, Git n'a pas de bons outils pour suivre les changements de fichiers individuels. Et il ne suit pas les changements dans les binaires, de sorte que toute révision stocke une autre copie complète dans le repo.

Bien sûr, cette utilisation de Git est un excellent moyen de se familiariser avec son fonctionnement.

+0

en utilisant git comme ça, mais il le gère très bien, et utiliser un autre VCS juste parce qu'il gère des fichiers binaires (ou des paquets aléatoires de fichiers) serait plus pratique (la commodité étant la seule raison pour laquelle je garde le répertoire dans git!) – dbr

+0

Git stocke «une autre copie complète» de n'importe quel fichier, il n'y a pas de différence est-ce un fichier texte ou un fichier binaire! Bien qu'il ne peut pas vous montrer les changements dans le fichier binaire. – tig

+2

Il est à noter que Git fait/fait/effectue delta-compression sur ses packfiles (http://stackoverflow.com/q/9478023/438886) et donc vous ne payez pas nécessairement pour le stockage d'un fichier binaire modifié deux fois si c'était une simple modification. Cependant, je suis d'accord avec @Paul que le stockage de gros fichiers dans Git n'est généralement pas une bonne idée. Git-annexe (http://git-annex.branchable.com/) pourrait être un bon moyen de combiner les deux si vous devez vraiment - je ne l'ai pas utilisé moi-même. –

44

Plus script Ruby compact:

#!/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 

Utilisation:

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

C'est une bonne réponse mais elle a un défaut. Les gros objets sont stockés dans le hash 'big_files' qui utilise' sha' comme clé unique. En théorie, c'est bien - chaque objet blob est unique après tout. Cependant, dans la pratique, il est concevable que vous ayez exactement le même fichier dans plusieurs emplacements dans votre référentiel. Par exemple, il peut s'agir d'un fichier de test qui nécessite des noms de fichiers différents mais pas de contenu physique différent. ** Des problèmes surviennent lorsque vous voyez un gros objet avec un chemin dont vous n'avez pas besoin mais que vous ne connaissez pas, ce même fichier existe ailleurs où il est nécessaire. ** –

+0

@ ben.snape J'ai ajouté cette ligne juste avant 'big_files [ sha] = ... 'pour que je puisse au moins savoir quand cela arrive:' warn 'Un autre chemin pour # {sha} est # {path} "if big_files.has_key? sha et big_files [sha] [0]! = path' – onionjake

+0

J'ai modifié ce script pour le rendre plus adapté aux grands dépôts: ne pas traiter plus de 1000 validations, afficher une sortie dans la console pendant le travail, éviter une erreur dans ' Commande git show' pour les fichiers qui ne sont pas dans l'arbre de travail: https://gist.github.com/victor-homyakov/690cd2991c77539ca4fe – Victor

6

Aïe ... que le premier scénario (par Aristote), est assez lent. Sur le repo de git.git, à la recherche de fichiers> 100k, il mâche le CPU pendant environ 6 minutes.

Il semble également que plusieurs SHA erronés soient imprimés - souvent un SHA sera imprimé sans rapport avec le nom de fichier mentionné dans la ligne suivante.

Voici une version plus rapide. Le format de sortie est différent, mais il est très rapide, et c'est aussi - pour autant que je puisse le dire - correct.

Le programme est un peu plus long mais beaucoup de verbiage.

#!/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"; 
14

script Python pour faire la même chose (basé sur this post):

#!/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 
+0

Pour les fichiers volumineux, il vaut mieux simplement faire 'bigfiles = trié (set (bigfiles), reverse = True) '. Ou, mieux encore, commencez par 'bigfiles = set()' et utilisez 'bigfiles.add' au lieu de' bigfiles.append'. – Dougal

+0

@Dougal: Correction ..... – SigTerm

+0

Oui, mais maintenant il n'y a plus besoin de le "regler"! :) – Dougal

1

Ma simplification python de 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('\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 
3
#!/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 

Source

+0

Nice et propre! (J'ai même appris quelque chose à propos de bash!) Malheureusement, c'est trop lent pour moi (juste trop d'objets dans le repo). J'ai donc fini par utiliser un outil de dimensionnement de dossier Windows (Explorer ++) pour trouver le plus grand dossier, puis l'objet en .git, suivi d'un git rev-list --objects --all | grep Ce n'est pas très chic, mais a travaillé pour moi. – FractalSpace

+2

Ok trouvé: https://github.com/cmaitchison/git_diet – FractalSpace

+0

@FractalSpace git_diet est génial! Merci! –

6

Vous voulez utiliser le BFG Repo-Cleaner, une alternative plus rapide et plus simple à git-filter-branch spécialement conçu pour supprimer de gros fichiers à partir de repos Git.

Télécharger le BFG jar (nécessite Java 6 ou supérieur) et exécutez la commande suivante:

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

Tous les fichiers sur 1 m de taille (qui ne sont pas dans votre dernière commettras) seront retirés de votre Git l'historique du dépôt. Vous pouvez ensuite utiliser git gc pour débarrasserez les données mortes:

$ git gc --prune=now --aggressive 

Le BFG est généralement 10-50x plus rapide que la course git-filter-branch et les options sont adaptées autour de ces deux communes cas d'utilisation:

  • Retrait fou gros fichiers
  • Suppression mots de passe, les pouvoirs & autres données privées

Déclarations complètes: Je suis l'auteur du BFG Repo-Cleaner.

+0

Cela me rappelle de http://en.wikipedia.org/wiki/BFG_(weapon) :) – quetzalcoatl

+0

@quetzalcoatl yup, le BFG a trois homonymes: 1) l'arme BFG, 2) le géant Big Friendly, et 3) ...ce sont les initiales de git-filter-branch, écrites à l'envers :-) Croyez-moi, le BFG est une arme splendide pour la destruction des données! –

+7

BFG est idéal pour effacer les fichiers, mais comment trouver les fichiers qui seront supprimés? –

-1

Un peu plus tard à la fête, mais git-fat a cette fonctionnalité intégrée.

Installez-le avec pip et exécuter git fat -a find 100000 où le numéro à la fin est en octets.

1

Cette bash "one-liner" affiche tous les objets blob dans le référentiel qui sont plus grands que 10 MiB et ne sont pas présents dans HEAD triés du plus petit au plus grand.

C'est très rapide, facile à copier & coller et ne nécessite que des utilitaires GNU standard.

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 

Cela va générer une sortie comme ceci:

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

Pour plus d'informations, y compris un format de sortie plus adapté pour un traitement ultérieur de script, voir mon original answer sur une question similaire.

Questions connexes