ひびのログ

日々ではないけどログを出力していくブログ

Git Hooks を複数実行したかった

何がしたかったのか

Git でコミットをする前に、コミットに使われるユーザーの名前を表示したかった。

なぜ?

会社の PC で、会社のリポジトリだけでなく自分の個人リポジトリにもアクセスしている。 具体的には、dotfiles を GitHub で管理しているのでそれの改善をしたかったり、社内勉強会の資料を個人リポジトリで作っていたりした。

その時、間違えて会社の Git アカウントで個人リポジトリの更新、ないしその逆をやらかしてしまうのが怖い。 事故をなるべく抑えたいので、とりあえず目視確認はできるようにしておきたいと考えた。

ちなみに、名前・メールアドレスはリポジトリごとに設定する運用にしている。 グローバルではなくローカルにだけ登録することで、リポジトリ単位で設定したユーザーでコミットできる。

もちろんこれも確実ではない。 間違えてローカルではなくグローバルに登録してしまう可能性がある。 だから、最終防衛ラインとしての対策を追加したかった。

環境

$ git --version
git version 2.25.1

執筆時点では 2.42.1 が最新なので、そこそこ古いものを使っていた。 Ubuntu だと最新のものが apt で入らないとかなんとか。 リポジトリから直接落としてくるほうがいいかも。

わかったこと

  • Git Hooks を配置するディレクトリを変えるなら、Git Config の core.hooksPath を変えればいい
  • core.hooksPath は複数設定できない(後勝ち)
  • Git のエイリアスは、元からあるコマンドの上書きができない

詳しく

core.hooksPath 関連

まず考えたのは、pre-commit hook を使う方法。 コミット前に何かをするならこれが一番オーソドックス。

今回やりたいことである「コミット前に名前を表示する」というのは、全リポジトリに適用したい。 しかし、個別のリポジトリで pre-commit hook を使っていることがあった。 具体的には、ESLint, Prettier, git-secrets の実行などだ。 つまり、「ローカルの pre-commit hook を生かしながら、グローバルの pre-commit hook を実行する」必要がある。

ここで、最低限 Git Hooks を配置するディレクトリを変更する方法はわからないといけない。 ドキュメントを見ると書いてある。

core.hooksPath By default Git will look for your hooks in the $GIT_DIR/hooks directory. Set this to different path, e.g. /etc/git/hooks, and Git will try to find your hooks in that directory

git-scm.com

設定がない状態ではデフォルトの .git/hooks/pre-commit が実行される。 仮に $HOME/.config/git/hooks にグローバルに実行したいフックを配置し、core.hooksPath の設定を書き換えてみる。 そしてコミットする。

$ cat ~/.config/git/hooks/pre-commit
#!/bin/bash
echo "### global hook"

$ git config --local --add core.hooksPath "~/.config/git/hooks"

$ git commit "test1"
### global hook
[master 5c0deb4] test1
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 test1

実行された。

次に、core.hooksPath に複数の値を登録してみる。 .git/config を直接書き換える方法を取った。

$ cat .git/config
[core]
    hooksPath = .git/hooks
    hooksPath = ~/.config/git/hooks

$ cat .git/hooks/pre-commit
#!/bin/bash
echo "### local hook"

$ git commit "test2"
### global hook
[master 1e9c7da] test2
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 test2

グローバルだけしか実行されなかった。 気になったので順番を入れ替えて実行してみる。

$ cat .git/config
[core]
    hooksPath = ~/.config/git/hooks
    hooksPath = .git/hooks

$ git commit "test3"
### local hook
[master 2ab4693] test3
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 test3

このことから、core.hooksPath は複数設定できず、後に定義したものが有効になるようだ。

ならばと、glob で複数ディレクトリを指定してみた。

$ cat .git/config
[core]
    hooksPath = {~/.config/git/hooks,.git/hooks}

$ git commit "test4"
[master 8c460ac] test4
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 test4

だめでした。

コードを見てみたところ、ファイル・関数名から推察するにおそらくこのあたり。 確かに単一のパスを返すことしかできなそうだ。

github.com

エイリアス関連

Git Hooks を複数実行するのは不可能だとわかった。 では git commit するときにエイリアスを設定すればいいのではないかと考えた。

To avoid confusion and troubles with script usage, aliases that hide existing Git commands are ignored.

git-scm.com

無慈悲。

自分は普段からエイリアスを活用しているので、そちらに設定すれば一定回避可能ではある。 しかし、容易に回避できてしまうルートが生まれるのは避けたく、採用は見送り。

init.templateDir

init.templateDir を使うことで、リポジトリを作成するときの設定を変えられる。 ただし、今回は既存のリポジトリにも適用したかったので、こちらの採用は見送った。

ではどうするのか

コミット時にやるのが無理なら、常時表示させればいい。 Zsh であればプロンプトのカスタマイズができる。 現在はブランチ名を表示させているので、その隣にでも出そうかな。 やり方はググれば出てくるのでここでは書かない。

あるいは、動作確認とかはしていないが、「グローバルのフックを有効にしておき、グローバルの pre-commit に、ローカルの pre-commit を実行する処理を書いておく」とかでも実現可能かもしれない。

#!/bin/bash

repo_dir="$(実行しているリポジトリのルートを取得する処理)"
exec "${repo_dir}/${GIT_DIR}/hooks/$(basename BASH_SOURCE[0])"