¿Qué commit tiene este blob?

150

Dado el hash de un blob, ¿hay alguna manera de obtener una lista de confirmaciones que tengan este blob en su árbol?

Solo lectura
fuente
2
"Hash of a blob" es el que devuelve git hash-objecto sha1("blob " + filesize + "\0" + data), y no simplemente la suma de los contenidos del blob.
Ivan Hamilton
1
Originalmente pensé que esta pregunta coincidía con mi pregunta, pero parece que no. Quiero saber el uno cometen el que primero introdujo esta mancha en el repositorio.
Jesse Glick
Si conoce la ruta del archivo, puede usar git log --follow filepath(y usar esto para acelerar la solución de Aristóteles, si lo desea).
Zaz
ProTip ™: coloque uno de los scripts de belew ~/.biny asígnele un nombre git-find-object. Luego puedes usarlo con git find-object.
Zaz
1
Nota: Con Git 2.16 (Q1 2018), podría considerar simplemente git describe <hash>: Vea mi respuesta a continuación .
VonC

Respuestas:

107

Los siguientes scripts toman el SHA1 del blob como primer argumento, y después, opcionalmente, cualquier argumento que git logcomprenda. Por ejemplo, --allpara buscar en todas las ramas en lugar de solo la actual, o -gpara buscar en el registro, o cualquier otra cosa que desee.

Aquí es como un script de shell: corto y dulce, pero lento:

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

Y una versión optimizada en Perl, todavía bastante corta pero mucho más rápida:

#!/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 );
}
Aristóteles Pagaltzis
fuente
9
Para tu información, tienes que usar el SHA completo de la burbuja. Un prefijo, incluso si es único, no funcionará. Para obtener el SHA completo de un prefijo, puede usargit rev-parse --verify $theprefix
John Douthat el
1
Gracias @ JohnDouthat por este comentario. Aquí se explica cómo incorporar eso en el script anterior (perdón por la inclusión en los comentarios): 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
Puede haber un error en el script de shell superior. El ciclo while solo se ejecuta si hay más líneas para leer, y por alguna razón, git log no está poniendo un corte final al final. Tuve que agregar un salto de línea e ignorar las líneas en blanco. 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
77
Esto solo encuentra confirmaciones en la rama actual a menos que pase --allcomo argumento adicional. (Encontrar todas las confirmaciones en todo el repositorio es importante en casos como eliminar un archivo grande del historial del repositorio ).
peterflynn
1
Consejo: pase el indicador -g al script de shell (después de la ID del objeto) para examinar el registro.
Bram Schoenmakers
24

Lamentablemente, los guiones fueron un poco lentos para mí, así que tuve que optimizar un poco. Afortunadamente, no solo tenía el hash sino también la ruta de un archivo.

git log --all --pretty=format:%H -- <path> | xargs -n1 -I% sh -c "git ls-tree % -- <path> | grep -q <hash> && echo %"
aragaer
fuente
1
Excelente respuesta porque es muy simple. Simplemente haciendo la suposición razonable de que el camino es conocido. Sin embargo, uno debe saber que devuelve la confirmación donde se cambió la ruta al hash dado.
Unapiedra
1
Si uno quiere la confirmación más reciente que contenga el <hash>en el dado <path>, entonces eliminar el <path>argumento de la git logvoluntad funcionará. El primer resultado devuelto es el commit deseado.
Unapiedra
10

Dado el hash de un blob, ¿hay alguna manera de obtener una lista de confirmaciones que tengan este blob en su árbol?

Con Git 2.16 (Q1 2018), git describesería una buena solución, ya que se le enseñó a excavar árboles más profundamente para encontrar uno <commit-ish>:<path>que se refiera a un objeto blob dado.

Consulte commit 644eb60 , commit 4dbc59a , commit cdaed0c , commit c87b653 , commit ce5b6f9 (16 de noviembre de 2017) y commit 91904f5 , commit 2deda00 (02 de noviembre de 2017) por Stefan Beller ( stefanbeller) .
(Fusionada por Junio ​​C Hamano - gitster- en commit 556de1a , 28 dic 2017)

builtin/describe.c: describe una gota

A veces, los usuarios reciben un hash de un objeto y desean identificarlo más a fondo (por ejemplo: usar verify-packpara encontrar los blobs más grandes, pero ¿qué son estos? O esta pregunta SO " ¿Qué commit tiene este blob? ")

Cuando describimos commits, tratamos de anclarlos a etiquetas o referencias, ya que estos están conceptualmente en un nivel más alto que el commit. Y si no hay una referencia o etiqueta que coincida exactamente, no tenemos suerte.
Entonces, empleamos una heurística para inventar un nombre para el commit. Estos nombres son ambiguos, puede haber diferentes etiquetas o referencias a las que anclar, y puede haber una ruta diferente en el DAG para viajar para llegar al compromiso con precisión.

Al describir un blob, también queremos describir el blob desde una capa superior, que es una tupla de (commit, deep/path)que los objetos del árbol involucrados son bastante poco interesantes.
El mismo blob puede ser referenciado por múltiples commits, entonces, ¿cómo decidimos qué commit usar?

Este parche implementa un enfoque bastante ingenuo al respecto: dado que no hay punteros hacia atrás desde los blobs hasta los commits en los que se produce el blob, comenzaremos a caminar a partir de cualquier consejo disponible, enumerando los blobs en el orden del commit y una vez que encontramos el blob, tomaremos la primera confirmación que enumeró el blob .

Por ejemplo:

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

nos dice que Makefiletal como estaba v0.99se introdujo en commit 7672db2 .

La caminata se realiza en orden inverso para mostrar la introducción de una gota en lugar de su última aparición.

Eso significa que la git describepágina del manual se suma a los propósitos de este comando:

En lugar de simplemente describir una confirmación utilizando la etiqueta más reciente a la que se puede acceder, en git describerealidad le dará a un objeto un nombre legible para humanos basado en una referencia disponible cuando se use como git describe <blob>.

Si el objeto dado se refiere a un blob, se describirá como <commit-ish>:<path>tal, que el blob se puede encontrar <path>en <commit-ish>, que describe la primera confirmación en la que este blob se produce en una caminata de revisión inversa desde HEAD.

Pero:

LOCO

Los objetos de árbol, así como los objetos de etiqueta que no apuntan a confirmaciones, no se pueden describir .
Al describir los blobs, las etiquetas livianas que apuntan a los blobs se ignoran, pero el blob todavía se describe a <committ-ish>:<path>pesar de que la etiqueta liviana es favorable.

VonC
fuente
1
Es bueno usarlo junto con 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, lo que te devuelve los 20 blobs más grandes. Luego puede pasar ID de blob de la salida anterior a git describe. Funcionó como un encanto! ¡Gracias!
Alexander Pogrebnyak
7

Pensé que esto sería algo generalmente útil, así que escribí un pequeño script en perl para hacerlo:

#!/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);
}

Pondré esto en Github cuando llegue a casa esta noche.

Actualización: Parece que alguien ya hizo esto . Ese usa la misma idea general pero los detalles son diferentes y la implementación es mucho más corta. ¡No sé cuál sería más rápido, pero el rendimiento probablemente no sea una preocupación aquí!

Actualización 2: por lo que vale, mi implementación es mucho más rápida, especialmente para un gran repositorio. Eso git ls-tree -rrealmente duele.

Actualización 3: debo tener en cuenta que mis comentarios de rendimiento anteriores se aplican a la implementación que vinculé anteriormente en la primera actualización. La implementación de Aristóteles es comparable a la mía. Más detalles en los comentarios para aquellos que tienen curiosidad.

Greg Hewgill
fuente
Hmm, ¿cómo puede ser que mucho más rápido? Estás caminando por el árbol de todos modos, ¿no? ¿Qué trabajo hace git-ls-tree que evitas? (NB .: grep se rescatará en el primer partido, SIGPIPE'ing the git-ls-tree.) Cuando lo intenté, tuve que hacer Ctrl-C en tu script después de 30 segundos; la mía se hizo en 4.
Aristóteles Pagaltzis
1
Mi script almacena en caché los resultados de subárboles en el hash% trees, por lo que no tiene que seguir buscando subárboles que no han cambiado.
Greg Hewgill
En realidad, estaba probando la implementación que encontré en github con la que me vinculé. El tuyo es más rápido en algunos casos, pero depende en gran medida de si el archivo que estás buscando está al principio o al final de la lista ls-tree. Mi repositorio tiene 9574 archivos en este momento.
Greg Hewgill
También se me ocurre que algunos historiales de proyectos no lineales pueden hacer que mi script haga mucho más trabajo del que necesita (esto se puede solucionar). Esta podría ser la razón por la que tomó tanto tiempo correr por usted. Mi repositorio es un espejo git-svn de un repositorio de Subversion, por lo que es muy lineal.
Greg Hewgill
En lugar de analizar la cat-file para obtener el árbol, simplemente hagagit rev-parse $commit^{}
jthill
6

Si bien la pregunta original no lo solicita, creo que es útil verificar también el área de preparación para ver si se hace referencia a un blob. Modifiqué el script bash original para hacer esto y encontré lo que hacía referencia a un blob corrupto en mi repositorio:

#!/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
fuente
3
Solo me gustaría dar crédito cuando es debido: gracias a la corrupción de RAM por causarme un BSOD y obligarme a reparar a mano mi repositorio git.
Mario
4

Entonces ... necesitaba encontrar todos los archivos por encima de un límite dado en un repositorio de más de 8GB de tamaño, con más de 108,000 revisiones. Adapté el guión perl de Aristóteles junto con un guión rubí que escribí para llegar a esta solución completa.

Primero, git gchaga esto para asegurarse de que todos los objetos estén en paquetes de archivos, no escaneamos objetos que no estén en archivos de paquetes.

A continuación, ejecute este script para localizar todos los blobs en CUTOFF_SIZE bytes. Capture la salida a un archivo como "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

A continuación, edite el archivo para eliminar cualquier blob que no espere y los bits INPUT_THREAD en la parte superior. una vez que tenga solo líneas para los sha1 que desea encontrar, ejecute el siguiente script de esta manera:

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

Donde el git-find-blobguión está abajo.

#!/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 salida se verá así:

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

Y así. Se enumerará cada confirmación que contenga un archivo grande en su árbol. Si grepsale de las líneas que comienzan con una pestaña, y uniqasí, tendrá una lista de todas las rutas que puede filtrar-ramificar para eliminar, o puede hacer algo más complicado.

Permítanme reiterar: este proceso se ejecutó con éxito, en un repositorio de 10GB con 108,000 confirmaciones. Tomó mucho más tiempo de lo que predije cuando se ejecutó en una gran cantidad de blobs, sin embargo, durante 10 horas, tendré que ver si el bit de memorización está funcionando ...

cmyers
fuente
1
Como respuesta de Aristóteles anteriormente, esto sólo encuentra compromete en la rama actual a menos que pase argumentos adicionales: -- --all. (Encontrar todas las confirmaciones en todo el repositorio es importante en casos como eliminar completamente un archivo grande del historial del repositorio ).
peterflynn
4

Además de git describeeso, lo menciono en mi respuesta anterior , git logy git diffahora también se beneficia de la --find-object=<object-id>opción " " para limitar los hallazgos a los cambios que involucran el objeto nombrado.
Eso está en Git 2.16.x / 2.17 (Q1 2018)

Ver commit 4d8c51a , commit 5e50525 , commit 15af58c , commit cf63051 , commit c1ddc46 , commit 929ed70 (04 de enero de 2018) por Stefan Beller ( stefanbeller) .
(Fusionada por Junio ​​C Hamano - gitster- en commit c0d75f0 , 23 ene 2018)

diffcore: agrega una opción de pico para encontrar un blob específico

A veces, los usuarios reciben un hash de un objeto y desean identificarlo más a fondo (por ejemplo: Use allow-pack para encontrar los blobs más grandes, pero ¿qué son estos? O esta pregunta de Stack Overflow " ¿Qué commit tiene este blob? ")

Uno podría verse tentado a extenderse git-describepara trabajar también con blobs, de modo que se git describe <blob-id>dé una descripción como ':'
Esto fue implementado aquí ; Como se ve por la gran cantidad de respuestas (> 110), resulta que es difícil acertar.
La parte difícil de acertar es elegir el 'commit-ish' correcto, ya que podría ser el commit que (re) introdujo el blob o el blob que eliminó el blob; la gota podría existir en diferentes ramas.

Junio ​​insinuó un enfoque diferente para resolver este problema, que implementa este parche.
Enseñe a la diffmaquinaria otra bandera para restringir la información a lo que se muestra.
Por ejemplo:

$ ./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:"

Observamos que el Makefilecomo enviado con 2.0apareció en v1.9.2-471-g47fbfded53y en v2.0.0-rc1-5-gb2feb6430b.
La razón por la cual estos commits ocurren antes de v2.0.0 son fusiones malignas que no se encuentran utilizando este nuevo mecanismo.

VonC
fuente