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