RedmineのチケットとGitのコミットを紐づけるために、コミットメッセージにrefs #チケット番号
を入れることをルール化したい。
クライアントサイドフックであるpre-commit hookを使ってチェックできるが、各リポジトリの.git/hooks/pre-commit
にフックを記載しなければいけなく、リポジトリごとに各人でそれぞれ対応しなければいけない。他のメンバーへの展開は漏れが出てくる可能性があるので、個人でpre-commit hookを設定する以外にもサーバサイドフックでチェックするようにしたい。
公式サイトでもサーバサイドフックは「システム管理者がプロジェクトのポリシーを強制させるために使うもの」と書かれている。
pushされたときにコミットメッセージを確認するにはpre-receive hookを使う。通常であればサーバの.git/hooks
にpre-receiveを書くが、GitLabの場合は.git/custom_hooks
以下に書くことになっている。(GitLabのCustom Git Hooks)
cd /var/opt/gitlab/git-data/repositories/<group>/<project>.git
sudo -u git mkdir custom_hooks
cd custom_hooks
sudo -u git vi pre-receive
chmod u+x pre-receive
pre-receiveの中は次のように作成した。
#!/bin/bash
while read oldrev newrev refname
do
# when new branch created
if [ "$oldrev" == "0000000000000000000000000000000000000000" ]; then
if [[ "$refname" == refs/heads/feature/RM* ]]; then
from="refs/heads/development"
elif [[ "$refname" == refs/heads/fix/RM* ]]; then
from="refs/heads/master"
elif [[ "$refname" == refs/heads/release* ]]; then
continue;
elif [[ "$refname" == refs/heads/master || "$refname" == refs/heads/development ]]; then
# should not reach here.
echo "[ERROR] $refname branch must exist." 1>&2
exit 1
else
echo "[ERROR] Branch name (other than 'master' and 'development') must start with 'feature/RM', 'fix/RM' or 'release'." 1>&2
exit 1
fi
# get latest ref from the branch from which this new branch must have been created in accordance with team rules.
oldrev=`git show-ref --hash --verify "$from"`
fi
# when branch deleted
if [ "$newrev" == "0000000000000000000000000000000000000000" ]; then
continue
fi
# check refs in commit messages
git log --oneline --format=%s "$oldrev".."$newrev" | grep -v '^Merge .*' | awk '{if (! / refs #[0-9]+$/ ) exit 1}'
ret=$?
if [ $ret -eq 1 ]; then
echo "[ERROR] Reference to ticket number is required. Include 'refs #NNN' at the end of a commit message." 1>&2
exit 1
fi
# check tag in commit messages
git log --oneline --format=%s "$oldrev".."$newrev" | grep -v '^Merge .*' | awk '{if (! /^\[(fix|feature)\]/ ) exit 1}'
ret=$?
if [ $ret -eq 1 ]; then
echo "[ERROR] Tag is required. Include '[fix] or [feature]' at the beginning of a commit message." 1>&2
exit 1
fi
done
pre-receiveはプッシュされた直後に1回だけ呼び出され、標準入力から次のようなフォーマット(更新前のコミットID 更新後のコミットID ブランチの参照情報)で渡される。
cfc3a79427568e93e22c490c70b5078b9d77584e 6c100b7afa3d35853dc48de405efee2681b1047c refs/heads/branch_name1
1306c5eb1b43c5141529bc6e55962eac55a7ffb4 22562fea781024df2ded8b9d3b3e21f63f07b418 refs/heads/branch_name2
ブランチごとに渡されるようなので全体をwhileループで囲んでいるが、主要な処理は以下の部分となる。
git log --oneline --format=%s "$oldrev".."$newrev" | grep -v '^Merge branch.*' | awk '{if (! / refs #[0-9]+$/ ) exit 1}'
Redmine番号の記載はコミットメッセージのどの部分にあっても紐づけができるのだが、コミットログ一覧を見たときの見やすさのため、ルールとして概要を記載するコミットメッセージ1行目の行末にRedmine番号を記載させるようにする。git log --oneline
でコミットメッセージの1行目を取り出し、format=%s
でコミットメッセージ概要部分だけに絞って出力している。
取り出すコミットの範囲は$oldrev
から$newrev
とすると、今回のプッシュに含まれる全コミットが取得できる。
取り出した全コミットをひとつずつ見ていく。行単位に処理をするのでawkを使うと便利。refs #チケット番号
の前後に文字列がくっついてはいけないので、正規表現を refs #[0-9]+$
(一番初めがスペース)とし、この正規表現にマッチしない行が出てきた時点でawkからexit 1
する。
awkの終了ステータスを確認して1であればpre-receive自体の終了ステータスを1としてexitすると、プッシュが中止される。
その他の細かい制御として、ブランチを削除するときにもpre-receiveが走るのでその場合はチェックをしないようにしたり、ブランチからmasterへマージするときのデフォルトのメッセージ'Merge branch ... into master'となっているコミットはチェック対象からgrep -v
で省いたりしている。
ブランチの作成のときにもpre-receiveが走るが、そのときの対処方法だけ複雑なので詳しく書く。
まずチームのルールとして以下を定めているとする。
master
ブランチ、development
ブランチがある。realease1.0.0
,release1.0.1
のようにリリースブランチがある。- リリースのタイミングで
master
にdevelopment
の内容がマージされ、master
からrelease*.*.*
が切られる。この時点で3つのブランチの内容はすべて同じ。 - バグ修正用にfixブランチを
master
から派生させ、ブランチ名をfix/RM番号
とする。fixブランチは全ブランチにマージされる。 - 新機能開発用にfeatureブランチを
development
から派生させ、ブランチ名をfeature/RM番号
とする。featureブランチはdevelopment
だけにマージされる。
pushすることによってリモートでブランチ作成が行われるとき、$oldrev
には0000000000000000000000000000000000000000
という不明な値が入るのでそのままgit log "$oldrev".."$newrev"
をすると、すべてのログが出力されてしまう。$oldrev
に入るべきはコミット済の最新の参照であるべきなので、master
もしくはdevelopment
の最新の参照を取る必要がある。
master
とdevelopment
のどちらから取るべきかは、今pushしようとしているのがfixブランチなのかfeatureブランチかによる。もしfixブランチをpushしようとしているのにdevelopment
ブランチの最新の参照を$oldrev
に入れてしまうと、master
ブランチから派生しているfixブランチからは不明な参照となってしまう可能性がある。その場合は$oldrev
が0000000000000000000000000000000000000000
である場合と同様にすべてのログが出力されてしまう。
チームのルールとしてブランチの命名規約を決めているので、それに従って$refname
をみて、どのブランチから最新の参照を取り出すかを決めている。参照を取り出すにはgit show-ref PATTERN
を実行する。