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

150

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

Readonly
источник
2
«Хэш блоба» - это то, что возвращается git hash-objectили sha1("blob " + filesize + "\0" + data), а не просто sha1sum содержимого блоба.
Иван Гамильтон
1
Сначала я думал, что этот вопрос соответствует моему вопросу, но, похоже, это не так. Я хочу знать один коммит, который первым представил этот BLOB-объект в хранилище.
Джесси Глик
Если вы знаете путь к файлу, вы можете использовать его git log --follow filepath(и использовать его, чтобы ускорить решение Аристотеля, если хотите).
Заз
ProTip ™: вставьте один из скриптов belew ~/.binи назовите его git-find-object. Затем вы можете использовать его с git find-object.
Заз
1
Примечание: С Git 2.16 (Q1 2018) вы можете подумать просто git describe <hash>: смотрите мой ответ ниже .
VonC

Ответы:

107

Оба следующих сценария принимают 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 );
}
Аристотель Пагальтзис
источник
9
К вашему сведению, вы должны использовать полный SHA блоба. Префикс, даже если он уникален, работать не будет. Чтобы получить полный SHA из префикса, вы можете использоватьgit rev-parse --verify $theprefix
John Douthat
1
Спасибо @JohnDouthat за этот комментарий. Вот как включить это в вышеприведенный сценарий (извините за вставку в комментарии): 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";
Инго Каркат
В верхнем скрипте может быть ошибка. Цикл while выполняется только в том случае, если нужно прочитать больше строк, и по какой-то причине git log не ставит окончательный crlf в конец. Мне пришлось добавить перевод строки и игнорировать пустые строки. 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
Это находит коммиты только в текущей ветке, если вы не передаете --allв качестве дополнительного аргумента. (Поиск всех коммитов в репо очень важен в таких случаях, как удаление большого файла из истории репо ).
peterflynn
1
Совет: передайте флаг -g сценарию оболочки (после идентификатора объекта), чтобы проверить журнал.
Брэм Шенмейкерс
24

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

git log --all --pretty=format:%H -- <path> | xargs -n1 -I% sh -c "git ls-tree % -- <path> | grep -q <hash> && echo %"
aragaer
источник
1
Отличный ответ, потому что это так просто. Просто сделав разумное предположение, что путь известен. Однако следует знать, что он возвращает коммит, в котором путь был изменен на данный хеш.
Унапьедра
1
Если кто-то хочет получить новейший коммит, содержащий <hash>в данный момент <path>, то удаление <path>аргумента из git logбудет работать. Первый возвращаемый результат - это требуемый коммит.
Унапьедра
10

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

С 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) .
(Слиты Junio C Hamano - gitster- в фиксации 556de1a , 28 дек 2017)

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

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

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

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

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

Но:

ОШИБКИ

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

VonC
источник
1
Хорошо использовать в сочетании с 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, который возвращает вас в топ-20 крупнейших капель. Затем вы можете передать идентификатор BLOB из вышеприведенного вывода git describe. Работал как шарм! Спасибо!
Александр Погребняк
7

Я подумал, что это будет вообще полезно, поэтому я написал для этого небольшой скрипт на 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: я должен отметить, что мои комментарии о производительности выше относятся к реализации, о которой я говорил выше в первом обновлении. Реализация Аристотеля сравнима с моей. Подробнее в комментариях для тех, кому интересно.

Грег Хьюгилл
источник
Хм, как это может быть , что гораздо быстрее? В любом случае, ты идешь по дереву? Какую работу делает git-ls-tree, которую вы избегаете? (NB .: grep будет спасаться при первом совпадении, SIGPIPE'ит git-ls-tree.) Когда я попробовал это, мне пришлось нажать Ctrl-C через 30 секунд; мой был сделан в 4.
Аристотель Пагальцис
1
Мой скрипт кэширует результаты поддеревьев в хеше% trees, поэтому он не должен продолжать поиск поддеревьев, которые не изменились.
Грег Хьюгилл
На самом деле, я пробовал реализацию, которую нашел на github, с которой связался. В некоторых случаях у вас быстрее, но это сильно зависит от того, находится ли искомый файл в начале или в конце списка ls-tree. В моем репозитории сейчас находится 9574 файла.
Грег Хьюгилл
Мне также приходит в голову, что некоторые нелинейные истории проекта могут заставить мой скрипт выполнять гораздо больше работы, чем нужно (это можно исправить). Это может быть то, почему для вас потребовалось много времени. Мой репозиторий является зеркалом git-svn репозитория Subversion, так что он довольно линейный.
Грег Хьюгилл
Вместо разбора cat-file, чтобы получить дерево, просто сделайтеgit rev-parse $commit^{}
Jthill
6

Хотя исходный вопрос не требует этого, я думаю, что полезно также проверить область подготовки, чтобы увидеть, есть ли ссылка на 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
Марио
источник
3
Я просто хотел бы отдать должное, где это происходит: спасибо за повреждение оперативной памяти за то, что я сделал BSOD и заставил меня отремонтировать мое git-репо.
Марио
4

Итак ... Мне нужно было найти все файлы с заданным лимитом в репо размером более 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://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

Затем отредактируйте файл, чтобы удалить все ожидаемые объекты и биты 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://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;
}

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

<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 часов мне нужно будет проверить, работает ли бит запоминания ...

cmyers
источник
1
Как ответ Аристотеля выше, это находит только коммиты на текущей ветке , если не передать дополнительные аргументы: -- --all. (Поиск всех коммитов в репо важен в таких случаях, как тщательное удаление большого файла из истории репо ).
peterflynn
4

Кроме того 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) .
(Слиты Junio C Hamano - gitster- в фиксации c0d75f0 , 23 января 2018)

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

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

Можно было бы соблазниться расширить git-describeработу и с BLOB-объектами, например, 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, являются злые слияния, которые не обнаруживаются с помощью этого нового механизма.

VonC
источник