俺のgithookはこうだ (commit/pushをカスタマイズ)

# 俺のgithookはこうだ (commit/pushをカスタマイズ)

## TL;DR
- ブランチごとにcommitできるユーザを制限する
- ブランチごとにpushできるユーザを制限する
- ブランチごとにcommit前にテストを実行する
- ブランチごとにpush前にテストを実行する
- [ここ](https://github.com/nadu-festival/mygithook)にコード置いてます

## 前書き
gitを使ったバージョン管理皆さん使ってますよね？（無言の圧）
皆さんご存じの通り，githookを使うことで，コミット前に自動テストを実行できたりします．
しかし，個人的に欲しい機能を付けようとすると，都度githookについて調べて，hook設定して・・・って工程が毎回かかってたので，この際テンプレート化するかって事で作ってみました．
（何番煎じやねん）

だた，あくまで本記事で紹介しているテンプレート類は各個人のお行儀のもとに成り立たせています．
（コミット制限もローカルの設定ファイルを編集すれば変えられます）

そのため，厳密にコミットを制限したりしたい場合は，ご自身で調査いただければと思います．

ぱっと持ってきてさっと使いたいという方は是非ご参考ください．


## 前準備
過去の偉人がすでにいい感じの使い方について解説されていますが，本記事でも参考に利用させていただくことにします．

1. hookスクリプトは`.githooks`というディレクトリに入れる (hookスクリプトもリポジトリで管理)
2. hookスクリプトの参照先を`.githooks`に切り替える
3. hoosスクリプト実行時に参照するユーザ名を`git config --local`で設定する

なお，工程2のhookスクリプト参照先の変更と工程3のユーザ名設定は下記コマンドで行います．

```sh
# hookスクリプトの参照先を設定します
git config --local core.hooksPath .githooks
# ユーザ名設定 (以降のアクセス制御時はこの設定名を参照します)
git config --local user.name "your-name"
```

注意点として，リモートに配置しているリポジトリをcloneしてもらう場合，clone後にhookスクリプトの参照先設定を切り替える必要があるので，そこは「お行儀」としてREADMEなり手順書等に書いておきましょう．
また，hookスクリプトの参照先設定時に"--local"でなく"--global"を設定するとややこしいことになるので，絶対に"--local"で設定しましょう．

## ブランチごとにcommit/pushできるユーザを制限する

### 仕様
- ブランチごとにcommit/pushできるユーザを制限する
- ブランチの設定にワイルドカードを使える（例：feature-*）
- ユーザの設定にワイルドカードを使える（例：usr-*）
- 設定ファイルに記載されてるいずれにも該当しない場合はアクセス制御なしとして全ユーザがcommit/push可

### 認証用の設定
認証を行うための設定を書きます．
ワイルドカードを使える様にしている都合上，複数の設定が該当することがありますが，その場合は最初に該当した設定でアクセス制御します．

```ini: .githooks/commit_users.conf または .githooks/push_users.conf
# masterブランチはcommit禁止 (merge-commitはできる)
[master]

# mainブランチはcommit禁止 (merge-commitはできる)
[main]

# developブランチはrootのみcommit可
[develop]
root

# パターン"feature-*"に当てはまるブランチの設定
# ユーザ名の指定に * を利用すると全てのユーザがcommit可になります
[feature-*]
*

# 上記に当てはまらないブランチへのcommit/pushをすべて禁止するには
# 下記の設定のコメントアウトを外す
# [*]
```

### 認証用のshellスクリプト
commit/pushで認証用スクリプトは共通化して利用します．

中身が気になる方はご覧ください．
ぱっと使いたいだけの方は読み飛ばしてください．
（shellベタ書きなのであんまりきれいじゃないですごめんね．）

<details>

<summary>
コードを表示する
</summary>
<div>

```sh: .githooks/authorize.sh
#!/bin/sh

usage(){
  echo "$0 [username] [branch] [config file]"
  echo "$? = 0: permission exist, 1: no permission or error"
}


if test $# -ne 3; then
  usage;
  exit 1
fi

username=`echo "$1" | sed 's/^ *\| *$//'`
branch=`echo "$2" | sed 's/^ *\| *$//'`
configfile=`echo "$3" | sed 's/^ *\| *$//'`

# 対象ブランチの設定があるか 0:なし, 1:あり
is_restricted=0

while read line
do

  # 前後の空白文字・改行文字を除去
  line=`echo "$line" | sed 's/^ *\| *$//'`

  # 空行かチェック
	if test -z "$line"; then
		# 空行の為処理スキップ
		continue
	fi

  # 読み込んだ行がコメント行か確認
	echo "$line" | grep -v '^#.*' > /dev/null
  if test $? -ne 0; then
    # コメント行の為処理スキップ
    continue
  fi

  section=`echo "$line" | sed -n -e 's/\[\(.\+\)\]/\1/p'`
  if test -n "$section" ; then
    # セクション名の場合
    if test $is_restricted -ne 0; then
      # 既にヒットする設定があったが次ブランチの設定に移ったので権限無し
      exit 1
    fi
    case "$branch" in
      $section)
                is_restricted=1
                ;;
    esac
  elif test $is_restricted -ne 0; then
    # チェック対象のブランチ
    # 対象ブランチの設定に記述があるか確認
    case "$username" in
      $line)
            exit 0;
            ;;
    esac
  fi

done < $configfile

# 設定ファイルに記述あれば 1 (権限無し), 記述無ければ 0 (権限あり)
exit $is_restricted
```

</div>
</details>

## ブランチごとにcommit/pushできるユーザを制限する

### 仕様
- ブランチごとにcommit/push時にテストコマンドを実行する
- ブランチの設定にワイルドカードを使える（例：feature-*）
- 複数の設定に該当する場合は**該当するすべての設定**に記載のテストコマンドを実行する

### テスト用の設定
テストを行うための設定を書きます．
ワイルドカードを使える様にしている都合上，複数の設定が該当することがありますが，その場合は**該当するすべての設定**に記載のテストコマンドを実行します．

commit時の設定ファイルとpush時の設定ファイルが別なので，「push時はunittestも合格してないとpushできないが，commitに関してはflake8でテスト合格してればOK」的なこともできます．
自作のテストを実行することもできます．
（終了コードが成功時0，失敗時1になるようなスクリプトを作って，設定ファイルに書けば動かします．）

以下push時に実行するテストの設定サンプル．

```ini: .githooks/push_pretest.conf
# flake8, unittestのテストが成功しなければコミット・プッシュ中断
[master]
flake8 ./
python -m unittest discover ./

[main]
flake8 ./
python -m unittest discover ./

[develop]
flake8 ./
python -m unittest discover ./

# feature-*ブランチは，flake8だけテスト
[feature-*]
flake8 ./
```

### テスト用のshellスクリプト
commit/pushでテスト用スクリプトは共通化して利用します．
（アクセス制御の時と同様．）

中身が気になる方はご覧ください．
ぱっと使いたいだけの方は読み飛ばしてください．
（shellベタ書きなのであんまりきれいじゃないですごめんね．）

<details>

<summary>
コードを表示する
</summary>
<div>

```sh: pretest.sh
#!/bin/sh

usage(){
  echo "$0 [branch] [config file]"
  echo "$? = 0: test success, 1: test failure"
}


if test $# -ne 2; then
  usage;
  exit 1
fi

branch=`echo "$1" | sed 's/^ *\| *$//'`
configfile=`echo "$2" | sed 's/^ *\| *$//'`

# 対象ブランチの設定か 0:対象外の設定, 1:対象の設定
is_target=0

working_section=""

while read line
do

  # 前後の空白文字・改行文字を除去
  line=`echo "$line" | sed 's/^ *\| *$//'`

  # 空行かチェック
	if test -z "$line"; then
		# 空行の為処理スキップ
		continue
	fi

  # 読み込んだ行がコメント行か確認
	echo "$line" | grep -v '^#.*' > /dev/null
  if test $? -ne 0; then
    # コメント行の為処理スキップ
    continue
  fi

  section=`echo "$line" | sed -n -e 's/\[\(.\+\)\]/\1/p'`
  if test -n "$section" ; then
    # セクション名の場合
    case "$branch" in
      $section)
                working_section=$section
                is_target=1
                ;;
      *)
        is_target=0
        ;;
    esac
  elif test $is_target -ne 0; then
    # テスト対象のブランチ
    eval $line > /dev/null
    code=$?
    if test $code -ne 0; then
      # 異常終了した
      echo "TEST FAIL: section=[\"$working_section\"] code=[\"${line}\"]"
      exit $code
    fi
  fi

done < $configfile

# 全てのテストが完了 or テスト項目の設定無ければ正常終了 (push許可)
exit 0
```

</div>
</details>

## hookスクリプト
具体的にpre-commit または pre-push を下記のように記述します．
（下記はpre-commitの例）

```sh: .githooks/pre-commit
#!/bin/sh

# Redirect output to stderr.
exec 1>&2

# ブランチ名
branch=`git symbolic-ref HEAD | sed -e 's:^refs/heads/::'`
# 操作ユーザ名
user=`git config user.name`

# commit可能ユーザかチェック
./.githooks/authorize.sh $user $branch .githooks/commit_users.conf
if test $? -ne 0; then
	# 権限なし
	echo "No permission to commit [$branch] branch for $user."
	exit 1
fi

./.githooks/pretest.sh $branch .githooks/commit_pretest.conf
if test $? -ne 0; then
	# 何かしらのテストコマンドが終了コード0以外で終了
	echo "Fail to pre-test on commit [$branch]."
	exit 1
fi

exit 0
```

## おわりに
ぱっと使えて便利と自称してます．
ただお行儀良くないと「全ブランチ全ユーザアクセスOK，push前テストなし」という設定もできちゃうので注意．
（ローカルで設定だけ書き換えられるともうどうしようもない）

あとhookスクリプトの参照先を変更しないと空気．

## 参考サイト
- [Git フック](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA-Git-%E3%83%95%E3%83%83%E3%82%AF)
- [特定のユーザのみ git push できるように制限する](https://maku77.github.io/git/limit-push-users.html)