Git 'pre-receive' hook и 'git -clang-format' script, чтобы надежно отклонить нажатие, нарушающее условные обозначения кода

Позвольте сразу начать с отлома крюка pre-receive, который я уже написал:

#!/bin/sh
##
  format_bold='\033[1m'
   format_red='\033[31m'
format_yellow='\033[33m'
format_normal='\033[0m'
##
  format_error="${format_bold}${format_red}%s${format_normal}"
format_warning="${format_bold}${format_yellow}%s${format_normal}"
##
stdout() {
  format="${1}"
  shift
  printf "${format}" "${@}"
}
##
stderr() {
  stdout "${@}" 1>&2
}
##
output() {
  format="${1}"
  shift
  stdout "${format}\n" "${@}"
}
##
error() {
  format="${1}"
  shift
  stderr "${format_error}: ${format}\n" 'error' "${@}"
}
##
warning() {
  format="${1}"
  shift
  stdout "${format_warning}: ${format}\n" 'warning' "${@}"
}
##
die() {
  error "${@}"
  exit 1
}
##
git() {
  command git --no-pager "${@}"
}
##
list() {
  git rev-list "${@}"
}
##
clang_format() {
  git clang-format --style='file' "${@}"
}
##
while read sha1_old sha1_new ref; do
  case "${ref}" in
  refs/heads/*)
    branch="$(expr "${ref}" : 'refs/heads/\(.*\)')"
    if [ "$(expr "${sha1_new}" : '0*$')" -ne 0 ]; then # delete
      unset sha1_new
      # ...
    else # update
      if [ "$(expr "${sha1_old}" : '0*$')" -ne 0 ]; then # create
        unset sha1_old
        sha1_range="${sha1_new}"
      else
        sha1_range="${sha1_old}..${sha1_new}"
        # ...
        fi
      fi
      # ...
             GIT_WORK_TREE="$(mktemp --tmpdir -d 'gitXXXXXX')"
      export GIT_WORK_TREE
             GIT_DIR="${GIT_WORK_TREE}/.git"
      export GIT_DIR
      mkdir -p "${GIT_DIR}"
      cp -a * "${GIT_DIR}/"
      ln -s "${PWD}/../.clang-format" "${GIT_WORK_TREE}/"
      error=
      for sha1 in $(list "${sha1_range}"); do
        git checkout --force "${sha1}" > '/dev/null' 2>&1
        if [ "$(list --count "${sha1}")" -eq 1 ]; then
          # What should I put here?
        else
          git reset --soft 'HEAD~1' > '/dev/null' 2>&1
        fi
        diff="$(clang_format --diff)"
        if [ "${diff%% *}" = 'diff' ]; then
          error=1
          error '%s: %s\n%s'                                                   \
                'Code style issues detected'                                   \
                "${sha1}"                                                      \
                "${diff}"                                                      \
                1>&2
        fi
      done
      if [ -n "${error}" ]; then
        die '%s' 'Code style issues detected'
      fi
    fi
    ;;
  refs/tags/*)
    tag="$(expr "${ref}" : 'refs/tags/\(.*\)')"
    # ...
    ;;
  *)
    # ...
    ;;
  esac
done
exit 0

Примечание:
Места с нерелевантным кодом обрезаются с помощью # ....

Примечание:
Если вы не знакомы с git-clang-format, посмотрите здесь.

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

Во-первых, обратите внимание, что я делаю копию открытого (серверного) годового репозитория в какой-то временный каталог и проверяю там код для анализа. Позвольте мне объяснить намерение этого. Обратите внимание, что я делаю несколько git checkout и git reset (из-за цикла for), чтобы проанализировать все подталкиваемые ревизии по отдельности с помощью git-clang-format. То, что я пытаюсь избежать здесь, - это (возможно) concurrency проблема с push-доступом к удаленному (серверному) репозиторию. То есть, у меня создается впечатление, что если несколько разработчиков попытаются одновременно нажать на удаленный компьютер с этим установленным крюком pre-receive, это может вызвать проблемы, если каждый из этих push-сессий не выполняет git checkout и git reset с его частной копией репозитория. Итак, проще говоря, имеет ли git-daemon встроенное управление блокировкой для одновременных push-сессий? Будет ли он выполнять соответствующие экземпляры крюка pre-receive строго последовательно или есть возможность чередования (что может потенциально вызвать поведение undefined)? Что-то подсказывает мне, что для этой проблемы должно быть встроенное решение с конкретными гарантиями, в противном случае, как бы удаленные работы вообще (даже без сложных перехватов) подвергались одновременным нажатиям? Если есть такое встроенное решение, то копия избыточна и просто повторное использование голого репозитория фактически ускорит обработку. Кстати, любая ссылка на официальную документацию по этому вопросу очень приветствуется.

Во-вторых, git-clang-format обрабатывает только поэтапные (но не зафиксированные) изменения по сравнению с определенным commit (HEAD по умолчанию). Таким образом, вы можете легко увидеть, где находится угол. Да, он с корнем совершает (ревизии). На самом деле, git reset --soft 'HEAD~1' не может применяться к корневым записям, поскольку у них нет родителей с reset to. Следовательно, следующая проверка с моим вторым вопросом:

        if [ "$(list --count "${sha1}")" -eq 1 ]; then
          # What should I put here?
        else
          git reset --soft 'HEAD~1' > '/dev/null' 2>&1
        fi

Я пробовал git update-ref -d 'HEAD', но это разбивает репозиторий таким образом, что git-clang-format больше не может его обрабатывать. Я полагаю, что это связано с тем, что все эти подталкиваемые ревизии, которые анализируются (включая этот корень), пока не принадлежат ни одной отрасли. То есть они находятся в состоянии HEAD. Было бы неплохо найти решение этого углового случая, так что начальные коммиты также могут пройти ту же проверку с помощью git-clang-format для соответствия соглашениям о стиле кода.

Мир.

Ответ 1

Примечание:
Для тех, кто ищет обновленное (более или менее) комплексное и проверенное решение, я размещаю соответствующий публичный репозиторий [1]. В настоящее время реализованы два важных крючка, основанных на git-clang-format: pre-commit и pre-receive. В идеале вы получаете максимальную автоматизацию и безупречный рабочий процесс при одновременном использовании обоих. Как обычно, предложения по улучшению приветствуются.

Примечание:
В настоящее время крюк pre-commit [1] требует патча git-clang-format.diff (также созданного мной) [1] для применения к git-clang-format. Примеры мотивации и использования для этого патча обобщены в официальном сообщении об обзоре исправлений для LLVM/Clang [2]. Надеюсь, он будет принят и вскоре будет объединен вверх по течению.


Мне удалось реализовать решение для второго вопроса. Я должен признать, что его было непросто найти из-за скудной документации Git и отсутствия примеров. Сначала рассмотрим соответствующие изменения кода:

# ...
clang_format() {
  git clang-format --commit="${commit}" --style='file' "${@}"
}
# ...
      for sha1 in $(list "${sha1_range}"); do
        git checkout --force "${sha1}" > '/dev/null' 2>&1
        if [ "$(list --count "${sha1}")" -eq 1 ]; then
          commit='4b825dc642cb6eb9a060e54bf8d69288fbee4904'
        else
          commit='HEAD~1'
        fi
        diff="$(clang_format --diff)"
        # ...
      done
      # ...

Как вы можете видеть, вместо повторного выполнения git reset --soft 'HEAD~1' я теперь явно инструктирую git-clang-format работать с HEAD~1 с опцией --commit (тогда как по умолчанию это HEAD, что подразумевалось в исходной версии представленный в моем вопросе). Тем не менее, это все еще не решает проблему самостоятельно, потому что, когда мы нажмем на root commit, это снова приведет к ошибке, поскольку HEAD~1 больше не будет ссылаться на действительную ревизию (аналогично тому, как было бы невозможно сделать git reset --soft 'HEAD~1'). Вот почему для этого конкретного случая я инструктирую git-clang-format действовать против хэша 4b825dc642cb6eb9a060e54bf8d69288fbee4904 [3, 4, 5, 6]. Чтобы узнать больше об этом хеше, обратитесь к ссылкам, но, вкратце, он относится к Git пустому древовидному объекту - тому, который ничего не поставил или не зафиксировал, что именно то, что нам нужно git-clang-format для работы в нашем случай.

Примечание:
Вам не нужно запоминать 4b825dc642cb6eb9a060e54bf8d69288fbee4904 наизусть, и лучше не жестко его кодировать (на всякий случай, если этот волшебный хэш когда-либо изменится в будущем). Оказывается, его всегда можно найти с помощью git hash-object -t tree '/dev/null' [5, 6]. Таким образом, в моей последней версии выше pre-receive hook у меня есть commit="$(git hash-object -t tree '/dev/null')".

P.S. Я все еще ищу хороший ответ на свой первый вопрос. Кстати, я задал эти вопросы в официальном списке рассылки Git и до сих пор не получил ответов, какой позор...