pre-commitというGitのpre-commit Hookを管理するフレームワークがあります。
このツールを利用すると、
GitのHookスクリプトへのフォーマッター等の設定をシンプルにすることが出来ます。

pre-commit
https://pre-commit.com/

Hookスクリプトがあると、
commitを行おうとした時に自動的にチェックが走るため、
コードレビュー前に修正でき、レビューがスムーズにできるようになります。

しかし、GitのHookスクリプトは、
開発担当者のPC環境に設定するものなので、
誰かが設定忘れをしているとチェック漏れが発生します。
この問題は、CI側でpre-commit用のGitHub Actionを使ってチェックすれば防止できます。

また、フォーマッターはcommit時に実行するよりも、
IDEでファイルを保存する度に動作する方が、都合が良いと思います。
こちらは、VSCodeやIntelliJ IDEAのプラグインを利用して対応します。

pre-commitの使い方

まずは、pre-commitをインストールします。
macOSでbrewを利用している場合は、次の方法でインストールできます。
pipなどでもインストールできるので、他の方法は、pre-commitのサイトを参照してください。

$ brew install pre-commit

git initでGitのリポジトリを作ります。

$ mkdir pre-commit-study && cd $_
$ git init

pre-commitの設定ファイルを作ります。
次の例では、末尾の空行削除・行末の空白削除を行うフォーマッターを設定しています。
(markdownの場合に、行末の空白削除は行わない設定も入れています)

.pre-commit-config.yaml

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.3.0
    hooks:
    -   id: end-of-file-fixer
    -   id: trailing-whitespace
        args:
          - --markdown-linebreak-ext=md

次のような、末尾に複数の改行があるファイルを作成します。

target.txt

hello

対象ファイルをGitの管理下に置きます。

$ git add target.txt

pre-commitを実行すると、次のように表示され、対象ファイルが修正されます。

$ pre-commit run
Fix End of Files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook

Fixing target.txt

Trim Trailing Whitespace.................................................Passed

問題が無さそうであれば、GitのHookスクリプトに登録します。

$ pre-commit install

先ほどの実行でファイルが修正されてしまったので、
target.txtの末尾に複数の改行を追加してからgit commitします。
先ほどと同じように対象ファイルが修正されることが確認できます。

$ git commit target.txt -m "try pre-commit"
Fix End of Files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook

Fixing target.txt

Trim Trailing Whitespace.................................................Passed

ファイルが修正された状態で、
もう一度git commitを実行すると、commitが成功します。

$ git commit target.txt -m "try pre-commit"
Fix End of Files.........................................................Passed
Trim Trailing Whitespace.................................................Passed
[master (root-commit) 45cd004] try pre-commit
 1 file changed, 1 insertion(+)
 create mode 100644 target.txt

GitのGitのHookスクリプトが不要になった場合は、次のようにuninstallすることができます。

$ pre-commit uninstall

GitHub Actionへのpre-commitの設定

pre-commitの設定と同じチェックをCI(GitHub Actions)でも行うように設定します。
公式に、GitHub Actionが提供されているので、こちらを利用します。

pre-commit/action | GitHub
https://github.com/pre-commit/action

次のようにGitHub Actionsのワークフロー設定ファイルを作成します。

.github/workflows/pre-commit.yaml

name: pre-commit

on:
  push:

jobs:
  pre-commit:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-python@v2
    - uses: pre-commit/action@v2.0.3

pre-commitの設定、ワークフローの設定ファイルをcommitします。

$ git add .pre-commit-config.yaml
$ git commit .pre-commit-config.yaml -m "add pre-commit setting"
$ git add .github/workflows/pre-commit.yaml
$ git commit .github/workflows/pre-commit.yaml -m "add pre-commit ci workflow"

GitHubにpushします。この状態ではワークフローは成功します。
(事前にremoteの設定は行っておいてください)

$ git push origin master

ワークフローが失敗することを確認するために、
target.txtの末尾に複数の改行を追加してからgit commitします。
(pre-commit uninstallでHookは外しておきます)

$ git commit target.txt -m "ng: end-of-file"
$ git push origin master

この状態でGitHub Actionsの処理結果を見ると、
次のように表示され、ワークフローの失敗を確認できます。

/opt/hostedtoolcache/Python/3.9.6/x64/bin/pre-commit run --show-diff-on-failure --color=always --all-files
Fix End of Files.........................................................Failed
- hook id: end-of-file-fixer
- exit code: 1
- files were modified by this hook

Fixing target.txt

Trim Trailing Whitespace.................................................Passed
pre-commit hook(s) made changes.
If you are seeing this message in CI, reproduce locally with: `pre-commit run --all-files`.
To run `pre-commit` as part of git workflow, use `pre-commit install`.
All changes made by hooks:
diff --git a/target.txt b/target.txt
index 7d989c8..ce01362 100644
--- a/target.txt
+++ b/target.txt
@@ -1,4 +1 @@
 hello
-
-
-
Error: The process '/opt/hostedtoolcache/Python/3.9.6/x64/bin/pre-commit' failed with exit code 1

この設定を行っておけば、
誰かがpre-commitの設定忘れをしている場合でも機械的にチェックできますね。

ファイル保存時に自動でpre-commitを動かす

ここまででは、末尾の空行削除をcommitのタイミングで修正しました。
このようなフォーマッターは、
VSCodeやIntelliJ IDEAでファイル保存する際に、
自動で動いた方が都合がよいので、それらの設定を行います。

Visual Studio Codeの場合

vscodeでは、次のプラグインでpre-commitを動かします。
このプラグインは、ファイル保存時に任意のコマンドを実行するプラグインです。

Run on Save | Visual Studio Marketplace
https://marketplace.visualstudio.com/items?itemName=emeraldwalk.RunOnSave

プラグインをインストールした後、
Setting -> Extentions -> Run on Save command configurationを選び、
Edit setting.jsonリンクから、setting.jsonを開きます。
次のように編集して、ファイル保存時にpre-commitが動くように設定します。

setting.json

{
    ※省略※
    "emeraldwalk.runonsave": {
        "commands": [
            {
                "match": ".*",
                "isAsync": true,
                "cmd": "pre-commit run --files ${file}"
            }
        ]
    }
    ※省略※
}

これらの設定を行った後、ファイルを編集して保存すると、
ファイルが自動的に修正(末尾の空行削除)されることが確認できます。

IntelliJ IDEAの場合

IntelliJ IDEAでは、次のプラグインでpre-commitを動かします。

File Watchers | JetBrains Marketplace
https://plugins.jetbrains.com/plugin/7177-file-watchers

プラグインをインストールした後、
メニューから「Preferences」を選び、
「Preferences」の左メニューから「Tools -> File Watchers」を選び、
左下「+」ボタンから「」を選びます。

New File Watcherダイアログは、次のように指定します。

  • Name: pre-commit run
  • File type: Any
  • Scope: Project Files
  • Program: pre-commit
  • Arguments: run –files $FilePathRelativeToProjectRoot$
  • Working directory: $ProjectFileDir$
  • Advanced Options: 全てチェックしない
  • Show console: Never

これらの設定を行った後、ファイルを編集して保存すると、
ファイルが自動的に修正(末尾の空行削除)されることが確認できます。

おわりに

フォーマッターやチェックツールの類は、
IDE、CIなどいろいろなところで動かすと便利なのですが、
ファイルの種類が増えてくると、
あちこちに設定したりや、設定を揃える手間がかかってしまうのが難点です。

pre-commitには様々なHookも用意されているので、
ここにできるだけ設定をまとめて、CIやIDEで流用する方法が良いかも知れません。

pre-commitのHookは、公式のものが以下にまとまっていますし、
探してみると、公式以外にもいろいろなHookが公開されています。

pre-commit/pre-commit-hooks | GitHub
https://github.com/pre-commit/action