Как правильно добавить stash/pop в хуки pre-commit, чтобы получить чистое рабочее дерево для тестов?
Я пытаюсь выполнить ловушку перед фиксацией с пустым запуском модульных тестов и хочу убедиться, что мой рабочий каталог чист. Компиляция занимает много времени, поэтому я хочу воспользоваться возможностью повторного использования скомпилированных двоичных файлов, когда это возможно. Мой сценарий соответствует примерам, которые я видел в Интернете:
# Stash changes
git stash -q --keep-index
# Run tests
...
# Restore changes
git stash pop -q
Это вызывает проблемы, хотя. Вот репродукция:
- добавлять
// Step 1
вa.java
git add .
- добавлять
// Step 2
вa.java
git commit
git stash -q --keep-index
# Копить изменения- Запустить тесты
git stash pop -q
# Восстановить изменения
В этот момент я столкнулся с проблемой. git stash pop -q
по-видимому, имеет конфликт и в a.java
я имею
// Step 1
<<<<<<< Updated upstream
=======
// Step 2
>>>>>>> Stashed changes
Есть ли способ, чтобы заставить это всплыть чисто?
4 ответа
Есть, но давайте доберемся немного окольным путем. (Также см. Предупреждение ниже: в коде тайника есть ошибка, которая, как мне показалось, была очень редкой, но, видимо, все больше людей сталкиваются.)
git stash save
(действие по умолчанию для git stash
) делает коммит, у которого есть по крайней мере два родителя (см. этот ответ на более простой вопрос о тайниках). stash
commit - это состояние рабочего дерева, а второй родительский коммит stash^2
это индексное состояние во время тайника.
После того, как тайник сделан (и при условии, что нет -p
вариант), сценарийgit stash
сценарий оболочки - использует git reset --hard
очистить изменения.
Когда вы используете --keep-index
, скрипт никак не изменяет сохраненный тайник. Вместо этого после git reset --hard
операция, скрипт использует дополнительный git read-tree --reset -u
стереть изменения рабочего каталога, заменив их "индексной" частью тайника.
Другими словами, это почти как делать:
git reset --hard stash^2
Кроме этого git reset
также переместит ветку - совсем не то, что вы хотите, следовательно, read-tree
метод вместо.
Это где ваш код возвращается. Вы сейчас # Run tests
на содержание индекса коммита.
Предполагая, что все идет хорошо, я полагаю, что вы хотите вернуть индекс в состояние, в котором он находился git stash
и верните рабочее дерево в его состояние.
С git stash apply
или же git stash pop
способ сделать это использовать --index
(не --keep-index
это просто время создания stash, чтобы сказать скрипту stash "удар по рабочему каталогу").
Просто используя --index
все равно потерпит неудачу, потому что --keep-index
повторно применил изменения индекса к рабочему каталогу. Таким образом, вы должны сначала избавиться от всех этих изменений... и для этого вам просто нужно (повторно) запустить git reset --hard
так же, как сам скрипт stash сделал ранее. (Возможно, вы тоже хотите -q
.)
Итак, это дает в качестве последнего # Restore changes
шаг:
# Restore changes
git reset --hard -q
git stash pop --index -q
(Я бы выделил их как:
git stash apply --index -q && git stash drop -q
сам, просто для ясности, но pop
сделаю тоже самое)
Как отмечено в комментарии ниже, окончательный git stash pop --index -q
немного жалуется (или, что еще хуже, восстанавливает старый тайник), если начальный git stash save
шаг не находит изменений для сохранения. Поэтому вы должны защитить шаг "восстановления" с помощью теста, чтобы увидеть, действительно ли шаг "сохранения" что-то спрятал.
Начальный git stash --keep-index -q
просто тихо завершает работу (со статусом 0), когда ничего не делает, поэтому нам нужно обработать два случая: не существует тайника ни до, ни после сохранения; и некоторое хранилище существовало до сохранения, и сохранение ничего не делало, поэтому старый существующий хранилище по-прежнему является вершиной стека хранилища.
Я думаю, что самый простой способ заключается в использовании git rev-parse
выяснить что refs/stash
имена, если что. Так что нам нужно, чтобы скрипт читал что-то вроде этого:
#! /bin/sh
# script to run tests on what is to be committed
# First, stash index and work dir, keeping only the
# to-be-committed changes in the working directory.
old_stash=$(git rev-parse -q --verify refs/stash)
git stash save -q --keep-index
new_stash=$(git rev-parse -q --verify refs/stash)
# If there were no changes (e.g., `--amend` or `--allow-empty`)
# then nothing was stashed, and we should skip everything,
# including the tests themselves. (Presumably the tests passed
# on the previous commit, so there is no need to re-run them.)
if [ "$old_stash" = "$new_stash" ]; then
echo "pre-commit script: no changes to test"
sleep 1 # XXX hack, editor may erase message
exit 0
fi
# Run tests
status=...
# Restore changes
git reset --hard -q && git stash apply --index -q && git stash drop -q
# Exit with status from test-run: nonzero prevents commit
exit $status
предупреждение: небольшая ошибка в git stash
На пути есть небольшая ошибка git stash
пишет свою "шкатулку". Хранилище состояния индекса корректно, но предположим, что вы делаете что-то вроде этого:
cp foo.txt /tmp/save # save original version
sed -i '' -e '1s/^/inserted/' foo.txt # insert a change
git add foo.txt # record it in the index
cp /tmp/save foo.txt # then undo the change
Когда ты бежишь git stash save
после этого index-commit (refs/stash^2
) имеет вставленный текст в foo.txt
, Фиксация дерева работ (refs/stash
) должна иметь версию foo.txt
без дополнительных вставленных вещей. Если вы посмотрите на него, вы увидите, что у него неправильная (модифицированная индексом) версия.
Сценарий выше использует --keep-index
чтобы настроить рабочее дерево в соответствии с индексом, что вполне нормально и правильно подходит для запуска тестов. После запуска тестов он использует git reset --hard
вернуться к HEAD
состояние фиксации (что все еще прекрасно) ... и затем он использует git stash apply --index
восстановить индекс (который работает) и рабочий каталог.
Вот где это идет не так. Индекс (правильно) восстанавливается из фиксации индекса stash, но рабочий каталог восстанавливается из фиксации рабочего каталога stash. Этот коммит рабочей директории имеет версию foo.txt
это в индексе. Другими словами, последний шаг -cp /tmp/save foo.txt
- это отменило изменение, не было сделано!
(Ошибка в stash
сценарий происходит потому, что сценарий сравнивает состояние рабочего дерева с HEAD
commit, чтобы вычислить набор файлов для записи в специальный временный индекс перед тем, как сделать специальный work-dir коммитом из stash-bag. поскольку foo.txt
без изменений по отношению к HEAD
не удается git add
это к специальному временному индексу. Затем выполняется специальная фиксация по рабочему дереву с помощью версии index-commit foo.txt
, Исправление очень простое, но никто не поместил его в официальный git [пока?].
Не то чтобы я хотел побуждать людей модифицировать свои версии git, но вот исправление.)
Благодаря ответу @torek мне удалось собрать скрипт, который также работает с неотслеживаемыми файлами. (Примечание: я не хочу использовать git stash -u
из-за нежелательного поведения git stash -u)
Упомянутый git stash
ошибка остается неизменной, и я еще не уверен, может ли этот метод столкнуться с проблемами, когда.gitignore находится среди измененных файлов. (то же самое относится и к ответу @ torek)
#! /bin/sh
# script to run tests on what is to be committed
# Based on http://stackru.com/a/20480591/1606867
# Remember old stash
old_stash=$(git rev-parse -q --verify refs/stash)
# First, stash index and work dir, keeping only the
# to-be-committed changes in the working directory.
git stash save -q --keep-index
changes_stash=$(git rev-parse -q --verify refs/stash)
if [ "$old_stash" = "$changes_stash" ]
then
echo "pre-commit script: no changes to test"
sleep 1 # XXX hack, editor may erase message
exit 0
fi
#now let's stash the staged changes
git stash save -q
staged_stash=$(git rev-parse -q --verify refs/stash)
if [ "$changes_stash" = "$staged_stash" ]
then
echo "pre-commit script: no staged changes to test"
# re-apply changes_stash
git reset --hard -q && git stash pop --index -q
sleep 1 # XXX hack, editor may erase message
exit 0
fi
# Add all untracked files and stash those as well
# We don't want to use -u due to
# http://blog.icefusion.co.uk/git-stash-can-delete-ignored-files-git-stash-u/
git add .
git stash save -q
untracked_stash=$(git rev-parse -q --verify refs/stash)
#Re-apply the staged changes
if [ "$staged_stash" = "$untracked_stash" ]
then
git reset --hard -q && git stash apply --index -q stash@{0}
else
git reset --hard -q && git stash apply --index -q stash@{1}
fi
# Run tests
status=...
# Restore changes
# Restore untracked if any
if [ "$staged_stash" != "$untracked_stash" ]
then
git reset --hard -q && git stash pop --index -q
git reset HEAD -- . -q
fi
# Restore staged changes
git reset --hard -q && git stash pop --index -q
# Restore unstaged changes
git reset --hard -q && git stash pop --index -q
# Exit with status from test-run: nonzero prevents commit
exit $status
Кажется, что большинству ответов здесь, в настоящее время, не менее 5 лет.
Я использовал это несколько раз, и, кажется, это работает — я написал это с нуля, просто используя документы в качестве руководства. Не прошел боевые испытания.
# We stash and un-stash changes ourselves.
# - If any pre-commit/lint-staged checks fail, any auto-fixes will be lost.
# Create stash
# index is the "staging area", so --keep-index means that anything you have already staged will be un-touched.
# NOTE: we always create a stash - possibly even a totally empty one.
git stash --keep-index --include-untracked --message="pre-commit auto-stash"
uncoloredStashedStat=$(git stash show --include-untracked stash@{0})
[[ $uncoloredStashedStat ]] && {
echo "Stashed:"
git diff --stat --staged stash@{0}
}
lintStagedStatus='failed'
yarn lint-staged --no-stash --concurrent $pre_commit_concurrency --shell "/bin/bash" && {
lintStagedStatus='passed'
}
outputSuppressed=$(git add --intent-to-add "**/*.snap")
diff=$(git diff)
[[ $diff ]] && {
echo "staging modifications from pre-commit scripts:"
git diff
git add .
}
# Pop stash
# We always create a stash - so we will always pop it.
# Popped stash should generally not cause merge conflicts,
# if your editor is formatting+autofixing code on save.
[[ $uncoloredStashedStat ]] && echo "restoring stash..."
git stash pop
if test "$lintStagedStatus" != 'passed'; then
exit 1;
fi
Основываясь на ответе Торека, я придумал метод, обеспечивающий правильное поведение при сохранении изменений без использования git rev-parse, вместо этого я использовал git stash create и git stash store (хотя использование git stash store не является строго обязательным) среда, в которой я работаю, мой скрипт написан на php вместо bash
#!/php/php
<?php
$files = array();
$stash = array();
exec('git stash create -q', $stash);
$do_stash = !(empty($stash) || empty($stash[0]));
if($do_stash) {
exec('git stash store '.$stash[0]); //store the stash (does not tree state like git stash save does)
exec('git stash show -p | git apply --reverse'); //remove working tree changes
exec('git diff --cached | git apply'); //re-add indexed (ready to commit) changes to working tree
}
//exec('git stash save -q --keep-index', $stash);
exec('git diff-index --cached --name-only HEAD', $files );
// dont redirect stderr to stdin, we will get the errors twice, redirect it to dev/null
if ( PHP_OS == 'WINNT' )
$redirect = ' 2> NUL';
else
$redirect = ' 2> /dev/null';
$exitcode = 0;
foreach( $files as $file ) {
if ( !preg_match('/\.php$/i', $file ) )
continue;
exec('php -l ' . escapeshellarg( $file ) . $redirect, $output, $return );
if ( !$return ) // php -l gives a 0 error code if everything went well
continue;
$exitcode = 1; // abort the commit
array_shift( $output ); // first line is always blank
array_pop( $output ); // the last line is always "Errors parsing httpdocs/test.php"
echo implode("\n", $output ), "\n"; // an extra newline to make it look good
}
if($do_stash) {
exec('git reset --hard -q');
exec('git stash apply --index -q');
exec('git stash drop -q');
}
exit( $exitcode );
?>
Скрипт php адаптирован здесь http://blog.dotsamazing.com/2010/04/ask-git-to-check-if-your-codes-are-error-free/