こんにちは。都内でiOSエンジニアをしている @zrn-ns です。
弊社のプロジェクトでは**SwiftLint**を利用しており、最低限のコーディング規約についてはSwiftLintでチェックする事ができるようになっています。
SwiftLintはビルドフェーズに含めることも想定されており、公式のガイドに従うだけでビルド時にLintチェックを行い、規約に沿った記述を徹底させることができます。
しかし今回諸事情があり、ビルドフェーズではなく、Gitでリモートリポジトリにpushする直前でLintチェックを行うようにしてみました。
なぜビルドフェーズではなく、Push時にチェックするのか
ビルドフェーズで実行する方式だとビルド時に多少のオーバーヘッドが発生してしまいます(差分チェックを行ってくれるようなので、ほぼ気にならないレベルですが)。
また弊社社内ではXcode派とAppCode派が存在します。AppCodeではSwiftLintのプラグインが存在するため、Lintチェックをビルドフェーズに含める方式だと、2重でLintチェックが行われてしまうことになります。
これらの問題を解消するため、ビルドフェーズでLintチェックを行うのをやめ、リモートリポジトリにコードをpushするタイミングでLintチェックを行うようにしてみました。
どうやってPush時に任意のスクリプトを実行するのか
Gitには、各アクション(commit, push, merge等)の実行の前後に任意のスクリプトを実行するための**Git Hooks**という仕組みがあります。
Git Hooksでは、commitやpush, mergeの前後のタイミングで、各Gitリポジトリの.git/hooks/
ディレクトリ下に配置したスクリプトが自動的に実行されます。
例えば、pushの直前でスクリプトを実行したい場合、.git/hooks/pre-push
というスクリプトが存在すれば、それが自動的に呼び出されます。
Git Hooksを利用して、push時にSwiftLintを実行する
今回はGit Hooksの設定を全メンバー間で共有できるようにし、全メンバーがpush前にLintチェックを行うことを強制できるようにします。
スクリプトの作成
.git/
ディレクトリ配下はgitで管理されない(コミットできない)ため、.git/hooks/pre-push
にスクリプトを記述するとメンバー間で設定を共有することができません。
そこで、git管理下にGit Hooks用のスクリプトを入れておくための専用のディレクトリを作成し、そこに配置したpre-pushスクリプトを参照するようにgitの設定を変更します。
cd {プロジェクトディレクトリ}
# git hooksを入れるためのディレクトリとスクリプトを作成
mkdir .githooks
touch .githooks/pre-push
# スクリプトに実行権限を与える
chmod 755 .githooks/pre-push
場合によってはLintチェックを無視してpushしたい場合もあるかと思うので、Lintチェックでエラーがあった場合、pushを続行するか選択できるようにします。
pre-pushのスクリプトの中身は下記のようにします。(※swiftlintのバイナリへのパスは適宜書き換えてください)
#!/bin/bash
set -eu
cd `dirname $0`
echo -n 'Linting... '
lint_result=$(swiftlint --reporter emoji --quiet --path ../ --config ../.swiftlint.yml)
if [ -n "$lint_result" ]; then
# Lintで警告が見つかった場合、push操作を続けるかユーザに判断させる
echo 'Some issues found.'
echo '====================================='
echo "$lint_result" | sed -e "s/^/>> /"
echo '====================================='
echo ''
echo -n 'Push anyway?[y/N]: '
exec < /dev/tty
read YN
if [ "$YN" = "y" ]; then
echo 'Ok. continue.'
else
echo 'Push aborted.';
exit 1;
fi
else
echo 'No issues found!'
fi
exit 0
最後に、Git Hooksの読み込み先ディレクトリを変更します。
この設定は各自の環境で実行する必要があるので、環境構築手順書に追加するのが良いかと思います。
# Git Hooksのディレクトリを.githooksディレクトリに変更
git config --local core.hooksPath .githooks/
動作確認
実際に動作させてみた結果は下記のような感じになります。
規約違反なし
% git push origin HEAD
Linting... No issues found!
Enumerating objects: 33, done.
Counting objects: 100% (33/33), done.
Delta compression using up to 8 threads
Compressing objects: 100% (27/27), done.
Writing objects: 100% (33/33), 8.73 KiB | 2.18 MiB/s, done.
Total 33 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), done.
To github.com:zrn-ns/swiftlint_on_pre_push.git
* [new branch] HEAD -> main
%
規約違反あり -> push続行
% git push origin HEAD
Linting... Some issues found.
=====================================
>> /path/to/project/AppDelegate.swift
>> ⚠️ Line 36: Files should have a single trailing newline.
>> ⚠️ Line 34: Limit vertical whitespace to a single empty line. Currently 2.
>> /path/to/project/SwiftLintOnPrePush/SceneDelegate.swift
>> ⚠️ Line 52: Files should have a single trailing newline.
>> ⚠️ Line 50: Limit vertical whitespace to a single empty line. Currently 2.
=====================================
Push anyway?[y/N]: y
Ok. continue.
Enumerating objects: 33, done.
Counting objects: 100% (33/33), done.
Delta compression using up to 8 threads
Compressing objects: 100% (27/27), done.
Writing objects: 100% (33/33), 8.73 KiB | 2.18 MiB/s, done.
Total 33 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), done.
To github.com:zrn-ns/swiftlint_on_pre_push.git
* [new branch] HEAD -> main
%
規約違反あり -> push中止
% git push origin HEAD
Linting... Some issues found.
=====================================
>> /path/to/project/AppDelegate.swift
>> ⚠️ Line 36: Files should have a single trailing newline.
>> ⚠️ Line 34: Limit vertical whitespace to a single empty line. Currently 2.
>> /path/to/project/SwiftLintOnPrePush/SceneDelegate.swift
>> ⚠️ Line 52: Files should have a single trailing newline.
>> ⚠️ Line 50: Limit vertical whitespace to a single empty line. Currently 2.
=====================================
Push anyway?[y/N]: n
Push aborted.
error: failed to push some refs to 'git@github.com:zrn-ns/swiftlint_on_pre_push.git'
%
サンプルプロジェクト
サンプルプロジェクトはこちらにアップしました。
FIXME
今回の手法では、Git Hooksのディレクトリをgit管理下に置くことで、全メンバーがスクリプトを共有する方法をとっています。
この方法は、スクリプトに修正が入った際にも環境間の同期が取れて便利な半面、各メンバーが独自のGit Hooksスクリプトを配置できなくなってしまいます。
(masterへの直pushを防止したりするためにもGit Hooksは便利に使えるので、個人でスクリプトを配置したい場面は多いと思います)
何か良いアイディアがあれば教えていただけると幸いです🙏
まとめ
push前にSwiftLintのチェックを行う方法を解説しました。
この方法を使えば、各ビルドごとにLintチェックのオーバーヘッドがかかることを防げますし、PullRequestの作成前に問題に気づくことができるので、PullRequest作成後にコードを微調整するコストをかなり減らせるはずです。
ただし、新規メンバーが参入した場合には、ビルドごとにLintチェックを掛けてあげたほうが定着は早そうなので、その際はビルドフェーズに追加する方法でLintチェックを行ったほうが良いのかなと思います。
謝辞
下記のサイトを参考にさせていただきました。