Что такое git эвристика для назначения изменений содержимого в пути к файлам?

Краткая версия:

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


Подробная версия:

В демонстрационном взаимодействии оболочки Unix) ниже, два файла a и b: "git-commit 'ted", затем они изменяются так, чтобы (эффективно) передавать большую часть контента a до b, и, наконец, два файла снова совершаются.

Ключевое значение, которое нужно искать, состоит в том, что вывод второго git commit заканчивается строкой

rename a => b (99%)

, даже если не было переименования файлов (в обычном смысле) (!?!).


Прежде чем показывать демонстрацию, это краткое описание упростит работу.

Содержимое файлов a и b генерируется путем объединения содержимого трех вспомогательных файлов ../A, ../B и ../C. Символьно, что состояния a и b могут быть представлены как

../A + ../C -> a
../B        -> b

перед первым фиксацией и

../A        -> a
../B + ../C -> b

прямо перед вторым.

ОК, здесь демо.


Сначала мы показываем содержимое вспомогательных файлов ../A, ../B и ../C:

head ../A ../B ../C
# ==> ../A <==
# ...
# 
# ==> ../B <==
# ###
# 
# ==> ../C <==
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================

(Строки, начинающиеся с # соответствуют выходу на терминал, фактические выходные линии не имеют ведущего #.)

Затем мы создаем файлы a и b, отображаем их содержимое и фиксируем их

cat ../A ../C > a
cat ../B      > b
head a b
# ==> a <==
# ...
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# 
# ==> b <==
# ###

git add a b
git commit --allow-empty-message -m ''
# [master (root-commit) 3576df7] 
#  2 files changed, 8 insertions(+)
#  create mode 100644 a
#  create mode 100644 b

Затем мы изменяем файлы a и b и отображаем их новое содержимое:

cat ../A      > a
cat ../B ../C > b
head a b
# ==> a <==
# ...
#
# ==> b <==
# ###
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================

Наконец, мы фиксируем измененные a и b; обратите внимание на вывод git commit:

git add a b
git commit --allow-empty-message -m ''
# [master 25b806f] 
#  2 files changed, 2 insertions(+), 8 deletions(-)
#  rewrite a (99%)
#  rename a => b (99%)

Я рационализирую это поведение следующим образом.

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

Поскольку как содержимое, так и имена (в том числе пути) файлов могут меняться между коммитами, git должен использовать эвристику для связывания имен путей с кусками содержимого. Но эвристика по самой своей природе не гарантируется в 100% случаев. Провал такой эвристики здесь имеет форму истории, которая точно не отражает то, что на самом деле произошло (например, оно сообщает о переименовании файла, даже если файл не был переименован в обычном смысле).

Следующее подтверждение этой интерпретации (а именно, что некоторые эвристики находятся в игре) заключается в том, что AFAICT, если размер переданного фрагмента недостаточно велик, вывод git commit не будет включать строки rewrite/rename, (Я включаю демонстрацию этого случая в конце этого сообщения, FWIW.)

Мой вопрос заключается в следующем: не хватает исходного кода git, где я могу найти полное описание эвристик, которые git использует для связывания фрагментов контента с определенными отслеживаемыми дорожками?


Эта вторая демонстрация идентична первой во всех отношениях, за исключением того, что вспомогательный файл ../C - это одна строка короче, чем раньше.

head ../A ../B ../C
# ==> ../A <==
# ...
# 
# ==> ../B <==
# ###
# 
# ==> ../C <==
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================

cat ../A ../C > a
cat ../B      > b
head a b
# ==> a <==
# ...
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# 
# ==> b <==
# ###

git add .
git commit -a --allow-empty-message -m ''
# [master (root-commit) a06a689] 
#  2 files changed, 7 insertions(+)
#  create mode 100644 a
#  create mode 100644 b

cat ../A      > a
cat ../B ../C > b
head a b
# ==> a <==
# ...
# 
# ==> b <==
# ###
# =================================================================
# =================================================================
# =================================================================
# =================================================================
# =================================================================

git add .
git commit -a --allow-empty-message -m ''
# [master 87415a1] 
#  2 files changed, 5 insertions(+), 5 deletions(-)

Ответ 1

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

Сначала собираются кандидаты. Любые новые файлы можно переименовывать цели, и любые удаленные файлы можно переименовать. Кроме того, переписывающие изменения прерываются таким образом, что файл, который более чем на 50% отличается от предыдущей версии, является как возможным источником переименования, так и возможной целью переименования.

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

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

Ответ 2

... не хватает исходного кода git, где я могу найти полное описание эвристик, которые git использует для связывания фрагментов контента с определенными отслеживаемыми дорожками?

В зависимости от того, что вы подразумеваете под "полным", я не думаю, что вы можете найти такое. (В частности, как рассчитываются "проценты"? Является ли это строками, символами/байтами или чем-то еще? Делает ли слово-ориентированный diff что-то менять?) Но магия все внутри git diff, где она вычисляется динамически каждый раз, когда должен отображаться diff; и эвристика имеет несколько регуляторов, которые дают сильные подсказки:

--no-renames

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

-B[<n>][/<m>], --break-rewrites[=[<n>][/<m>]]

Перерыв завершите переписывание изменений в пары delete и create.            Это служит двум целям:

  • Это влияет на изменение, которое равно общей перезаписи        файл не как серию удаления и вставки, смешанных вместе с        очень мало строк, которые соответствуют текстуально как контекст, но        как единое удаление всего старого, за которым следует один        вставка всего нового, а число m контролирует этот аспект        опции -B (по умолчанию 60%). -B/70% указывает, что меньше        чем 30% оригинала должны оставаться в результате для git       рассмотрите его как полную переписывание (т.е. в противном случае полученный патч        будет серией удаления и вставки, смешанной с        контекстные строки).

  • При использовании с -M полностью перезаписанный файл также рассматривается как        источник переименования (обычно -M рассматривает только файл, который        исчез как источник переименования), а число n элементов управления        этот аспект опции -B (по умолчанию - 50%). -B20% указывает        что изменение с добавлением и удалением по сравнению с 20% или более        размер файла может быть выбран в качестве возможного        источник переименования в другой файл.

и т.д.; см. документацию для git -diff.