19
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Git Hooks】コミット時にコミットメッセージの自動チェックを行う。【commit-msg】

Last updated at Posted at 2022-09-12

前提

この記事ではコミットメッセージを自動的にチェックして、ルールにそぐわない場合はコミットの実行を取り消す方法について解説していきます。
今回は凡例としてコミットメッセージに以下のようなルールを作ります。

  • Prefixを必ず付ける
  • issue番号("#1", "#2", etc...)を必ず付ける

Git Hooksとは

Git Hooksとはgitの機能の一つで、git commitgit pushが実行された際に、スクリプトを実行することができ、実行結果によってコマンドを通すかどうか自動的に判断させることができます。
機能の詳細については以下をご参考ください。
8.3 Git のカスタマイズ - Git フック

完成形

コマンド成功時

コミットメッセージ'feat: test #1'
スクリーンショット 2022-10-09 21.14.43.png

コマンド失敗時1

コミットメッセージ'test'
スクリーンショット 2022-10-09 21.15.20.png

コマンド失敗時2

コミットメッセージ'fix: test
スクリーンショット 2022-10-09 21.15.46.png

コマンド失敗時3

コミットメッセージ'test #1'
スクリーンショット 2022-10-09 21.17.25.png

手順

git initを実行したディレクトリには.gitというディレクトリが作られています。
.gitの中にはhooksというディレクトリが存在しており、その配下に予約された名前のスクリプトファイルを設置しておくと、そのファイルの名前に従ったタイミングでファイル内に書かれているスクリプトが実行される仕組みです。

  1. .git/hooks/commit-msgファイルを作成する
  2. コミットメッセージのルールをスクリプトに書く
  3. コミットを作成してみる

commit-msgファイルを作成

先に以下のコマンドでgit環境を作っておきます。
$ mkdir sample; cd sample; git init; touch test.txt

Git HooksはGitリポジトリごとに(git initごとに)設定ができます。
以下はgit initしたての状態のhooksディレクトリになります。

$ ls .git/hooks
applypatch-msg.sample     post-update.sample        pre-merge-commit.sample   pre-receive.sample        update.sample
commit-msg.sample         pre-applypatch.sample     pre-push.sample           prepare-commit-msg.sample
fsmonitor-watchman.sample pre-commit.sample         pre-rebase.sample         push-to-checkout.sample

ここに設置されているスクリプトファイルはそれぞれ所定のタイミングで実行されます。
(最初は全てのファイルが.sampleとなっており、これらのスクリプトは実行されていません。)

今回必要になるのは、commit-msgというファイルです。
元から用意されているcommit-msg.sampleには手をつけず、新規で作成していきます。

$ vi .git/hooks/commit-msgcommit-msgファイルを作成し、以下のように書きます。

#!/bin/bash

echo -e "\033[37;1m🪝 Running Git Hooks: commit-msg\033[0m"
# コミットメッセージを定義
MSG="$(cat "$1")"
readonly MSG
# 終了コードを定義。0: OK, 1: NG
code=0

# Prefixの存在チェック
echo -en " - Prefixの存在チェック: "
## 必要なPrefixを定義
readonly CORRECT_PREFIXES=("feat" "fix" "docs" "style" "refactor" "pref" "test" "chore")
## 各要素に": "を追加
for i in "${!CORRECT_PREFIXES[@]}"; do
  correct_prefixes[i]="${CORRECT_PREFIXES[i]}: "
done
## `grep -E`で配列からOR検索をするため、半角スペース(" ")の区切り文字をパイプ("|")に変更
prefixes="$(
  IFS="|"
  echo "${correct_prefixes[*]}"
)"
if ! echo "$MSG" | grep -Eq "${prefixes}"; then
  echo -e "\033[31;22mNG"
  echo -e "================================================================"
  echo -e "コミットメッセージにPrefixが含まれていません。"
  echo -e ""
  echo -e "Prefix"
  echo -e "  ${correct_prefixes[*]}"
  echo -e "================================================================\033[0m\n"
  code=1
else
  echo -e "\033[32;22mOK\033[0m"
fi

# issue番号の存在チェック
echo -en " - issue番号の存在チェック: "
readonly ISSUE_NO="\#[1-9]"
if ! echo "$MSG" | grep -q "${ISSUE_NO}"; then
  echo -e "\033[31;22mNG"
  echo -e "================================================================"
  echo -e "コミットメッセージにissue番号が含まれていません。"
  echo -e ""
  echo -e "Example: #1234"
  echo -e "================================================================\033[0m\n"
  code=1
else
  echo -e "\033[32;22mOK\033[0m"
fi

# 終了宣言
if [ ${code} -eq 0 ]; then
  echo ""
  echo -e "\033[32;1m✨ALL PASS!!\033[0m\n\n"
else
  echo ""
  echo -e "\033[31;1mGit Hooks: commit-msg: NG\033[0m\n\n"
fi

exit ${code}

コードの解説は後述します。

最後に、作成したスクリプトファイルに実行権を与えます。
$ chmod a+x .git/hooks/commit-msg

解説

echo -e "\033[37;1m🪝 Running Git Hooks: commit-msg\033[0m"
# コミットメッセージを定義
MSG="$(cat "$1")"
readonly MSG
# 終了コードを定義。0: OK, 1: NG
code=0
  • echo -e "\033[37;1m🪝 Running Git Hooks: commit-msg\033[0m"
    commit-msgファイルが実行されたことをわかりやすくするために、コミット実行中にターミナルに文字を出力しています。
    echoで出力される文字の装飾については以下をご参考ください。
    シェルでechoの文字に色をつける方法

  • MSG="$(cat "$1")"
    変数MSGを定義し、$ cat $1の出力結果を代入しています。
    変数$1には最新のコミットメッセージが書かれているファイルのパス.git/COMMIT_EDITMSGが代入されており、その中をcatコマンドで出力(変数MSGに代入)しています。

  • code=0
    このスクリプトがエラー終了なのか正常終了なのかのフラグです。
    初期値0は正常終了、つまりコミットのプロセスが問題なく実行されます。
    この後のチェック項目に失敗すると、codeに1を代入しています。

# Prefixの存在チェック
echo -en " - Prefixの存在チェック: "
## 必要なPrefixを定義
readonly CORRECT_PREFIXES=("feat" "fix" "docs" "style" "refactor" "pref" "test" "chore")
## 各要素に": "を追加
for i in "${!CORRECT_PREFIXES[@]}"; do
  correct_prefixes[i]="${CORRECT_PREFIXES[i]}: "
done
## `grep -E`で配列からOR検索をするため、半角スペース(" ")の区切り文字をパイプ("|")に変更
prefixes="$(
  IFS="|"
  echo "${correct_prefixes[*]}"
)"
if ! echo "$MSG" | grep -Eq "${prefixes}"; then
  echo -e "\033[31;22mNG"
  echo -e "================================================================"
  echo -e "コミットメッセージにPrefixが含まれていません。"
  echo -e ""
  echo -e "Prefix"
  echo -e "  ${correct_prefixes[*]}"
  echo -e "================================================================\033[0m\n"
  code=1
else
  echo -e "\033[32;22mOK\033[0m"
fi
  • readonly CORRECT_PREFIXES=("feat" "fix" "docs" "style" "refactor" "pref" "test" "chore")
    コミットメッセージに必要となるPrefixを定義しています。
    チームによって「どのようなPrefixをつけるか」、「そもそもPrefixはつけない」などあると思うので、この設定はご自身の環境に合わせてください。
    ここで設定しているPrefixは以下の記事を参考にしています。
    僕が考える最強のコミットメッセージの書き方
    readonlyによって、変数CORRECT_PREFIXESは読み込み専用と

  • for i in "${!CORRECT_PREFIXES[@]}"; do correct_prefixes[i]="${CORRECT_PREFIXES[i]}: " done
    配列で定義した各要素の末尾に": "を追加して配列を新しく作っています。

  • prefixes="$(IFS="|"; echo "${correct_prefixes[*]}")"
    この後のgrepでOR検索ができるように、配列の区切り文字の半角スペース(" ")をパイプ("|")に変換しています。

  • if ! echo "$MSG" | grep -Eq "${prefixes}";
    grepコマンドの成否判定でPrefixの有無を確認します。
    -qオプションはターミナルにgrepコマンドの結果を出力させず、
    -Eオプションはパイプ("|")で区切られた文字をOR検索します。
    詳しくは$ man grepをご参考ください。

次にissue番号の有無を確認します。

echo -en " - issue番号の存在チェック: "
readonly ISSUE_NO="\#[1-9]"
if ! echo "$MSG" | grep -q "${ISSUE_NO}"; then
  echo -e "\033[31;22mNG"
  echo -e "================================================================"
  echo -e "コミットメッセージにissue番号が含まれていません。"
  echo -e ""
  echo -e "Example: #1234"
  echo -e "================================================================\033[0m\n"
  code=1
else
  echo -e "\033[32;22mOK\033[0m"
fi

  • ISSUE_NO="\#[1-9]"
    変数issue_numberにはハッシュ("#")から始まり、後に数字の1~9のいずれかが続く文字列を代入しています。
    正規表現:数字の表現。桁数や範囲など
    こちらもreadonlyによって変数ISSUE_NOは読み取り専用になっています。

  • if ! echo "$MSG" | grep -q "${ISSUE_NO}";
    grepコマンドの成否判定でissue番号(変数ISSUE_NO)の有無を確認します。

  • exit ${code}
    ここまでのチェックを通して、チェックに失敗していた場合はcodeに1が代入されているので、exit 1エラー終了となります。
    スクリプトがエラー終了した場合は、コミットのプロセスは中断されます。(git commit前、git add後の状態に戻ります。)
    スクリプトがexit 0正常終了した場合は、そのままコミットのプロセスは継続されます。(他にスクリプトがなければこれでコミットも完了します。)

# 終了宣言
if [ ${code} -eq 0 ]; then
  echo ""
  echo -e "\033[32;1m✨ALL PASS!!\033[0m\n\n"
else
  echo ""
  echo -e "\033[31;1mGit Hooks: commit-msg: NG\033[0m\n\n"
fi

exit ${code}
  • if [ ${code} -eq 0 ]; then
    ここまでのチェックで問題がなければcodeの値は0のままとなるので✨ALL PASS!!の文字を表示させるか、
    チェックに問題がありcodeの値が1になっていればGit Hooks: commit-msg: NGの文字を表示させ、
    最後にexitで正常終了か異常終了します。
    このスクリプトが正常終了の場合はgit commitコマンドが実行され、異常終了の場合はgit commitコマンドは取り消されます。

実行

実際に動かしていきます。
準備の所で用意したtest.txtをステージングし、コミットメッセージは'test'としてコミットしてみます。

$ git add test.txt
$ git commit -m 'test'
スクリーンショット 2022-10-09 21.15.20.png
コミットメッセージ"test"にはPrefixもissue番号も入っていないため、上記のようなエラーがターミナル上に出力され、$ git logを確認してみてもコミットが実行されていないことがわかります。

コミット時にスクリプトの実行をスキップしたい場合

スクリプトの実行をスキップ(今回で言うとコミットメッセージのチェックを行いたくない)したい場合は--no-verifyオプションつけて$ git commitを行えば、チェックを通さず、すぐにコミットが実行されます。

Sourcetreeの場合

コミット時に以下のようなダイアログが表示されます。
スクリーンショット 2022-10-09 21.41.00.png
(Sourcetreeだとテキスト装飾がそのまま表示されるのは解決できませんでした・・・。おそらくシェルの違いによるものだと思われます。)

Sourcetreeではコミット作成時の右にあるコミットオプションから[コミットフックをバイパス]にチェックを入れることで、ターミナルコマンドの--no-verifyオプションと同じ挙動になります。
スクリーンショット 2022-09-12 20.26.09.png

まとめ

今回はPrefix、issue番号の存在チェックを例にして紹介しましたが、チームによってルールはさまざまかと思いますので、ご自身の環境に合わせてカスタマイズしてみてください。

今回ご紹介したスクリプトファイルは私のGitHubリポジトリにも同じものを置いてあります。

参考になれば幸いです。

エラー集

hint: The '.git/hooks/commit-msg' hook was ignored because it's not set as executable.
hint: You can disable this warning with `git config advice.ignoredHook false`.

この場合は作成したファイルに実行権が与えられていないため、スクリプトは実行されていません。

$ ls -l .git/hooks
...
-rw-r--r--  1 masaosasaki  staff   256 Sep  8 23:09 commit-msg
-rwxr-xr-x  1 masaosasaki  staff   896 Jun  5 14:22 commit-msg.sample
...

上記の.sampleように本来であれば実行権(x)が必要となるので、chmod a+x .git/hooks/commit-msgを実行します。

$ chmod a+x .git/hooks/commit-msg
$ ls -l .git/hooks
...
-rwxr-xr-x  1 masaosasaki  staff   256 Sep  8 23:09 commit-msg
-rwxr-xr-x  1 masaosasaki  staff   896 Jun  5 14:22 commit-msg.sample
...

参考

サイト

Linking a pull request to an issue
8.3 Git のカスタマイズ - Git フック
シェルでechoの文字に色をつける方法
正規表現:数字の表現。桁数や範囲など
逆引きシェルスクリプト/if文の条件式でand, or, notを使う方法
僕が考える最強のコミットメッセージの書き方
Bashの配列でjoinやsplitする
【Shellスクリプト】文字列置換「bash」「sed」について!
シェルスクリプトに挑戦しよう(16)配列[基本編]
ShellCheck - VScode拡張機能

19
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?