git pre-commit hook: запускать только код, который нужно зафиксировать?

Я хотел сделать хук перед фиксацией, который проверял бы, соответствует ли код Python pep8, поэтому я сделал это, как в https://www.stavros.io/posts/more-pep8-git-hooks/

Я создал .git/hooks/pre-commit файл. И добавил этот контент:

#!/bin/sh
flake8 .

Затем: chmod +x .git/hooks/pre-commit

Но когда я ввожу git commit, он фактически проверяет всю ветку, и если он находит в ветке что-то, что не следует за pep8, он завершается (этот репозиторий немного устарел, и какой-то код не следовал за pep8 с самого начала, так что я это знаю может быть рефакторинг, но мне не нужно pre-hook, чтобы сказать мне это об уже зафиксированном коде).

Как я могу сделать так, чтобы он проверял только код текущего коммита, который должен быть зафиксирован?


person Andrius    schedule 22.04.2016    source источник
comment
Не совсем отвечает на этот вопрос, но это обрабатывается в pre-commit.com. В этом проекте используется комбинация git diff --staged --name-only и git checkout + git apply (по сути тайник).   -  person Anthony Sottile    schedule 20.09.2016


Ответы (2)


Как я могу сделать так, чтобы он проверял только код текущего коммита, который должен быть зафиксирован?

Используйте git checkout-index для извлечения файлов, которые вы фиксируете, во временный каталог, а затем запустите flake8 в этом временном каталоге.

Сначала создадим временный каталог:

tmpdir=$(mktemp -d commitXXXXXX)
trap "rm -rf $tmpdir" EXIT

Затем извлеките файлы, которые нужно зафиксировать в этом каталоге:

git checkout-index --prefix=$tmpdir/ -af

Получите список файлов, измененных в этом коммите, и запустите для них flake8:

git diff --cached --name-only --diff-filter=ACM | grep '\.py$' |
(cd $tmpdir; xargs --no-run-if-empty flake8)
person larsks    schedule 22.04.2016
comment
Хм, наверное, мне следует изменить сценарий, который я только что написал, чтобы использовать git checkout-index, а не обычный git checkout (тогда не понадобится аргумент --work-tree). - person torek; 22.04.2016
comment
Я делаю что-то неправильно? Я добавил этот код вместо того, который я написал, но все же он проверяет больше, чем мой текущий коммит. Шаги, которые я сделал. Заменил хук pre-commit кодом, который вы написали, немного изменил один файл, чтобы он был против pep8, затем добавил файл, а затем написал git commit. Затем получил целую кучу предупреждений о том, что другие модули неверны. - person Andrius; 22.04.2016
comment
Это потому, что я торопился раньше и пропустил шаг. Все исправлено. - person larsks; 22.04.2016
comment
@larsks: В ситуациях, когда в репозитории git буквально 20 тысяч файлов, но коммиты состоят из 1-5 файлов (с некоторым стандартным отклонением, но вы понимаете, что я имею в виду), вы все еще думаете, что полный git checkout-index лучше всего? Или лучше просто запустить git show :{} (для каждого файла) в xargs? - person Mort; 27.04.2016
comment
Это может быть лучше. Репозитории, с которыми я работаю, как правило, не такие большие, а инструменты, которые я запускаю перед фиксацией, предпочитают работать с файлами на диске, а не со стандартным вводом. Либо должно работать. - person larsks; 27.04.2016

Во-первых, давайте ответим на вопрос, который вы на самом деле задали, потому что вам все равно нужно это знать:

Как я могу сделать так, чтобы он проверял только код текущего коммита, который должен быть зафиксирован?

Содержимое коммита, который будет сделан, — это то, что находится в индексе.

Это каждый файл, а не только файлы, которые вы недавно git addредактировали. Например, если в вашем рабочем дереве есть восемь файлов *.py Python с контролируемым исходным кодом, и вы изменили и git add отредактировали два из них, на этот раз будет зафиксировано восемь *.py файлов. Каждая фиксация содержит каждый файл, а не набор изменений из какого-то предыдущего файла(ов).1

Чтобы проверить эти восемь файлов (а не текущее содержимое рабочего дерева), вам нужно где-то извлечь содержимое индекса.

Конечно, это не то, что вы на самом деле хотите:

на самом деле он проверяет всю ветку, и если он находит в ветке что-то, что не следует за pep8, он прекращает работу (этот репозиторий немного устарел, и некоторый код не следует за pep8 с самого начала, поэтому я знаю, что его можно реорганизовать, но я не нужен предварительный хук, чтобы сообщить мне об уже зафиксированном коде)

Опять же, на самом деле он не проверяет ветвь; он проверяет рабочее дерево. Хотя это не поможет вам сразу добраться туда, куда вам нужно, это важно.

Здесь нам нужно извлечь, возможно, из индекса файлы, которые отличаются от текущего коммита.

Способ сделать это — сделать новый коммит. В идеале мы могли бы сделать эту фиксацию не в текущей ветке, а в другом месте, потому что, если мы сделаем это в ветке, позже нам придется отменить ее снова.

Есть команда, которая делает это — делает коммиты из индекса, но не из ветки — и это команда git stash. К сожалению, есть ошибка в git stash, когда вы используете его таким образом. Вместо того, чтобы использовать обходные пути, которые я описываю в этом ответе, мы можем сделать что-то другое: мы можем создать собственное дерево во временном каталоге после сравнения текущего индекса с коммитом HEAD.

Сценарий для этого (даже протестированный!) приведен ниже.


1Вы можете возразить, что git log -p или git show показывают вам изменения. Вы правы в том, что он показывает вам изменения, но делает это, запуская git diff против предыдущего коммита, который также содержит все файлы. Сравнивая предыдущую версию «всего» со следующей версией «всего», git обнаруживает, что изменилось.


#! /bin/sh

# run-checks: run some checking command(s) on a proposed commit.
#
# Optionally, run it only on files that differ from those in
# the current commit (added or modified, treating rename as
# modify), and/or do not run it at all if there are
# no such files (e.g., if the commit consists only of file
# removals).

usage()
{
    echo "usage: $0 [-d] checkcmd [args ...]" 1>&2
    exit 1
}

# probably should use git rev-parse feature now, oh well
diffmode=false
skipempty=true
while true; do
    case "$1" in
    -d|--diff) diffmode=true; shift;;
    -z|--run-even-if-empty) skipempty=false; shift;;
    -dz) diffmode=true; skipempty=false; shift;;
    *) break;;
    esac
done

case "$#" in
0) usage;;
esac

# from here on, exit on error
set -e

# get temporary directory and arrange to clean it up
tdir=$(mktemp -d -t run-checks)
trap "rm -rf $tdir" 0 1 2 3 15

# Get list of changed files (whether or not we are using
# only the changed files).  This includes deleted files.
# For efficiency, we treat renames as delete/add pairs here.
# Require that new commit not match current commit.
if test $(git diff --cached --name-only --no-renames HEAD | wc -l) -eq 0; then
    echo "no changes to test before committing"
    exit 1
fi

# Populate work tree in temp dir.  If we only want changed
# files, limit the checkout to files added or modified.  Note
# that this list might be empty.
if $diffmode; then
    git diff --cached --name-only --no-renames --diff-filter=AM -z HEAD |
        xargs -0 git --work-tree=$tdir checkout -f --
else
    git --work-tree=$tdir checkout -f -- .
fi

# Now run checker in temp work tree.  Our exit status is
# its exit status.  Do not use exec since we must still clean
# up the temp dir, and optionally skip checker if work tree is empty.
cd $tdir
if test $(ls -A | wc -l) -eq 0; then
    is_empty=true
else
    is_empty=false
fi
if $skipempty && $is_empty; then exit 0; fi

if $is_empty; then
    $@
else
    $@ *
fi
person torek    schedule 22.04.2016