Тестирование того, что должно быть зафиксировано в хуке предварительной фиксации

Интернет абсолютно завален неправильными и неидеальными ответами на этот вопрос. Это прискорбно, потому что вы думаете, что это будет обычным делом, которое вы хотели бы сделать.

Проблема: когда pre-commit Хук работает, хранилище может быть не чистым. Поэтому, если вы будете наивно запускать свои тесты, они будут не против того, что вы делаете, а из-за грязи в вашем рабочем дереве.

Очевидная вещь, которую нужно сделать, это git stash --keep-index --include-untracked в начале pre-commit а также git pop на выходе. Таким образом, вы тестируете по (чистому) индексу, чего мы и хотим.

К сожалению, это генерирует маркеры конфликта слияния, если вы используете git add --patch(особенно если вы редактируете фрагменты), так как содержимое stash@{0} может не совпадать с рабочим деревом после коммита.

Другим распространенным решением является клонирование репозитория и запуск тестов в новом временном. С этим есть две проблемы: во-первых, мы еще не зафиксировали, поэтому мы не можем легко получить копию хранилища в состоянии, которое мы собираемся зафиксировать (я уверен, что есть способ сделать это, но мне это не интересно, потому что:). Во-вторых, мои тесты могут быть чувствительны к местоположению текущего рабочего каталога. Например, из-за локальной конфигурации среды.

Итак: Как я могу восстановить свое рабочее дерево до того состояния, в котором оно находилось до git stash --keep-index --include-untrackedбез введения маркеров конфликта слияния и без изменения пост-фиксации HEAD?

6 ответов

Git Write-Tree полезен в pre-commit крючки. Он записывает дерево в репо индекса (это дерево будет использовано повторно, если и когда фиксация будет завершена.)

После того, как дерево записано в репо, вы можете использовать git archive | tar -x записать дерево во временный каталог.

Например:

#!/bin/bash

TMPDIR=$(mktemp -d)
TREE=$(git write-tree)
git archive $TREE | tar -x -C $TMPDIR

# Run tests in $TMPDIR

RESULT=$?
rm -rf "$TMPDIR"
exit $RESULT

Если клонирование всего репо слишком дорого, возможно, вам просто нужна копия рабочего каталога. Сделать копию будет проще, чем пытаться справиться с конфликтами. Например:

#!/bin/sh -e

trap 'rm -rf $TMPD' 0
mkdir ${TMPD=$PWD/.tmpdir}
git ls-tree -r HEAD | while read mod type sha name; do
    if test "$type" = blob; then
        mkdir -p $TMPD/$( dirname "$name" ) 
        git show $sha > $TMPD/"$name";
        chmod $mod $TMPD/"$name"
    fi
done
cd $TMPD
git diff --cached HEAD | patch
# Run tests here

Это выведет состояние дерева в том виде, в каком оно будет после коммита в $TMPD, поэтому вы можете запускать там свои тесты. Вы должны получить временный каталог более безопасным способом, чем здесь, но для того, чтобы последний diff работал (или для упрощения сценария и cd ранее), он должен быть дочерним по отношению к рабочему каталогу.

Если вы можете позволить себе использовать временный каталог (т.е. сделать полную копию текущей проверки), вы можете использовать временный каталог следующим образом:

tmpdir=$(mktemp -d) # Or put it wherever you like
git archive HEAD | tar -xf - -C "$tmpdir"
git diff --staged | patch -p1 -d "$tmpdir"
cd "$tmpdir"
...

Это в основном решение Уильяма Перселла, но оно использует преимущества git archive что делает код проще, и я ожидаю, что будет быстрее.

В качестве альтернативы, сначала cd'ing:

cd somewhere
git -C path/to/repo archive HEAD | tar -xf -
git -C path/to/repo diff --staged | patch -p1
...

git -C требует Git 1.8.5.

Я нашел следующее, чтобы быть полезным:

## bash declare -a files readarray -t files < <(git status --porcelain | perl -ane 'print $F[1],qq(\n) if m/^[ACM] /') # declare -a delfiles readarray -t delfiles < <(git status --porcelain | perl -ane 'print $F[1],qq(\n) if m/^D /') # declare -a huhfiles readarray -t huhfiles < <(git status --porcelain | perl -ane 'print $F[1],qq(\n) if m/^\? /')

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

Вам нужно протестировать именно то, что фиксируется, а не весь рабочий каталог. Такие решения, как сохранение, могут привести к конфликтам слияния, особенно при редактировании фрагментов.
Решение не должно нарушать существующий рабочий каталог и должно восстанавливать его исходное состояние после тестирования. И ваши тесты могут зависеть от среды и конфигурации текущего рабочего каталога.

Метод Гэри ван предполагает запись индекса в дерево с помощью git write-tree, а затем извлеките его во временный каталог. Он эффективно изолирует содержимое коммита для тестирования, не затрагивая рабочий каталог. Метод
user1249680 аналогичен по своей концепции, он использует git archive HEADчтобы создать снимок последнего коммита, а затем применить поэтапные изменения с помощью git diff --staged | patch.

Учитывая ваши требования, метод, предложенный user72911 кажется наиболее подходящим. Он четко изолирует изменения для тестирования, не затрагивая ваш рабочий каталог или среду. Это эффективно и менее подвержено ошибкам по сравнению с множественным сохранением и удалением.

Более подробный сценарий для перехватчика перед фиксацией будет выглядеть так:

      #!/bin/bash

# Create a temporary directory
TMPDIR=$(mktemp -d)
# Write the index to a tree
TREE=$(git write-tree)
# Extract the tree to the temporary directory
git archive $TREE | tar -x -C $TMPDIR

# Change to the temporary directory and run tests
pushd $TMPDIR
# Run your tests here
TEST_RESULT=$?
popd

# Clean up the temporary directory
rm -rf "$TMPDIR"

# Exit with the test result status
exit $TEST_RESULT

Для больших репозиториев этот метод эффективен, поскольку позволяет избежать дублирования всего рабочего каталога.
Поскольку тесты запускаются в структуре, аналогичной рабочему каталогу, это должно свести к минимуму проблемы, связанные с чувствительностью среды.
Этот метод не нарушает исходный рабочий каталог, сохраняя состояние перед фиксацией без дополнительных действий.
Сценарий можно адаптировать к различным сценариям тестирования, и он достаточно универсален для различных настроек проекта.

      ┌──────────────────┐    copy    ┌────────────────────────┐
│ Git Repository   ├───────────►│ Temporary Staging Area │
│ Index with       │            │ (temp_dir)             │
│ Staged Changes   │            └────────────────────────┘
└──────────────────┘                │
    │                               │
    │                               │ run tests
    ▼                               ▼
┌──────────────────┐            ┌────────────────────────┐
│ Working Directory│            │ Tests Results          │
│ (untouched)      │            └────────────────────────┘
└──────────────────┘

Но вы также можете использовать еще более прямой (но похожий) подход с :

      #!/bin/bash

# Create a temporary directory
temp_dir=$(mktemp -d)

# Copy the staged changes to the temporary directory
git checkout-index --all --prefix=${temp_dir}/

# Change to the temporary directory and run tests
pushd ${temp_dir}
# Run your tests here
popd

# Clean up the temporary directory
rm -rf ${temp_dir}

# Exit with the test run status

The Команда используется для копирования файлов из индекса (промежуточной области) в рабочий каталог или другое место. В этом контексте это позволяет вам создать копию именно того, что подготовлено для фиксации.

Это значит:

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

Я наконец нашел решение, которое искал. Проверяется только состояние индекса перед коммитом, и он оставляет индекс и рабочее дерево в точности так, как это было до коммита.

Если вы видите какие-либо проблемы или лучший способ, пожалуйста, ответьте, либо как комментарий, либо как ваш собственный ответ.

Это предполагает, что ничто иное не будет пытаться спрятать или иным образом изменить репозиторий git или рабочее дерево во время его работы. Это приходит без гарантии, может быть неправильно и выбросить ваш код на ветер. ИСПОЛЬЗУЙТЕ С ОСТОРОЖНОСТЬЮ.

# pre-commit.sh
REPO_PATH=$PWD
git stash save -q --keep-index --include-untracked # (stash@{1})
git stash save -q                                  # (stash@{0})

# Our state at this point:
# * clean worktree
# * stash@{0} contains what is to be committed
# * stash@{1} contains everything, including dirt

# Now reintroduce the changes to be committed so that they can be tested
git stash apply stash@{0} -q

git_unstash() {
    G="git --work-tree \"$REPO_PATH\" --git-dir \"$REPO_PATH/.git\"" 
    eval "$G" reset -q --hard             # Clean worktree again
    eval "$G" stash pop -q stash@{1}      # Put worktree to original dirty state
    eval "$G" reset -q stash@{0} .        # Restore index, ready for commit
    eval "$G" stash drop -q stash@{0}     # Clean up final remaining stash
}
trap git_unstash EXIT

... tests against what is being committed go here ...
Другие вопросы по тегам