やりたいこと

普段Goでチーム開発しているが、 人によってコードのフォーマット(インデントなど)が異なることを避けるためにLinterとFormatterを自動でかけるようにしたい。

いつかけるか

一応今までもIDEで自動でかけようということにはなっていたのだが、微妙な設定の違いなどでうまくかかっていないコードがpushされることがあった。 こういうのは開発で必ず行うcommit前に自動でやってしまうのがいい。 ということでgit のhook機能のひとつ、pre-commitを使うことでcommit前に設定したスクリプトでLinterとFormatterを実行することにした。

ちょっと問題が

pre-commitでLinterとFormatterをかけるのはできる。 だが、 .git ディレクトリの中に pre-commit という名前でスクリプトを置かないといけない。 そして、.git の中身はGithubで共有できない。 開発メンバー全員がこの設定を使うようにしたいので工夫が必要だ。

環境変数の共有にdirenvを使っていた

我々のチームではリポジトリごとの環境変数の共有にdirenvを使っていた。 これは主に、ディレクトリごとに環境変数を定義して、そのディレクトリに移動したときだけ環境変数を有効にする用途で使う。 direnvは .envrcexport HELLO=WORLD のようにして書いた環境変数がbashによって設定されるが、 普通に cp などのコマンドも使えるので今回はそれを使うことにした。

プロジェクトルートのディレクトリに入ったらpre-commitが設定される

pre-commitスクリプトは .pre-commit という名前でプロジェクトルートに配置した。 これでgithubでの共有ができる。

しかしこのままではgitがpre-commitスクリプトとして使ってくれないので、 .git/hooks/pre-commit にコピーする必要がある。 そこで先に述べた direnv を使う。 .envrccp .pre-commit ./.git/hooks/pre-commit と書けばそのディレクトリに入った時点でコピーされるので目的が達成できる。 pre-commitを有効にするには git init も必要なので、 .envrc の中身は以下のようになる。

cp -a ./.pre-commit .git/hooks/pre-commit
git init

これでディレクトリの中に入ればpre-commitが設定され、コミット時にLinterとFormatterを走らせることができる。

pre-commitスクリプトの中身

実際のpre-commitスクリプトは、 goimportsgo vet を走らせる処理を書いている。

まず差分があるgoファイルがあるディレクトリ名を取得し、先頭に ./ を付けている。 go vet ではこれがないと対象を認識してくれなかった。 その後、各ディレクトリに go vet をかけ、問題なかったら強制的に goimports によるformatterかけをしてaddする。

これがcommitをする前に走るようになるので、全てのcommitのフォーマットが統一され、linterもかかっているので問題のあるコードがcommitされにくくなる。 CI/CDのときにチャックする方法もあるだろうけど、こっちの方が全commitが綺麗になるしいい気がした。

#!/bin/sh

echo "Info: Runnning linter and formatter"

gopackages=$(git diff --cached --name-only --diff-filter=ACM | grep '.go$'| xargs -n1 dirname| sort -u| sed 's/^/.\//' )
[ -z "$gopackages" ] && exit 0

echo "Info: git diff(package)"
echo $gopackages
echo "Info: Runnning go vet"
linterr="$(go vet $gopackages 2>&1 )"
if [ -n "$linterr" ] ; then
    echo "fatal:"
    echo $linterr
    exit 1
fi

echo "Info: go vet    --- OK"

unformatted=$(goimports -l $gopackages 2>&1 )
if [ -n "$unformatted" ] ; then
    echo "Info: unformatted file detected"
    echo "Info: $unformatted"
    echo "Info: Running goimports."
    for fn in $unformatted; do
        goimports -w $fn
        echo >&2 "goimports -w $fn"
        git add $fn
    done
fi

echo "Info: goinports --- OK"

exit 0