Неожиданное поведение с «git commit».когда ловушка pre-commit изменяет промежуточные файлы - PullRequest
1 голос
/ 09 апреля 2019

По моему опыту git commit -a имел поведение, эквивалентное git commit . Однако недавно я создал ловушку предварительной фиксации, которая автоматически форматирует мой исходный код, и теперь git commit . имеет некоторые неожиданные побочные эффекты: файл, который являетсяКогда команда commit завершится, то значение commit будет изменено в рабочем каталоге и в индексе.Это не происходит с git commit -a.Я пытаюсь понять, что происходит за кулисами при запуске git commit ., из-за чего это происходит, и посмотреть, есть ли способ правильно обработать это в моем скрипте ловушки перед фиксацией.

pre-commit hook:

git_toplevel=$(git rev-parse --show-toplevel)

git --no-pager diff -z --cached --name-only --diff-filter=ACMRT | $git_toplevel/meta/reformat.bash -s files
git --no-pager diff -z --name-only --diff-filter=ACMRT | xargs -0 --no-run-if-empty git add

В настоящее время используется версия git 1.8.3.1, но в более поздних версиях наблюдается то же поведение.

Ниже приведена последовательность команд для простого пробела, добавленная вначало строки:

[]$ git status
# On branch eroller/format-clean-filter
# Your branch is ahead of 'origin/eroller/format-clean-filter' by 1 commit.
#   (use "git push" to publish your local commits)
#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   src/host/cnv/denovo/denovo_cnv.cpp
#
no changes added to commit (use "git add" and/or "git commit -a")

-

[]$ git diff
diff --git a/src/host/cnv/denovo/denovo_cnv.cpp b/src/host/cnv/denovo/denovo_cnv.cpp
index 7cfb8dc..14058e3 100644
--- a/src/host/cnv/denovo/denovo_cnv.cpp
+++ b/src/host/cnv/denovo/denovo_cnv.cpp
@@ -28,7 +28,7 @@ using namespace std;
 namespace cnv {
 namespace denovo {

-SegmentsBySample LoadCallsForSamples(const vector<string>& callFiles, const ReferenceDictionary& reference)
+ SegmentsBySample LoadCallsForSamples(const vector<string>& callFiles, const ReferenceDictionary& reference)
 {
   function<SegmentsBySample::value_type(const string&)> loadCalls = [&](string callFile) {
     return LoadCalls(callFile, reference);

-

[]$ git commit -m 'test' .

-

[]$ git status
# On branch eroller/format-clean-filter
# Your branch is ahead of 'origin/eroller/format-clean-filter' by 2 commits.
#   (use "git push" to publish your local commits)
#
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       modified:   src/host/cnv/denovo/denovo_cnv.cpp
#
# Changes not staged for commit:
#   (use "git add <file>..." to update what will be committed)
#   (use "git checkout -- <file>..." to discard changes in working directory)
#
#       modified:   src/host/cnv/denovo/denovo_cnv.cpp
#

-

[]$ git diff
diff --git a/src/host/cnv/denovo/denovo_cnv.cpp b/src/host/cnv/denovo/denovo_cnv.cpp
index 14058e3..7cfb8dc 100644
--- a/src/host/cnv/denovo/denovo_cnv.cpp
+++ b/src/host/cnv/denovo/denovo_cnv.cpp
@@ -28,7 +28,7 @@ using namespace std;
 namespace cnv {
 namespace denovo {

- SegmentsBySample LoadCallsForSamples(const vector<string>& callFiles, const ReferenceDictionary& reference)
+SegmentsBySample LoadCallsForSamples(const vector<string>& callFiles, const ReferenceDictionary& reference)
 {
   function<SegmentsBySample::value_type(const string&)> loadCalls = [&](string callFile) {
     return LoadCalls(callFile, reference);

-

[]$ git diff --cached
diff --git a/src/host/cnv/denovo/denovo_cnv.cpp b/src/host/cnv/denovo/denovo_cnv.cpp
index 7cfb8dc..14058e3 100644
--- a/src/host/cnv/denovo/denovo_cnv.cpp
+++ b/src/host/cnv/denovo/denovo_cnv.cpp
@@ -28,7 +28,7 @@ using namespace std;
 namespace cnv {
 namespace denovo {

-SegmentsBySample LoadCallsForSamples(const vector<string>& callFiles, const ReferenceDictionary& reference)
+ SegmentsBySample LoadCallsForSamples(const vector<string>& callFiles, const ReferenceDictionary& reference)
 {
   function<SegmentsBySample::value_type(const string&)> loadCalls = [&](string callFile) {
     return LoadCalls(callFile, reference);

ОБНОВЛЕНИЕ: Используя очень подробный ответ @torek (спасибо!), Я решил выдать ошибку в ловушке перед фиксацией, если пользователь пытается использовать git commit .или git commit [--only] -- <files>.Вот проверка в моем сценарии предварительной фиксации:

if [[ $GIT_INDEX_FILE != *"/index" ]] && [[ $GIT_INDEX_FILE != *"/index.lock" ]] ; then
  echo "Error: pre-commit reformatting using unsupported index file ($GIT_INDEX_FILE)." >&2
  echo "       Are you using 'git commit [--only] -- <files>' to bypass staging?" >&2
  echo "       Use git commit -a or stage your files before committing using git add -- <files>" >&2
  echo "       Use '--no-verify' to bypass reformatting (not recommended)" >&2
  exit 1
fi

1 Ответ

4 голосов
/ 09 апреля 2019

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

Индекс , под которым я имею в виду один стандартный, - это файл в .git с именем index. Если вы проверите свой каталог .git, вы найдете такой файл. В прошлом действительно был только этот файл. В современном Git (версии 2.5 и выше) картина значительно более мутная из-за добавленных рабочих деревьев: на каждое рабочее дерево фактически существует один индексный файл, так что .git/index - это всего лишь индекс для main work-tree. Для каждого рабочего дерева есть вспомогательный индекс , но это не совсем то, к чему я стремлюсь, здесь, это просто случай, показывающий, как предположение, что существует один единственный индекс, уже изнашивается по краям. , По общему признанию, вы используете Git 1.8.3.1 (который на самом деле довольно старый), но он также является более сложным, чем простая простая настройка «один индекс».

Когда вы используете git commit -a, Git создает новый дополнительный индекс. Когда вы используете git commit ., вы вызываете git commit --only . ( см. Подробности в документации ), и Git создает два новых дополнительных индекса (индекса?).

Все части Git имеют возможность перенаправить rest Git для использования другого, нестандартного индекса, и эти различные опции для git commit используют эту функцию. Обратите внимание, что git commit -a эквивалентно git commit --include, за которым следуют имена любых файлов, которые необходимо добавить. Действительно сложный случай - это тот, который вы используете, git commit --only.

Как только вы начинаете умножать индексные файлы, все становится запутанным!

Помните, что индекс, по сути, предлагает следующий коммит . Если существует только один индекс (для этого рабочего дерева, если мы говорим о Git 2.5 или более поздней версии), будет предложен только один следующий коммит. Это не так уж сложно, мы просто должны учесть, что три копии каждого файла. Давайте выберем файл, такой как README.md:

  • HEAD:README.md является текущей версией README.md. Вы не можете это изменить. (Вы можете переместить HEAD сам, но зафиксированная копия README.md находится внутри фиксации, как обнаружено по хеш-идентификатору фиксации, и не изменится.)

    Имя HEAD:README.md работает только внутри Git. Это имя обращается к этой замороженной Git-ified, лиофилизированной копии файла; эта копия никогда не изменится. Вы можете видеть это, например, git show HEAD:README.md.

  • :README.md является копией README.md в индексе. Первоначально он был таким же, как HEAD:README.md, но если вы запустили git add README.md, теперь он может отличаться.

    Имя :README.md также работает только внутри Git. Это имя обращается к этой заменяемой, но Git-ified (замораживаемой форме) копии файла, хранящейся в индексе. Вы можете заменить это в любое время на git add.

  • Наконец, README.md - это обычный (не Git-ified) файл. Это не в Git! Это не в индексе! Он находится в вашем рабочем дереве , где вы можете видеть его и работать над ним, используя все ваши обычные компьютерные инструменты. Git действительно не использует этот файл ни для чего, он просто перезаписывает его или удаляет его, когда вы проверяете какой-то другой коммит. Единственное, что Git делает с ним, кроме проверки с помощью git status и т. Д., Это позволяет вам использовать git add до копировать обратно в индекс, перезаписывая то, что было раньше (и сублимационной сушкой). это в процессе).

Бег git status Бег два git diff с:

  • Первый сравнивает коммит HEAD с индексом, то есть то, что находится в текущем коммите, с тем, что находится в предлагаемом следующем коммите. Все, что отличается здесь, указано как для коммита . Что-нибудь такое же, Гит просто тихо ничего не говорит.

  • Второй git diff сравнивает индекс с рабочим деревом, т. Е. Что находится в предлагаемом коммите, с тем, что вы можете скопировать в индекс.Все, что отличается здесь указано как , не предназначенное для фиксации .Все то же самое, опять же, Git тихо ничего не говорит.

  • (Затем есть последний проход для проверки файлов в рабочем дереве, которых вообще нет в индексе. Gitбудет скулить по этому поводу, говоря, что они не отслежены, если только вы не перечислите их в .gitignore. Присутствие в списке в .gitignore не меняет, есть ли копия файла в индексе, оно просто меняет, скулит ли Git.)

Когда вы запускаете git commit, Git упаковывает все, что находится в индексе, и использует его для создания нового коммита ... , если вы не используете --only, --include или -a.

Индексы из wazoo

С git commit --only Git создает три индексных файла:

  • Один из них стандартный.Это нетронутым в начале.Это обычный .git/index.
  • Один - копия этого с --only файлами git add, отредактированными к нему.Это в .git/index.lock в какой-то момент. Может быть это всегда здесь!Если это так, то это даст способ разобраться с делом, которое я изложу ниже.Но нет документации, которая обещает это.
  • Третий - свежий, сделанный сначала извлечением HEAD, а затем git add добавлением в него --only файлов.

Если вы не сделали git add что-либо до , вы запустили git commit -a, первый и третий индексные файлы совпадают, потому что добавление файлов --only в обычный индекс имеет тот же эффект, что и создание нового временного индексаиз HEAD и добавив в него файлы --only.Но в противном случае все три файла могут отличаться!

Затем Git делает новый коммит из третьего индекса.Если новая фиксация завершается успешно, Git заменяет обычный индекс на second index (эта замена происходит через системный вызов rename).В противном случае Git возвращается к нормальному индексу.(Обратите внимание, что с рабочим деревом вообще ничего не происходит.)

Если вы используете git commit --include или git commit -a, Git создает только один дополнительный индекс, поэтому у вас есть:

  • стандартный индекс в .git/index со всем, что вы добавили до сих пор;и
  • дополнительный индекс во временном файле: он начинается как копия стандартного индекса, но затем Git добавляет перечисленные файлы или другие измененные файлы к этому индексу.

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

Введение хуков предварительной фиксации

Git запускает хук предварительной фиксации после подготовки любых дополнительных индексных файлов.Специальная переменная окружения $GIT_INDEX_FILE называет индекс, который Git будет использовать для создания нового коммита.Итак, есть три случая, два из которых не так уж плохи, а другой ужасен:

  • Вы делаете нормальный коммит.GIT_INDEX_FILE называет нормальный индекс, и все нормально.
  • Вы делаете git commit --include или git commit -a и GIT_INDEX_FILE, называет второй индекс;третьего индекса нет;если фиксация завершится, Git переименует второй индекс.
  • Вы делаете git commit --only и GIT_INDEX_FILE имена третьего индекса.Не существует простого способа найти второго индекса, который будет существовать после фиксации, если фиксация будет успешной!

Ваша работа, если вы решите внести измененияк файлам, хранящимся в индексе, это сделать их индексом, который Git будет использовать для фиксации.Чтобы сделать это, вы можете использовать git add, если хотите, так как это скопирует файлы из рабочего дерева в индекс, указанный в $GIT_INDEX_FILE.

Первая проблема, однако, заключается в том, что не должен просматривать файлы в рабочем дереве .Они не имеют значения!Они могут содержать нечто совершенно отличное от того, что в индексе.Это особенно верно во время git commit --only.

Вторая и более серьезная проблема заключается в том, что если вы обновили третий индекс, который использует git commit --only, вам также следует обновить second index, который используется git commit --only.Эта часть хитрая, потому что нет простого способа найти ее, кроме как предположить, что она в .git/index.lock.Хотя это может сработать, я не буду советовать это здесь.

У меня действительно нет никаких предложений для этого - любой хитрый метод, который вы найдете, может сломаться, так как код для работы с этим третьим индексом (который является текущим 2.21-ишем)Git называет «фальшивый индекс») сильно изменился между 1.8 и современным Git.Обычная рекомендация - , а не , чтобы вообще выполнять какое-либо специальное форматирование в Git-хуке.Вместо этого, пусть Git hook просто проверяет , правильно ли индексная копия файла отформатирована: если это так, продолжите фиксацию, а если нет, отмените фиксацию.Оставьте остальное пользователю.

Еще одна альтернатива

Альтернатива, которую я видел и использовал, заключается в проверке фактической настройки $GIT_INDEX_FILE.Если установлено значение .git/index, пользователь использует git commit без каких-либо специальных настроек.Еще одна хитрость в этой самой ловушке предварительной фиксации (которая вызывает clang-format и autopep8) - сравнить индекс и рабочее дерево для файлов, которые будут отформатированы, и отказаться от запуска, если они не совпадают.

...