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