simple-git-hooksでもブランチ名からIssue番号抽出をやりたくて、わちゃわちゃした話

開発関連技術

shell, Git


こんにちは、よしです。
今回は記事リハビリということで、ちょっとした小ネタ記事を書いてみました。

今回やりたかったこと#

簡潔に言うと
「simple-git-hooks を使った Git フック設定で、ブランチ名から Issue 番号を抽出してコミットメッセージへ自動付与するようにしたい」
というやつです。

背景はいいから実際の設定方法見せてくれや、という方は設定方法の結論へどうぞ。

もうちょっと背景を説明してみる#

Git フック#

皆さんおなじみの Git に Git フックという機能があります。

他のバージョンコントロールシステムと同じように、Git にも特定のアクションが発生した時にカスタムスクリプトを叩く方法があります。
このようなフックは、クライアントサイドとサーバーサイドの二つのグループに分けられます。
クライアントサイドフックはコミットやマージといったクライアントでの操作の際に、サーバーサイドフックはプッシュされたコミットの受け取りといったネットワーク操作の際に、それぞれ実行されます。
これらのフックは、さまざまな目的に用いることができます。

公式にも書いてある通り、Git の特定のアクションが発生した時にカスタムスクリプトを叩く仕組みのことです。
(詳細やフックの種類は当記事では説明しないので、公式ページをご参照ください)

この Git フックを使った設定としてよくあるのが…

  • コミット前に Linter、フォーマッターでコード整形する
  • ブランチ名から Issue 番号を抽出して、コミットメッセージに自動で付与する

など。

前提として、コミットメッセージに Issue 番号を書いておくと、自動的にその Issue と紐づけをしてくれるのもおなじみですね。(PR なんかでもそうですが)
自分は以前から手動で Issue 番号を書いていたので、これを Git フックで自動化したいなぁと思ったのでした。

Git フックに関するパッケージ#

Git フックを設定となると、多くの方はそれを扱いやすくしてくれるパッケージを利用しているのではないでしょうか。
割と有名なものだと Husky とか。

自分が Git フックを活用するようになった頃、ちょうど Husky はライセンス問題でごたごたしていた時だったので、自分は simple-git-hooks の方を採用したのでした。
Husky に対して、こちらは Zero dependency かつ軽量版なやつ。


じゃあ、simple-git-hooks で今回やりたい仕組みを設定しよう!となったわけですが、ググっても Husky の記事ばかりヒットしまして。
基本的な設定方法はおおよそ同じとは思いつつ、実際にやってみるといろいろハマったので、せっかくなら記事にするかとなった次第です。

前提#

  • simple-git-hooks を初期設定済み

動作確認環境.

  • Windows:Git Bash × GNU sed
  • Mac:iTerm × BSD sed

設定方法の結論#

使用する Git フック#

検索でヒットした多くの記事は commit-msg フックを使用して、コミットメッセージの先頭か末尾に番号を付与しているような感じでした。
ただ、自分の場合はコミットメッセージの一行目末尾に Issue 番号をつけたくて。

commit-msg フックだとちょっとめんどくさくね…?
一行目だけ抽出して末尾に番号付けて、残りの行を結合するとかやらないけなくね…?

ということで、prepare-commit-msg フックを使用する方向に。
あらかじめコミットメッセージテンプレに置換用の文字を入れておき、コミットメッセージ入力前にブランチ名から抽出した Issue 番号に置換する、ということをやることにしました。

このやり方は、こちらの記事を参考にさせていただきました。

コミットメッセージテンプレ#

使用するコミットメッセージテンプレの1行目に置換用のテキストを入れておきます。

.gitmessage
(#Issue)

prepare-commit-msg フックのスクリプト#

prepare-commit-msg はコミットメッセージを入力する前に実行されるフックです。
後述しますが、sed コマンドが GNU でも BSD でも動作するように組んでます。

ブランチ名の前部分がブランチ種別/Issue番号という命名だと抽出する仕組み。
ブランチ種別部分は一旦以下の3つにしました。

  • feature
  • bugfix
  • release

ブランチ名の例.

  • feature/1
  • feature/1-test
.githooks/prepare-commit-msg
#!/bin/sh

## sed コマンドが GNU か BSD か確認
GNU_SED=true
sed --version 1>/dev/null 2>/dev/null || GNU_SED=false
echo $GNU_SED

## コミットメッセージ入力前に、ブランチ名から Issue 番号を抽出して置換する
COMMIT_MSG_FILE=$1
MESSAGE=$(cat "$COMMIT_MSG_FILE")

ISSUE_NUMBER=$(git rev-parse --abbrev-ref HEAD | grep -Eo "^(feature|bugfix|release)/[0-9]+" | grep -Eo "[0-9]+")
if [ -n "$ISSUE_NUMBER" ]; then
  if [ "$GNU_SED" == "true" ]; then
    sed -i "s/(#Issue)/(#$ISSUE_NUMBER)/" $COMMIT_MSG_FILE
  else
    sed -i "" "s/(#Issue)/(#$ISSUE_NUMBER)/" $COMMIT_MSG_FILE
  fi
  exit 0
fi

## Issue 番号が抽出できなかった + そのまま続行する場合は、置換用文字列を除去する
read -p "Issue 番号がブランチ名にないので置換できませんが、続行しますか? (y/N): " YM < /dev/tty
case "$YM" in
  [yY]*)
    if [ "$GNU_SED" == "true" ]; then
      sed -i "s/(#Issue)//" $COMMIT_MSG_FILE
    else
      sed -i "" "s/(#Issue)//" $COMMIT_MSG_FILE
    fi;;
  *) echo "abort." ; exit 1 ;;
esac

Mac の場合は、ファイル作成時に実行権限がつかない?ようだったので実行権限を付与しておく必要がありました。

chmod a+x .githooks/prepare-commit-msg

simple-git-hooks の設定#

前述のカスタムのフックスクリプトを実行するように書けばいいわけですが、\"$@\"で引数を渡すのを忘れずに(引数に関しては後述も参照)

pakcage.json
// 抜粋
"simple-git-hooks": {
  "prepare-commit-msg": "./.githooks/prepare-commit-msg \"$@\""
},

これらの内容で、simple-git-hooks の設定を反映。
以降は、コミットしてコミットメッセージ入力前にフックスクリプトが動作して、置換用テキストがブランチ名から抽出した Issue 番号に指し変わっているはずです。

躓いたところと補足説明#

たったこれだけの設定をするだけでも、意外といろんなところに躓きました。
(普段、シェル書き慣れてないのもあるかも😇)

せっかくなので、どのあたりに躓いたのかと、その補足説明も書いておこうかと。

引数が入らない?#

最初、いろんな記事を読んでみたところ、フックスクリプトの引数$1にコミットメッセージファイルの情報が入ってくることがわかりました。
なので、$1を使うようなシェルを書いていったわけですが、いざ実行するとなぜか値が入ってこない…。
引数の数が入る$#を確認すると0。

どういうこと??? simple-git-hooks だとなんか引数の渡り方が違うんか???と混乱。
原因としては、simple-git-hooks の設定定義時に引数を渡すような書き方をしていなかったせいでした。

// 誤
"prepare-commit-msg": "./.githooks/prepare-commit-msg"

// 正
"prepare-commit-msg": "./.githooks/prepare-commit-msg \"$@\""

simple-git-hooks は設定したフックの種類に応じて、.git/hooks配下にフックファイルを作成します。
(今回だと.git/hooks/prepare-commit-msg
そのフックファイルの中で、simple-git-hooks 設定で設定したコマンドを実行する、というような仕組みになっているのです。

.git/hooks/prepare-commit-msg
#!/bin/sh
./git-hook/commit-msg "$@"

そのため、元々のフックファイルへ渡されるようになっている引数をカスタムのフックの方でも使用したい場合は、そのまま引数を渡してあげれば OK。

引数渡してないんだから、そりゃあ引数ないってなるやん。

read の処理で止まってくれない#

ブランチ名から Issue 番号を抽出するにあたり、もし番号が見つからなかった場合は、そのまま続行してよいか応答処理を入れるようにしました。

こちらの記事を見て、シェルで応答処理を実現するには read を使えばいいらしいぞとわかり、書いてみて動作確認。
すると、本来はユーザの入力待ち状態になり止まってくれるはずなのですが、止まってくれない…。
どうもユーザ入力部分が勝手に入力されて、y でも Y でもないので続行しない方の処理に進んでしまっているようでした。

この件を調べてみると、ループ処理の中で read を使っているとそういうことがあるという記事がいくつかヒット。
今回はループ処理の中ではないんだけどなと思いつつ。
コンソールからの入力しか受け付けないようにするには< /dev/ttyをつけるとよいとのことだったので、つけたところ止まってくれるようになりました。

sed コマンドの違い#

Windows(Git Bash)でも Mac でも使用できる sed コマンドですが、どうも2種類あって、微妙に挙動が違うらしいとわかり。
種類としては以下の2つ。

  • Windows(Git Bash)or Linux:GNU sed
  • Mac:BSD sed

今回使用している、ファイル上書きの-iオプションも挙動が違うものの1つでして。
BSD sed の方では、-iオプションの後ろにバックアップファイルの拡張子をつける必要があるとのこと。
(バックアップファイルを作りたくない場合は、空文字にすると作られません)

# GNU sed
sed -i "s/(#Issue)/(#$ISSUE_NUMBER)/" $COMMIT_MSG_FILE

# BSD sed
sed -i "" "s/(#Issue)/(#$ISSUE_NUMBER)/" $COMMIT_MSG_FILE

そのため、できればどちらでも動作するようなスクリプトにしたいなぁと試行錯誤でわちゃわちゃして。
結果的に、sed --versionコマンドの実行結果でどちらの sed なのか判断するフラグ変数を作り、それで分岐させるようにしました。
BSD sed の場合は、--versionオプションが存在しないのでエラーになることを利用したものです。

それとsed --versionの実行結果を出力はしたくなかったので、1>/dev/null 2>/dev/nullで出力しないようにしています。


以上、小ネタ記事をお送りしました。
自分は普段シェルをあまり書いてない人間なので、なかなかうまくいかなくて四苦八苦…😇

とはいえ、前からやりたいと思っていた仕組みを作ることができたので、これからばっちり活用していきたいですね。

採用こそしなかったですが、Git フックってほかの言語でも書けるらしく、へーそうなんだーという発見があったり。
Git フックの理解が少し深まったので、ほかにも便利そうなスクリプト組んでみるのもよさそうです。
また、何かあったら記事にするかもしれません。

参考リンクまとめ#