По моему опыту, поведение git commit -a
было эквивалентно git commit .
Однако недавно я создал хук перед фиксацией, который автоматически форматирует мой исходный код, и теперь git commit .
имеет некоторые неожиданные побочные эффекты: файл, который был зафиксирован, оказывается измененным в рабочем каталоге. и в индексе после завершения команды фиксации. С git commit -a
такого не бывает. Я пытаюсь понять, что происходит за кулисами при запуске git commit .
, из-за чего это происходит, и посмотреть, есть ли способ правильно справиться с этим в моем скрипте ловушки перед фиксацией.
хук перед фиксацией:
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
Фундаментальная проблема здесь в том, что Git делает коммиты не из рабочего дерева, а из индекса, поэтому вам в первую очередь нужно git add
файлы, но индекс в — это своего рода ложь во благо, потому что индексных файлов на возможно больше, чем только один стандартный. (Индекс также называется плацдарм или тайник, в зависимости от того, какая часть Git выполняет вызов.)
Индекс То, под которым я подразумеваю один стандартный, представляет собой файл в .git
с именем index
. Если вы проверите свой каталог .git
, вы найдете такой файл. Раньше действительно был только этот файл. В современном Git (2.5 и выше) картина значительно мутнее из-за добавленных рабочих деревьев: фактически на каждое рабочее дерево приходится один индексный файл, так что .git/index
— это только индекс в для рабочего дерева основной. Для каждого рабочего дерева есть вспомогательный индекс в, но это не совсем то, что я имею в виду, здесь это просто случай демонстрации того, как предположение о наличии одного единственного индекса уже изнашивается по краям. По общему признанию, вы используете Git 1.8.3.1 (который действительно довольно старый), но он также более сложен, чем хорошая простая настройка «одного индекса».
Когда вы используете git commit -a
, Git создает новый дополнительный индекс. Когда вы используете git commit .
, вы вызываете git commit --only .
(подробности см. в документации), и Git создает два новые дополнительные индексы (индексы?).
Все части Git имеют возможность перенаправить отдых 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 (сублимированный формат) копии файла, хранящейся в файле index. Вы можете заменить это в любое время на git add
.
Наконец, README.md
— это обычный (не Git-ифицированный) файл. Это не в Git! Его нет в индексе! Он находится в вашем рабочее дерево, где вы можете его увидеть и поработать с ним, используя все ваши обычные компьютерные инструменты. Git действительно ни для чего не использует этот файл, он просто перезаписывает его или удаляет, когда вы проверяете какой-либо другой коммит. Единственное, что Git делает с ним, кроме проверки с помощью git status
и тому подобного, — это позволяет вам использовать git add
для копировать обратно в индекс, перезаписывая то, что было там раньше (и лиофилизируя в процессе).
Бег git status
запускается два git diff
s:
Первый сравнивает коммит HEAD
с индексом, т. е. что в текущем коммите и что в предложенном следующем коммите. Все разные здесь указано как подготовлен для фиксации. Все то же самое, Git просто тихо ничего не говорит.
Второй git diff
сравнивает индекс с рабочим деревом, т. е. то, что находится в предлагаемом коммите, и то, что вы можете скопировать в индекс. Все разные здесь указано как не подготовлен для фиксации. Все то же самое, опять же, Git спокойно ничего не говорит.
(Затем есть последний проход для проверки файлов в рабочем дереве, которых вообще нет в индексе. Git будет жаловаться на них, говоря, что они не отслеживаются, если вы не перечислите их в .gitignore
. Наличие в списке .gitignore
не не изменить, есть ли копия файла в индексе, он просто меняет, скулит ли Git.)
Когда вы запускаете git commit
, Git упаковывает все, что находится в индексе, и использует это для создания нового коммита... пока не вы используете --only
, --include
или -a
.
С помощью 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 заменяет обычный индекс индексом второй (эта замена происходит через системный вызов rename
). В противном случае Git возвращается к обычному индексу. (Обратите внимание, что с рабочим деревом вообще ничего не происходит.)
Если вы используете git commit --include
или git commit -a
, Git создает дополнительный индекс только один, так что у вас есть:
.git/index
со всем, что вы добавили до сих пор; а такжеЗатем 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
, вы также должны обновить индекс второй, который использует git commit --only
. Эта часть сложна, потому что нет простого способа найти ее, кроме как предположить, что она находится в .git/index.lock
. Хотя это может сработать, я не буду советовать это здесь.
У меня действительно нет предложений по этому поводу — любой хитрый метод, который вы найдете, может сломаться, так как код для работы с этим третьим индексом (который в текущей версии 2.21 Git называется «ложным индексом») сильно изменился между 1.8 и современным Git. Обычно рекомендуется использовать нет для любого специального форматирования в хуке Git. Вместо этого попросите хук Git просто проверить будь то индексную копию файла на правильность форматирования: если это так, продолжите фиксацию, а если нет, прервите фиксацию. Остальное оставьте пользователю.
Альтернативой, которую я видел и использовал, является проверка фактической настройки $GIT_INDEX_FILE
. Если установлено значение .git/index
, пользователь использует git commit
без каких-либо специальных настроек. Еще один трюк в тот же хук перед фиксацией (который вызывает clang-format и autopep8) заключается в сравнении индекса и рабочего дерева для файлов, которые будут отформатированы, и отказе от запуска, если они не совпадают.
@EricRoller: интересно, хук, который я использовал (в общедоступном репо), просто проверяет .git/index.lock
и работал у меня в прошлом с простым git commit
. Это может быть еще одним отличием между 1.8 и 2.x.
Только что протестировано с 2.13.5, и $GIT_INDEX_FILE
в начале хука предварительной фиксации стоит .git/index
. Разве это не согласуется с вашим ответом, где вы сказали: You're doing a normal commit. GIT_INDEX_FILE names the normal index, and everything is normal.
Да, это имеет смысл. Я просто сообщаю, что находится в этом общедоступном репозитории Git. Может я что-то пропустил: вот собственно хук: github.com/NetSys/bess/blob/master/.hooks/pre-commit... Ага, это проверка на .git/index
не .git/index.lock
(где я тогда взял index.lock?!). Исправлю ответ выше.
Поскольку хук форматирования перед фиксацией работает, как и ожидалось, с git commit -a
, я думаю, что позволю индексу быть либо .git/index
, либо .git/index.lock
и просто потерплю неудачу в противном случае.
If it's set to .git/index.lock, the user is using git commit without any special settings
- Со стандартнымgit commit
я вижу, что он установлен на.git/index
, а соgit commit -a
я вижу, что он установлен на.git/index.lock
. Сgit commit --only
это какой-то временный файл (например, .git/next-index-100213.lock)