前提
git commitの時にコミットメッセージに特定の文字が含まれているか確認するにはどうしたらいいんだろうか?
— まさ🧩アプリ開発 (@masa_soli_s) September 7, 2022
git hooks の pre-commitで表現できるのか🤔
変数でコミットメッセージ受け取る方法ある、、、?#Git
この記事ではコミットメッセージを自動的にチェックして、ルールにそぐわない場合はコミットの実行を取り消す方法について解説していきます。
今回は凡例としてコミットメッセージに以下のようなルールを作ります。
- Prefixを必ず付ける
- issue番号("#1", "#2", etc...)を必ず付ける
Git Hooksとは
Git Hooksとはgitの機能の一つで、git commit
やgit push
が実行された際に、スクリプトを実行することができ、実行結果によってコマンドを通すかどうか自動的に判断させることができます。
機能の詳細については以下をご参考ください。
8.3 Git のカスタマイズ - Git フック
完成形
コマンド成功時
コマンド失敗時1
コマンド失敗時2
コマンド失敗時3
手順
git init
を実行したディレクトリには.git
というディレクトリが作られています。
.git
の中にはhooksというディレクトリが存在しており、その配下に予約された名前のスクリプトファイルを設置しておくと、そのファイルの名前に従ったタイミングでファイル内に書かれているスクリプトが実行される仕組みです。
-
.git/hooks/commit-msg
ファイルを作成する - コミットメッセージのルールをスクリプトに書く
- コミットを作成してみる
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-msg
commit-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'
コミットメッセージ"test"にはPrefixもissue番号も入っていないため、上記のようなエラーがターミナル上に出力され、$ git log
を確認してみてもコミットが実行されていないことがわかります。
コミット時にスクリプトの実行をスキップしたい場合
スクリプトの実行をスキップ(今回で言うとコミットメッセージのチェックを行いたくない)したい場合は--no-verify
オプションつけて$ git commit
を行えば、チェックを通さず、すぐにコミットが実行されます。
Sourcetreeの場合
コミット時に以下のようなダイアログが表示されます。
(Sourcetreeだとテキスト装飾がそのまま表示されるのは解決できませんでした・・・。おそらくシェルの違いによるものだと思われます。)
Sourcetreeではコミット作成時の右にあるコミットオプションから[コミットフックをバイパス]にチェックを入れることで、ターミナルコマンドの--no-verify
オプションと同じ挙動になります。
まとめ
今回は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拡張機能