Quel commit a ce blob?

150

Étant donné le hachage d'un blob, existe-t-il un moyen d'obtenir une liste des validations qui ont ce blob dans leur arborescence?

Lecture seulement
la source
2
Le "hachage d'un objet blob" est celui renvoyé par git hash-objectou sha1("blob " + filesize + "\0" + data), et pas simplement le sha1sum du contenu de l'objet blob.
Ivan Hamilton
1
Au départ, je pensais que cette question correspondait à ma question, mais il semble que non. Je veux savoir l' un commit qui a d' abord introduit ce blob au référentiel.
Jesse Glick
Si vous connaissez le chemin du fichier, vous pouvez utiliser git log --follow filepath(et l'utiliser pour accélérer la solution d'Aristote, si vous le souhaitez).
Zaz
ProTip ™: insérez l'un des scripts ci-dessous ~/.binet nommez-le git-find-object. Vous pouvez ensuite l'utiliser avec git find-object.
Zaz
1
Remarque: avec Git 2.16 (Q1 2018), vous pouvez envisager simplement git describe <hash>: voir ma réponse ci-dessous .
VonC

Réponses:

107

Les deux scripts suivants prennent le SHA1 de l'objet blob comme premier argument, et après, éventuellement, tous les arguments qui git logcomprendront. Par exemple, --allpour rechercher dans toutes les branches au lieu de la seule en cours, ou -gpour rechercher dans le reflog, ou tout ce que vous voulez.

Ici, c'est comme un script shell - court et doux, mais lent:

#!/bin/sh
obj_name="$1"
shift
git log "$@" --pretty=format:'%T %h %s' \
| while read tree commit subject ; do
    if git ls-tree -r $tree | grep -q "$obj_name" ; then
        echo $commit "$subject"
    fi
done

Et une version optimisée en Perl, encore assez courte mais beaucoup plus rapide:

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

my $obj_name;

sub check_tree {
    my ( $tree ) = @_;
    my @subtree;

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

        while ( <$ls_tree> ) {
            /\A[0-7]{6} (\S+) (\S+)/
                or die "unexpected git-ls-tree output";
            return 1 if $2 eq $obj_name;
            push @subtree, $2 if $1 eq 'tree';
        }
    }

    check_tree( $_ ) && return 1 for @subtree;

    return;
}

memoize 'check_tree';

die "usage: git-find-blob <blob> [<git-log arguments ...>]\n"
    if not @ARGV;

my $obj_short = shift @ARGV;
$obj_name = do {
    local $ENV{'OBJ_NAME'} = $obj_short;
     `git rev-parse --verify \$OBJ_NAME`;
} or die "Couldn't parse $obj_short: $!\n";
chomp $obj_name;

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

while ( <$log> ) {
    chomp;
    my ( $tree, $commit, $subject ) = split " ", $_, 3;
    print "$commit $subject\n" if check_tree( $tree );
}
Aristote Pagaltzis
la source
9
Pour info, vous devez utiliser le SHA complet du blob. Un préfixe, même s'il est unique, ne fonctionnera pas. Pour obtenir le SHA complet à partir d'un préfixe, vous pouvez utilisergit rev-parse --verify $theprefix
John Douthat
1
Merci @JohnDouthat pour ce commentaire. Voici comment intégrer cela dans le script ci-dessus (désolé pour l'inclusion dans les commentaires): my $blob_arg = shift; open my $rev_parse, '-|', git => 'rev-parse' => '--verify', $blob_arg or die "Couldn't open pipe to git-rev-parse: $!\n"; my $obj_name = <$rev_parse>; chomp $obj_name; close $rev_parse or die "Couldn't expand passed blob.\n"; $obj_name eq $blob_arg or print "(full blob is $obj_name)\n";
Ingo Karkat
Il peut y avoir un bogue dans le script shell supérieur. La boucle while ne s'exécute que s'il y a plus de lignes à lire, et pour une raison quelconque, git log ne met pas un crlf final à la fin. J'ai dû ajouter un saut de ligne et ignorer les lignes vides. obj_name="$1" shift git log --all --pretty=format:'%T %h %s %n' -- "$@" | while read tree commit cdate subject ; do if [ -z $tree ] ; then continue fi if git ls-tree -r $tree | grep -q "$obj_name" ; then echo "$cdate $commit $@ $subject" fi done
Mixologic
7
Cela ne trouve que les commits sur la branche actuelle, sauf si vous passez --allcomme argument supplémentaire. (Il est important de trouver tous les commits à l'échelle du dépôt dans des cas comme la suppression d'un gros fichier de l'historique du dépôt ).
peterflynn
1
Astuce: passez l'indicateur -g au script shell (après l'ID d'objet) pour examiner le reflog.
Bram Schoenmakers
24

Malheureusement, les scripts étaient un peu lents pour moi, j'ai donc dû optimiser un peu. Heureusement, j'avais non seulement le hachage mais aussi le chemin d'un fichier.

git log --all --pretty=format:%H -- <path> | xargs -n1 -I% sh -c "git ls-tree % -- <path> | grep -q <hash> && echo %"
Aragaer
la source
1
Excellente réponse car c'est si simple. Juste en faisant l'hypothèse raisonnable que le chemin est connu. Cependant, il faut savoir qu'il renvoie le commit où le chemin a été changé pour le hachage donné.
Unapiedra
1
Si l'on veut que le dernier commit contenant le <hash>at the donné <path>, alors la suppression de l' <path>argument de la git logfonctionnera. Le premier résultat renvoyé est le commit voulu.
Unapiedra
10

Étant donné le hachage d'un blob, existe-t-il un moyen d'obtenir une liste des validations qui ont ce blob dans leur arborescence?

Avec Git 2.16 (Q1 2018), ce git describeserait une bonne solution, car il a été appris à creuser des arbres plus profondément pour trouver un <commit-ish>:<path>qui fait référence à un objet blob donné.

Voir commit 644eb60 , commit 4dbc59a , commit cdaed0c , commit c87b653 , commit ce5b6f9 (16 novembre 2017) et commit 91904f5 , commit 2deda00 (02 novembre 2017) par Stefan Beller ( stefanbeller) .
(Fusionné par Junio ​​C Hamano - gitster- in commit 556de1a , 28 déc 2017)

builtin/describe.c: décrire un blob

Parfois, les utilisateurs reçoivent un hachage d'un objet et veulent l'identifier davantage (par exemple: utilisez verify-packpour trouver les plus gros blobs, mais que sont-ils? Ou cette question très SO " Quel commit a ce blob? ")

Lors de la description des commits, nous essayons de les ancrer à des balises ou des références, car ils sont conceptuellement à un niveau plus élevé que le commit. Et s'il n'y a pas de référence ou de tag qui correspond exactement, nous n'avons pas de chance.
Nous utilisons donc une heuristique pour créer un nom pour le commit. Ces noms sont ambigus, il peut y avoir différentes balises ou références à ancrer, et il peut y avoir un chemin différent dans le DAG à parcourir pour arriver au commit avec précision.

Lors de la description d'un blob, nous voulons également décrire le blob à partir d'une couche supérieure, ce qui est un tuple (commit, deep/path)car les objets d'arbre impliqués sont plutôt inintéressants.
Le même objet blob peut être référencé par plusieurs commits, alors comment décider quel commit utiliser?

Ce patch implémente une approche plutôt naïve à ce sujet: comme il n'y a pas de pointeurs de retour des blobs vers les commits dans lesquels le blob se produit, nous commencerons à marcher à partir de tous les conseils disponibles, en listant les blobs dans l'ordre du commit et une fois que nous avons trouvé le blob, nous prendrons le premier commit qui a répertorié le blob .

Par exemple:

git describe --tags v0.99:Makefile
conversion-901-g7672db20c2:Makefile

nous dit que le Makefiletel qu'il était a v0.99été introduit dans le commit 7672db2 .

La marche est effectuée dans l'ordre inverse pour montrer l'introduction d'une tache plutôt que sa dernière occurrence.

Cela signifie que la git describepage de manuel ajoute aux objectifs de cette commande:

Au lieu de simplement décrire un commit en utilisant la balise la plus récente accessible à partir de celui-ci, git describedonnera en fait à un objet un nom lisible par l'homme basé sur une référence disponible lorsqu'il est utilisé comme git describe <blob>.

Si l'objet donné fait référence à un blob, il sera décrit comme <commit-ish>:<path>, tel que le blob peut être trouvé <path>dans le <commit-ish>, qui décrit lui-même la première validation dans laquelle ce blob se produit dans une marche de révision inverse à partir de HEAD.

Mais:

BUGS

Les objets d'arborescence ainsi que les objets de balise ne pointant pas sur les validations ne peuvent pas être décrits .
Lors de la description des objets blob, les balises légères pointant sur les blobs sont ignorées, mais l'objet blob est toujours décrit comme <committ-ish>:<path>malgré le fait que la balise légère soit favorable.

VonC
la source
1
Bon à utiliser en conjonction avec 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 -r | head -n 20, qui vous renvoie un top 20 des plus gros blobs. Ensuite, vous pouvez transmettre l'ID de l'objet blob de la sortie ci-dessus à git describe. A travaillé comme un charme! Merci!
Alexander Pogrebnyak
7

Je pensais que ce serait une chose généralement utile à avoir, alors j'ai écrit un petit script perl pour le faire:

#!/usr/bin/perl -w

use strict;

my @commits;
my %trees;
my $blob;

sub blob_in_tree {
    my $tree = $_[0];
    if (defined $trees{$tree}) {
        return $trees{$tree};
    }
    my $r = 0;
    open(my $f, "git cat-file -p $tree|") or die $!;
    while (<$f>) {
        if (/^\d+ blob (\w+)/ && $1 eq $blob) {
            $r = 1;
        } elsif (/^\d+ tree (\w+)/) {
            $r = blob_in_tree($1);
        }
        last if $r;
    }
    close($f);
    $trees{$tree} = $r;
    return $r;
}

sub handle_commit {
    my $commit = $_[0];
    open(my $f, "git cat-file commit $commit|") or die $!;
    my $tree = <$f>;
    die unless $tree =~ /^tree (\w+)$/;
    if (blob_in_tree($1)) {
        print "$commit\n";
    }
    while (1) {
        my $parent = <$f>;
        last unless $parent =~ /^parent (\w+)$/;
        push @commits, $1;
    }
    close($f);
}

if (!@ARGV) {
    print STDERR "Usage: git-find-blob blob [head ...]\n";
    exit 1;
}

$blob = $ARGV[0];
if (@ARGV > 1) {
    foreach (@ARGV) {
        handle_commit($_);
    }
} else {
    handle_commit("HEAD");
}
while (@commits) {
    handle_commit(pop @commits);
}

Je mettrai ça sur github quand je rentrerai à la maison ce soir.

Mise à jour: il semble que quelqu'un l'ait déjà fait . Celui-ci utilise la même idée générale mais les détails sont différents et la mise en œuvre est beaucoup plus courte. Je ne sais pas ce qui serait le plus rapide mais les performances ne sont probablement pas un problème ici!

Mise à jour 2: Pour ce que ça vaut, ma mise en œuvre est beaucoup plus rapide, en particulier pour un grand référentiel. Cela git ls-tree -rfait vraiment mal.

Mise à jour 3: Je dois noter que mes commentaires sur les performances ci-dessus s'appliquent à l'implémentation que j'ai liée ci-dessus dans la première mise à jour. L'implémentation d'Aristote est comparable à la mienne. Plus de détails dans les commentaires pour ceux qui sont curieux.

Greg Hewgill
la source
Hmm, comment peut - il être que beaucoup plus rapide? Vous marchez dans l'arbre de toute façon, n'est-ce pas? Quel travail fait git-ls-tree que vous évitez? (NB: grep sera libéré lors du premier match, SIGPIPE en utilisant git-ls-tree.) Quand je l'ai essayé, j'ai dû Ctrl-C votre script après 30 secondes; le mien a été fait en 4.
Aristote Pagaltzis
1
Mon script met en cache les résultats des sous-arbres dans le hachage% tree, il n'a donc pas besoin de continuer à rechercher des sous-arbres qui n'ont pas changé.
Greg Hewgill
En fait, j'essayais l'implémentation que j'ai trouvée sur github et à laquelle j'ai lié. Le vôtre est plus rapide dans certains cas, mais cela dépend fortement du fait que le fichier que vous recherchez se trouve au début ou à la fin de la liste ls-tree. Mon référentiel contient actuellement 9574 fichiers.
Greg Hewgill
Il me vient également à l'esprit que certains historiques de projets non linéaires peuvent amener mon script à faire beaucoup plus de travail que nécessaire (cela peut être corrigé). C'est peut-être pourquoi il a fallu beaucoup de temps pour courir pour vous. Mon référentiel est un miroir git-svn d'un référentiel Subversion, donc il est bien linéaire.
Greg Hewgill
Au lieu d'analyser le fichier cat pour obtenir l'arbre, faites-legit rev-parse $commit^{}
jthill
6

Bien que la question d'origine ne le demande pas, je pense qu'il est utile de vérifier également la zone de transit pour voir si un blob est référencé. J'ai modifié le script bash d'origine pour ce faire et j'ai trouvé ce qui faisait référence à un blob corrompu dans mon référentiel:

#!/bin/sh
obj_name="$1"
shift
git ls-files --stage \
| if grep -q "$obj_name"; then
    echo Found in staging area. Run git ls-files --stage to see.
fi

git log "$@" --pretty=format:'%T %h %s' \
| while read tree commit subject ; do
    if git ls-tree -r $tree | grep -q "$obj_name" ; then
        echo $commit "$subject"
    fi
done
Mario
la source
3
Je voudrais juste donner du crédit là où c'est dû: merci la corruption de RAM pour m'avoir causé un BSOD et m'avoir forcé à réparer à la main mon repo git.
Mario
4

Donc ... j'avais besoin de trouver tous les fichiers dépassant une limite donnée dans un dépôt de plus de 8 Go, avec plus de 108 000 révisions. J'ai adapté le script perl d'Aristote avec un script ruby ​​que j'ai écrit pour atteindre cette solution complète.

Premièrement, git gc- faites ceci pour vous assurer que tous les objets sont dans des fichiers pack - nous n'analysons pas les objets qui ne sont pas dans des fichiers pack.

Suivant Exécutez ce script pour localiser tous les objets blob sur des octets CUTOFF_SIZE. Capturez la sortie dans un fichier comme "large-blobs.log"

#!/usr/bin/env ruby

require 'log4r'

# The output of git verify-pack -v is:
# SHA1 type size size-in-packfile offset-in-packfile depth base-SHA1
#
#
GIT_PACKS_RELATIVE_PATH=File.join('.git', 'objects', 'pack', '*.pack')

# 10MB cutoff
CUTOFF_SIZE=1024*1024*10
#CUTOFF_SIZE=1024

begin

  include Log4r
  log = Logger.new 'git-find-large-objects'
  log.level = INFO
  log.outputters = Outputter.stdout

  git_dir = %x[ git rev-parse --show-toplevel ].chomp

  if git_dir.empty?
    log.fatal "ERROR: must be run in a git repository"
    exit 1
  end

  log.debug "Git Dir: '#{git_dir}'"

  pack_files = Dir[File.join(git_dir, GIT_PACKS_RELATIVE_PATH)]
  log.debug "Git Packs: #{pack_files.to_s}"

  # For details on this IO, see http://stackoverflow.com/questions/1154846/continuously-read-from-stdout-of-external-process-in-ruby
  #
  # Short version is, git verify-pack flushes buffers only on line endings, so
  # this works, if it didn't, then we could get partial lines and be sad.

  types = {
    :blob => 1,
    :tree => 1,
    :commit => 1,
  }


  total_count = 0
  counted_objects = 0
  large_objects = []

  IO.popen("git verify-pack -v -- #{pack_files.join(" ")}") do |pipe|
    pipe.each do |line|
      # The output of git verify-pack -v is:
      # SHA1 type size size-in-packfile offset-in-packfile depth base-SHA1
      data = line.chomp.split(' ')
      # types are blob, tree, or commit
      # we ignore other lines by looking for that
      next unless types[data[1].to_sym] == 1
      log.info "INPUT_THREAD: Processing object #{data[0]} type #{data[1]} size #{data[2]}"
      hash = {
        :sha1 => data[0],
        :type => data[1],
        :size => data[2].to_i,
      }
      total_count += hash[:size]
      counted_objects += 1
      if hash[:size] > CUTOFF_SIZE
        large_objects.push hash
      end
    end
  end

  log.info "Input complete"

  log.info "Counted #{counted_objects} totalling #{total_count} bytes."

  log.info "Sorting"

  large_objects.sort! { |a,b| b[:size] <=> a[:size] }

  log.info "Sorting complete"

  large_objects.each do |obj|
    log.info "#{obj[:sha1]} #{obj[:type]} #{obj[:size]}"
  end

  exit 0
end

Ensuite, modifiez le fichier pour supprimer tous les objets blob que vous n'attendez pas et les bits INPUT_THREAD en haut. une fois que vous n'avez que des lignes pour les sha1 que vous voulez trouver, exécutez le script suivant comme ceci:

cat edited-large-files.log | cut -d' ' -f4 | xargs git-find-blob | tee large-file-paths.log

Où le git-find-blobscript est ci-dessous.

#!/usr/bin/perl

# taken from: http://stackoverflow.com/questions/223678/which-commit-has-this-blob
# and modified by Carl Myers <[email protected]> to scan multiple blobs at once
# Also, modified to keep the discovered filenames
# vi: ft=perl

use 5.008;
use strict;
use Memoize;
use Data::Dumper;


my $BLOBS = {};

MAIN: {

    memoize 'check_tree';

    die "usage: git-find-blob <blob1> <blob2> ... -- [<git-log arguments ...>]\n"
        if not @ARGV;


    while ( @ARGV && $ARGV[0] ne '--' ) {
        my $arg = $ARGV[0];
        #print "Processing argument $arg\n";
        open my $rev_parse, '-|', git => 'rev-parse' => '--verify', $arg or die "Couldn't open pipe to git-rev-parse: $!\n";
        my $obj_name = <$rev_parse>;
        close $rev_parse or die "Couldn't expand passed blob.\n";
        chomp $obj_name;
        #$obj_name eq $ARGV[0] or print "($ARGV[0] expands to $obj_name)\n";
        print "($arg expands to $obj_name)\n";
        $BLOBS->{$obj_name} = $arg;
        shift @ARGV;
    }
    shift @ARGV; # drop the -- if present

    #print "BLOBS: " . Dumper($BLOBS) . "\n";

    foreach my $blob ( keys %{$BLOBS} ) {
        #print "Printing results for blob $blob:\n";

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

        while ( <$log> ) {
            chomp;
            my ( $tree, $commit, $subject ) = split " ", $_, 3;
            #print "Checking tree $tree\n";
            my $results = check_tree( $tree );

            #print "RESULTS: " . Dumper($results);
            if (%{$results}) {
                print "$commit $subject\n";
                foreach my $blob ( keys %{$results} ) {
                    print "\t" . (join ", ", @{$results->{$blob}}) . "\n";
                }
            }
        }
    }

}


sub check_tree {
    my ( $tree ) = @_;
    #print "Calculating hits for tree $tree\n";

    my @subtree;

    # results = { BLOB => [ FILENAME1 ] }
    my $results = {};
    {
        open my $ls_tree, '-|', git => 'ls-tree' => $tree
            or die "Couldn't open pipe to git-ls-tree: $!\n";

        # example git ls-tree output:
        # 100644 blob 15d408e386400ee58e8695417fbe0f858f3ed424    filaname.txt
        while ( <$ls_tree> ) {
            /\A[0-7]{6} (\S+) (\S+)\s+(.*)/
                or die "unexpected git-ls-tree output";
            #print "Scanning line '$_' tree $2 file $3\n";
            foreach my $blob ( keys %{$BLOBS} ) {
                if ( $2 eq $blob ) {
                    print "Found $blob in $tree:$3\n";
                    push @{$results->{$blob}}, $3;
                }
            }
            push @subtree, [$2, $3] if $1 eq 'tree';
        }
    }

    foreach my $st ( @subtree ) {
        # $st->[0] is tree, $st->[1] is dirname
        my $st_result = check_tree( $st->[0] );
        foreach my $blob ( keys %{$st_result} ) {
            foreach my $filename ( @{$st_result->{$blob}} ) {
                my $path = $st->[1] . '/' . $filename;
                #print "Generating subdir path $path\n";
                push @{$results->{$blob}}, $path;
            }
        }
    }

    #print "Returning results for tree $tree: " . Dumper($results) . "\n\n";
    return $results;
}

La sortie ressemblera à ceci:

<hash prefix> <oneline log message>
    path/to/file.txt
    path/to/file2.txt
    ...
<hash prefix2> <oneline log msg...>

Etc. Chaque commit contenant un gros fichier dans son arborescence sera répertorié. si vous grepsortez les lignes qui commencent par un onglet, et uniqque, vous aurez une liste de tous les chemins que vous pouvez filtrer-branche à supprimer, ou vous pouvez faire quelque chose de plus compliqué.

Permettez-moi de répéter: ce processus s'est déroulé avec succès, sur un dépôt de 10 Go avec 108 000 validations. Cela a pris beaucoup plus de temps que je ne l'avais prédit lors de l'exécution sur un grand nombre de blobs, cependant, plus de 10 heures, je devrai voir si le bit de mémorisation fonctionne ...

cmyers
la source
1
Comme la réponse d'Aristote ci - dessus, cela ne trouve commits sur la branche actuelle à moins que vous passez des arguments supplémentaires: -- --all. (Il est important de trouver tous les commits à l'échelle du référentiel dans des cas tels que la suppression complète d'un fichier volumineux de l'historique du référentiel ).
peterflynn
4

De plus git describe, que je mentionne dans ma réponse précédente , git loget git diffbénéficie désormais également de l' --find-object=<object-id>option " " pour limiter les résultats aux changements qui impliquent l'objet nommé.
C'est dans Git 2.16.x / 2.17 (Q1 2018)

Voir commit 4d8c51a , commit 5e50525 , commit 15af58c , commit cf63051 , commit c1ddc46 , commit 929ed70 (4 janvier 2018) par Stefan Beller ( stefanbeller) .
(Fusionné par Junio ​​C Hamano - gitster- in commit c0d75f0 , 23 janv.2018 )

diffcore: ajoutez une option de pioche pour trouver une goutte spécifique

Parfois, les utilisateurs reçoivent un hachage d'un objet et veulent l'identifier davantage (par exemple: utilisez verify-pack pour trouver les plus gros blobs, mais que sont-ils? Ou cette question de Stack Overflow " Quel commit a ce blob? ")

On pourrait être tenté d'étendre git-describepour travailler également avec des blobs, de sorte que git describe <blob-id>la description soit «:».
Cela a été mis en œuvre ici ; comme le montre le nombre de réponses (> 110), il s'avère que c'est difficile à faire.
La partie la plus difficile à faire est de choisir le bon 'commit-ish' car cela pourrait être le commit qui (ré) introduit le blob ou le blob qui a supprimé le blob; le blob peut exister dans différentes branches.

Junio ​​a fait allusion à une approche différente pour résoudre ce problème, que ce patch implémente.
Apprenez à la diffmachine un autre drapeau pour restreindre les informations à ce qui est affiché.
Par exemple:

$ ./git log --oneline --find-object=v2.0.0:Makefile
  b2feb64 Revert the whole "ask curl-config" topic for now
  47fbfde i18n: only extract comments marked with "TRANSLATORS:"

nous observons que le Makefiletel que livré avec 2.0était apparu dans v1.9.2-471-g47fbfded53et dans v2.0.0-rc1-5-gb2feb6430b.
La raison pour laquelle ces validations se produisent toutes les deux avant la v2.0.0 sont des fusions malveillantes qui ne sont pas trouvées en utilisant ce nouveau mécanisme.

VonC
la source