Почему ловушки предварительной фиксации оставляют файлы "наполовину подготовленными"? - PullRequest
0 голосов
/ 10 июня 2019

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

См., Например, этот сценарий .Он правильно добавляет файлы после их изменения и говорит, что должен работать в Windows.Тот факт, что хуки не работают для меня, когда они работают для других людей, заставляет меня поверить, что что-то не так с моей средой.

Это происходит, когда хук изменяет файл с лишним разрывом строки:

$ git status -s
A  src/hello.c
$ git commit src/hello.c

Add 'Hello World!'
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# On branch master
# Changes to be committed:
#   new file:   src/hello.c
#
# Changes not staged for commit:
#   modified:   src/hello.c
#
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   src/hello.c

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/hello.c
$ git diff
warning: LF will be replaced by CRLF in src/hello.c.
The file will have its original line endings in your working directory
diff --git a/src/hello.c b/src/hello.c
index 5e4b595..768d31a 100644
--- a/src/hello.c
+++ b/src/hello.c
@@ -1,6 +1,5 @@
 #include <stdio.h>

-
 int main() {
   printf("Hello, World!");
   return 0;

$ git diff --staged
diff --git a/src/hello.c b/src/hello.c
index 768d31a..5e4b595 100644
--- a/src/hello.c
+++ b/src/hello.c
@@ -1,5 +1,6 @@
 #include <stdio.h>

+
 int main() {
   printf("Hello, World!");
   return 0;

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

Ответы [ 2 ]

1 голос
/ 10 июня 2019

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


Вы не показывали ловушку напрямую, но у вас была ссылка на ссылка на репозиторий GitHub , содержащаякрюк;вот более прямая ссылка на сам крючок ).Я процитирую несколько строк из хука.

Хук делает некоторые довольно дерзкие предположения, потому что когда вы запускаете git commit, есть как минимум три того, что я люблю называть "«Активные копии» каждого файла, и эта ловушка недостаточно сложна, чтобы заметить расхождения между ними.

Три копии файлов, иногда с различным содержанием

Три копии:

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

  • Копия index .Этот файл можно изменить.Это то, что вы предлагаете зафиксировать: если ваши хуки pre-commit и commit-message разрешают фиксацию, а все остальное идет правильно, копия файла, содержащегося в индексе, является той копией, которая будет зафиксирована.Следовательно, вы можете думать об индексе - который Git также называет промежуточной областью - как, по сути, предложенный следующий коммит .

    Эти первые два файла - замороженыHEAD copy и index copy - в специальном сжатом формате Git-only.Хотя индексную копию можно изменить, это всегда делается с помощью , заменяя , обычно используя git add для ее перезаписи.Команда git add сжимает файл в формате Git-only и помещает сжатую копию - ну, технически, ссылку на сжатую копию - в индекс.

  • Работа-копия дерева.Этот файл является обычным файлом, который вы можете просматривать и манипулировать им.

Теперь вы используете перевод Lit / CRLF в Git, как указано:

warning: LF will be replaced by CRLF in src/hello.c

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

git status, git add и существующий хук

Давайте теперь перейдем к сценарию, ивзгляните на несколько строк:

for line in $(git status -s)

(технически это должно быть git status --porcelain, но на данный момент они в значительной степени делают одно и то же: главная опасность состоит в том, что --short output можно * раскрасить , что сломает следующий бит)

  if [[ $line == A* || $line == M* ]]

Теперь пришло время рассмотреть, что печатает git status. В документации говорится о кратком формате:

... состояние каждого пути отображается в виде одной из этих форм

   XY PATH
   XY ORIG_PATH -> PATH

где ORIG_PATH - откуда пришло переименованное / скопированное содержимое. ORIG_PATH отображается только тогда, когда запись переименована или скопирована. XY - это двухбуквенный код состояния.[фрагмент] X показывает состояние индекса, а Y показывает состояние рабочего дерева.

(Помимо: скопировано в настоящее время не является возможным статусом для git status. Внутренний механизм сравнения, который вызывает git status, может установить это, но для этого вызывающая сторона должна включить его, а git status просто нет. Если git status получил новые флаги командной строки или записи конфигурации, которые позволили обнаружение копирования, вы могли бы получить C статусы, но на данный момент вы не можете.)

Ключевой элемент hДело в том, что первая буква, которую тестирует скрипт, основана на статусе индекса . То есть это сводка результата сравнения коммита HEAD с индексом - с предложенным коммитом. Файл будет A dded, если он новый в индексе (не появляется в коммите HEAD), или M odified, если он будет одновременно в индексах и HEAD, но индексная копия отличается от HEAD commit.

Здесь необходимо понять, что, соответствует ли копия index копии head , копия work-tree полностью является третьим файлом. Это может сильно отличаться от одной или обеих из этих двух других копий! Это нормально , и на самом деле это преднамеренно, если вы используете git add -p для выборочной обработки только части файла рабочего дерева. Просто помни об этом, пока мы пашем.

Теперь вернемся к сценарию ловушки перед фиксацией:

    if [[ $line == *.c || $line == *.cc || $line == *.h || $line == *.cpp ]]
    then
      # format the file
      clang-format -i -style=file $(pwd)/${line:3}

       # and then add the file (so that any formatting changes get committed)
      git add $(pwd)/${line:3}
    fi

Если имя файла в конце строки - для файлов состояния A и M - это только одно имя файла; только R файлы состояния будут иметь два имени; но скрипт неисправен, так как не проверяет R файлы состояния, поскольку файл может быть переименован в и изменен - ​​оканчивается на .c, .cc и т. д., при этом выполняется clang-format.

Вход - clang-format - это файл work-tree . Входные данные почти наверняка должны быть индексной копией файла, но это не так. Таким образом, сценарий предполагает, что индекс и копия рабочего дерева совпадают.

После запуска clang-format сценарий затем запускает git add, чтобы скопировать (обновленный) файл рабочего дерева обратно в индекс. Если мы хотим сделать это правильно, нам нужно отформатировать индексную копию, а затем добавить отформатированную индексную копию, что довольно сложно. Вероятно, это , почему сценарий немного ленив, но это определенно стоит отметить.

Файл рабочего дерева, написанный clang-format, вероятно, будет иметь окончания строк только для LF (см. https://reviews.llvm.org/D19031). Это соответствует тексту предупреждения:

предупреждение: LF будет заменен CRLF в src / hello.c.

Это говорит о том, что текущая копия рабочего дерева, src/hello.c, имеет окончания строк только для LF. Git было сказано, что когда Git копирует из индекса обратно в рабочее дерево, Git должен изменить окончания LF-only на окончания CRLF.

Более трех экземпляров

Теперь все усложняется. Я упомянул выше, что существует как минимум трех копий каждого файла, а затем описал места, в которых эти три копии живут. Это коммит HEAD, индекс и дерево работы. Единственный недостаток этого описания - фраза индекс , поскольку Git иногда использует временный индекс. Это относится к некоторым git commit командам, но не ко всем из них.

Полная история git commit заключается в том, что он всегда строит ваш новый коммит из из индекса, но не обязательно из * индекса. Существует индекс "the" - один особенный, выделенный индекс, который идет вместе с рабочим деревом. 1 Затем существуют дополнительные индексные файлы, которые некоторые команды Git создают для различных целей - например, git stash создает временный индекс для сохранения рабочего дерева, и git filter-branch создает много временных файлов индекса при запуске. Здесь, однако, нас интересует git commit, и git commit иногда создает один или два собственных временных файла индекса.

Если вы запускаете git commit - без дополнительных аргументов вообще - git commit просто использует индексный файл . Это ваш предложенный коммит, и в нем уже есть все файлы. Если ваш хук предварительной фиксации запускает git add, он копирует новые файлы в индекс , заменяя старые, которые были в индексе, и в конечном итоге git commit записывает новый коммит, используя новые файлы. Если новые файлы поступили из рабочего дерева, то в основном они совпадают, за исключением, возможно, окончания строк CRLF.

Но если уВы запускаете git commit --only или git commit --include, или даже просто git commit -a, Git принимает поворот. Если вы запускаете git commit file1.cc, то означает, например, git commit --only file1.cc, если только вы не добавите --include, в этом случае это означает git commit --include file1.cc.

Для выполнения этих операций - фактически включая простой git commit - Git создает как минимум один временный индексный файл, хотя для простого git commit это происходит как можно позже. Один временный индексный файл называется index.lock (ну, .git/index.lock, в зависимости от того, где находится каталог .git). Этот временный индекс будет истинным источником файлов для нового коммита. Когда фиксация завершена, и если все это успешно, Git снимает блокировку, переименовывая .git/index.lock в .git/index.

Мы можем видеть их в действии через фиктивный .git/hooks/pre-commit, который просто печатает имя переменной окружения $GIT_INDEX_FILE, а затем завершается с ошибкой для предотвращения фиксации:

$ cat .git/hooks/pre-commit
$ git commit
$GIT_INDEX_FILE is .git/index
$ git commit -a
$GIT_INDEX_FILE is [path]/git/.git/index.lock
$ git commit --only cache.h
$GIT_INDEX_FILE is [path]/.git/next-index-53061.lock
$ git commit --include cache.h
$GIT_INDEX_FILE is [path]/.git/index.lock

Итак:

  • Обычный git commit использует обычный индексный файл. Если ваш хук запускает git add, вы замените файлы в индексе. Когда Git приступает к созданию файла блокировки index.lock, он создает его из index, а когда git commit завершает (при условии успеха), ваши изменения индекса, сделанные вашей ловушкой, будут вступают в силу.

  • A git commit -a или git commit --include работает аналогично. Блокировка выполняется ранее, но git add должен обновить index.lock на месте, а когда git commit завершится, ваш основной индекс должен иметь обновления. (Я не проверял это, но это кажется очевидным.)

  • Но git commit --only создает временный индекс (next-index-53061.lock), а также блокирует основной индекс и git add передает файлы --only в основной заблокированный индекс. Когда фиксация завершится, файлы нового коммита будут из временного индекса, включая все, что вы обновили; но индекс main будет взят из index.lock, который является старым индексом с обновленными конкретными файлами. Когда они будут обновлены, они будут контролировать, что на самом деле находится в этом индексе.


1 Когда вы используете git worktree add для создания дополнительного рабочего дерева, дополнительное рабочее дерево получает свой собственный особый индекс, поэтому индекс является парным с рабочим деревом : добавленное рабочее дерево является отдельным рабочим деревом с отдельным индексом. Добавленное рабочее дерево также получает свой собственный HEAD, что особенно усложняет работу в Windows, но нам не нужно идти туда.


Заключение

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

Вы можете изменить выполняемый коммит; вам просто нужно знать об этих странных случаях.

0 голосов
/ 10 июня 2019

Один обходной путь использует хук post-commit, например:

#!/bin/sh
for line in $(git diff-tree --no-commit-id --name-only -r HEAD)
do
    git add $(pwd)/${line}
done

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

...