Rust プロジェクトで自動で Git hook をセットできる cargo-husky をつくった

npm でよく使っている husky というツールがあります.これは npm install などを hook して Git hook を自動でセットしてくれるツールで,リモートにプッシュする前のチェックを強制してくれます.もちろん CI でもテストを回しているのですが,プッシュ前にもチェックすることによりケアレスミスに早く気付くことができます.

Rust でもこの仕組みを使いたかったのですが,そういうツールが無かった&つくれそうだったので cargo-husky というツールをつくりました.

github.com

基本的な使い方

cargo-husky パッケージを Cargo.tomldev-dependencies に追加して cargo test を実行するだけです.

[dev-dependencies]
cargo-husky = "1"
$ cargo test

cargo はそのパッケージが必要になった段階でパッケージをダウンロードします. dev-dependencies なので cargo build ではなく cargo test を実行する必要があります.大抵開発中にテストを一度は実行すると思うので,意識して cargo test を呼ぶ必要はなく,知らぬ間に Git hook がセットされているという意図です.

.git/hooks/ を見ると,こんな感じの pre-push スクリプトが置かれているはずです.

#!/bin/sh
#
# This hook was set by cargo-husky v1.0.0: https://github.com/rhysd/cargo-husky#readme
# Generated by script /path/to/cargo-husky/build.rs
# Output at /path/to/target/debug/build/cargo-husky-xxxxxx/out
#

set -e

echo '+cargo test'
cargo test

これによって git push 前に cargo test が自動で実行されるようになります.

フックをカスタマイズしたいとき

デフォルトでは pre-pushcargo test を実行するフックが置かれますが,cargo-husky パッケージの feature flag を使ってこの挙動を変えることができます. cargo book にもある通り,feature flag を指定するには [dev-dependencies.cargo-husky] というセクションをつくります.

[dev-dependencies.cargo-husky]
version = "1"
default-features = false # デフォルトの挙動を無効化する
features = ["precommit-hook", "run-cargo-test", "run-cargo-clippy"]

features の配列に指定している値が有効にする機能です.この例では「コミット前に実行する(precommit-hook)」,「cargo test を実行する(run-cargo-test)」,「cargo clippy を実行する(run-cargo-clippy)」機能を有効にすることにより,毎コミット前に cargo testcargo clippy を実行する git hook pre-commit が生成されます.

利用可能な feature flag は下記の通りです.

feature flag 意味 デフォルト値
prepush-hook pre-push hook を生成 有効
precommit-hook pre-commit hook を生成 無効
postmerge-hook post-merge hook を生成 無効
run-cargo-test hook で cargo test を実行 有効
run-cargo-clippy hook で cargo clippy を実行 無効
user-hooks 次の章を参照 無効

すでに生成された hook がある場合は,一旦 .git/hooks/ 以下を削除してから cargo test を実行してパッケージを再コンパイルしてください.

さらにフックをカスタマイズしたいとき

feature flag は固定値しか記述できないため,「自前で用意したスクリプトを実行したい」「cargo に特定のオプションを渡して実行したい」といったカスタマイズはできません.

さらなるカスタマイズを可能にするために,user-hooks という feature flag が用意されています.

[dev-dependencies.cargo-husky]
version = "1"
default-features = false
features = ["user-hooks"]

この flag が有効になっていると,cargo-husky は自前で git hook を生成せず,リポジトリ直下に置かれている .cargo-husky というディレクトリを探し,その中のスクリプトを代わりに .git/hooks に配置します.

your-repository/
├── .git
└── .cargo-husky
    └── hooks
        ├── post-merge
        └── pre-commit

例えばディレクトリ構成がこのようになっているとき, pre-commit および post-merge.git/hooks 以下に置かれる対象になります. cargo-husky は .git/hooksスクリプトを置く際,ファイルの先頭にメタ情報(cargo-husky のバージョンなど)をヘッダとして挿入します. その際,# 始まりを行コメントと想定しているため,それに準じた言語でスクリプトを書く必要があります.また,実行可能属性がついていないファイルはスクリプトとして認識せず無視します.なのでスクリプトには実行可能属性を付けておいてください(chmod +x).これは意図しないファイルが .git/hooks に置かれてしまわないようにするためです.

実装

husky は npm の install フックなどで Git hook をセットしますが,cargo にはそういった仕組みはありません. 代わりに cargo の build script 機能を濫用することで cargo-husky は実装されています. 本来は外部ライブラリのビルドなどを設定するためのbuild.rsでフックをセットする処理を行っています.

cargo がビルド時に自動でセットする $OUT_DIR に設定されたディレクトリを元にプロジェクトの .git ディレクトリを特定するので,万一 $OUT_DIRリポジトリの外になっているような特殊なケースでは動きません.

cargo-husky は Linux/macOS/Windows で stable チャンネルのツールチェーンを使ってテストされており(LinuxmacOSTravis CI,Windows は Appveyor),MIT ライセンスで配布されています.