Quale commit ha questo BLOB?


150

Dato l'hash di un BLOB, c'è un modo per ottenere un elenco di commit che hanno questo BLOB nel loro albero?


2
"Hash of a blob" è quello restituito da git hash-objecto sha1("blob " + filesize + "\0" + data), e non semplicemente, dal contenuto del BLOB.
Ivan Hamilton,

1
Inizialmente pensavo che questa domanda corrispondesse alla mia domanda, ma sembra che non lo sia. Voglio conoscere l' unico commit che per primo ha introdotto questo BLOB nel repository.
Jesse Glick,

Se conosci il percorso del file, puoi usare git log --follow filepath(e usarlo per accelerare la soluzione di Aristotele, se vuoi).
Zaz,

ProTip ™: inserisci uno degli script belew ~/.bine chiamalo git-find-object. Puoi quindi usarlo con git find-object.
Zaz,

1
Nota: con Git 2.16 (Q1 2018), potresti considerare semplicemente git describe <hash>: vedi la mia risposta di seguito .
VonC

Risposte:


107

Entrambi i seguenti script prendono SHA1 del BLOB come primo argomento e, successivamente, facoltativamente, qualsiasi argomento che git logcapirà. Ad esempio --allper cercare in tutti i rami anziché solo quello corrente, o -gper cercare nel reflog, o qualsiasi altra cosa tu voglia.

Qui è come uno script di shell - breve e dolce, ma 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 una versione ottimizzata in Perl, ancora piuttosto breve ma molto più veloce:

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

9
Cordiali saluti, è necessario utilizzare l'intero SHA del BLOB. Un prefisso, anche se univoco, non funzionerà. Per ottenere lo SHA completo da un prefisso, è possibile utilizzaregit rev-parse --verify $theprefix
John Douthat il

1
Grazie @JohnDouthat per questo commento. Ecco come incorporarlo nella sceneggiatura sopra (scusate per l'interpretazione nei commenti): 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,

Potrebbe esserci un bug nello script della shell superiore. Il ciclo while si esegue solo se ci sono più righe da leggere, e per qualsiasi motivo git log non sta mettendo alla fine un crlf finale. Ho dovuto aggiungere un avanzamento riga e ignorare le righe vuote. 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
Questo trova solo commit sul ramo corrente a meno che non passi --allcome argomento aggiuntivo. (Trovare tutti i commit a livello di repository è importante in casi come l' eliminazione di un file di grandi dimensioni dalla cronologia dei repository ).
peterflynn,

1
Suggerimento: passare il flag -g allo script della shell (dopo l'ID oggetto) per esaminare il reflog.
Bram Schoenmakers,

24

Sfortunatamente gli script sono stati un po 'lenti per me, quindi ho dovuto ottimizzare un po'. Fortunatamente non avevo solo l'hash ma anche il percorso di un file.

git log --all --pretty=format:%H -- <path> | xargs -n1 -I% sh -c "git ls-tree % -- <path> | grep -q <hash> && echo %"

1
Ottima risposta perché è così semplice. Solo facendo il ragionevole presupposto che il percorso sia noto. Tuttavia, si dovrebbe sapere che restituisce il commit in cui il percorso è stato modificato nell'hash dato.
Unapiedra,

1
Se si desidera il commit più recente contenente il <hash>dato <path>, quindi rimuovere l' <path>argomento dalla git logvolontà funzionerà. Il primo risultato restituito è il commit desiderato.
Unapiedra,

10

Dato l'hash di un BLOB, c'è un modo per ottenere un elenco di commit che hanno questo BLOB nel loro albero?

Con Git 2.16 (Q1 2018), git describesarebbe una buona soluzione, poiché è stato insegnato a scavare alberi più in profondità per trovare un oggetto <commit-ish>:<path>che si riferisca a un determinato oggetto BLOB.

Vedi commit 644eb60 , commit 4dbc59a , commit cdaed0c , commit c87b653 , commit ce5b6f9 (16 nov 2017) e commit 91904f5 , commit 2deda00 (02 nov 2017) di Stefan Beller ( stefanbeller) .
(Unita da Junio ​​C Hamano - gitster- in commit 556de1a , 28 dic 2017)

builtin/describe.c: descrivi un blob

A volte agli utenti viene assegnato un hash di un oggetto e vogliono identificarlo ulteriormente (es .: utilizzare verify-packper trovare i BLOB più grandi, ma quali sono? O questa domanda molto SO " Quale commit ha questo BLOB? ")

Nel descrivere i commit, proviamo ad ancorarli a tag o ref, poiché questi sono concettualmente ad un livello superiore rispetto al commit. E se non ci sono ref o tag che corrispondono esattamente, siamo sfortunati.
Quindi impieghiamo un euristico per inventare un nome per il commit. Questi nomi sono ambigui, potrebbero esserci diversi tag o riferimenti a cui ancorare e potrebbe esserci un percorso diverso nel DAG per viaggiare per arrivare al commit con precisione.

Quando descriviamo un blob, vogliamo descrivere anche il blob da un livello superiore, che è una tupla di (commit, deep/path)come gli oggetti dell'albero coinvolti sono piuttosto poco interessanti.
Lo stesso BLOB può essere referenziato da più commit, quindi come decidiamo quale commit utilizzare?

Questa patch implementa un approccio piuttosto ingenuo su questo: poiché non ci sono puntatori posteriori dai BLOB ai commit in cui si verifica il BLOB, inizieremo a camminare da tutti i suggerimenti disponibili, elencando i BLOB in ordine di commit e una volta trovato il BLOB, prenderemo il primo commit in cui è elencato il BLOB .

Per esempio:

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

ci dice Makefilecome era v0.99stato introdotto in commit 7672db2 .

La camminata viene eseguita in ordine inverso per mostrare l'introduzione di un blob anziché la sua ultima occorrenza.

Ciò significa che la git describepagina man si aggiunge agli scopi di questo comando:

Invece di descrivere semplicemente un commit usando il tag più recente raggiungibile da esso, git describein realtà darà a un oggetto un nome leggibile dall'uomo basato su un riferimento disponibile quando usato come git describe <blob>.

Se l'oggetto specificato si riferisce a un BLOB, verrà descritto come <commit-ish>:<path>, in modo tale che il BLOB possa essere trovato <path>in <commit-ish>, che descrive il primo commit in cui questo BLOB si verifica in una passeggiata di revisione inversa da HEAD.

Ma:

BUGS

Non è possibile descrivere gli oggetti dell'albero e gli oggetti tag che non puntano ai commit .
Quando si descrivono i BLOB, i tag leggeri che puntano ai BLOB vengono ignorati, ma il BLOB viene comunque descritto come <committ-ish>:<path>nonostante il tag leggero sia favorevole.


1
Buono da usare in combinazione 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, che ti restituisce uno dei 20 più grandi BLOB più grandi. Quindi è possibile passare l'ID BLOB dall'output sopra a git describe. Ha funzionato come un incanto! Grazie!
Alexander Pogrebnyak,

7

Ho pensato che sarebbe stata una cosa generalmente utile da avere, quindi ho scritto un piccolo script perl per farlo:

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

Lo metto su github quando torno a casa stasera.

Aggiornamento: sembra che qualcuno l'abbia già fatto . Quello usa la stessa idea generale ma i dettagli sono diversi e l'implementazione è molto più breve. Non so quale sarebbe più veloce ma le prestazioni probabilmente non sono un problema qui!

Aggiornamento 2: per quello che vale, la mia implementazione è ordini di grandezza più veloci, soprattutto per un grande repository. Questo git ls-tree -rfa davvero male.

Aggiornamento 3: dovrei notare che i miei commenti sulle prestazioni di cui sopra si applicano all'implementazione che ho collegato sopra nel primo aggiornamento. L'implementazione di Aristotele si comporta in modo simile al mio. Maggiori dettagli nei commenti per coloro che sono curiosi.


Hmm, come può essere che molto più veloce? Stai camminando sull'albero comunque, vero? Che lavoro fa git-ls-tree che eviti? (NB: grep salterà al primo incontro, SIGPIPE sull'albero git-ls.) Quando l'ho provato, ho dovuto Ctrl-C il tuo script dopo 30 secondi; la mia fu fatta in 4.
Aristotele Pagaltzis,

1
Il mio script memorizza nella cache i risultati dei sottotitoli nell'hash% trees, quindi non deve continuare a cercare sottotitoli che non sono cambiati.
Greg Hewgill,

In realtà, stavo provando l'implementazione che ho trovato su Github che ho collegato. Il tuo è più veloce in alcuni casi, ma dipende molto dal fatto che il file che stai cercando sia all'inizio o alla fine dell'elenco ls-tree. Il mio repository ha 9574 file in questo momento.
Greg Hewgill,

Mi viene anche in mente che alcune storie di progetti non lineari potrebbero far sì che la mia sceneggiatura faccia molto più lavoro del necessario (questo può essere risolto). Questo potrebbe essere il motivo per cui ci è voluto molto tempo per correre per te. Il mio repository è un mirror git-svn di un repository Subversion, quindi è piacevolmente lineare.
Greg Hewgill,

Invece di analizzare il file cat per far fare l'alberogit rev-parse $commit^{}
jthill

6

Mentre la domanda originale non lo richiede, penso che sia utile controllare anche l'area di gestione temporanea per vedere se viene fatto riferimento a un BLOB. Ho modificato lo script bash originale per fare questo e ho trovato ciò che faceva riferimento a un BLOB corrotto nel mio repository:

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

3
Vorrei solo dare credito dove è dovuto: grazie alla corruzione RAM per avermi causato un BSOD e avermi costretto a riparare a mano il mio repository git.
Mario

4

Quindi ... dovevo trovare tutti i file oltre un determinato limite in un repository di dimensioni superiori a 8 GB, con oltre 108.000 revisioni. Ho adattato lo script perl di Aristotele insieme a uno script ruby ​​che ho scritto per raggiungere questa soluzione completa.

Innanzitutto, git gcfai questo per assicurarti che tutti gli oggetti siano nei file pack, non eseguiamo la scansione di oggetti non nei file pack.

Quindi eseguire questo script per individuare tutti i BLOB su byte CUTOFF_SIZE. Cattura l'output in un file come "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

Successivamente, modifica il file per rimuovere tutti i BLOB che non aspetti e i bit INPUT_THREAD in alto. una volta che hai solo le righe per gli sha1 che vuoi trovare, esegui il seguente script in questo modo:

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

Dove lo git-find-blobscript è sotto.

#!/usr/bin/perl

# taken from: http://stackoverflow.com/questions/223678/which-commit-has-this-blob
# and modified by Carl Myers <cmyers@cmyers.org> 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;
}

L'output sarà simile al seguente:

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

E così via. Ogni commit che contiene un file di grandi dimensioni nella sua struttura verrà elencato. se si grepescono le linee che iniziano con una scheda e uniqche, si avrà un elenco di tutti i percorsi che è possibile filtrare-ramo per rimuovere o si può fare qualcosa di più complicato.

Permettetemi di ribadirlo: questo processo è andato a buon fine, su un repository da 10 GB con 108.000 commit. Ci è voluto molto più tempo di quanto mi aspettassi quando sono in esecuzione su un gran numero di BLOB anche se, oltre 10 ore, dovrò vedere se il bit di memorizzazione funziona ...


1
Come risposta di Aristotele sopra, questo trova solo commit sul ramo corrente a meno che non si passa argomenti aggiuntivi: -- --all. (Trovare tutti i commit a livello di repository è importante in casi come l' eliminazione completa di un file di grandi dimensioni dalla cronologia dei repository ).
peterflynn,

4

Inoltre git describe, menziono nella mia precedente risposta , git loge git diffora beneficia anche --find-object=<object-id>dell'opzione " " per limitare i risultati alle modifiche che coinvolgono l'oggetto nominato.
Cioè in Git 2.16.x / 2.17 (Q1 2018)

Vedi commit 4d8c51a , commit 5e50525 , commit 15af58c , commit cf63051 , commit c1ddc46 , commit 929ed70 (04 gennaio 2018) di Stefan Beller ( stefanbeller) .
(Unita da Junio ​​C Hamano - gitster- in commit c0d75f0 , 23 gen 2018)

diffcore: aggiungi un'opzione piccone per trovare un BLOB specifico

A volte agli utenti viene assegnato un hash di un oggetto e vogliono identificarlo ulteriormente (es .: utilizzare il pacchetto di verifica per trovare i BLOB più grandi, ma quali sono? O questa domanda di overflow dello stack " Quale commit ha questo BLOB? ")

Si potrebbe essere tentati di estenderlo git-describeper lavorare anche con i BLOB, in modo tale git describe <blob-id>da fornire una descrizione come ':'.
Questo è stato implementato qui ; come si evince dal puro numero di risposte (> 110), risulta che questo è difficile da ottenere.
La parte difficile da correggere è scegliere il "commit-ish" corretto in quanto potrebbe essere il commit che (ri) ha introdotto il BLOB o BLOB che ha rimosso il BLOB; il blob potrebbe esistere in diversi rami.

Junio ​​ha suggerito un diverso approccio per risolvere questo problema, implementato da questa patch.
Insegnare al diffmacchinario un'altra bandiera per limitare le informazioni a ciò che viene mostrato.
Per esempio:

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

osserviamo che il Makefilecome spedito con 2.0è apparso dentro v1.9.2-471-g47fbfded53e dentro v2.0.0-rc1-5-gb2feb6430b.
Il motivo per cui entrambi questi commit si verificano prima della v2.0.0 sono le fusioni malvagie che non si trovano usando questo nuovo meccanismo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.