JavaでもGit Hooksでコードをきれいに保ちたい(Spotless + Lefthook + devcontainer)
はじめに
コードにフォーマッタをかけ忘れてCI or レビュワーに指摘されたことありますよね?
指摘されて直せばOKですが、Gitに余計なコミットがログに残るし、時間のロスもあります。
なので、pushもしくはcommit前にチェックしておきたいです。
今回はJavaでコードをきれいに保つための仕組みを、Spotless + Lefthook + devcontainerの組み合わせで考えます。
JavaのFormatter事情
JavaScriptのFormatterといえばPrettier、Goはgofmt、Rustはrustfmtと大体デファクトスタンダードなFormatterがあるものですが、Javaにはないような気がしています。
自分がよく経験した構成は以下のものでした。
- CheckstyleをLinterとして利用
- エディタでフォーマット
全然悪くないのですが、以下のような問題点があります。
- Checkstyleの設定と、エディタ側のフォーマットの設定が二重管理
- フォーマットの設定がエディタに依存するため特定のエディタしか利用不可
- エディタの設定漏れで決まったコードスタイルにフォーマットできていない
Spotless
Spotlessは、JavaのビルドツールであるMaven、Gradleのプラグインとして利用できるFormatter兼Linterです。
Formatter兼Linterのため設定が同一であり、Maven、Gradleのプラグインのためエディタに依存しません。
また、設定を pom.xml
などに記載するためリポジトリをcloneした、その瞬間からFormatterの設定がされている状態になります。
Spotlessの利用イメージは以下です。
-
pom.xml
でプラグイン導入、設定を記述する - プラグインで導入されるゴール(
spotless:*
)などを実行
基本的にはお好みで設定すればよいですが、今回はエディタとの設定の同期のしやすさを考えて、以下のように設定します。
- お好みの設定を記述したEclipse formatter xmlを用意する
- 今回はGoogle Style Guideをそのまま利用
- eclipse jdtの設定方法でEclipse formatter xmlを指定して設定する
- お好みのエディタでEclipse formatter xmlをコードスタイルとしてインポートする
- 少なくともEclipse、VSCode、IntelliJでは利用可能
pom.xml
は以下のように記述します。
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>${spotless.version}</version>
<configuration>
<formats>
<format>
<includes>
<include>.gitattributes</include>
<include>.gitignore</include>
</includes>
</format>
</formats>
<java>
<eclipse>
<file>${config.dir}/format/eclipse-java-google-style.xml</file>
</eclipse>
<importOrder/>
<removeUnusedImports/>
<formatAnnotations/>
<licenseHeader>
<content>/* (C)$YEAR */</content> <!-- or <file>${project.basedir}/license-header</file> -->
</licenseHeader>
</java>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
pom.xml
を見るとわかりますが、エディタではフォーマットと別操作になりがちな、不要なインポート削除、インポートの整列などもSpotlessで行ってくれるのは、地味に嬉しいポイントですね。
Mavenでは、以下のようなコマンドでLinter、Formatterを実行します。
mvn spotless:check # Linter
mvn spotless:apply # Formatter
Git Hooks
Git Hooksは、gitのオペレーションの中の様々なフェーズの前後に任意の処理を実行できる仕組みです。
具体的には、 .git/hooks
ディレクトリに特定の名前の実行形式ファイル(スクリプト)を配置することで、ファイル名に対応するタイミングで処理が実行されます。
そんなGit Hooksですが、.git
ディレクトリがGit管理下に置かれないことから、Git Hooksのスクリプトを管理するためのツールが存在します。
メジャーどころでいうとHuskyがありますが、JavaScript(Node.js)以外では少々使いづらいところがあります。(Node.jsで動作すること、package.jsonに設定を記載することなど)
Lefthook
Lefthookは、Git Hooks管理ツールで以下のような特徴を持ちます。
- 速い。Goで書かれている。コマンドを並列実行できる。
- 強力。コマンドに渡すファイルや実行を制御できる。
- シンプル。依存関係のない単一のバイナリで、どのような環境でも動作します。
シングルバイナリで動作するのでJavaでも利用しやすいです。
Lefthookは、利用イメージは以下です。
-
lefthook.yml
を作成して、Git Hooksの設定を記述する -
lefthook install
を実行して、Git Hooksのスクリプトをインストールする
今回は、lefthook.yml
は必要最低限に以下のようにしました。
pre-commit:
commands:
lint:
run: mvn spotless:check
上記では、Linterをかけるのみとしていますが、Formatterまでかけて変更ファイルをステージするといったこともできそうです。
あとは、 各々の環境で開発前に lefthook install
を実行して、いつも通りGitのオペレーションを実施するだけです。
。。。
lefthook install
忘れてた!!😇
devcontainer
そんなおっちょこちょいさんを救済するため、devcontainerを利用しましょう。
devcontainerは言わずとしれたコンテナベースの開発環境共有ツールです。
今回は、devcontainerにLefthookをインストールし、 postCreateCommand
で lefthook install
を実行しておく作戦を採用します。
devcontainerへのLefthookインストールにも様々な方法があると思いますが、今回はDockerfileを作成してインストールします。
FROM mcr.microsoft.com/devcontainers/java:21-bullseye
LABEL authors="root"
ARG USERNAME=developer
ENV SETUP_SCRIPT=setup.deb.sh
RUN useradd -m -s /bin/bash ${USERNAME} \
&& curl -1sLf "https://dl.cloudsmith.io/public/evilmartians/lefthook/${SETUP_SCRIPT}" | sudo -E bash \
&& apt-get update && apt-get install -y lefthook \
&& rm -rf ${SETUP_SCRIPT} \
&& rm -rf /var/lib/apt/lists/* \
devcontainer.json
に以下のように記載し、自動で lefthook install
が実行されるようにします。
{
// ...
"postCreateCommand": "lefthook install",
// ...
}
ここまでで、devcontainer内で開発すれば、Lefthookで設定したGit Hooksが必ず実行され、Spotlessがコードをきれいに保ってくれる世界のできあがりです。
まとめ
今回は、Spotless + Lefthook + devcontainerの組み合わせで、Javaでコードをきれいに保つための仕組みを紹介しました。
Formatterに関して開発者が能動的に実施する手順はないため設定漏れが発生しません。
また、commit前にFormatterがかかるのでpushしてから指摘されることはもうありません。
余談:Git関連のツールってJavaScript製多くない?
Git Hooks周りのことを調べると大体JavaScript界隈の情報がたくさん出てきます。
例えば、Husky + list-staged + commitizen + commitlint みたいな。
Husky(lint-staged)は、シングルバイナリで動くLefthookがありますが、commitizenとcommitlintのようなシングルバイナリで動くいい感じのツールあればなーと思った今日この頃です。
なんでJavaScript界隈はGit関連ツール豊富なんだろう🤔
Discussion