huskyとlint-stagedでコミット時にコードを整える(v7対応)

Gitフックでソースコードをデトックス

Soudai Sasada

Soudai Sasada

huskyはGitのフックを管理するためのツールとなっており、このツールを使うことで複数人開発時に各々のローカル環境で共通のGitフックを容易に展開できます。

Gitのフックは色々な種類がありますがhuskyのよくある使い方の一つにpre-commitフックを使い変更がコミットされる直前にリンタやフォーマッタを走らせルールに違反したコードがあったらコミットを中断しそうしたコードが紛れ込まないようにする使い方があります。
そうした使い方を実現するためにlint-stagedというGitのステージングに上がっているソースコードを対象にリント / フォーマットを簡単に行えるツールがあり、これをhuskyと組み合わせることで「Gitのフックによるコミットをトリガーとしたスクリプトの呼び出し」と「ステージングに上がっているソースコード上の差分を対象にしたリント / フォーマットの実行」を自動化することができます。
huskyの面白いところはnpmスクリプトをうまく使うことでこうした仕組みを当該リポジトリを使うメンバー全員に適用できる点です。フォーマットのみを行なっているような無駄なコミットをなくすにはソースコードに変更を加える全員がツールを導入する必要がありますがGitのフックは通常ローカルマシンのみに適用され追跡対象とすることはないためにメンバー全員へ強制的に適用することができません。が、huskyは独自の仕組みによりこれをうまく解決しています。
この記事ではセットアップ方法を紹介しつつそのあたりの仕組みについても触れていければと思います。

では早速セットアップしていきましょう。まずはhuskyのセットアップです。yarnを使う場合はこのようになります。npmを使う場合にはコメントアウトしたコマンドをご参照ください。

yarn
$ yarn add -D husky lint-staged
$ npm set-script prepare "husky install"
$ yarn prepare
$ yarn husky add .husky/pre-commit "yarn lint-staged"
$ git add .husky/pre-commit
# 以下npmの場合
# npm install -D husky lint-staged
# npm set-script prepare "husky install"
# npm run prepare
# npx husky add .husky/pre-commit "yarn lint-staged"
# git add .husky/pre-commit

理解を深めるために次にこれらが何をやっているか個別に見ていきます。以下、yarnの抜粋となります。

$ yarn add -D husky lint-staged
$ npm set-script prepare "husky install"
$ yarn prepare

一行目( yarn add -D husky lint-staged )は依存のインストールですね。これは言うまでもないですね。ローカルにおけるGitのフックで使用するので本番のバンドルに含む必要はありません。devDependenciesとして追加します。

二行目( npm set-script prepare "husky install" )は package.json にスクリプトをセットするコマンドとなっています。結果、 prepare というキーのスクリプトに husky install という値がセットされます(yarnにはnpmのset-scriptと同等のコマンドがないためにやむなくnpmを使っていますがpackage.jsonを直接編集することと同義です)。
この prepare というスクリプトはパッケージマネージャが提供する特別なコマンドのうちの一つとなっており install による依存解決後に自動で実行されるコマンドとして定義することができます。
これにより新しいマシンでアプリケーションを動かすために依存をインストールしたらインストール後に自動で husky install が叩かれるようになります。

三行目( yarn prepare )はまさに今セットした prepare コマンドです。これはつまり husky install です。これはこのマシンではまだ husky install をしていないためですね。
husky install が叩かれると、Gitフックの参照先の変更、参照されるシェルスクリプトの作成を行います。ここがミソでこれにより既存のGitフックを汚すことなくリポジトリにすでに設定されている各Gitフックをローカルマシンへ適用することができます。

まずGitの core.hookspath.husky という値をセットします(参照先の変更)。これはその名の通りGitのフックが配置されているディレクトリの相対パスを指しています。これにより通常の .git/hooks の代わりに以降のGit上の操作でフックのトリガーとなる操作が発動すると .husky が参照されるようになります。
次に今まさに参照先として設定した .husky/_ というディレクトリを作り .husky/_/husky.sh.husky/_/.gitignore が作成されます(参照されるシェルスクリプトの作成)。 .husky/_/.gitignore の中身は * となっておりこれにより .husky/_/ に入っているもの全てを追跡対象外としています。つまり .husky/_/ というディレクトリはソースコードへコミットされることはありません。ではその .husky/_/husky.sh の中身はというと次のようになっています。

.husky/_/husky.sh
#!/bin/sh
if [ -z "$husky_skip_init" ]; then
  debug () {
    if [ "$HUSKY_DEBUG" = "1" ]; then
      echo "husky (debug) - $1"
    fi
  }

  readonly hook_name="$(basename "$0")"
  debug "starting $hook_name..."

  if [ "$HUSKY" = "0" ]; then
    debug "HUSKY env variable is set to 0, skipping hook"
    exit 0
  fi

  if [ -f ~/.huskyrc ]; then
    debug "sourcing ~/.huskyrc"
    . ~/.huskyrc
  fi

  export readonly husky_skip_init=1
  sh -e "$0" "$@"
  exitCode="$?"

  if [ $exitCode != 0 ]; then
    echo "husky - $hook_name hook exited with code $exitCode (error)"
  fi

  exit $exitCode
fi

これは .husky 以下に配置されている各フックの実行時に共通で呼び出されhuskyで登録した処理を実行するためのシェルスクリプトとなっています。

では次にpre-commitフックを登録していきます。続きのコマンドを見ていきましょう。

$ yarn husky add .husky/pre-commit "yarn lint-staged"
$ git add .husky/pre-commit

上( yarn husky add .husky/pre-commit "yarn lint-staged" )はhuskyでpre-commitフックを追加するコマンドになります。これにより .husky/pre-commit というファイルが作られます。中身は次のようになっています。

.husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

yarn lint-staged

ファイルの末尾に yarn lint-staged とありますがこれは yarn husky add コマンドに渡した引数となっています。pre-commitという名前のファイルはGitのpre-commitフックで実行され、このファイルにあるとおり .husky/_/husky.sh を通じて yarn lint-staged を呼び出すようになっています。

続き( git add .husky/pre-commit )は単に今追加されたpre-commitフックを動作検証のためにステージングに上げているだけです。ではちゃんと動作するか変更をコミットして確認してみましょう。

$ git commit -m "Add the pre-commit hook"
$ /Users/sou/products/try_husky_lint-staged/node_modules/.bin/lint-staged
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
husky - pre-commit hook exited with code 1 (error)

意図したとおりの振る舞いですね。Gitはpre-commitフックの参照先として .husky/pre-commit を読み込み、huskyを経由して yarn lint-staged が呼ばれ、その結果のexit codeがゼロではなかったために処理は中断されコミットされずに済みました :)

ちなみになぜエラーが出たのかというとこれはlint-stagedの設定が終わっていなかったためです。では次にlint-stagedの設定をして動くようにしましょう。

$ echo -e "{\n  \"*.js\": \"eslint --fix\",\n  \"*.{json,md}\": \"prettier --write\"\n}" > .lintstagedrc.json
$ git commit -m "Add the pre-commit hook"

1行目( echo -e "{\n \"*.js\": \"eslint --fix\",\n \"*.{json,md}\": \"prettier --write\"\n}" > .lintstagedrc.json )は .lintstagedrc.json というファイルを作成しています。記事の都合上ターミナルで完結できた方が楽なので横着しています。中身は次のとおりです。

.lintstaged.json
{
  "*.js": "eslint --fix",
  "*.{json,md}": "prettier --write"
}

lint-stagedは設定方法が複数ありこのようにjsonファイルを対象のルートディレクトリに配置し読み込ませることができます。他にもpackage.jsonに設定を記述することもできますがモノレポ構成を取るような場合にはこちらの方が都合が良かったりします。

2行目( git commit -m "Add the pre-commit hook" )では先ほどステージングへ上げた変更(pre-commitフックの登録)のコミットをリトライしています。結果はつぎのようになります。

$ git commit -m "Add the pre-commit hook"
rn run v1.22.10
$ /Users/sou/products/try_husky_lint-staged/node_modules/.bin/lint-staged
→ No staged files match any configured task.
✨  Done in 0.80s.
[main d00c9a3] Add the pre-commit hook
 1 file changed, 4 insertions(+)
 create mode 100755 .husky/pre-commit

先ほどと違い今回はlint-stagedが正常に動き、今回コミットされた内容に .lintstaged.json に該当するファイルが存在しなかったためにexit codeがゼロを返し無事コミットされました。

実際のプロジェクトですとここからさらにリンタやフォーマッタのセットアップをするのではないかと思います。が、そのあたりまで書き始めるととても長くなってしまう(のとそのあたりは他にも詳しい記事がたくさんあると思う)のでこの記事の範疇外とさせていただきます。

lint-stagedの詳しい設定についてはぜひドキュメントをご覧ください。

まとめ

今回はhuskyとlint-stagedを紹介させていただきました。huskyは開発を効率化するうえで非常に便利なGitのフックをチームで管理するためのツールとして活用できます。husky導入にあたり色々なバージョンの情報があったのでまとめてみましたがこの記事がこれから導入しようとしている方のお役に立てたら嬉しいです。

この記事がお役に立てたら一杯のコーヒーを恵んで頂けると励みになります 😊

Buy Me A Coffee
© 2020, Soudai Sasada