Gitフックをグローバルに設定可能にする

目次

前置き

Gitには、Gitフックという便利な機能があります。git commitgit pushの実行をフックして、なんらかの処理を差し込めるようにするものです。コミット前にLintやFormatterをかけるような使い方をよく見かけます。

そんな便利なGitフックですが、これは完全に.gitごと(以後「ローカル」と呼びます)にしか設定できません。リポジトリに対して設定して、そのリポジトリにコミットするごとに同じLintをかけるようなことはできません1。ユーザごとに好きなフックを、すべてのリポジトリに対して実行する、ようなこともできません。

それをなんとかしよう、というのが今回の話です。

経緯

元々、init.templateDirという設定があり、これを使うとリポジトリの初期化時に自動でコピーされるフックを好きなものに置き換えることはできたようです。ただ初期化時にコピーするだけなので、その後追加したフックはぜんぶ手動でなんとかするしかないし、変更したフックは反映されないし、とグローバルに設定したいという要求を満たすようなものではありませんでした。

しばらくそんな状況であきらめていたのですが、先日調べ直したところ、いつの間にか2core.hooksPathという、フックを探しに行くディレクトリを指定する設定ができていることを以下のページで知りました。

globalなgit-hooksを設定して、すべてのリポジトリで共有のhooksを使う #Git - Qiita

# ...
[core]
	hooksPath = ~/.config/git/hooks
# ...

これを使えば解決、と思ったのですが、この設定は設定したパスを見に行くようになるだけなので、グローバルなフックを設定した場合、今度はローカルなフックを設定することができません。あるリポジトリでのみ使いたいフックもあるので、これでは不便です。

上記ページには、単にグローバルなフックからローカルなフックを実行するようにして対応する方法も書かれていたので、そちらを参考にローカルなフックも動くようにします。

# pre-commit等
# ...
local_git_hook_path="$(git rev-parse --git-dir)"/hooks/"$(basename "$0")"

if [ -x "$local_git_hook_path" ]; then
    "$local_git_hook_path" "$@"
fi
# ...

これで解決! と思いきや、今度はHuskyを競合してしましました。Huskyとは、前置きで「できない」と書いた、リポジトリごとのフックを実現するスクリプトです。Huskyの少なくとも最近のバージョンでは、ローカルにcore.hooksPathを設定することでリポジトリごとのフックを実現しているため、グローバルにcore.hooksPathを設定する方法と競合します。Huskyを使用している環境では、Huskyによって設定されたフックしか動作しないのです。

仕方ないのでHuskyは無理矢理無効にした上で、ローカルなフックと同様に、グローバルなフックから実行する形にします。Huskyには初期化スクリプトを実行する機能があるため、そこでローカルなcore.hooksPathをクリアすることによってグローバルな設定を有効にするというハックです。

#!/bin/bash
# ~/.config/husky/init.sh等
if git config core.hooksPath | grep -q '^\.husky'; then
    git config --unset core.hooksPath
    echo Huskyによる変更を修正しました。再実行してください。
    exit 1
fi

これにも制限があり、Husky側に設置していないフックが実行されても初期化スクリプトは動作しないので、その場合は手動でなんとかしたりする必要があるのですが……。

とはいえ、最終的にはそれっぽくできました。

まとめ

最終的には、以下のような形でGitフックをグローバルに設定しつつ、ローカルに設定することも可能にし、またHuskyが使われているリポジトリでもHuskyと併用できるようにしました。

  1. core.hooksPathをグローバルに設定する。
  2. 1で設定したパスの全フックに、以下の処理を行うスクリプトを設置。
    1. グローバルなフックを実行する。
    2. ローカルなフックもあれば実行する。
    3. Huskyが使用されている場合は、リポジトリに設定されているフックも実行する。
  3. Huskyが使用されている場合は、Huskyの初期化スクリプト実行機能を使うことで、Huskyが設定するローカルなcore.hooksPathをクリアして、無理矢理グローバルなcore.hooksPathのフックを使わせる。

実例として私が使っているフック回りのスクリプトを参考にしてください。

HAYASHI-Masayuki/global-git-hooks

この記事のライセンス

クリエイティブ・コモンズ・ライセンス

この文書はCC BY(クリエイティブ・コモンズ表示4.0国際ライセンス)で公開します。


  1. と言いつつ、Huskyを使うとできます。 ↩︎

  2. できたの、2016年のようです……。 https://github.com/git/git/blob/05219a1276341e72d8082d76b7f5ed394b7437a4/Documentation/RelNotes/2.9.0.txt#L127 ↩︎