皆さん、git-hooksは使っているでしょうか。
${REPOSITORY_ROOT}/.git/hooks/
以下に決められた名前でスクリプトを作成すると、対応した操作に合わせて、その名前のスクリプトが自動で実行されます。
例えば、${REPOSITORY_ROOT}/.git/hooks/pre-commit
ならコミット前にそのスクリプトを実行して、スクリプトの終了コードが0でないとき、commit前に処理が停止します。
よく使われるのは、pre-commitを使用して、コミット直前にCIと同様の構文チェックや、rubocop等のテストをすることではないでしょうか。
基本的には、上記のようにRepositoryごとにhooksは書くのがデフォルトですが、
最近、私はすべてのRepositoryで共通するような、設定や除外ファイルの記載をしたgitconfigやgitignoreをその他設定系ファイルとまとめてgit管理しています。
そんな中、git-hooksもすべてのリポジトリで共通化できないかと思い、調べてみました。
(この記事は基本的にMac環境での動作を前提に書いています。ほとんどの処理は他環境でも動くと思いますが、一部Homebrewに依存した処理があるので、その部分は読み替えてください。)
先行事例の調査
「git hook global」でGoogle検索するとそれっぽい情報がいくつか出てきます。
しかしこれらを読んでいくと、あくまでinitやclone時のテンプレートに含めて、最初にテンプレートからコピー=実質的にすべてのリポジトリで共通化という感じでした。
この方法だと以下のような欠点が考えられます。
- 一度init, cloneしたリポジトリに一律で新しい変更を反映できない
- すでにinit, clone済みのリポジトリには自動で反映されない
ただ記事自体の最終更新日も古く、gitは2系になっていろいろ新しい機能・設定も追加されています。
そのため、 gitconfigをグローバルで設定するときに使われる ~/.gitconfig
のようなやり方があるのではないかと、公式ドキュメントを確認しました。
ありました。
このドキュメントには、以下のように記載されています。
By default the hooks directory is $GIT_DIR/hooks, but that can be changed via the core.hooksPath configuration variable (see git-config[1]).
すなわち、 core.hooksPath
にhooksディレクトリのパスを指定すれば、そちらを優先的に読み込んでくれるようです。
なので、私は管理しやすいように core.hooksPath を .config/git/hooks
に指定しました。
(最近のgitでは、 .config/git/
にグローバルなデフォルト設定を書き込むことが多いのでそれに倣った形です)
core.hooksPath
に書き込むには以下のコマンドを実行する、あるいは ~/.gitconfig
or ~/.config/git/config
に直接書き込む方法があります。
git config --global core.hooksPath .config/git/hooks
設定ファイルに直接書き込む場合は、以下の設定をconfigファイルに追記します。
[core]
hooksPath = ~/.config/git/hooks
これで、pre-commitなどのgit-hooks時に ${GIT_ROOT}/.git/hooks
ではなく、設定したディレクトリのファイルを読み込んでくれるようになりました。
gitリポジトリ内のgit-hooksも実行するようにする
上記の方法ですべてのgitリポジトリにおいて、git-hooksでは .config/git/hooks
のスクリプトを実行するようになりました。
しかし、もしもリポジトリごとに個別のgit-hooksを設定したくなったとき、このままでは ${GIT_ROOT}/.git/hooks
にファイルを置いても実行されません。
そのため、リポジトリ側においたスクリプトも実行されるように .config/git/hooks
においたスクリプト側で設定します。
この記事のスクリプトとこの記事のgitリポジトリのルートを取得するコマンドを参考に、以下のようなスクリプトを作成しました。
#!/bin/bash
GIT_ROOT=`git rev-parse --show-superproject-working-tree --show-toplevel | head -1`
HOOK_NAME=`basename $0`
LOCAL_HOOK="${GIT_ROOT}/.git/hooks/${HOOK_NAME}"
if [ -e $LOCAL_HOOK ]; then
source $LOCAL_HOOK
fi
このスクリプトでは、そのリポジトリ内に同名のgit-hooks用のスクリプトが存在している場合、そのスクリプトを実行するようになっています。
このファイルをすべてのgit-hooksスクリプトで実行されるようにします。
すべてのgit-hooksスクリプトを .config/git/hooks
に一括で作成するため、gitに用意されているsampleファイルからスクリプト名を取得して作成するコマンドを用意しました。
ここでは↑に書いたスクリプトを _local-hook-exec
というファイル名で作成済と仮定します。
(_
を先頭につけているのは、git-hooks用のコマンドか否かをわかりやすくするためです)
また、sampleのPATHを取得するため、6行目でbrewのコマンドでgitがインストールされた場所を取得しています。
Mac以外、あるいはHomebrew以外でインストールした場合は、適切に読み替えてください。
GIT_HOOKS_DIR="${HOME}/.config/git/hooks"
echo '#!/bin/bash' >> hooks_template
echo '' >> hooks_template
echo 'source `dirname ${0}`/_local-hook-exec' >> hooks_template
ls `brew --prefix git`/share/git-core/templates/hooks | sed s/.sample//g | xargs -I{} cp -n hooks_template ${GIT_HOOKS_DIR}/{}
chmod 700 ${GIT_HOOKS_DIR}/*
rm hooks_template
このコマンドでは git側にあるhooksのテンプレートからhooksのファイル名一覧を取得→取得したファイルに対して、以下のスクリプトをコピーしています。
#!/bin/bash
source `dirname ${0}`/_local-hook-exec
これにより、すべてのコマンド実行時に _local-hook-exec
が呼ばれる=gitリポジトリ側に設置したgit-hooksスクリプトが実行されるようになりました。
また、今後git側で対応するhooksが増えても同様のスクリプトを実行するだけで、同様の状態にすることができます。
まとめ
この手順を踏むことで、すべてのRepository共通のhooks、および各Repository専用のhooksをそれぞれ実行できる様になりました。
これで、言語共通で同じ処理をしたいときは、ファイルの拡張子ごとにその処理をするように、プロジェクト特有の処理はそのRepository内で定義して実行できるようになりました。
例えば、すべてのRepositoryで共通してやりたいこととしては、AWSアクセスキーをgitに誤って登録しないようにすることなど、セキュリティ的に含まないようにする処理などいろいろ考えられます。
私自身もどこまでglobalにするかは考えている最中ですが、私のglobalなgit-hooksはここで公開していますので、興味がある方は参考にしてください。