git hook はじめの一歩

# はじめに
この記事はGitアドベントカレンダー2016 16日目の記事になります。

昨日は@knsh14さんのgit diff --indent-heuristicに関する概要と実装についての記事でした。これからのgit diffライフが楽しくなる素敵な記事でした。

私からはgit hookについて初学者向けの内容を書きたいと思います。

# 本記事のゴール
git hookを言葉だけ知っていたけど概要が理解できること。
また、利用したことがまだない方がhookの種類を適切に選んで、活用できるようになること。


目次は以下となります。

1. git hookとは？
2. hookの種類は？
3. どうやって利用するの？(クライアントサイド/サーバサイド)
4. おわりに

# git hookとは？
特定のアクションが行われた時に、スクリプトを実行できます。
例えばコミットやマージ時にリポジトリの運用ルールを破っていないかチェックしたり、違反しているなら該当のアクションを拒否したり、アクションが正常終了したら通知を送ったりできます。

# hookの種類は？
まずは結論から、下記は種類を樹形図で示したものです。

![スクリーンショット 2016-12-15 10.00.41.png](https://qiita-image-store.s3.amazonaws.com/0/25180/d3323ee9-4800-ece8-fe38-b35dbf7af0f6.png)


沢山の種類がありますね。
大きくはクライアント側とサーバ側に分けられます。
先の樹形図に概要を記載したものを準備しました。
(字が小さくて見にくいので画像別窓表示推奨です。)

![スクリーンショット 2016-12-15 10.02.21.png](https://qiita-image-store.s3.amazonaws.com/0/25180/f03b9851-4f5c-7115-ad17-0100ccb50721.png)


# どう利用するの？

様々なアクション、タイミングでスクリプトが実行できることはわかりました。
ではクライアントサイドとサーバーサイドのhookについて利用方法の説明に移ります。

## クライアントサイド編
git initした直後、sampleが生成されています。

```bash
/Users/himuro/devel/project/git/hook-test/.git/hooks
$ tree
.
├── applypatch-msg.sample
├── commit-msg.sample
├── post-update.sample
├── pre-applypatch.sample
├── pre-commit.sample
├── pre-push.sample
├── pre-rebase.sample
├── prepare-commit-msg.sample
└── update.sample

0 directories, 9 files
```

.git/hooks/以下に利用したいhookの名称をつけたスクリプトファイルを設置して
実行権限を付与してあげればhookのタイミングで起動してくれます。

commit-msg.sampleを実際に動かして見ましょう。


```bash
# renameと実行権限の確認
$ mv commit-msg.sample commit-msg

$ ls -ltra commit-msg
-rwxr-xr-x  1 himuro  staff  896 12 15 10:23 commit-msg

```
```bash:.git/hooks/commit-msg
SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"

test "" = "$(grep '^Signed-off-by: ' "$1" |
         sort | uniq -c | sed -e '/^[   ]*1[    ]/d')" || {
        echo >&2 Duplicate Signed-off-by lines.
        exit 1
}
```

実際にcommitすると...

```bash
$ touch commit-msg-test.txt
$ git add .
$ git commit -m 'init'
[master cbd1039] init Signed-off-by: mhimuro <h.mikuto@gmail.com>
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 commit-msg-test.txt

```

initとしか書かなかったコミットメッセージに署名が自動で付与されました。
(コミットメッセージの調整はprepare-commit-msgの方が適しています)

成功を示す0以外のコードが返却され、コミットが弾かれた時は以下のメッセージが表示されています。

```bash:失敗時の出力
(master +)$ git commit
Duplicate Signed-off-by lines.
```

hookからの起動時に与えられている引数の詳細、また
スクリプトの技術を磨くことでどんどん工夫できそうですね。
続けてサーバーサイドのhookの利用方法の説明に移ります。

## サーバーサイド編 gitlab利用

ここではgitlabにpre-receiveを仕掛けたいと思います。
動きとしてはmasterへのpushを拒否するスクリプトです。
(時間の都合で簡単なものになってしまいました。より詳細なテクニックは[ProGit](https://progit2.s3.amazonaws.com/ja/2016-02-11-8cb2f/progit-ja.1016.pdf)のGit ポリシーの実施例の章を参照ください)

```bash
# gitlabではgit-data/repositories/username で管理されています。
cd /var/opt/gitlab/git-data/repositories/mhimuro/my-git-project.git

# hooksではなくcustom_hooksという名称でディレクトリを準備するのがpointです。
mkdir custom_hooks
touch custom_hooks/pre-receive
chmod 755 -R custom_hooks
```

```bash:pre-receive
#!/bin/sh
read old_revision new_revision refs_name

if [ $refs_name = "refs/heads/master" ]
then
echo "Cannot push to master"
exit -1
fi

echo "pre-receive OK"
exit 0

```

```bash:masterにcheckout中
$ touch server-hook-test.txt
$ git add .
$ git commit -m 'add test file' .
[master c0bb9d3] add testfile
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 server-hook-test.txt

$ git push
Counting objects: 2, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 275 bytes | 0 bytes/s, done.
Total 2 (delta 0), reused 0 (delta 0)
remote: Cannot push to master
To http://163.44.175.43:65501/mhimuro/my-git-project.git
 ! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'http://163.44.175.43:65501/mhimuro/my-git-project.git'

```
Cannot push to masterと表示されてpushが拒否されました。
では他のbranchではどうでしょうか？

```bash:hook-test-branchにcheckout中

$ touch server-hook-test2.txt
$ git add .
$ git commit -m 'add test file' .
[hook-test-branch 5f30dc0] add test file
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 server-hook-test2.txt

$ git push
Counting objects: 2, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 223 bytes | 0 bytes/s, done.
Total 2 (delta 1), reused 0 (delta 0)
remote: pre-receive OK
remote:
remote: Create merge request for hook-test-branch:
remote:   http://163.44.175.43:65501/mhimuro/my-git-project/merge_requests/new?merge_request%5Bsource_branch%5D=hook-test-branch
remote:
To http://163.44.175.43:65501/mhimuro/my-git-project.git
   89bc660..5f30dc0  hook-test-branch -> hook-test-branch
```

pre-receive OKが出力されてpushが正常終了していますね。
今回は簡単なsampleになりましたが引数で受け取っている新旧の参照とgit rev-listを組み合わせることでより詳細なcheckが可能になりますので興味のある方は調べて見てください。

# 終わりに

リポジトリの運用ルールに反するコミットをpush時のhookで初めて弾くと、
余計な手間が発生したり、そもそも対応方針が異なってきたり、トラブルの原因になるのでクライアントサイドで早めに弾いてくれる方が幸せかと思います。

クライアントサイドhookはリポジトリをクローンしても一般的に同梱されないので
チーム内で利用を強制する場合は別の場所での管理、メンテナンスが必要となります。

私自身今回の記事のテーマに選ぶことでhookを利用するきっかけになりました。明日は@TakesxiSximadaさんのGitPythonに関する記事の予定です。引き続き2016年アドベントカレンダーをお楽しみください。

# 参考資料
1.https://git-scm.com/book/ja/v2/Git-のカスタマイズ-Git-フック
2.https://docs.gitlab.com/ee/administration/custom_hooks.html
3.https://ez-net.jp/article/1B/eoyhWdZI/k7CLR_5idahI/