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

皆さん、git-hooksは使っているでしょうか。

`${REPOSITORY_ROOT}/.git/hooks/` 以下に決められた名前でスクリプトを作成すると、対応した操作に合わせて、その名前のスクリプトが自動で実行されます。

例えば、`${REPOSITORY_ROOT}/.git/hooks/pre-commit` ならコミット前にそのスクリプトを実行して、スクリプトの終了コードが0でないとき、commit前に処理が停止します。

よく使われるのは、pre-commitを使用して、コミット直前にCIと同様の構文チェックや、rubocop等のテストをすることではないでしょうか。

基本的には、上記のようにRepositoryごとにhooksは書くのがデフォルトですが、
最近、私はすべてのRepositoryで共通するような、設定や除外ファイルの記載をしたgitconfigやgitignoreを[その他設定系ファイルとまとめてgit管理](https://github.com/ik11235/Dotfile)しています。

そんな中、git-hooksもすべてのリポジトリで共通化できないかと思い、調べてみました。

(この記事は基本的にMac環境での動作を前提に書いています。ほとんどの処理は他環境でも動くと思いますが、一部Homebrewに依存した処理があるので、その部分は読み替えてください。)

# 先行事例の調査

「git hook global」でGoogle検索するとそれっぽい情報がいくつか出てきます。

- [gitのhookを全リポジトリで共有する \- Qiita](https://qiita.com/k0kubun/items/5cb8209e3d1854ac2e2e)
- [gitのhooksを管理する\(自分用テンプレートを使う\) \- Qiita](https://qiita.com/quattro_4/items/59fdf8b9aa9ef48ecbdf)

しかしこれらを読んでいくと、あくまでinitやclone時のテンプレートに含めて、最初にテンプレートからコピー＝実質的にすべてのリポジトリで共通化という感じでした。

この方法だと以下のような欠点が考えられます。

- 一度init, cloneしたリポジトリに一律で新しい変更を反映できない
- すでにinit, clone済みのリポジトリには自動で反映されない

ただ記事自体の最終更新日も古く、gitは2系になっていろいろ新しい機能・設定も追加されています。
そのため、 gitconfigをグローバルで設定するときに使われる `~/.gitconfig` のようなやり方があるのではないかと、公式ドキュメントを確認しました。

**ありました。**

[Git \- githooks Documentation](https://git-scm.com/docs/githooks)

このドキュメントには、以下のように記載されています。

> 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` に直接書き込む方法があります。

```sh
git config --global core.hooksPath .config/git/hooks
```

設定ファイルに直接書き込む場合は、以下の設定をconfigファイルに追記します。

```config:~/.gitconfig
 [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` においたスクリプト側で設定します。

[この記事のスクリプト](https://qiita.com/k0kubun/items/5cb8209e3d1854ac2e2e#%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%83%95%E3%83%83%E3%82%AF%E3%81%8C%E5%BF%85%E8%A6%81%E3%81%AA%E5%A0%B4%E5%90%88)と[この記事のgitリポジトリのルートを取得するコマンド](https://qiita.com/dalance/items/b04c288d8dde06ce2b59)を参考に、以下のようなスクリプトを作成しました。

```sh:_local-hook-exec
#!/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以外でインストールした場合は、適切に読み替えてください。

```sh
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のファイル名一覧を取得→取得したファイルに対して、以下のスクリプトをコピーしています。

```sh
#!/bin/bash

source `dirname ${0}`/_local-hook-exec
```

これにより、すべてのコマンド実行時に `_local-hook-exec` が呼ばれる＝gitリポジトリ側に設置したgit-hooksスクリプトが実行されるようになりました。

また、今後git側で対応するhooksが増えても同様のスクリプトを実行するだけで、同様の状態にすることができます。

# まとめ

この手順を踏むことで、すべてのRepository共通のhooks、および各Repository専用のhooksをそれぞれ実行できる様になりました。

これで、言語共通で同じ処理をしたいときは、ファイルの拡張子ごとにその処理をするように、プロジェクト特有の処理はそのRepository内で定義して実行できるようになりました。

例えば、すべてのRepositoryで共通してやりたいこととしては、[AWSアクセスキーをgitに誤って登録しないようにする](https://dev.classmethod.jp/etc/git_aws_access_key/)ことなど、セキュリティ的に含まないようにする処理などいろいろ考えられます。

私自身もどこまでglobalにするかは考えている最中ですが、私のglobalなgit-hooksは[ここ](https://github.com/ik11235/Dotfile/tree/master/DOT_LINK_TARGET/config/git/hooks)で公開していますので、興味がある方は参考にしてください。