この記事は?
Git Hooks のcommit-msg
を用いて、コミットメッセージの書式が一定のルールに沿っているかを確認するスクリプトを実装してみました。
Git の基本、基本的なコマンド (ちょっと応用的な話もあるけど・・・最低限cat
やecho
、パイプなど) が分かる前提で話を進めます。
2022/07/29 編集
過去に自分の書いた実装方法 (作るの項) が、後になって見返してみたらあまりにも気に入らなかったので作り直しました。ただ、まるっきり消し去るのもちょっと忍びなかったので、旧版は折りたたみに格納しました。また、他の項目についても一部編集を加えました。
Git Hooks とは?
簡単に Git Hooks の説明。
Git Hooks は、Git リポジトリで特定の操作が発生した際に、特定のプログラム (シェルスクリプト) を実行する仕組みです。
そのうちの 1つcommit-msg
は、コミットメッセージの入力が完了し、コミットが生成される直前に呼び出されるものです。exit 1
など、返り値 0以外を返した場合、コミットが中止されます。
作りたいもの
メッセージの書式は、[Prefix] Message
というように、接頭詞と本文で構成されているようにします。具体的には、以下のようなルールを設けることにします。
- 1行だけであること
- 接頭詞と本文の間に半角スペースが 1つだけあること
- 0文字でないこと
- 2文字以上でないこと
- 接頭詞の前にスペースなどが入っていないこと
-
[Prefix]
は[Add], [Mod], [Del]
のいずれかであること -
Message
が空でないこと ([Prefix]
だけ書かれたメッセージでないこと)
git commit -m <message>
で 1行だけのコミットメッセージを入れることが大半ですが、複数行のコミットメッセージを入れることも可能です。「メッセージは簡潔であるべき」ということにし、ここでは 2行以上のメッセージは許可しないこととします。(1つのコミットで変更が多くなり過ぎないようにすべき、という考え方にも基づきます)
作る (旧版)
クリックして旧班を表示
コミットメッセージの取得
まずは、入力されたコミットメッセージの取得です。さほど難しいことはありません。
commit-msg
のコマンド引数の 1つ目に、入力されたコミットメッセージが書かれているCOMMIT_EDITMSG
へのパスが入力されます。
これをcat
コマンドで取得してやれば OK です。
mes=`cat $1`
(シェルスクリプトのお約束#!/bin/sh
は、ここでは省略しています。)
メッセージの下処理
さて、COMMIT_EDITMSG
の内容ですが、下処理をしなくてはなりません。
git commit
で-m
オプションを付けてメッセージが入力された場合はそこまで問題にはなりません。
Any message
しかし、-m
オプション無しで、テキストエディタからメッセージが入力された場合は、次のような入力が考えられます。
Any message
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# ブランチ master
#
# 最初のコミット
#
# コミット予定の変更点:
# new file: any_file
#
コミット生成で、#
から始まる行は無視されます。
また、ファイルの初めから文字のある行までの間に空行があった場合、それも無視されます。
正しく書式判定をするため、メッセージ取得でこの処理を書きます。
mes=`cat $1 | sed -e '/^#/d'`
while [ "`echo "$mes" | sed -n -e "1p"`" = "" ]; do
mes=`echo "$mes" | sed -e "1d"`
done
-
#
で始まる行 (正規表現で^#
) を削除 (/.../d
) し、変数mes
へ格納。 - 変数
mes
の 1行目を取得し、
- 空であれば (空行であれば)、
sed
コマンドでmes
の 1行目を削除 (1d
) し、mes
へ格納 - 空でなければ (文字のある行であれば) ループ終了
接頭詞判定
下処理のされたメッセージが格納されているmes
から、接頭詞[Prefix]
を取得します。
pre=`echo "$mes" | sed -n -e "1p" | sed -e 's/\] .*//' | sed -e 's/^\[//'`
if [ "$pre" != "Add" ] && [ "$pre" != "Upd" ] && [ "$pre" != "Fix" ] && [ "$pre" != "Rmv" ]; then
echo "Invalid prefix."
exit 1
fi
-
mes
の 1行目を取得 (1p
) (無くてもいいかも) -
]_
(_
: スペース) 以降を削除 (「]_
の後に任意の文字 0字以上 (.*
) が続く文字列」を無に置換) - テキストの一番最初の
[
を削除 (「テキストの初め (^
) と[
が続く文字列」を無に置換)
[Add] First commit
というメッセージが入力されたなら、pre
にはAdd
という文字列が入ります。
[Add]First commit
と入力されたならAdd]First commit
と。(2 の処理が行われない)
First commit
と入力されたならFirst commit
と得られます。(2, 3 の処理が行われない)
ここまできたら、あとはif
を使って条件分岐するだけです。
ルールとしている接頭詞のいずれにも該当しない場合は、exit 1
(返す数字は任意) と返して、コミットを中止します。
本文判定
今度は、接頭詞以降の文字を取得し、ボディの有無を判定します。
bod=`echo "$mes" | sed -e 's/^.*\] //'`
if [ "$bod" = "" ]; then
echo "No message body specified."
exit 2
fi
- 文字の初めから
]_
(_
: スペース) までを削除 (「テキストの初め (^
) と任意の文字 0字以上 (.*
) と]_
が続く文字列」を無に置換)
[Add] First commit
と入力されたなら、bod
にはFirst commit
という文字列が入ります。
[Add]_
と入力されたなら、bod
の中は空になります。
これで、接頭詞[Prefix]_
の部分を削除できます。
あとはif
を使って条件分岐するだけです。
bod
が空であれば、ボディが無いということになりますので、exit 2
(返す数字は任意) と返して、コミットを中止します。
完成品
以上をまとめ、以下のようなスクリプトが出来上がります。
(エラーメッセージも具体的にしました。echo
の代わりにcat <<
を使っていますが、どちらでもできることは基本同じです。)
#!/bin/sh
# Message pre-process
mes=`cat $1 | sed -e '/^#/d'`
while [ "`echo "$mes" | sed -n -e "1p"`" = "" ]; do
mes=`echo "$mes" | sed -e "1d"`
done
# Prefix get
pre=`echo "$mes" | sed -n -e "1p" | sed -e 's/\] .*//' | sed -e 's/^\[//'`
# Body get
bod=`echo "$mes" | sed -e 's/^.*\] //'`
# Prefix check
if [ "$pre" != "Add" ] && [ "$pre" != "Upd" ] && [ "$pre" != "Fix" ] && [ "$pre" != "Rmv" ]; then
cat << EOF
Invalid commit message prefix.
Commit syntax:
[Prefix] <Message body>
Prefixes:
[Add] : Added file
[Upd] : Updated file
[Fix] : Fixed file
[Rmv] : Removed file
EOF
exit 1
fi
# Body check
if [ "$bod" = "" ]; then
cat << EOF
No commit message body specified.
Can't set empty body commit message.
Commit syntac:
[Prefix] <Message body>
EOF
exit 2
fi
# Quit
exit 0
作る (新版)
実装するスクリプトは、大まかに以下のような手順になります。
- メッセージを取得
- 下処理
- 判定: メッセージが 1行だけであるか
-
]
(カッコと半角スペース 1つ) を境に分割 (接頭詞と本文に分割) - 判定: 接頭詞
- 判定: 本文
メッセージを取得
コマンド引数の 1つ目に、入力されたコミットメッセージが書かれているCOMMIT_EDITMSG
へのパスが入力されます。これをcat
コマンドで取得してやれば OK です。
mes=`cat $1`
(シェルスクリプトのお約束#!/bin/sh
は、ここでは省略しています。)
下処理
さて、取得したメッセージですが、以下のルールに従っての下処理が必要です。
-
#
で始まる行はコメントアウトとして扱われる- -> 削除
- 空白文字や改行のみのメッセージは登録できない
- -> 空白文字だけの行、および空行を削除
以上の下処理をsed
コマンドで実装します。
mes=`cat $1 | sed -e '/^#/d' | sed -e '/^\s*$/d' | sed -e '/^$/d'`
判定: メッセージは 1行であるか
- 1行だけであること
- ->
wc -l
の結果が 1になる
wc
コマンドで、メッセージが何行であるかを確認します。結果、1行でなければエラーとします。(ついでに、メッセージが空であった場合の判定も入れておきます。)
if [ "$mes" = "" ]; then
# On empty message
elif [ `echo "$mes" | wc -l` != "1" ]; then
# On equal or over 2 lines message
fi
接頭詞と本文を分割
]
(カッコと半角スペース 1つ) を境に分割します。分割した左側は接頭詞、右側は本文となります。
pre=`echo "${mes%\] *}"`
bod=`echo "${mes#*\] }"`
mes が "[Add] Any Mes" とあるとき
pre: [Add
bod: Any Mes
POSIX で規定されている機能により、sed
を使わずに部分削除などを行うことができます。詳細は以下の記事が参考になります。
判定: 接頭詞
- 接頭詞と本文の間に半角スペースが 1つだけあること
- 0文字でないこと
- -> 0文字のとき、接頭詞の分割がされない
- 接頭詞の前にスペースなどが入っていないこと
[Prefix]
は[Add], [Mod], [Del]
のいずれかであること
考えられる不正な入力と、そのときの変数$pre
$bod
は、以下が考えられます。
mes が "[Add]Wrong Smp" (スペースが無い) とあるとき
pre: [Add]Wrong Smp
bod: [Add]Wrong Smp (いずれも部分削除がされない)
mes が "_[Add] Wrong Smp" (接頭詞前に何らかの文字) とあるとき
pre: _[Add]
bod: Wrong Smp
上記に対応するようエラーチェックを書きます。
if [ "$pre" != "[Add" ] && [ "$pre" != "[Mod" ] && [ "$pre" != "[Del" ]; then
# On invalid prefix
fi
いずれについても、変数$pre
をif then
するだけの簡単なお仕事です。(ここで、先の工程で閉じカッコ]
が消えている点に配慮します。)
判定: 本文
- 接頭詞と本文の間に半角スペースが 1つだけあること
- 2文字以上でないこと
- -> 2文字以上のとき、
$bod
がスペースで始まるMessage
が空でないこと ([Prefix]
だけ書かれたメッセージでないこと)
- -> 空のとき、
$bod
が空、または$pre
と同一になる
mes が "[Add] Wrong Smp" (スペース 2つ以上) とあるとき
pre: [Add]
bod: Wrong Smp (スペースで始まる)
mes が "[Add]" (接頭詞のみ) とあるとき
pre: [Add]
bod: [Add] (部分削除がされない)
mes が "[Add] " (接頭詞とスペースのみ) とあるとき
pre: [Add]
bod: (空)
上記に対応するようエラーチェックを書きます。今度はちょっと複雑になります。
if [ "`echo "$bod" | grep '^\s'`" != "" ] || [ "$bod" = "$pre" ] || [ "$bod" = "" ]; then
# On invalid body
fi
後半 2つはイコールで比較するだけですが、最初については少し工夫が要ります。
-
$bod
がスペースで始まる- -> スペースで始まる行がある
- ->
grep
でスペースから始まる行 (^\s
) を抽出した結果が空でない (!= ""
)
- ->
- -> スペースで始まる行がある
というように実装しました。
完成品
以上の点とエラーメッセージなどを整え、完成したスクリプトがこちらになります。
#!/bin/sh
mes=`cat "$1" | sed -e '/^#/d' | sed -e 's/\s*$//g' | sed -e '/^$/d'`
pre=`echo "${mes%\] *}"`
bod=`echo "${mes#*\] }"`
is_ok=1
if [ "$mes" = "" ]; then
echo "Invalid: Empty message"
is_ok=0
elif [ "`echo "$mes" | wc -l`" != "1" ]; then
echo "Invalid: Equal or over 2 lines"
is_ok=0
fi
if [ "$pre" != "[Add" ] && [ "$pre" != "[Mod" ] && [ "$pre" != "[Del" ]; then
echo "Invalid: Prefix"
is_ok=0
fi
if [ "`echo "$bod" | grep '^\s'`" != "" ] || [ "$bod" = "$pre" ] || [ "$bod" = "" ]; then
echo "Invalid: Body"
is_ok=0
fi
if [ "$is_ok" != "1" ]; then
echo ''
echo 'Rule:'
echo ' * Syntax: [Pre] Message'
echo ' * Specify 1 line message (not be over 2 lines left)'
echo ' * "[Pre]" must be "[Add]", "[Mod]" or "[Del]"'
echo ' * Between "[Pre]" and "Message", only 1 space must be left'
echo ' * "Message" must be left any character(s) without space'
exit 1
fi
exit 0
インストール
一応、スクリプトのインストール方法を簡単に。
- 上のコードを
.git/hooks/commit-msg
に書き込む - 実行権限を付与する (
chmod +x .git/hooks/commit-msg
)
.git
は、ローカルリポジトリのルート上に隠しフォルダで存在します。また、(セキュリティの観点などから) リポジトリのクローンで Hook スクリプトはクローンされません。クローンする都度、設定する必要があります。
おしまいに
やっぱり、テキスト処理にsed
コマンドは欠かせませんね。
ちなみに、このようなコミットメッセージの lint (文法チェック) を行うスクリプトを生成してくれるツールなんかもあるみたいです。非常に特殊なルール管理が要求されるのでない場合は、こういったツールを使う方が早いです。(記事の存在意義ェ)