Gitフックでブランチの派生元を制御したい

バグフィックスのためにfixブランチをmasterブランチから切り、また、機能開発のためにfeatureブランチをdevelopmentブランチから切るというルールを制定しているとする。

ブランチの派生元を誤って作業しないように、クライアントサイドGitフックを作成する。

post-checkout

新たにブランチを作ってcheckoutしたときに検査するため、.git/hooks/post-checkoutにフックを作る。

#!/bin/sh

prev_head=$1
new_head=$2
checkout_type=$3

warning() {
  echo -e "\e[31m[WARNING] $1\e[m"
}

# new branch name
b_name=`git rev-parse --abbrev-ref HEAD`
# number of checkout
num_checkouts=`git reflog --date=local | egrep -o " ${b_name}( |$)" | wc -l`

# if the refs of the previous and new heads are the same 
# and the number of checkouts equals 1, a new branch has been created
if [ $prev_head == $new_head ] && [ $num_checkouts -eq 1 ]; then
  start_with_fix=`echo $b_name | grep -q "^fix/"; echo $?`
  start_with_feature=`echo $b_name | grep -q "^feature/"; echo $?`

  # commit id of remote master branch
  master=`git fetch origin master >/dev/null 2>&1 && git log origin/master -n 1 --format=%H`
  # commit id of development branch
  development=`git fetch origin development > /dev/null 2>&1 && git log origin/development -n 1 --format=%H`

  if [ $start_with_fix -eq 0 ]; then
    if [ $master != $prev_head ]; then
      warning "fix branch should be branched from latest master.\nhint: git checkout master; git pull; git checkout -b fix/???"
      exit 1
    fi
  elif [ $start_with_feature -eq 0 ]; then
    if [ $development != $prev_head ]; then
      warning "feature branch should be branched from latest development.\nhint: git checkout development; git pull; git checkout -b feature/???"
      exit 1
    fi
  else
      warning "branch name should start with '(fix|feature)'."
      exit 1
  fi
fi

フックスクリプトに実行権限をつける。

$ chmod a+x .git/hooks/post-checkout 

スクリプトのロジック

checkout前後のHEADが同じ、かつreflogで当該ブランチにcheckoutした回数を数えて1回しかないとき、新規ブランチが作成されてcheckoutしたと判定し、チェックロジックに入る。reflogは各個人のローカルリポジトリに存在し、ブランチの切り替え、新たに加えられた変更のプル、履歴の書き換え、あるいは単なる新規コミットの実行などを記録している。

ブランチ名がfixで始まるかfeatureで始まるかあるいはそれ以外かで処理を分けている。
例えばfixではじまっているのにcheckout前のHEADがリモートのmasterのHEADと異なる場合は、以下の二点が考えられる。

  • fixで始まっているのに、masterブランチから派生していない
  • masterからfixブランチを切っていたとしても、ローカルリポジトリのmasterが古い

ルール違反を検知できても、作成してしまったブランチを削除したり作成する直前で止めることはできないため、赤字で警告を出すことにする。

動作確認

git checkout -bで動作確認する。

$ git branch | grep '*'
* development

$ git checkout -b fix/12345
Switched to a new branch 'fix/12345'
[WARNING] fix branch should be branched from latest master.
hint: git checkout master; git pull; git checkout -b fix/???

スクリプトの問題点

checkoutで作業内容を元に戻すときの誤検知

ブランチを切り替えるときはうまく動いているのだが、checkoutはブランチを切り替えるだけのものではないため、ブランチを切り替える目的以外でcheckoutしたときに誤検知してしまう問題がある。

現在fix/23456にいるとする。ローカルで変更した作業内容を元に戻したいと考え、個別のファイルを指定してgit checkout <filename>や、全てを元に戻すためにgit checkout .を実行する。

if [ $prev_head == $new_head ] && [ $num_checkouts -eq 1 ]; thenの条件のうち、ブランチが変わらないので前半は必ず True になる。後半の条件は、fix/23456に初回切り替えてから一度もブランチを移動していないければ、再度 Trueになってしまう。

開発作業中にremoteのmasterブランチで更新があると、fix/23456は最新のmasterブランチから派生していないと判定してしまうため警告が出る。

とはいえ警告を出すだけで何も制限はかからないため作業に問題は発生しない。

git fetchでパスワードを聞かれる場合

SSH鍵ではなくHTTPSでgitを使っている場合、git fetchの箇所でパスワードを聞かれてしまう。

対策として、git config credential.helper 'cache --timeout=86400'を実行することで、1日パスワードを記憶してくれる。1週間にするためにtimeoutに604800を指定するのもいいだろう。

参考