git hooks を git 管理して monorepo でも動かせるように作ってみる

最近 husky を使っていたアプリケーションが monorepo に組み込まれ、husky の設定を見直さなければいけなくなってしまった。husky は monorepo サポートしているが、monorepo の root に husky をインストールしないといけないため、root に husky を devDependencies に入れただけの package.json を作らないといけないと思うと「それってスマートなんだろうか」と考えるようになってしまった。
そのため、「husky が git hooks を簡単に扱えるようにするものなんだったら、git hooks を自分で書けるようになったらいいんじゃないか」と思い、自分で git hooks を書いてみることにした。

試せる環境

GitHub - tyankatsu0105/sandbox-git-hooks

git hooks で使いたいファイルを git 管理する

.git/hooksに決まった名前のファイルを置くと、git コマンドにフックしてそのファイル内の処理を動かしてくれる。Git - githooks Documentation

実行権限つけないと動かないので注意

しかし、.git内は git 管理できないので、どこか別の場所で hooks ファイルを管理しないといけない。
cp.git/hooksに移すことを前提でディレクトリを作るといい。

.
├── .git
└── .githooks
    └── pre-commit

そして、.githooksの中を実際に移動する script をこんな感じで置くといい。

#!/bin/sh

cd `dirname $0`

GIT_HOOKS_DIRECTORY='../.githooks/'
ORIGINAL_GIT_HOOKS_DIRECTORY='../.git/hooks/'

cp -f -r $GIT_HOOKS_DIRECTORY $ORIGINAL_GIT_HOOKS_DIRECTORY
chmod -R +x $ORIGINAL_GIT_HOOKS_DIRECTORY

ファイルの中の処理を書く

今回はpre-commitの処理を書く

  • 差分ファイルの名前をリストで取得
  • リストを for で回す
  • 差分ファイル名中のディレクトリ文字列で条件分岐
  • 該当のディレクトリに移動して処理実行
  • 処理でエラーが出たらその時点で処理中断する
  • 処理実行はディレクトリ毎に一回だけ
#!/bin/sh

function my_error() {
  echo ""
  echo "🤦 Oops... There is an error..."
  echo ""
  exit 1
}

FILES=$(git diff --name-only HEAD)
ROOT=$(git rev-parse --show-toplevel)

IS_CHACKED_LIBS_UTILS=false
IS_CHACKED_APPS_CLIENT=false

for file in $FILES; do
  cd $ROOT

  case "$file" in
  libs/utils/*)
    if $IS_CHACKED_LIBS_UTILS; then continue
    fi
    IS_CHACKED_LIBS_UTILS=true

    cd libs/utils
    npm run git-hooks:pre-commit || my_error
    ;;

  apps/client/*)
    if $IS_CHACKED_APPS_CLIENT; then continue
    fi
    IS_CHACKED_APPS_CLIENT=true

    cd apps/client
    npm run git-hooks:pre-commit || my_error
    ;;

  *)
    ;;
  esac
done

shell script 構文に強くないので、もっとスマートに書きたい

感想

husky 便利だけど、monorepo にした途端めんどうになるので、husky をやめて自分たちで git hooks 処理を作って管理するようにすると npm 依存しなくて済むし、汎用的になるかもしれない。