Список последних дат фиксации для большого количества файлов, быстро

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

Ради конкретности предположим, что я хочу получить последние даты фиксации всех файлов *.txt внутри определенного подкаталога. Всего в репозитории насчитывается десятки тысяч файлов, а количество соответствующих файлов *.txt находится на стадионе в нескольких сотнях. В репозитории уже есть тысячи коммитов.

Я пробовал три разных подхода.


Решение 1. Этот вопрос дает один ответ на основе git log. Однако, если я попытаюсь сделать что-то подобное, это очень медленно:

find . -name '*.txt' |
    xargs -n1 git log --format=format:%ai -n1 --all -- '{}'

В моем тестовом примере потребовалось несколько минут - слишком медленно для моих целей.


Решение 2. Что-то вроде этого было бы намного быстрее, менее одной секунды:

git log --format=format:%ai --name-only .

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


Решение 3. Я также пробовал что-то вроде этого, чтобы избавиться от ненужных файлов:

git log --format=format:%ai --name-only `find . -name '*.txt'`

Однако это оказалось более медленным, чем решение 2. (В течение времени работы было различие фактора 3.) Кроме того, он по-прежнему печатает старые коммиты, которые больше не нужны.


Вопрос. Я что-то упустил? Есть ли быстрый и удобный подход? Предпочтительно, что-то, что работает не только сейчас, но и в будущем, когда мы имеем гораздо большее количество коммитов?

Ответы

Ответ 1

Попробуйте это.

В git каждая фиксация ссылается на объект дерева, который имеет указатели на состояние каждого файла (файлы являются блочными объектами).

Итак, что вы хотите сделать, это написать программу, которая начинается со списка всех файлов, в которых вас интересует, и начинается с объекта HEAD (транзакция SHA1, полученная с помощью git rev-parse HEAD). Он проверяет, изменяется ли какой-либо из "интересующих файлов" в этом дереве (дерево получено из атрибута "tree" git cat-file commit [SHA1]) - обратите внимание, вам нужно будет спуститься к поддеревьям для каждого каталога. Если они изменены (это означает, что хэш-код SHA1 отличается от того, который был у них в "предыдущей" редакции), он удаляет каждый из них из набора интересов и печатает соответствующую информацию. Затем он продолжает каждый родитель текущего дерева. Это продолжается до тех пор, пока набор интересов не будет пустым.

Если вы хотите максимальную скорость, вы будете использовать API git C. Если вы не хотите такой большой скорости, вы можете использовать git cat-file tree [SHA1 hash] (или, проще, git ls-tree [SHA1 hash] [files]), который будет выполнять абсолютный минимальный объем работы для чтения определенного объекта дерева (его часть слоя сантехники).

Вопрос о том, насколько хорошо это будет продолжаться в будущем, но если forward-compat является более серьезной проблемой, вы можете подняться на уровень от git cat-file, но, как вы уже обнаружили, git log является сравнительно медленным, поскольку часть фарфора, а не сантехника.

Смотрите здесь за довольно хороший ресурс о том, как работает объектная модель git.

Ответ 2

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

Я написал несколько script в perl для изменения времени доступа, и после некоторых изменений это версия, которая должна печатать то, что вы после:

#!/usr/bin/perl
my $commit = $ARGV[0];

$commit = 'HEAD' unless $commit;

# git a list of access times and files
my @logbook = `git whatchanged --pretty=%ai $commit`;

my %seen;
my $timestamp;
my $filename;
foreach (@logbook) {
    next if /^$/; # skip emtpy lines
    if (/^:/) {
        next unless /.txt$/;
        chomp ($filename = (split /\t/)[1]);
        next if $seen{$filename};
        print "$timestamp $filename\n";
        $seen{$filename} = 1;
    } else {
        chomp ($timestamp = $_);
    }
}

Я использовал git whatchanged вместо git log, чтобы иметь удобный формат с несрочными линиями, начинающимися с :, поэтому я могу легко разделить строки с файлами с последних времен модификации.

Ответ 3

Я немного опаздываю на вечеринку здесь, но здесь немного Bash script, который использует вызов в OP # 2, и выполняет постобработку в awk. (Для моего использования мне не нужно было видеть файлы, которые были удалены с текущей даты, поэтому проверка наличия тоже.)

#!/bin/bash
(
    git ls-files | sed 's/^/+ /'
    git log --format=format:"~ %aI" --name-only .
) | gawk '
/^~/ {date=$2;}
/^+/ {extant[$2] = 1;}
/^[^~+]/ {dates[$1] = date;}
END { for (file in dates) if(extant[file]) print(dates[file], file); }
' | sort

Ответ 4

Вот функция Powershell

function Get-GitRevisionDates($Path='.', $Ext='.md')
{
    [array] $log = git --no-pager log --format=format:%ai --name-only $Path

    $date_re = "^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d .\d{4}$"
    [array] $dates = $log | Select-String $date_re | select LineNumber, Line

    $files = $log -notmatch "^$date_re$" | ? { $_.EndsWith($Ext) } | sort -unique

    $res = @()
    foreach ($file in $files) {
        $iFile = $log.IndexOf($file) + 1
        $fDate = $dates | ? LineNumber -lt $iFile | select -Last 1
        $res += [PSCustomObject]@{ File = $file; Date = $fDate.Line }
    }

    $res | sort Date -Desc
}