Подтвердить что ты не робот

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

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

# 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

Есть ли способ получить это поп чисто?

4b9b3361

Ответ 1

Есть - но давайте туда немного круговым способом. (Также см. Предупреждение ниже: есть ошибка в кошельке, который, как я думал, был очень редок, но, видимо, больше людей сталкиваются.)

git stash save (действие по умолчанию для git stash) делает фиксацию, у которой есть как минимум два родителя (см. этот ответ на более простой вопрос о ставках), Конец stash - это состояние дерева работы, а второй parent commit stash^2 - это индексное состояние во время сохранения.

После того, как задержка сделана (и если нет опции -p), script - git stash является оболочкой script -uses git reset --hard, чтобы очистить изменения.

Когда вы используете --keep-index, script не меняет сохраненный штамп. Вместо этого, после операции git reset --hard, script использует дополнительный 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 (not --keep-index, который только для времени создания тайника, чтобы сообщить stash script "удар в рабочем каталоге" ).

Просто использование --index все равно будет работать, потому что --keep-index повторно применил изменения индекса в рабочий каталог. Поэтому вы должны сначала избавиться от всех этих изменений... и для этого вам просто нужно (пере) запустить git reset --hard, как и сам стат script. (Возможно, вы также хотите -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, если что угодно. Поэтому мы должны иметь script читать что-то большее:

#! /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 без дополнительных вставленных файлов. Если вы посмотрите на это, вы увидите, что у него есть неправильная (с индексом) версия.

В приведенном выше примере script используется --keep-index, чтобы получить рабочее дерево, настроенное как индекс, и все это прекрасно, и делает правильную вещь для запуска тестов. После запуска тестов он использует git reset --hard, чтобы вернуться в состояние фиксации HEAD (это все еще отлично)... и затем использует git stash apply --index для восстановления индекса (который работает) и рабочего каталога.

Здесь все идет не так. Индекс (правильно) восстанавливается из фиксации индекса stash, но рабочий каталог восстанавливается из фиксации work-directory stash. В этой команде work-directory есть версия foo.txt, которая указана в индексе. Другими словами, этот последний шаг - cp /tmp/save foo.txt - то, что сменил изменение, не был отменен!

(Ошибка в stash script возникает из-за того, что script сравнивает состояние дерева работы с фиксацией HEAD, чтобы вычислить набор файлов для записи в специальном временном индексе перед тем, как сделать специальная команда work-dir фиксирует часть суммарного пакета. Поскольку foo.txt не изменяется по отношению к HEAD, он не может git add его использовать специальный временный индекс. Затем выполняется специальное компиляционное дерево с индексом -компилировать версию foo.txt. Исправление очень просто, но никто не разместил его в официальном git [еще?].

Не хочу, чтобы люди поощряли изменения своих версий git, но здесь исправление.)

Ответ 2

Благодаря ответу @torek я смог собрать script, который также имеет дело с необработанными файлами. (Примечание: я не хочу использовать 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://stackoverflow.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 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 [email protected]{0}
else
    git reset --hard -q && git stash apply --index -q [email protected]{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

Ответ 3

на основе ответа torek Я придумал метод для обеспечения правильного поведения изменений без с помощью git rev-parse, вместо этого я использовал git stash create и git stash store (хотя использование git хранится в хранилище не обязательно) Примечание из-за среды, в которой я работаю в моем script, написано в 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/