👔

JavaでもGit Hooksでコードをきれいに保ちたい(Spotless + Lefthook + devcontainer)

2024/05/02に公開

はじめに

コードにフォーマッタをかけ忘れて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

https://github.com/diffplug/spotless

Spotlessは、JavaのビルドツールであるMaven、Gradleのプラグインとして利用できるFormatter兼Linterです。
Formatter兼Linterのため設定が同一であり、Maven、Gradleのプラグインのためエディタに依存しません。
また、設定を pom.xml などに記載するためリポジトリをcloneした、その瞬間からFormatterの設定がされている状態になります。

Spotlessの利用イメージは以下です。

  • pom.xml でプラグイン導入、設定を記述する
  • プラグインで導入されるゴール(spotless:*)などを実行

基本的にはお好みで設定すればよいですが、今回はエディタとの設定の同期のしやすさを考えて、以下のように設定します。

  • お好みの設定を記述したEclipse formatter xmlを用意する
  • eclipse jdtの設定方法でEclipse formatter xmlを指定して設定する
  • お好みのエディタでEclipse formatter xmlをコードスタイルとしてインポートする
    • 少なくともEclipse、VSCode、IntelliJでは利用可能

pom.xml は以下のように記述します。

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

https://github.com/evilmartians/lefthook

Lefthookは、Git Hooks管理ツールで以下のような特徴を持ちます。

  • 速い。Goで書かれている。コマンドを並列実行できる。
  • 強力。コマンドに渡すファイルや実行を制御できる。
  • シンプル。依存関係のない単一のバイナリで、どのような環境でも動作します。

シングルバイナリで動作するのでJavaでも利用しやすいです。

Lefthookは、利用イメージは以下です。

  • lefthook.yml を作成して、Git Hooksの設定を記述する
  • lefthook install を実行して、Git Hooksのスクリプトをインストールする

今回は、lefthook.yml は必要最低限に以下のようにしました。

lefthook.yml
pre-commit:
  commands:
    lint:
      run: mvn spotless:check

上記では、Linterをかけるのみとしていますが、Formatterまでかけて変更ファイルをステージするといったこともできそうです。

あとは、 各々の環境で開発前に lefthook install を実行して、いつも通りGitのオペレーションを実施するだけです。

。。。

lefthook install 忘れてた!!😇

devcontainer

そんなおっちょこちょいさんを救済するため、devcontainerを利用しましょう。
devcontainerは言わずとしれたコンテナベースの開発環境共有ツールです。

今回は、devcontainerにLefthookをインストールし、 postCreateCommandlefthook 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 が実行されるようにします。

devcontainer.json
{
// ...
    "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