it-swarm.com.ru

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

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

129
Readonly

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

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

#!/bin/sh
obj_name="$1"
shift
git log "[email protected]" --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 );
}
92
Aristotle Pagaltzis

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

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

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

7
Greg Hewgill

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

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

См. commit 644eb6 , commit 4dbc59a , commit cdaed0c , commit c87b65 , commit ce5b6f9 (16 ноября 2017 г.) и коммит 91904f5 , коммит 2deda (02 ноября 2017 г.) Стефан Беллер (stefanbeller) .
(Объединено с Junio ​​C Hamano - gitster - в коммит 556de1a , 28 декабря 2017 г.)

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

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

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

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

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

Например:

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

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

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

Это означает, что git describe man page добавляет к целям этой команды:

Вместо простого описания коммита с использованием самого последнего тега, доступного из него, git describe на самом деле даст объекту удобочитаемое имя на основе доступного ref при использовании в качестве git describe <blob>.

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

Но:

ОШИБКИ

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

6
VonC

Хотя оригинальный вопрос не требует этого, я думаю, что полезно также проверить область подготовки, чтобы увидеть, есть ли ссылка на 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 "[email protected]" --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
6
Mario

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

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

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

3
cmyers

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

Смотрите commit 4d8c51a , commit 5e50525 , commit 15af58c , commit cf63051 , commit c1ddc46 , совершить 929ed7 (04 января 2018 г.) Стефан Беллер (stefanbeller) .
(Объединено с Junio ​​C Hamano - gitster - в commit c0d75f , 23 января 2018 г.)

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

Иногда пользователям дают хэш объекта, и они хотят идентифицировать его дальше (например: используйте 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, являются злые слияния, которые не обнаруживаются с помощью этого нового механизма.

2
VonC