追加の依存パッケージなしでプロジェクトごとのGitコミットフックを設定する方法
Git 2.9以降はcore.hooksPath
というオプションでグローバルまたはローカルのGitフックのディレクトリを指定できるようになっています。
Gitのcore.hooksPath
オプションを利用するとhusky、simple-git-hooksのような追加の依存がなくても、Gitの機能だけでGitフックのコードをバージョン管理して、プロジェクトのセットアップ時にプロジェクトごとのGitフックを設定できます。
📝 類似するGitフックを管理するツールとしてpre-commitやLefthookもあります。これらのツールはGitフックの管理だけではなく、ファイルの種類ごとに実行するコマンドをわけて書けるようになっています。 つまり、lint-stagedのような機能も含むので、この記事で紹介するアプローチ以上の機能も同梱されています。
Node.jsプロジェクトの例
ここでは具体例として、Node.jsのプロジェクトでの設定方法を書いています。
Node.jsのプロジェクトではPrettierやESLintなどをpre-commitフックで実行するパターンをよく扱うためです。
core.hooksPath
は、Gitの機能なので特にNode.js(どちらかというnpmといったパッケージマネージャ)に依存した話ではありません。
Node.jsプロジェクトでの設定方法
.githooks
ディレクトリ(名前は何でも大丈夫です)を作成して、このディレクトリにpre-commit
といったGitフックで実行されるファイルを作成します。
具体的には次のような手順でGitフックファイルを作成していきます。
-
.githooks
ディレクトリを作成する
-
.githooks/pre-commit
を作成して、コミットフックの処理を書く
.githooks/pre-commmit
:
#!/bin/sh
echo "it is hooks!"
このままでは、この.githooks
ディレクトリがGitフックのディレクトリとして扱われません。
そのため、次のようにnpm install
などのプロジェクトのインストール時のlife cycle hookでcore.hooksPath
オプションを設定します。
-
package.json
のscripts.prepare
ライフサイクルスクリプトでcore.hooksPath
を設定する
"scripts": {
"prepare": "git config --local core.hooksPath .githooks"
},
これで設定は完了です。
あとは、このプロジェクトでnpm install
でインストールされると、prepare
スクリプトが実行されて、.githooks
ディレクトリがGitフックとして扱われます。このサンプルでは、コミットするたびに”it is hooks!“という表示が出るだけになります。
📝 Yarn v2以降はprepare
をデフォルトではサポートしていないため、プラグインでlifecycleスクリプトを実行できるようにする必要があるそうです。
もっと具体的なサンプルプロジェクトは、次のリポジトリに作成してあります。
このサンプルプロジェクトでは、huskyなどとよく組み合わせて利用されるlint-stagedを使って、実際にコミットに含まれる差分のファイルだけをPrettierで整形しています。
huskyの代わりに、Gitのcore.hooksPath
を直接使うイメージになっています。
具体的な.githooks/pre-commitの内容は、インストールされているlint-staged
コマンドをpre-commit
フックで叩いているだけです。
.githooks/pre-commit
:
#!/bin/sh
npx --no-install lint-staged
次のような手順でGitフックが設定されていることを確認できます。
git clone [email protected]:azu/githook-lint-staged-example.git
cd githook-lint-staged-example
npm install # Gitフックが設定される
# Gitフックが動いてるかを確認
echo "const v =[1, 2,3 ]" > sample.js
git add .
git commit -m up
✔ Preparing...
✔ Running tasks...
✔ Applying modifications...
✔ Cleaning up...
[master 9074895] up
1 file changed, 1 insertion(+)
create mode 100644 sample.js
メリット
依存が少なくなる
プロジェクトにGitフックを設定するために追加の依存(husky、simple-git-hooks)が不要になる点です。
huskyも内部的にはcore.hooksPath
を使っているので、huskyはcore.hooksPath
をラップしているツールになっています。
pre-commit
フックなどは差分ファイルのみを対象としたいため、引き続きlint-stagedなどと組み合わせて利用すると思います。
デメリット
グローバルフックとローカルフックが両立しにくい
pre-commit、simple-git-hooksは独自のGitフックディレクトリを作るのではなく、.git/hooks
のディレクトリにフックファイルを書き込む仕組みになっています。(husukyもv4まではこの挙動でしたが、v5でcore.hooksPath
を使う方法に変わりました)
.git/hooks
を直接変更するメリットは、グローバルのGitフックとローカル(プロジェクト)のGitフックを両立しやすい点です。
Gitでは、git config --global core.hooksPath ~/.githooks
のようにすれば、Gitフックが設定されてない場合に実行されるグローバルなGitフックを設定できます。
これを利用すると、プロジェクト関係なくグローバルフックを使ってsecretlintでコミット内容をチェックして、ローカルのGitフックも実行するのが簡単にできます。
しかし、Git(2.29.2で確認)では、core.hooksPath
のローカルのフックが設定されていると、グローバルのフックは実行されないという挙動になります。
git config --global core.hooksPath ~/.githooks
cd path/to/project
git config --local core.hooksPath ./.githooks
# このプロジェクトではローカルのGitフックが常に優先されるため、グローバルのGitフックは実行されない
この制限があるため、グローバルのフックとローカルのフックを同時に実行にするには、ちょっとしたハックが必要になります。
自分のグローバルフックのリポジトリのZsh Integrationに書いていますが、Zshでコマンド実行前にプロジェクトのcore.hooksPath
をグローバルの方を参照し直すように書き換えるという無理やりな手順がひつようになります。
# Source: https://github.com/azu/git-hooks
# Override <project>/.githook → <global>/git-hooks/hooks/
function preexec_git_global_hooks() {
inside_git_repo="$(git rev-parse --is-inside-work-tree 2>/dev/null)"
if [ "$inside_git_repo" ]; then
githooksDir=$(git rev-parse --show-toplevel)"/.githooks"
if [ -d "${githooksDir}" ]; then
git config --local core.hooksPath "/path/to/global-git-hooks/hooks"
fi;
fi
}
autoload -Uz add-zsh-hook
add-zsh-hook preexec preexec_git_global_hooks
この方法は.githooks
というディレクトリ名に依存しているのと余計な処理という感じがあるので、もっとクリーンな方法あるといいなと思います。
このデメリット?は、core.hooksPath
によるものなのでhuskyも同様です。
その他
このcore.hooksPath
を使ったプロジェクトのGitフック管理は、基本的にはHuskyと同じやり方なので、ハマりどころなどはHuskyのドキュメントが参考になります。
Huskyとの違いとしては、husky add
のようなコマンドがなかったり、HUSKY_*
の環境変数がないなどの違いがあります。
(基本的にlint-staged
をpre-commit
で実行するぐらいにしかGitフックを使ってないので、あんまり違いを感じるケースがありませんでした)
また、npm install
はするが.git
ディレクトリがない環境というのもたまに存在します。
具体的にはCloudflare PagesやHerokuなどは、gitリポジトリをcloneはしますが.git
ディレクトリがない状態なのでgit config
コマンドが叩けません。
この場合に、npm install
時のprepare
life cycleでgit config
が実行されると次のようにGitリポジトリではないというエラーが出ます。
fatal: Not a git repository
このようなケースは、git config
の設定ができないのを、単純に無視することで回避しています。
"scripts": {
- "prepare": "git config --local core.hooksPath .githooks"
+ "prepare": "git config --local core.hooksPath .githooks || echo 'Can not set git hooks'"
},
おわりに
Git 2.9以降はcore.hooksPath
オプションでグローバルまたはローカルのGitフックのディレクトリを指定できるので、Huskyなど使わなくてGitフックを管理しやすくなっています。
また、言語ではなくGitの機能なので、別の言語でも同じアプローチが使えて良い気はしています。
最近の自分のプロジェクトでは、この記事で書いたような方法でGit core.hooksPath
+ lint-stagedで、Gitフックの処理を動かしています。
サンプルプロジェクトは次のリポジトリにおいてあります。
参考:
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。