【GitHooks】mainブランチへのコミットを禁止する。【pre-commit】

## 前提
mainブランチやmasterブランチで動作を確認している中で、そのままコードを修正し、勢いでコミットまでやってしまい、後から「あれ？作業ブランチ切れてなくね・・・？」したことありませんか？

私はあります。

リモートにpush前ならすぐに`$ git reset --soft HEAD^`して作業ブランチでコミットし直せばいいですし、push後ならgit revertしたらいいですが、それも手間なので根本的に制限しておきます。

同じGitHooksを使って、以前はこんな記事も書きました。
[【Git Hooks】コミット時にコミットメッセージの自動チェックを行う。【commit-msg】](https://qiita.com/MasaoSasaki/items/f10ab4cd54e228fb436f)

この記事ではmainブランチなど主要ブランチへのコミットの制限方法について解説していきます。

## Git Hooksとは
Git Hooksとはgitに標準で備わっている機能の一つで、`git commit`や`git push`が実行された際に、スクリプトを実行することができ、そのスクリプトの実行結果によってコマンドを通すかどうかを自動的に判断させることができます。
[8.3 Git のカスタマイズ - 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)

実行されるスクリプトファイルは以下の場所に置かれています。
```
$ 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
```
これらのファイル名は予約されていて、gitコマンドが実行されたときに任意のタイミングでそれぞれのスクリプトが動作することになっています。
デフォルトでは全てのファイルの拡張子が`.sample`となっているため、どのスクリプトファイルも動作していませんが、拡張子`.sample`を取り除くと、gitコマンドのそれぞれのタイミングでファイルに記述されているスクリプトが実行されます。
(シェルスクリプトの拡張子`.sh`は不要です。)

## 完成形
![スクリーンショット 2022-10-09 19.35.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643639/7449b2ac-b7ab-42f6-ccee-ed5a50d5f927.png)
![スクリーンショット 2022-10-09 19.36.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643639/b36455d1-1021-2f75-dd22-93824010bf03.png)

## 作業ブランチのチェック
ここではコミットが実行される前に、現在作業中のブランチが事前に設定したブランチ(main, master)かどうかを判定し、
該当するならコミットの実行を取り消し、
該当しなければコミットの実行を行う(次のGitHooksを実行する)。
ということをやっていきます。

### 準備

先に以下のコマンドでgit環境を作っておきます。
`$ mkdir sample; cd sample; git init; touch test.txt`

次にデフォルトで用意されているGitHooksスクリプトを確認します。

```
$ 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
```

作業ブランチのチェックで必要になるのは、`pre-commit`というファイルです。
元から用意されている`pre-commit.sample`には手をつけず、新規で作成していきます。

`$ vi .git/hooks/pre-commit` pre-commitファイルを作成し以下のように書きます。

```shell
#!/bin/bash

echo -e "\033[37;1m🪝 Running Git Hooks: pre-commit\033[0m"

# 現在のブランチ名を定義
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
readonly BRANCH_NAME
# 終了コードを定義。0: OK, 1: NG
code=0

# コミットするブランチ名のチェック
echo -en " - コミットするブランチ名のチェック: "
## コミットを拒否するブランチ名を定義
readonly INCORRECT_BRANCHES=("master" "main")
if echo "${INCORRECT_BRANCHES[@]}" | grep -q "$BRANCH_NAME"; then
  echo -e "\033[31;22mNG\033[0m"
  echo -e "\033[31;22m================================================================"
  echo -e "${BRANCH_NAME}ブランチへのコミットは禁止されています。"
  echo -e ""
  echo -e "禁止されているブランチ"
  echo -e "  ${INCORRECT_BRANCHES[*]}"
  echo -e "================================================================\033[0m"
  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: pre-commit: NG\033[0m\n\n"
fi

exit ${code}

```
コードの解説は後述します。

最後に、作成したスクリプトファイルに実行権を与えます。
`$ chmod a+x .git/hooks/pre-commit`

## 解説
```
#!/bin/bash
```
シバン行。
このスクリプトはbash(`$ `)環境用に作成しました。
zsh(`# `)等他の環境でshellを使用されている場合は文字の色など正常に表示されない可能性があります。

```shell
echo -e "\033[37;1m🪝 Running Git Hooks: pre-commit\033[0m"

# 現在のブランチ名を定義
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
readonly BRANCH_NAME
# 終了コードを定義。0: OK, 1: NG
code=0
```
* `echo -e "\033[37;1m🪝 Running Git Hooks: pre-commit\033[0m"`
pre-commitが実行されていることをターミナルに出力します。

* `BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)`
* `readonly BRANCH_NAME`
変数`BRANCH_NAME`に`$ git rev-parse --abbrev-ref HEAD`の実行結果、つまり現在のブランチ名を代入します。
readonlyで変数`BRANCH_NAME`は読み込み専用となるため、再代入等をできないようにします。

* `code=0`
このスクリプトがエラー終了なのか正常終了なのかのフラグです。
初期値0は正常終了、つまりコミットのプロセスが問題なく実行されます。
この後のチェック項目(バリデーション等)に失敗すると、codeに1を代入しています。

```shell
# コミットするブランチ名のチェック
echo -en " - コミットするブランチ名のチェック: "
## コミットを拒否するブランチ名を定義
readonly INCORRECT_BRANCHES=("master" "main")
```

* `echo -en " - コミットするブランチ名のチェック: "`
`-e` 出力文字の色を変えるためのオプション
`-n` 末尾に改行を付けない。ので、次にechoで出力される文字列は続けて横に表示されます。
* `readonly INCORRECT_BRANCHES=("master" "main")`
変数`INCORRECT_BRANCHES`を宣言し、静的な配列を代入しています。
静的な値を代入する際は変数の宣言と同時に`readonly`をつけることができます。
ここで代入している配列はコミットを禁止するブランチ名です。

```shell
if echo "${INCORRECT_BRANCHES[@]}" | grep -q "$BRANCH_NAME"; then
  echo -e "\033[31;22mNG\033[0m"
  echo -e "\033[31;22m================================================================"
  echo -e "${BRANCH_NAME}ブランチへのコミットは禁止されています。"
  echo -e ""
  echo -e "禁止されているブランチ"
  echo -e "  ${INCORRECT_BRANCHES[*]}"
  echo -e "================================================================\033[0m"
  code=1
else
  echo -e "\033[32;22mOK\033[0m"
fi
```

* `if echo "${INCORRECT_BRANCHES[@]}" | grep -q "$BRANCH_NAME"; then`
制限するブランチの配列に対してgrepコマンドを使って、現在のブランチ名が含まれているかどうかで成否判定を行います。
`-q`オプションはターミナルにgrepコマンドの結果を出力させません。

```shell
# 終了宣言
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: pre-commit: NG\033[0m\n\n"
fi

exit ${code}
```
* `if [ ${code} -eq 0 ]; then`
ここまでのチェックで問題がなければcodeの値は`0`のままとなるので`✨ALL PASS!!`の文字を表示させるか、
チェックに問題がありcodeの値が`1`になっていれば`Git Hooks: pre-commit: NG`の文字を表示させ、
最後にexitで正常終了か異常終了します。
このスクリプトが正常終了の場合はgit commiitコマンドが実行され、異常終了の場合はgit commitコマンドは取り消されます。


## 実行
実際に動かしていきます。
mainブランチ等の制限されたブランチに移動し、準備の所で用意したtest.txtをステージングし、コミットメッセージは仮に'test'としてコミットしてみます。
`$ git branch`
`$ git commit -m 'test'`
![スクリーンショット 2022-10-09 20.24.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643639/2626f869-803b-f3b0-75b9-9871e429c1fe.png)


`$ git log --oneline`を確認すると、先ほどのコミットが実行されていないことが確認できます。

次に正常終了するパターンを確認します。
developブランチを作成して移動し、同様にコミットしてみます。

`$ git checkout -b develop`
`$ git branch`
`$ git commit -m 'test'`
![スクリーンショット 2022-10-09 20.27.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643639/4f4feacd-4567-9074-d8f1-36d36906e5eb.png)
これで再度`$ git log --oneline`を確認するとコミットができていることが確認できるかと思います。

### コミット時にスクリプトの実行をスキップしたい場合
スクリプトの実行をスキップしたい場合は`--no-verify`オプションつけてgit commitを行えば、チェックを通さず、コミットが即時に実行されます。

## まとめ
今回ご紹介したスクリプトファイルは私の[GitHubリポジトリ](https://github.com/MasaoSasaki/sample-codes/blob/githooks/main/githooks/.githooks/pre-commit)にも同じものを置いてあります。

参考になれば幸いです。


## 参考
[【Git Hooks】コミット時にコミットメッセージの自動チェックを行う。【commit-msg】](https://qiita.com/MasaoSasaki/items/f10ab4cd54e228fb436f)
[シェルスクリプトのechoで”問題なく”色をつける(bash他対応)](https://qiita.com/ko1nksm/items/095bdb8f0eca6d327233)
[git rev-parseを使いこなす](https://qiita.com/karupanerura/items/721962bb7da3e34187e1)