GitHooksのpre-pushを共有してレポジトリを健全に保つ

Tatsuya Asami
12 min readJul 26, 2019

今まで1人で小さい案件のフロントエンドを担当していましたが、引き継ぎや研修などで何人か入ってきたので、ヒューマンエラー防止に今まで試してみたかったGithubのpre-pushの設定をしてみました。

今回設定したのは

  1. master, developにpushする場合は ’本当にpushしますか?’ という確認が入る。yesと入力しないとpushを中止する。

2. pushをする前にdevelopブランチからmergeするか確認が入る。noと入力した場合のみスキップ。コンフリクトした場合はpushを中止。

3. pushをする前にユニットテストを実行する。noと入力した場合のみスキップ。テストに落ちたら ‘落ちたけどpushしますか?’ という確認が入る。yesと入力しないとpushを中止する。

4. pushをする前にE2Eテストを実行する。noと入力した場合のみスキップ。テストに落ちたら ‘落ちたけどpushしますか?’ という確認が入る。yesと入力しないとpushを中止する。

この4つ。

つまり思考停止でエンターを連打した場合

  • master, developにはpushされない
  • developからmergeしてコンフリクトが発生せず、単体テストとユニットテストをパスしたらpushされる

という設定。

コンセプトとしては

  • masterやdevelopにうっかりpushするのを防ぐ。(レポジトリには自動デプロイの設定がされている。)
  • masterやdevelopにmergeするときにコンフリクト起こるとだるいから、それぞれのブランチでコンフリクト解消する。
  • テスト通らなかったpushさせないとは言わないけど、実行忘れは防ぎたい。

といったところ。

下記コマンドでhooksをスキップする。

git push--no-verify

しかし、これを使われるとhooksの意味が半減する。(無意識にno verifyつけてしまう恐れ)ので、コンフリクトさえしていなければ、pushできるようにした。

大きく分けると

  • GitHooksの設定共有
  • pre-pushのスクリプトを書く。

となる。

GitHooksとは

ざっくり言うとcommit, pushなどをトリガーに何かアクションを走らせる設定。

GitHooksの設定共有

デフォルトだとレポジトリ直下の .git/hooks ディレクトリを読み込みに行く。 .git/hooks ディレクトリの中に各hooksファイルとサンプルがあり、これらが実行される。
しかし、通常 .git ディレクトリはGitHubで共有されない。

大量にhooksファイルとサンプルがある。

そこで読み込みに行くディレクトリを変更する。

まずはhooksファイルを格納するディレクトリを作成。レポジトリ直下に .githooks というディレクトリを作る。

mkdir .githooks

その中に必要なファイルを作成。今回はpushの前にコマンドを走らせたいので、pre-pushを作る。

touch pre-push

この .githooks/pre-push ファイルを読み込むようにlinuxコマンドを打つ。

git config core.hooksPath .githooks

ファイルに実行権限を与える

chmod a+x .githooks/pre-push

これでpushをすると、pre-pushに書いてあるスクリプトが実行される。

コマンドはローカルで入力する必要があるので、GitHubのwikiかread meの環境構築の項目に下記コマンドを入力するように書いておくといいでしょう。

git config core.hooksPath .githookschmod a+x .githooks/pre-push

参考文献

pre-pushの設定

シェルスクリプトを書く。(コピペの組み合わせ)nodeでもrubyでも動作する模様。

シェルスクリプト初めてなのでおかしい点はあると思うが、狙い通りの動作はしている。
(confirmPushOrNotを2番目以降に実行しようとするとうまくいかなかったので、confirmPushOrNotの中に他の関数を入れた。)

#!/bin/bash# pushを確認するブランチ
readonly MASTER='master'
readonly DEVELOP='develop'
readonly testBranch='#129'
readonly RELEASE='^.*release.*$' # 「release」文字列を含む全てのブランチ
checkMergeDevelopBranch () {
echo 'Is it ok if fetch and merge origin/develop branch? [Y/n]'
exec < /dev/tty
read answer
case $answer in
'n' | 'no' ) echo '[info] Not merge origin/develop branch';;
* ) echo '[info] OK. fetch and merge origin/develop branch '; git fetch; git merge origin/develop;;
esac
}
runUnitTest () {
echo 'Do you want to run unit test? [Y/n]'
exec < /dev/tty
read answer
case $answer in
'n' | 'no') echo '[info] skip to run unit test';;
* ) echo 'preparing for unit test...';
npm run test:unit;
status=$?

if [[ $status = 0 ]]; then
echo '[info] Pass all the unit tests'
else
echo '[error] Not pass unit tests, are you sure to push? [y/N]'
exec < /dev/tty
read answer

case $answer in
'y' | 'yes') echo '[info] continue to push';;
* ) echo '[error] stop pushing';exit 1;;
esac
fi ;;
esac
}
runE2eTest () {
echo 'Do you want to run e2e test? [Y/n]'
exec < /dev/tty
read answer
case $answer in
'n' | 'no') echo '[info] skip to run e2e test';;
* ) echo 'preparing for e2e test...';
npm run test:e2e;
status=$?

if [[ $status = 0 ]]; then
echo '[info] Pass all the e2e tests'
else
echo '[error] Not pass e2e tests, are you sure to push? [y/N]'
exec < /dev/tty
read answer

case $answer in
'y' | 'yes') echo '[info] continue to push';;
* ) echo '[error] stop pushing';exit 1;;
esac
fi ;;
esac
}
checkConflict() {
local FILES="$(git diff --cached --name-only --diff-filter=AMCR | tr '\n' ' ')"
local FILE
for FILE in $FILES; do
conflict=`grep -3 -E '(<<<<<<<|>>>>>>>)' $FILE | grep -v '^$'`
if [ -n "${conflict}" ]; then
printf "\e[31m[Error]: ${FILE} Please resolve conflict before push\e[0m\n"
printf "${conflict}\n"
exit 1
fi
done
}
confirmPushOrNot () {
while read local_ref local_sha1 remote_ref remote_sha1
do
if [[ "${remote_ref##refs/heads/}" = $testBranch ]]; then

checkMergeDevelopBranch
checkConflict
runUnitTest
runE2eTest
echo '[warn] You tried to push to testBranch branch, continue? [y/N]'exec < /dev/tty
read answer
case $answer in
'y' | 'yes') echo '[info] OK. push start.';;
* ) echo '[error] push failed.';exit 1;;
esac
exit 0

elif [[ "${remote_ref##refs/heads/}" = $MASTER ]]; then
checkMergeDevelopBranch
checkConflict
runUnitTest
runE2eTest
echo '[warn] You tried to push to master branch, continue? [y/N]'exec < /dev/tty
read answer
case $answer in
'y' | 'yes') echo '[info] OK. push start.';;
* ) echo '[error] push failed.';exit 1;;
esac
exit 0

elif [[ "${remote_ref##refs/heads/}" = $DEVELOP ]]; then
checkMergeDevelopBranch
checkConflict
runUnitTest
runE2eTest
echo '[warn] You tried to push to develop branch, continue? [y/N]'exec < /dev/tty
read answer
case $answer in
'y' | 'yes') echo '[info] OK. push start.';;
* ) echo '[error] push failed.';exit 1;;
esac
exit 0

elif [[ "${remote_ref##refs/heads/}" = $RELEASE ]]; then
checkMergeDevelopBranch
checkConflict
runUnitTest
runE2eTest
echo '[warn] You tried to push to release branch, continue? [y/N]'exec < /dev/tty
read answer
case $answer in
'y' | 'yes') echo '[info] OK. push start.';;
* ) echo '[error] push failed.';exit 1;;
esac
exit 0
else
checkMergeDevelopBranch
checkConflict
runUnitTest
runE2eTest
exit 0
fi
done
}
confirmPushOrNot // この関数を実行

簡単に解説すると

  • #!/bin/bash を書くとシェルスクリプトとして認識される。
  • 各関数を書く。
  • 最後にconfirmPushOrNotを実行。

と言う処理。

  • exit 0でpush実行、exit 1で中止。
  • 直前に実行した処理が成功すると status=$? に0, 失敗すると1が代入される。

らへんが重要なポイント。

huskyというライブラリを使っていたが、それだと直前に実行した処理が成功、失敗で処理を変える方法がわからなかったため、スクリプトを書いた。

最後に、忘れずにGithubのwikiかread meに下記コマンドを打つように書いておきましょう。

git config core.hooksPath .githookschmod a+x .githooks/pre-push

今後の案件でもmasterやdevelopへのpushする前に確認、developやmasterなどの特定のブランチからmergeする。という2つは必須で入れて共有しようと思った。

参考文献

Git hooks を使って push を行うときに確認を出し誤 push を防止する|クラスメソッドブログ

git commit する前にコンフリクトの残りがないかチェックする

--

--