web-dev-qa-db-fra.com

Quel commit a ce blob?

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

131
Readonly

Les deux scripts suivants prennent le SHA1 du blob comme premier argument, puis, éventuellement, tous les arguments qui git log comprendra. Par exemple. --all pour rechercher dans toutes les branches au lieu de la seule, ou -g pour rechercher dans le reflog, ou tout ce que vous voulez.

Le voici 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 );
}
92

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 %"
16
aragaer

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 ce soir.

Mise à jour: il ressemble à quelqu'un l'a déjà fait . Celui-ci utilise la même idée générale mais les détails sont différents et l'implémentation est beaucoup plus courte. Je ne sais pas lequel serait le plus rapide mais les performances ne sont probablement pas un problème ici!

Mise à jour 2: pour ce que ça vaut, mon implémentation est des ordres de grandeur plus rapide, surtout pour un grand référentiel. Cette git ls-tree -r fait 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. implémentation d'Aristote fonctionne de manière comparable à la mienne. Plus de détails dans les commentaires pour les curieux.

7
Greg Hewgill

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

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

Voir commit 644eb6 , commit 4dbc59a , commit cdaed0c , commit c87b65 , commit ce5b6f9 (16 nov 2017), et commit 91904f5 , commit 2deda (02 nov 2017) par Stefan Beller (stefanbeller) .
(Fusionné par Junio ​​C Hamano - gitster - in commit 556de1a , 28 décembre 2017)

builtin/describe.c : décrire un blob

Parfois, les utilisateurs reçoivent un hachage d'un objet et ils veulent l'identifier davantage (ex.: Utiliser verify-pack pour trouver les plus gros blobs, mais quels sont-ils? ou très SO question " Quel commit a ce blob? ")

Lors de la description des validations, nous essayons de les ancrer aux balises ou aux références, car celles-ci sont conceptuellement à un niveau supérieur à la validation. 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 la validation. 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 précisément à la validation.

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

Ce correctif met en œuvre 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 allons commencer à 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 indique le Makefile tel qu'il était dans v0.99 a été introduit dans commit 7672db2 .

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

Cela signifie que le git describe man page ajoute aux objectifs de cette commande:

Au lieu de simplement décrire un commit à l'aide de la balise la plus récente accessible depuis celui-ci, git describe donnera réellement à 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>, de sorte que le blob se trouve à <path> dans le <commit-ish>, qui décrit lui-même le premier commit dans lequel ce blob se produit dans une marche de révision inverse de HEAD.

Mais:

BOGUES

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

6
VonC

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
6
Mario

Donc ... je devais 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ère, git gc - faites cela pour vous assurer que tous les objets sont dans des fichiers pack - nous n'analysons pas les objets qui ne sont pas dans les fichiers pack.

Exécutez ce script pour localiser tous les objets blob sur les 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 blobs 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-blob le script 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 grep sortez les lignes qui commencent par un onglet et uniq cela, vous aurez une liste de tous les chemins que vous pouvez filtrer pour 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 commits. Cela a pris beaucoup plus de temps que prévu lors de l'exécution sur un grand nombre d'objets blob, mais sur 10 heures, je devrai voir si le bit de mémorisation fonctionne ...

3
cmyers

En plus de git describe, que je mentionne dans ma réponse précédente , git log et git diff bénéficie désormais également du "--find-object=<object-id> "option pour limiter les résultats aux modifications 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 929ed7 (04 janvier 2018) par Stefan Beller (stefanbeller) .
(Fusionné par Junio ​​C Hamano - gitster - in commit c0d75f , 23 janvier 2018)

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

Parfois, les utilisateurs reçoivent un hachage d'un objet et ils veulent l'identifier davantage (ex.: Utilisez Verify-pack pour trouver les plus gros blobs, mais quels sont-ils? Ou cette question de débordement de pile " Quel commit a ce blob ? ")

On pourrait être tenté d'étendre git-describe pour fonctionner également avec des objets blob, tels que git describe <blob-id> donne une description comme ':'.
C'était implémenté ici ; comme en témoigne le nombre de réponses (> 110), il s'avère que c'est difficile de bien faire les choses.
La partie la plus difficile à obtenir est de choisir le bon "commit-ish" car cela pourrait être le commit qui a (ré) introduit le blob ou le blob qui a supprimé le blob; la goutte peut exister dans différentes branches.

Junio ​​a fait allusion à une approche différente pour résoudre ce problème, que ce correctif implémente.
Apprenez à la machinerie diff un autre indicateur 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 Makefile livré avec 2.0 est apparu dans v1.9.2-471-g47fbfded53 et en v2.0.0-rc1-5-gb2feb6430b.
.

2
VonC