Как правильно добавить stash/pop в хуки pre-commit, чтобы получить чистое рабочее дерево для тестов?

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

# Stash changes
git stash -q --keep-index

# Run tests
...

# Restore changes
git stash pop -q

Это вызывает проблемы, хотя. Вот репродукция:

  1. добавлять // Step 1 в a.java
  2. git add .
  3. добавлять // Step 2 в a.java
  4. git commit
    1. git stash -q --keep-index # Копить изменения
    2. Запустить тесты
    3. 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 лет. был переписан на C, появились новые ошибки, и я не знаю, насколько он надежен.

Я использовал это несколько раз, и, кажется, это работает — я написал это с нуля, просто используя документы в качестве руководства. Не прошел боевые испытания.

      # 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/

Другие вопросы по тегам