Git のクライアントサイドフックを使ってコミット時に自動フォーマットなどを行う

Git には Hooks という仕組みがあり、git commit コマンドを実行した時とかに自動実行するスクリプトを設定できる。

今回はこの仕組を使って、pre-commit のタイミング、つまり git commit コマンド実行時に自動フォーマットをかけたりしてみる。

フックスクリプトはどこにある?

フックを設定するためのスクリプトがどこにあるかというと、Git プロジェクト配下の ./.git/hooks/ 配下にある。

デフォルトではココに pre-commit.sample といったファイルが置いてあるかと思う。実際にフックを有効にするには pre-commit というファイル名にリネームすることで使えるようになる。

今回は pre-commit.sample はそのままに、空の pre-commit ファイルを置いてみる。

$ cd ./.git/hooks/
$ touch pre-commit

pre-commit フックに限らず、クライアントサイドフックのスクリプトは通常 Git 管理の対象外 (.git/ ディレクトリ配下だし…) なので、チーム開発で使う場合は何らかの方法でフックスクリプトの導入を徹底させる必要がある。

このファイルを色々と書き換えてフックスクリプトを用意し、後で実際に git commit コマンドを叩いて検証してみる。

コミット時に TextLint の自動修正を行う

まずは簡単な例。コミット時に TextLint の自動修正を行い、文章を修正してみようと思う。

対象のプロジェクトには textlint および Auto-Fix 可能なルールプラグインを入れておき、npm run textlinttextlint コマンドが使えるようにしておこう。

次に、pre-commit ファイルに以下のように書き込む。中身は Bash スクリプトだ。

#!/bin/bash

for FILE in `git diff --cached --name-only | grep .md`; do
  npm run textlint -- --fix $FILE
  git add $FILE
done

ヒアドキュメントを使ってコマンドラインから書いても良いかも。

$ cat << EOL > ./.git/hooks/pre-commit
#!/bin/bash

for FILE in `git diff --cached --name-only | grep .md`; do
  npm run textlint -- --fix $FILE
  git add $FILE
done
EOL

この書き方知ってるとスゴイ人っぽい。w

さて、このファイルで何をやっているかというと、git diff --cached --name-only (--cached--staged と同じ) で差分のあるファイルの名前を抽出し、.md (Markdown) ファイルのみを抽出する。そしてそれらに対して順に textlint を実行し、再度 git add している。

--cached と書いておけば新規作成したファイルも対象になる。変更後は再度 git add しないことには変更がコミットされないのでこうなっている。

textlint コマンドについては、textlint --fix [File] という書式がデフォだが、npm run でオプション引数を渡すために間に -- が入っている。

とてもシンプルなスクリプトだが、ファイル名にスペースが含まれているとうまく展開できなくておかしくなるので、この次のサンプルで直すとする。

また、「Auto-Fix できない Lint エラーがあるならコミットしたくない」という場合にも対応できるよう、もう少し直してみる。

TextLint チェックエラーがある場合はコミットさせない

TextLint チェックエラーがある場合はコミットさせないよう、終了コードを設定してみよう。pre-commit コミットフックは終了コードに 0 以外を渡すと、コミットメッセージを入力する画面に移動せず、処理を中断してくれる。

#!/bin/bash

# 終了コード
IS_ERROR=0

# $FILE でスペース区切りのファイルを受け取れるよう、区切り文字を改行のみにする
IFS=$'\n'

for FILE in `git diff --cached --name-only | grep .md`; do
  # Auto-Fix できそうなモノを先に直す
  npm run textlint -- --fix "$FILE"
  
  # それでもエラーがあれば終了コードを 1 に設定する
  if ! npm run textlint "$FILE"; then
    IS_ERROR=1
  fi
  
  git add "$FILE"
done

if [ $IS_ERROR -eq 1 ] ; then
  echo エラーがあります。修正してからコミットしてください。
fi

exit $IS_ERROR

途中に IFS=$'\n' という行があるが、この設定によって、スペース区切りのファイルも for ... in で扱えるようになっている。IFS は Internal Field Separator の略らしい。

TextLint の場合、先に --fix オプションで Auto-Fix してから、純粋なエラーチェックだけを再実施している。if ! 【コマンド】 で、コマンドの実行結果がエラーの場合に終了コード用変数を書き換えるようにした。

だんだんシェルスクリプトを書くのがつらくなってきた…。無理せず、ちょっとずつ必要なフックを入れていこう。