Qual confirmação possui esse blob?

149

Dado o hash de um blob, existe uma maneira de obter uma lista de confirmações que possuem esse blob em sua árvore?

Somente leitura
fonte
2
"Hash de um blob" é aquele retornado por git hash-objector sha1("blob " + filesize + "\0" + data), e não simplesmente o sha1sum do conteúdo do blob.
Ivan Hamilton
1
Originalmente, pensei que essa pergunta correspondesse à minha, mas parece que não. Eu quero saber a uma cometem que primeiro introduziu este blob para o repositório.
Jesse Glick
Se você conhece o caminho do arquivo, pode usar git log --follow filepath(e usar isso para acelerar a solução de Aristóteles, se desejar).
Zaz
ProTip ™: coloque um dos scripts mais populares ~/.bine nomeie-o git-find-object. Você pode usá-lo com git find-object.
Zaz
1
Nota: Com o Git 2.16 (primeiro trimestre de 2018), você pode considerar simplesmente git describe <hash>: Veja minha resposta abaixo .
VonC

Respostas:

107

Os dois scripts a seguir tomam o SHA1 do blob como o primeiro argumento e, depois dele, opcionalmente, quaisquer argumentos que git logentenderão. Por exemplo, --allpara pesquisar em todos os ramos, em vez de apenas no atual, ou -gno reflog, ou qualquer outra coisa que você queira.

Aqui está como um script de shell - curto e doce, mas 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

E uma versão otimizada no Perl, ainda bastante curta, mas muito mais 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
fonte
8
Para sua informação, você deve usar o SHA completo do blob. Um prefixo, mesmo que único, não funcionará. Para obter o SHA completo a partir de um prefixo, você pode usargit rev-parse --verify $theprefix
John Douthat
1
Obrigado @JohnDouthat por este comentário. Veja como incorporar isso no script acima (desculpe o inlining nos comentários): 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
Pode haver um erro no script do shell superior. O loop while só é executado se houver mais linhas para ler e, por qualquer motivo, o git log não está colocando um crlf final no final. Eu tive que adicionar um avanço de linha e ignorar linhas em branco. 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
Isso encontra apenas confirmações na ramificação atual, a menos que você passe --allcomo um argumento adicional. (Encontrar todas as confirmações em todo o repo é importante em casos como excluir um arquivo grande do histórico do repo ).
Peterflynn
1
Dica: passe o sinalizador -g para o script do shell (após o ID do objeto) para examinar o reflog.
Bram Schoenmakers
24

Infelizmente, os scripts foram um pouco lentos para mim, então tive que otimizar um pouco. Felizmente, eu tinha não apenas o hash, mas também o caminho de um arquivo.

git log --all --pretty=format:%H -- <path> | xargs -n1 -I% sh -c "git ls-tree % -- <path> | grep -q <hash> && echo %"
aragaer
fonte
1
Excelente resposta, porque é muito simples. Apenas fazendo uma suposição razoável de que o caminho é conhecido. No entanto, deve-se saber que ele retorna a confirmação onde o caminho foi alterado para o hash especificado.
Unapiedra
1
Se alguém quiser que o commit mais recente contenha o <hash>dado <path>, remover o <path>argumento do git logirá funcionar. O primeiro resultado retornado é o commit desejado.
Unapiedra
10

Dado o hash de um blob, existe uma maneira de obter uma lista de confirmações que possuem esse blob em sua árvore?

Com o Git 2.16 (primeiro trimestre de 2018), git describeseria uma boa solução, pois foi ensinado a cavar árvores mais profundamente para encontrar um <commit-ish>:<path>que se refira a um determinado objeto de blob.

Consulte commit 644eb60 , commit 4dbc59a , commit cdaed0c , commit c87b653 , commit ce5b6f9 (16 de novembro de 2017) e commit 91904f5 , commit 2deda00 (02 de novembro de 2017) por Stefan Beller ( stefanbeller) .
(Mesclado por Junio ​​C Hamano - gitster- na commit 556de1a , 28 de dezembro de 2017)

builtin/describe.c: descreva um blob

Às vezes os usuários recebem um hash de um objeto e eles querem identificá-lo ainda mais (ex .: Use verify-packpara encontrar as maiores bolhas, mas o que são estes? Ou essa mesma SO pergunta " Qual cometer tem esse blob? ")

Ao descrever confirmações, tentamos ancorá-las em tags ou refs, pois elas são conceitualmente em um nível superior ao commit. E se não houver ref ou tag que corresponda exatamente, não teremos sorte.
Portanto, empregamos uma heurística para criar um nome para o commit. Esses nomes são ambíguos, pode haver diferentes tags ou referências a serem ancoradas e pode haver um caminho diferente no DAG a ser percorrido para chegar ao commit com precisão.

Ao descrever um blob, também queremos descrevê-lo a partir de uma camada superior, que é uma tupla, (commit, deep/path)pois os objetos da árvore envolvidos são bastante desinteressantes.
O mesmo blob pode ser referenciado por várias confirmações, então como decidimos qual confirmação usar?

Este patch implementa uma abordagem bastante ingênua sobre isso: como não há ponteiros de retorno de blobs para commits nos quais o blob ocorre, começaremos a seguir as dicas disponíveis, listando os blobs na ordem do commit e assim que encontrarmos o blob, tomaremos o primeiro commit que listou o blob .

Por exemplo:

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

diz-nos Makefilecomo v0.99foi introduzido no commit 7672db2 .

A caminhada é realizada na ordem inversa para mostrar a introdução de um blob em vez de sua última ocorrência.

Isso significa que a git describepágina de manual é adicionada aos propósitos deste comando:

Em vez de simplesmente descrever um commit usando a tag mais recente alcançável, git describeo objeto passará a ter um nome legível por humanos com base em uma referência disponível quando usado como git describe <blob>.

Se o objeto especificado se referir a um blob, ele será descrito como <commit-ish>:<path>, de modo que o blob possa ser encontrado <path>em <commit-ish>, que descreve a primeira confirmação na qual esse blob ocorre em uma revisão reversa a partir de HEAD.

Mas:

INSETOS

Objetos de árvore e objetos de marca que não apontam para confirmações não podem ser descritos .
Ao descrever blobs, as tags leves que apontam para blobs são ignoradas, mas o blob ainda é descrito, <committ-ish>:<path>apesar de a tag leve ser favorável.

VonC
fonte
1
É bom usar em conjunto com 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, o que gera um dos 20 maiores blobs. Em seguida, você pode passar o ID do blob da saída acima para git describe. Trabalhou como um encanto! Obrigado!
Alexander Pogrebnyak
7

Eu pensei que isso seria uma coisa geralmente útil, então eu escrevi um pequeno script perl para fazer isso:

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

Vou colocar isso no github quando chegar em casa hoje à noite.

Atualização: Parece que alguém já fez isso . Essa usa a mesma ideia geral, mas os detalhes são diferentes e a implementação é muito mais curta. Não sei o que seria mais rápido, mas o desempenho provavelmente não é uma preocupação aqui!

Atualização 2: Para o que vale a pena, minha implementação é de ordens de magnitude mais rápidas, especialmente para um grande repositório. Isso git ls-tree -rrealmente dói.

Atualização 3: devo observar que meus comentários de desempenho acima se aplicam à implementação que eu vinculei acima na primeira atualização. A implementação de Aristóteles tem um desempenho comparável ao meu. Mais detalhes nos comentários para quem está curioso.

Greg Hewgill
fonte
Hmm, como pode ser que muito mais rápido? Você está andando na árvore de qualquer maneira, não é? Que trabalho o git-ls-tree faz para evitar? (Nota: grep será liberado na primeira partida, SIGPIPE na árvore git-ls.) Quando tentei, tive que pressionar Ctrl-C no seu script após 30 segundos; o meu foi feito em 4.
Aristóteles Pagaltzis 22/10/08
1
Meu script armazena em cache os resultados de subárvores no hash% trees, para que ele não precise continuar pesquisando subárvores que não foram alteradas.
Greg Hewgill 22/10/08
Na verdade, eu estava tentando a implementação que encontrei no github ao qual me vinculei. O seu é mais rápido em alguns casos, mas depende muito se o arquivo que você está procurando está no início ou no final da lista ls-tree. Meu repositório possui 9574 arquivos no momento.
Greg Hewgill 22/10/08
Também me ocorre que alguns históricos não lineares do projeto podem fazer com que meu script faça muito mais trabalho do que o necessário (isso pode ser corrigido). Talvez seja por isso que demorou muito tempo para você. Meu repositório é um espelho git-svn de um repositório Subversion, portanto é bem linear.
Greg Hewgill 22/10/08
Em vez de analisar cat-file para obter a árvore apenas fazergit rev-parse $commit^{}
jthill
6

Embora a pergunta original não peça, acho útil verificar também a área de preparação para ver se um blob é referenciado. Modifiquei o script bash original para fazer isso e encontrei o que estava fazendo referência a um blob corrompido no meu repositório:

#!/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
fonte
3
Gostaria apenas de dar crédito onde é devido: obrigado corrupção da RAM por me causar um BSOD e me forçar a reparar manualmente meu repositório Git.
Mario
4

Então ... eu precisava encontrar todos os arquivos acima de um determinado limite em um repositório com mais de 8 GB de tamanho, com mais de 108.000 revisões. Adaptei o script perl de Aristóteles junto com um script rubi que escrevi para alcançar essa solução completa.

Primeiro, git gc- faça isso para garantir que todos os objetos estejam em arquivos de pacote - não examinamos objetos que não estão em arquivos de pacote.

Próximo Execute este script para localizar todos os blobs sobre os bytes CUTOFF_SIZE. Capture a saída para um arquivo 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

Em seguida, edite o arquivo para remover todos os blobs que você não espera e os bits INPUT_THREAD na parte superior. Depois de ter apenas linhas para os sha1s que deseja encontrar, execute o seguinte script como este:

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

Onde o git-find-blobscript está abaixo.

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

A saída será assim:

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

E assim por diante. Todo commit que contém um arquivo grande em sua árvore será listado. se você grepdefinir as linhas que começam com uma guia e uniq, terá uma lista de todos os caminhos que você pode filtrar para remover ou se pode fazer algo mais complicado.

Permitam-me reiterar: esse processo foi executado com êxito, em um repo de 10 GB com 108.000 confirmações. Demorou muito mais tempo do que eu previa ao executar um grande número de blobs, porém, em 10 horas, terei que ver se o bit de memorização está funcionando ...

cmyers
fonte
1
Como resposta de Aristóteles acima, isso só encontra commits no ramo atual a menos que você passar argumentos adicionais: -- --all. (Encontrar todas as confirmações em todo o repo é importante em casos como excluir completamente um arquivo grande do histórico do repo ).
Peterflynn 11/07/2013
4

Além de git describeque eu menciono na minha resposta anterior , git loge git diffagora beneficia assim do " --find-object=<object-id>" opção para limitar os resultados a mudanças que envolvem o objeto nomeado.
Isso está no Git 2.16.x / 2.17 (primeiro trimestre de 2018)

Consulte commit 4d8c51a , commit 5e50525 , commit 15af58c , commit cf63051 , commit c1ddc46 , commit 929ed70 (04 jan 2018) por Stefan Beller ( stefanbeller) .
(Incorporado por Junio ​​C Hamano - gitster- in commit c0d75f0 , 23 de janeiro de 2018)

diffcore: adicione uma opção de picareta para encontrar um blob específico

Às vezes, os usuários recebem um hash de um objeto e desejam identificá-lo ainda mais (por exemplo: use o pacote de verificação para encontrar os maiores blobs, mas o que são esses? Ou a pergunta Stack Overflow " Qual confirmação possui esse blob? ")

Pode-se tentar estender-se git-describepara também trabalhar com blobs, de modo que git describe <blob-id>dê uma descrição como ':'.
Isso foi implementado aqui ; como visto pelo grande número de respostas (> 110), verifica-se que é difícil acertar.
A parte mais difícil de acertar é escolher o 'commit-ish' correto, pois pode ser o commit que (re) introduziu o blob ou o blob que removeu o blob; o blob pode existir em diferentes ramos.

Junio ​​sugeriu uma abordagem diferente para resolver esse problema, implementado por este patch.
Ensine ao diffmecanismo outra bandeira para restringir as informações ao que é mostrado.
Por exemplo:

$ ./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 o Makefileitem enviado 2.0foi exibido dentro v1.9.2-471-g47fbfded53e dentro v2.0.0-rc1-5-gb2feb6430b.
A razão pela qual essas confirmações ocorrem antes da v2.0.0 são mesclagens incorretas que não são encontradas usando esse novo mecanismo.

VonC
fonte