Какой коммит имеет этот BLOB-объект?

Учитывая хэш блоба, есть ли способ получить список коммитов, у которых этот блоб находится в их дереве?

9 ответов

Решение

Оба следующих сценария принимают SHA1 большого двоичного объекта в качестве первого аргумента, а после него, необязательно, любые аргументы, которые git log пойму. Например --all искать во всех ветках, а не только в текущей, или -g искать в рефлоге, или как вам еще нравится.

Вот он как скрипт оболочки - короткий и приятный, но медленный:

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

И оптимизированная версия на Perl, все еще довольно короткая, но намного быстрее:

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

Для людей наиболее полезной командой, вероятно, будет

      git whatchanged --all --find-object=<blob hash>

Это показывает через --all ветки, любые коммиты, которые добавили или удалили файл с этим хешем вместе с тем, каким был путь.

      git$ git whatchanged --all --find-object=b3bb59f06644
commit 8ef93124645f89c45c9ec3edd3b268b38154061a 
⋮
diff: do not show submodule with untracked files as "-dirty"
⋮
:100644 100644 b3bb59f06644 8f6227c993a5 M      submodule.c

commit 7091499bc0a9bccd81a1c864de7b5f87a366480e 
⋮
Revert "submodules: fix of regression on fetching of non-init subsub-repo"
⋮
:100644 100644 eef5204e641e b3bb59f06644 M  submodule.c

Обратите внимание, что git whatchanged уже включает хэши больших двоичных объектов до и после в свои выходные строки.

К сожалению, скрипты были немного медленными для меня, поэтому мне пришлось немного оптимизировать. К счастью, у меня был не только хеш, но и путь к файлу.

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

В дополнение к git describe , что я упоминаю в моем предыдущем ответе, git log а также git diff теперь также выигрывает от --find-object=<object-id> "опция, позволяющая ограничить выводы изменениями, затрагивающими названный объект.
Это в Git 2.16.x/2.17 (Q1 2018)

См. Коммит 4d8c51a, коммит 5e50525, коммит 15af58c, коммит cf63051, коммит c1ddc46, коммит 929ed70 (04 января 2018 г.). Автор Stefan Beller ( stefanbeller )
(Объединено Юнио С Хамано - gitster - в коммите c0d75f0, 23 января 2018 г.)

diffcore: добавить опцию кирки, чтобы найти конкретный блоб

Иногда пользователям дается хэш объекта, и они хотят идентифицировать его дальше (например: используйте verify-pack, чтобы найти самые большие BLOB-объекты, но что это такое? Или этот вопрос переполнения стека " Какой коммит имеет этот BLOB-объект? ")

Можно поддаться искушению продлить git-describe также работать с каплями, такими, что git describe <blob-id> дает описание как ":".
Это было реализовано здесь; как видно по большому количеству ответов (>110), оказывается, что это сложно сделать правильно.
Трудная часть для правильного выбора - выбрать правильный 'commit-ish', поскольку это может быть коммит, который (повторно) представил BLOB-объект или BLOB-объект, который удалил BLOB-объект; капля может существовать в разных ветках.

Junio ​​намекнул на другой подход к решению этой проблемы, который реализует этот патч.
Научить diff механизм другого флага для ограничения информации тем, что показано.
Например:

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

мы видим, что Makefile как поставляется с 2.0 появился в v1.9.2-471-g47fbfded53 И в v2.0.0-rc1-5-gb2feb6430b,
Причиной, по которой эти коммиты происходят до версии 2.0.0, являются злые слияния, которые не обнаруживаются с помощью этого нового механизма.

Учитывая хэш блоба, есть ли способ получить список коммитов, у которых этот блоб находится в их дереве?

С Git 2.16 (Q1 2018), git describe было бы хорошим решением, так как учили копать деревья глубже, чтобы найти <commit-ish>:<path> это относится к данному объекту BLOB-объекта.

См. Коммит 644eb60, коммит 4dbc59a, коммит cdaed0c, коммит c87b653, коммит ce5b6f9 (16 ноября 2017 г.) и коммит 91904f5, коммит 2deda00 (02 ноября 2017 г.) от Stefan Beller ( stefanbeller )
(Объединено Юнио С Хамано - gitster - в коммите 556de1a, 28 декабря 2017 г.)

builtin/describe.c: описать блоб

Иногда пользователям дают хэш объекта, и они хотят идентифицировать его далее (напр.: Использование verify-pack найти самые большие капли, но что это? или это очень ТАК вопрос " Какой коммит имеет этот блоб? ")

При описании коммитов мы пытаемся привязать их к тегам или ссылкам, поскольку они концептуально находятся на более высоком уровне, чем коммит. И если нет никакого ref или признака, который точно соответствует, нам не повезло.
Поэтому мы используем эвристику, чтобы придумать имя для коммита. Эти имена неоднозначны, могут быть разные теги или ссылки для привязки, и в DAG может быть другой путь для точного достижения фиксации.

При описании BLOB-объектов мы хотим описать BLOB-объекты и из более высокого слоя, который является кортежем (commit, deep/path) поскольку вовлеченные объекты дерева довольно неинтересны.
На один и тот же BLOB-объект может ссылаться несколько коммитов, так как мы решаем, какой коммит использовать?

Этот патч реализует довольно наивный подход к этому: так как нет обратных указателей от BLOB-объектов до коммитов, в которых возникает BLOB-объект, мы начнем идти с любых доступных советов, перечисляя BLOB-объекты в порядке фиксации, и как только мы найдем blob, мы возьмем первый коммит, который перечислил blob.

Например:

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

говорит нам Makefile как это было в v0.99 был введен в коммите 7672db2.

Ходьба выполняется в обратном порядке, чтобы показать появление капли, а не ее последнее появление.

Это означает, что git describe Страница man добавляет к целям этой команды:
Вместо того, чтобы просто описывать коммит с использованием самого последнего тега, доступного из него, git describe будет фактически давать объекту удобочитаемое имя на основе доступного ссылки при использовании в качестве git describe <blob>,

Если данный объект ссылается на BLOB-объект, он будет описан как <commit-ish>:<path> такой, что капля может быть найдена в <path> в <commit-ish>, который сам описывает первый коммит, в котором этот BLOB-объект встречается в обходе ревизии от HEAD.

Но:

ОШИБКИ

Объекты дерева, а также объекты тегов, не указывающие на коммиты, не могут быть описаны.
При описании BLOB-объектов легкие теги, указывающие на BLOB-объекты, игнорируются, но BLOB-объект все еще описывается как <committ-ish>:<path> несмотря на то, что легкий ярлык выгоден.

Я подумал, что это будет вообще полезно, поэтому я написал для этого небольшой Perl-скрипт:

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

Я положу это на github, когда вернусь домой этим вечером.

Обновление: похоже, кто-то уже сделал это. Тот использует ту же общую идею, но детали отличаются, и реализация намного короче. Я не знаю, что будет быстрее, но производительность здесь, наверное, не проблема!

Обновление 2: моя реализация на несколько порядков быстрее, особенно для большого репозитория. Тот git ls-tree -r действительно больно.

Обновление 3: я должен отметить, что мои комментарии о производительности выше относятся к реализации, о которой я говорил выше в первом обновлении. Реализация Аристотеля сравнима с моей. Подробнее в комментариях для тех, кому интересно.

Хотя первоначальный вопрос не требует этого, я думаю, что полезно также проверить область подготовки, чтобы увидеть, есть ли ссылка на BLOB-объект. Я изменил исходный скрипт bash, чтобы сделать это, и нашел то, что ссылалось на поврежденный BLOB-объект в моем хранилище:

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

Итак... Мне нужно было найти все файлы с заданным лимитом в репо размером более 8 ГБ с более чем 108 000 ревизий. Я адаптировал Perl-скрипт Аристотеля вместе со сценарием ruby, который написал, чтобы достичь полного решения.

Первый, git gc - сделать это, чтобы убедиться, что все объекты находятся в пакетных файлах - мы не проверяем объекты не в пакетных файлах.

Далее Запустите этот скрипт, чтобы найти все BLOB-объекты в байтах CUTOFF_SIZE. Захватить вывод в файл типа "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://stackru.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

Затем отредактируйте файл, чтобы удалить все ожидаемые объекты и биты INPUT_THREAD вверху. если у вас есть только строки для sha1, которые вы хотите найти, запустите следующий скрипт:

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

Где git-find-blob скрипт ниже.

#!/usr/bin/perl

# taken from: http://stackru.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;
}

Вывод будет выглядеть так:

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

И так далее. Каждый коммит, который содержит большой файл в своем дереве, будет перечислен. если ты grep из строк, которые начинаются с вкладки, и uniq что у вас будет список всех путей, которые вы можете удалить с помощью filter-branch, или вы можете сделать что-то более сложное.

Позвольте мне повторить: этот процесс прошел успешно, на репо 10 ГБ со 108 000 коммитов. Это заняло гораздо больше времени, чем я ожидал, при работе с большим количеством больших двоичных объектов, хотя через 10 часов мне нужно будет проверить, работает ли бит запоминания...

Вот подробности сценария, который я отработал как ответ на похожий вопрос, и здесь вы можете увидеть его в действии:

http://adamspiers.org/computing/git-ls-dir.png

Другие вопросы по тегам