AppBrewでiOSエンジニアをしていますはるふ(@_ha1f)です。 2019/10にAppBrewに入社しまして、開発の傍らに、開発環境の改善などに取り組んでいます。
近年のiOS界隈を取り巻く「開発環境」といえば、Danger, mint, xcodegen, swiftlint等思い浮かべるかもしれませんが、 今回の記事ではそういうハイカラなツールではなく、iOSに限らず使えるpre-commitというGitの機能を紹介します。
pre-commitにより、コミットするブランチを間違えていないかや、コンフリクト未解消マーカーが含まれていないかなど、いろいろな制約を「ローカルでコミット前に」自動チェック出来ます。
Dangerなどを使っているとCIを待って修正して再度pushしないといけなかったり作業が煩わしいことがありますが、 ローカルなので手戻り少なく、レビューコストやミスを減らすことができます。
本記事では、我々が実際にどんな処理をしているのかも合わせてご紹介します。
pre-commitとは
Gitには標準で、特定のタイミングでスクリプトを実行する機能があります。 pre-commitもその一つで、コミットの実行前に実行されます。 また、エラーを返すことでコミットを中断させることも出来ます。
参考: Git - Git フック
設定の仕方は簡単で、 .git/hooks/pre-commit
というファイルが存在すれば自動的に実行されることになっています。
git init
でリポジトリを作成した場合は、サンプルのファイル .git/hooks/pre-commit.sample
が存在しています。
とりあえずはファイル名から .sample
を取り除くと動作を確認することが出来ます。
sampleではnon-asciiなファイル名の追加や、末尾ホワイトスペースとタブスペースの混合を検出してエラーを出しています。
pre-commitの書き方
pre-commit.sampleを一部省略して抜粋しています。
#!/bin/sh if git rev-parse --verify HEAD >/dev/null 2>&1 then against=HEAD else # Initial commit: diff against an empty tree object against=$(git hash-object -t tree /dev/null) fi # Redirect output to stderr. exec 1>&2 if 条件; then echo "error" exit 1 fi
任意の場所で exit 1
とかくと、コミットが中断されます。
exec 1>&2
しておくとechoなどに渡した文字列がが標準エラー出力に出力されます。
何も起こらず一番下にたどり着いたり、exit 0
すると、そのままコミットが実行されます。
差分に対して何らかの処理、確認などを行いたいときには $against
に保持している値を使います。
HEADが存在しない場合は、空のツリーと比較されます。
pre-commitをGitで管理する
強力なpre-commitですが、gitの管理ディレクトリの中にあるので、基本的にはgitで管理することが出来ません。 チームメンバで共有するためには、少し工夫をする必要があります。
Git管理下のファイルから.gitにコピーする
これを解決するために、Git管理下にある親ファイルから自動的に .git/hooks/pre-commit
にコピーする仕組みを作りました。
LIPSのiOSプロジェクトでは、依存性のインストール、コード生成などをまとめて行うスクリプトが用意されています。 そのスクリプトの中でpre-commitファイルをコピーすることにより、pre-commitが適用されることになりました。
PRECOMMIT_MASTER_FILE='./script/pre-commit' cp $PRECOMMIT_MASTER_FILE ./.git/hooks/pre-commit
ただし、これでは一度コピーされても再度スクリプトが実行されるのはもっと先になるかもしれず、 pre-commitが確実に更新されるとは限らないという問題がありました。
確実に最新のpre-commitを走らせる
pre-commitを確実に最新のものを使うため、pre-commitの冒頭で親ファイルとの差分を確認し、必要ならコピーしてから再度コミットさせるようにしました。
# pre-commit自体が最新かどうかをチェック PRECOMMIT_MASTER_FILE='./script/pre-commit' if [ -e $PRECOMMIT_MASTER_FILE ]; then diff -s $PRECOMMIT_MASTER_FILE ./.git/hooks/pre-commit > /dev/null 2>&1 if [ $? -ne 0 ]; then cp $PRECOMMIT_MASTER_FILE ./.git/hooks/pre-commit echo "updated pre-commit." echo "pre-commit was not latest. Please run again." 1>&2 exit 1 fi else echo "$PRECOMMIT_MASTER_FILE doesn't exist." fi
$PRECOMMIT_MASTER_FILE
が親ファイルです。
pre-commitを実行するたびにこのように差分チェックが実行され、コピーを行ったのちコミットが中断されます。
コピーされたときは2回commit実行する必要があり少し面倒ですが、単純のためにこうしています。
別の方法(gitconfigを使う)
git hooksのパスはgitconfigで書き換えることが出来るので、git管理下のディレクトリを指定しておくと実行されるようになります。
git config --local core.hooksPath script/githooks
この場合でもgit configは.git以下に保存されるので、一度各マシンで設定を行う必要があります。
実際に利用しているpre-commitコマンド
実際にLIPSのiOSのプロジェクトで使っているスクリプトをご紹介します!
master, develop branchでのcommitを禁止
master, developへはPullRequest経由でマージすることになっています。 それらのブランチでのcommitはミスのことが多いので、禁止するようにしました。
# master, develop branchでのcommitを禁止 BRANCH_NAME=`git symbolic-ref HEAD | sed -e 's:^refs/heads/::'` if test $BRANCH_NAME = master -o $BRANCH_NAME = develop; then echo "We cannot commit on ${BRANCH_NAME} branch." exit 1 fi
参考: pre-commit hookでmasterへのcommitを禁止した - @znz blog
コンフリクト未解消ファイルがあったらコミットさせない
コンフリクト未解消マークが残ったままコミットすることはないので、これを禁止しています。 注意点としては当たり前ですが、このスクリプト自体にマーカーが入ってしまうので、検査対象ファイルから省いておく必要があります。
# コンフリクト未解消ファイルがあったら警告 for FILE in `git diff-index --name-status $against | grep -E '\.(swift|pbxproj|m|h|xib|storyboard)$' | cut -c3-`; do if [ -e $FILE ]; then # 削除されたファイルは飛ばす continue fi grep_result=`grep -E '(<<<<<<<|>>>>>>>)' $FILE | grep -v '^$'` if [ -n "${grep_result}" ]; then echo $'\e[1;31m'$FILE$'\e[m' ' <- コンフリクト未解消ファイルがあります' echo $grep_result exit 1 fi done
参考: git commit する前にコンフリクトの残りがないかチェックする - Qiita
CircleCIのconfigをvalidateする
CiecleCIのconfigファイルのフォーマットが正しいかどうか、ローカルでチェックすることが出来ます。 ※ homebrewなどでCircleCIのCLIツールをインストールしておく必要があります
configファイルを書き換えていたときのみ、validateを実行するようにしています。
# CircleCIのconfigをvalidate for FILE in `git diff-index --name-status $against | grep -E '.circleci/config.yml' | cut -c3-`; do if ! eMSG=$(circleci config validate -c .circleci/config.yml); then echo "Error: CircleCI Configuration is invalid." echo $eMSG exit 1 fi done
注意点として、circleciコマンドはネットワークを挟んだりして、処理に時間がかかることがあったり、またまれにCircleCIの障害でCLIが正しく動かないことがあったりするので、 確実に変更があったときのみにするのがおすすめです。
また、いざというときにはpre-commitを無効化する方法をチームメンバが知っていると良いと思います。
register_devicesをvalidate
fastlane/register_devices
ファイルはスペース区切りではなくタブ区切りです。
通常Xcodeなどで開いて編集するとスペースになってしまって、CIの時点でエラーに気づくことが多かったので、これも検査することにしました。
# register_devicesをvalidate for FILE in `git diff-index --name-status $against | grep -E 'fastlane/register_devices.txt' | cut -c3-`; do for row in `cat fastlane/register_devices.txt | awk -F '\t' '{print NF}'`; do if [ $row != "2" ]; then echo "Error: lines in fastlane/register_devices.txt must be separated with tab character." exit 1 fi done done
おわりに
基本機能なので、Dangerを整備するのとそんなに変わらないぐらいの手間で手戻りも少なく自動検査できて、 pre-commitはかなり強い味方だと思います。
pre-commitは強力な機能ですが、かなりの回数実行されるので、ネットワークを挟む処理を避けたり、範囲を最小限にするなど、できるだけ短時間で終わるようにする必要があります。
そういう場合にはDangerや pre-push
等、その他の方法を検討してみると良いかもしれません。
実際LIPSではlinterの検査などはDangerで行っていたりします。
自分のチームでも最適な検査を見つけて、使ってみてください。
We are Hiring!
弊社AppBrewでは現在、500万DL突破のコスメクチコミアプリ「LIPS」をはじめとした「個性を解放するものづくりを。」に携わるエンジニアを積極採用中です。
目的に真摯に向き合い、技術でそれを解決したいと思うエンジニアの方はぜひ一度ご応募ください!お待ちしております。