Gitコミット前に自動でフォーマッターをかけて、リポジトリ内のソースコード全体のフォーマットを整える設定です。
今回は、複数人開発を前提に、上記フォーマット設定を開発者全員が共通設定として統一できるように設定をGit管理するところまでを手順としてまとめてみます。
実現したいこと
今回実現したい内容は、以下三つです。
- ソースコード全体のフォーマットを整えたい
- フォーマッターを実行する手間の削減やかけ忘れ防止の為、自動実行させたい
- フォーマットの設定をリポジトリ全体の共通設定として統一させたい
ソースコードのフォーマットを整えることのメリットは幾つかありますが、突き詰めると可読性を上げるという点に尽きると思います。
可読性を上げると、必然的にプロダクトの品質を上げる事にも繋がります。
フォーマットを整えるには、対象ファイルを指定して各種フォーマッターを実行する方法をとります。
ただ、毎回何かのファイルを弄る度に手動でフォーマッターを実行するのは面倒なので、使用者が意識する必要なく自動で実行されるようにしたいです。
任意のタイミングでの手動実行ではなく自動で実行させるようにしておく事で、フォーマッターのかけ忘れも防止できます。
個人で開発する場合のフォーマットが整った状態を作るのはもちろんとして、複数人で開発する場合でも全員が同じ設定を使用してリポジトリ全体として統一したフォーマットが整っている状態を作りたいです。
実現手順
という事で、上記三つを実現させる環境を一つずつ作っていきます。
1. Git Repositoryの作成(用意)
まずは、対象のGit Repositoryを用意します。
一から作っても良いですが、今回は一つ前の記事で作成したexample_repositoryというリポジトリを継続して使用します。
2. フォーマッターの選定/導入
フォーマットを整える為に使用するフォーマッターの選定を行います。
用意したexample_repositoryのディレクトリ構成は以下のようになっており、今回の例ではPythonファイルとYAMLファイルの二種類のフォーマットを整える必要がありそうです。
1 2 3 4 5 6 7 8 9 |
$ tree . ├── README.md ├── buildspec.yml ├── docker-compose.yml ├── main.py └── tests ├── __init__.py └── test_main.py |
- Pythonファイル
- YAMLファイル
Pythonのフォーマッターは色々あるようですが、一般的によく使用されているもの(他に比較要素が見当たらなかったのでGitHub Repositoryのスター数基準)として、autorep8、yapf、blackの三つが代表的です。
三つそれぞれに特徴があるのでどれを使用するかはその時々の判断基準により決定すれば良いと思いますが、今回はリポジトリ全体を共通のフォーマットで統一したいというのがやりたい事で本線とは少し逸れる為、単純に一番スター数が多かったblackを使用することにします。
blackはこの中で最も制限の強いフォーマッターになりますが、フォーマット設定はきつめのフォーマットで統一させる方が個人的に好みなので普段からよく使用させてもらっています。
フォーマッターの選定が終わり次は使用方法ですが、blackを使用するにはbrew or pipで各自のローカル環境にインストールして…
1 |
$ brew install black |
or
1 |
$ pip install black |
コマンドのblackが認識されるようになったら、第一引数にフォーマットを整えるファイル名を渡して実行するだけです。
1 2 3 4 |
$ black main.py reformatted main.py All done! ✨ 🍰 ✨ 1 file reformatted. |
以上で終わりのシンプルな手順ですが、blackを使用可能にする為のインストールだけはどうしても各自のローカル環境でやっておいてもらう必要があるので、リポジトリのROOTに置くREADMEなどの環境構築手順にblackをインストールする1ステップを忘れずに追加する必要があります。
YAMLのフォーマッターも幾つかありますが、ReactやBabelなどフロントエンドで採用実績のあるPrettierをYAMLでも使用するのが一般的なようなので、今回もPrettierを使用することにします。
PrettierはNode.js製のライブラリでpackage.jsonで管理できる為、フォーマッター用途以外でも多くの用途で使用されているNodeパッケージ群の一つとしてGit管理されているpackage.jsonに1行追加するだけで、各開発者が個別にインストール手順を踏まないでも通常のパッケージインストールを行うと自動で追加される場合が多いのもメリットです。
example_reposioryに含まれるファイルの中でマークダウンファイル(README.md)もprettierでフォーマットを整える事が可能ですが、マークダウンファイルに対してprettierのフォーマッターを実行すると、日本語とアルファベットの間に強制的にスペースが入れられてしまったり日本語ドキュメントと相性が良くない為、ここではYAMLファイルだけを対象にします。
– インストール
Prettierのインストールは、他の必要なNodeパッケージをインストールする時と同じく、yarnやnpmで追加するだけです。
今回使用するexample_repositoryではまだNodeパッケージを何も使用していなかった為、package.jsonを新規作成します。
1 2 3 4 5 6 |
{ "name": "example_repository", "version": "1.0.0", "license": "UNLICENSED", "private": true } |
1 |
$ yarn add -D prettier |
これでpackage.jsonにprettierが追加されるので、他の開発者の環境でもyarn installをするだけでprettierが使用可能になります。
– 使用方法
prettierの使用方法は、ファイルを上書きするwriteオプションを指定した上で第一引数にファイル名を渡して実行します。
1 2 |
$ prettier --write buildspec.yml buildspec.yml 46ms |
フォーマッター以外の用途で環境構築手順に既にyarn installの手順が記載されている場合が多く不要なケースが大半だと思いますが、今回のように新規にNodeパッケージインストールの手順が追加された場合は、Pythonの時のblackを追加時と同じく、ROOTに置くREADMEなどの環境構築手順にNodeパッケージをインストールする1ステップを忘れずに追加する必要があります。
フォーマッター以外の用途でNodeパッケージのインストールが既に手順化されている場合は、この手順追加は不要。
3. Gitコミット前をフックしたフォーマッターの自動実行設定
各ファイルに対してフォーマットを整えられる環境ができたので、次はフォーマッターの実行を手動実行ではなく自動で実行されるようにします。
実行タイミングは色々選択肢がありますが、やりたいことがソースコード全体のフォーマットを整えたいというのを踏まえると、リモートリポジトリ上のフォーマットが整った状態を常に担保する為にも、Gitコミット前にフックするのが理想です。
Gitのコミット前をフックするには/.git/hooks/pre-commitに処理を書く事で実現できますが、デフォルトではファイルが存在しない為、まずは用意されているpre-commit.sampleをコピーしてファイルを作成します。
1 |
$ cp .git/hooks/pre-commit.sample .git/hooks/pre-commit |
次に、コミット前にフォーマッターを実行する以下の内容でpre-commitを上書きします。
1 2 3 4 5 6 7 8 9 10 |
#!/bin/sh for FILE in `git diff --staged --name-only | grep -e .py -e .yml`; do if [ ${FILE##*.} = "py" ]; then black $FILE elif [ ${FILE##*.} = "yml" ]; then prettier --write $FILE fi git add $FILE done |
stagedに追加されたファイルを対象に、拡張子が.pyのファイルと.ymlのファイルそれぞれに対して、Pythonファイルにはblackを、YAMLファイルにはprettierでフォーマットを整えた後で再度gid addしてコミットさせるようにしています。
このようにpre-commitの中身はいたって普通のスクリプトの為、フォーマットをかける以外にもターミナルで実現できる事は基本的に何でもできます。
4. pre-commitに記載した設定をGit管理に移行
Gitのコミット前に自動でフォーマッターを実行させる事はできるようになりましたが、コミット前をフックしているpre-commitファイルはROOT直下の.gitディレクトリ配下に存在している為、コミット対象に含める事ができずにリモートリポジトリにpushして複数人開発時などに開発者間で設定を共有することができません。
この問題を解決するには、git管理に置いた共有可能なスクリプトファイルなどで各開発者のローカル環境のpre-commitファイルを作成/上書きする方法をとります。
例として、手順3で作成したpre-commitファイルを作成するには、以下のようなスクリプトを用意して各自のローカル環境で実行してもらう感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#!/bin/sh PRE_COMMIT_FILE_PATH="$(cd $(dirname $0);pwd)/.git/hooks/pre-commit" OUTPUT_PRE_COMMIT="#!/bin/sh\n" OUTPUT_PRE_COMMIT="${OUTPUT_PRE_COMMIT}\nfor FILE in `git diff --staged --name-only | grep -e .py -e .yml`; do\n" OUTPUT_PRE_COMMIT="${OUTPUT_PRE_COMMIT}\tif [ \${FILE##*.} = \"py\" ]; then\n" OUTPUT_PRE_COMMIT="${OUTPUT_PRE_COMMIT}\t\tblack \$FILE\n" OUTPUT_PRE_COMMIT="${OUTPUT_PRE_COMMIT}\telif [ \${FILE##*.} = \"yml\" ]; then\n" OUTPUT_PRE_COMMIT="${OUTPUT_PRE_COMMIT}\t\tprettier --write \$FILE\n" OUTPUT_PRE_COMMIT="${OUTPUT_PRE_COMMIT}\tfi\n" OUTPUT_PRE_COMMIT="${OUTPUT_PRE_COMMIT}\tgit add \$FILE\n" OUTPUT_PRE_COMMIT="${OUTPUT_PRE_COMMIT}done" echo $OUTPUT_PRE_COMMIT > $PRE_COMMIT_FILE_PATH echo `chmod 755 ${PRE_COMMIT_FILE_PATH}` |
このような予め用意したスクリプトを開発者のローカル環境で実行してもらう事で共通した設定のpre-commitファイルを作成する事ができるようになりますが、各開発者がローカル環境でスクリプトを実行してもらう事前提の共通設定になる為、以下のような課題が残ります。
- フォーマッターの自動実行の為だけにスクリプトを流してもらうのは面倒
- 用途不明な自作スクリプトにより、プロダクト全体の理解の妨げになる
開発環境の構築手順などに書いてスクリプトを実行する運用の形になると思いますが、開発環境の構築手順は増えれば増えるほど面倒になることもあり可能な限り簡略化したい為、フォーマットを整えるだけの為に手順が一つ増えるのは気になります。
また、pre-commitの内容が更新される度に都度スクリプトを実行し直してもらうのもとても面倒で、スクリプト再実行をやる人/やらない人が出てきて、結果としてリポジトリ全体で統一されたフォーマットにならなくなる未来が容易に想像できます。
これはこのスクリプトに限った話ではないですが、開発環境で実行する自作スクリプトなどは、内容を知らない人にとってはよく分からない謎のスクリプトに映る為、プロダクト全体の理解の為の複雑度を上げる要因になります。
どうしてもここだけ○○したいなどの理由で仕方なく使用するのは仕方ないと思いますが、汎用的な一般知識で理解できる管理可能な各種ライブラリなどで運用していくのが望ましいです。
上記二つの課題を解消する為、huskyとlint-stagedを使ってpre-commitファイルを上書きする方法で、自作スクリプトが行っていた内容を置き換えます。
- インストール
- 設定
huskyとlint-stagedはともにprettierと同じくNodeパッケージ群の為、インストール手順もyarnやnpmで追加するだけです。
package.jsonで管理できる一般的なライブラリの為、自作ツール使用時に課題となっていた用途不明なオレオレスクリプトを使わなければならなかった問題を解消できますが、huskyはインストール時にGitの各種フックタイミングのファイル内容(pre-commitなど)を上書きするライブラリの為、既にフックイベントを他の用途で使用している場合はインストール前に内容を退避させておく必要があります。
フックしていた処理は全てhusky経由で行う形に変わるので、内容を移行/復元する為に必ず先に退避しておいてください。
1 |
$ mv .git/hooks/pre-commit .git/hooks/pre-commit.old |
退避が完了したら、huskyとlint-stagedをインストールします。
1 |
$ yarn add -D husky lint-staged |
これでpackage.jsonに二つのライブラリが追記される為、各開発者の開発環境にインストールする際も他の必要なNodeパッケージ群をインストールする時と同じく yarn install で自動的に追加されるようになり、環境構築手順からフォーマッタの自動実行の為だけの専用手順を無くす事ができます。
1 2 3 4 5 6 7 8 9 |
$ cat .git/hooks/pre-commit /Users/masakimisawa/dev/example_repository #!/bin/sh # husky # Created by Husky v4.2.5 (https://github.com/typicode/husky#readme) # At: 2020/6/6 23:44:53 # From: /Users/masakimisawa/dev/example_repository/node_modules/husky (https://github.com/typicode/husky#readme) . "$(dirname "$0")/husky.sh" |
pre-commitファイルの中身を見るとhuskyで書き換わっている事が確認でき、また、huskyがやっている事がpre-commitのタイミングでhusky.shの実行をフックしているだけなのも見てとれます。
pre-commitでhusky.shの実行をフックできるようになったところで、次はhuskyとlint-stagedでフォーマッターを実行できるように設定していきます。
まずは、huskyからpre-commit時にlint-stagedを呼ぶようにする為、ROOT直下に以下のような.huskyrcファイルを作成します。
この設定はpackage.jsonに追記しても大丈夫ですが、package.jsonはインストールするNodeパッケージ群を管理するだけの役割にしたいので、huskyの設定ファイルは別に切り出す方が個人的には好みです。
1 2 3 4 5 |
{ "hooks": { "pre-commit": "lint-staged" } } |
これでhuskyがフックしているpre-commit時の動作にlint-stagedを紐付ける事ができました。
今回はpre-commitをフックしていますが、pre-pushやpost-commitなどのフックをしたい場合もhuskyの設定で定義しておく事で自由にカスタマイズ可能です。
次に、linst-stagedでフォーマッターの実行を設定していきます。
lint-stagedは、Gitのstagedに追加されたファイルを対象に指定した拡張子のファイルに対して任意のコマンドを実行可能なライブラリで、今回は拡張子が.pyと.ymlのファイルに対して、それぞれのフォーマッター実行コマンドを定義していきます。
huskyの設定時と同じく、lint-stagedの設定もROOT直下に以下のような.lintstagedrcファイルを作成して定義します。
こちらの設定も同じくpackage.jsonに追記しても大丈夫ですが、同様の理由でlint-stagedの設定ファイルは別に切り出す方が個人的には好みです。
1 2 3 4 5 6 7 8 9 10 |
{ "*.py": [ "black", "git add" ], "*.yml": [ "prettier --write", "git add" ] } |
手順3でpre-commitファイルに書いていた内容がlint-stagedで設定する内容になりますが、スクリプトよりもこちらの定義ファイルの方がシンプルにまとめられていて良いですね。
また、huskyとlint-stagedインストール前に退避しておいたpre-commitの内容を移行する際も、.lintstagedrcファイルに追記する形で復元します。
pre-commit時以外のフック内容を復元する場合は、.huskyrcに該当のフックタイミングと実行するコマンドを追記して復元させます。
例として、よく使われているgit-secretsのフックイベントを追記した例が以下になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "*": [ "git secrets --scan" ], "*.py": [ "black", "git add" ], "*.yml": [ "prettier --write", "git add" ] } |
pre-commit時に実行するフォーマッター(コマンド)の内容が変わった場合にも、このファイルの内容を変更するだけで自動的に全開発者の環境に適用されるので、内容が変更される度に毎回スクリプトを流したりなどの各環境への適用手順を踏む必要がないのが非常に嬉しいです。
他用途と同じく、新しいNodeパッケージを入れる場合は、yarn installを実行し直す必要があります。
以上で全設定が完了で、フォーマットが整っていないファイルが含まれた状態でコミットを行おうとすると、自動でフォーマッターが実行された後の内容がコミットされるようになるのが確認できると思います。
各開発者毎にフォーマット設定が違っていると、コードレビュー時に本来見なければならない点以外にも不要な差分が含まれるようになったりなど開発効率を落とす要因になるので、可能な限り本質的な開発作業に集中できるようにしたいですね。
ということで、今回は終わりです!