commit と push 周りの GitHooks を勉強しただけ

# この記事は？

Git Hooks での、コミット生成の制御と push 受け取りの制御について学んだのでメモ。
Git の運用ルール (コミットメッセージやブランチ名のルール等) を、スクリプトで規制したい、という方に役立てられれば幸いです。

Git の基礎的な使い方が分かっていること、シェルスクリプトがある程度読める・書けることを前提に話を進めます。

# Git Hooks とは？

Git Hooks というのは、Git で何らかのアクション (例: コミットを作成, マージ, リモートへ Push 等) が発生した際に、特定のプログラム (主にシェルスクリプト) を実行する仕組みです。
検出できるアクションはいくつかあります。

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

こちらの記事で図に分けて説明してくださっています。

https://qiita.com/mhimuro/items/adfc4e5a168d2d9295ae

これら全てを活用するのは、本格的で大規模な開発であるときと思います。
この記事では、コミット関係 (クライアントサイド) と、push 受け取り関係 (サーバサイド) を扱います。

# Git Hooks スクリプトの置き場所

クライアントサイド (ローカルリポジトリ) では、リポジトリのルート上から`.git/hooks/` (隠しフォルダ) に。
サーバサイド (リモートリポジトリ) では、リポジトリのルート上から`hooks/`に。
このディレクトリに、各アクションに対応するファイル名で、スクリプトを配置します。

```sh
ls .git/hooks/
# applypatch-msg.sample*      pre-merge-commit.sample*
# commit-msg.sample*          pre-push.sample*
# fsmonitor-watchman.sample*  pre-rebase.sample*
# post-update.sample*         pre-receive.sample*
# pre-applypatch.sample*      prepare-commit-msg.sample*
# pre-commit.sample*          update.sample*
```

デフォルトでは、`.sample`と書かれたサンプルのプログラム (シェルスクリプト) が配置されています。
Git Hooks を動作させる際には、対応させるアクションのファイル名で、`.sample`を付けず、プログラムを配置します。(実行権限の付与もお忘れなく。)

また、リモートリポジトリ上の`hooks/`と、ローカルリポジトリ上の`.git/hooks/`は、クローン (同期) されません。
そのため、開発者全員に特定のルールに沿った Git Hooks を使用させるには、別途設定をさせる必要があります。
(一見すると不便な仕様に見えるかもしれませんが、悪意あるスクリプトを実行させないためでしょう。特に、GitHub 等で公開されているオープンなリポジトリでは。)

# コミット関係 (クライアントサイド)

コミットに関する Git Hooks は、全部で 4つあります。
Git でのコミット生成の動作と合わせて説明すると、以下のように呼び出されます。

1. `git commit`が実行される
2. --- `pre-commit`が実行される
3. (1. で`-m "<message>"`オプションがあったなら、`<message>`が書き込まれた) `.git/COMMIT_EDITMSG`が生成される
4. --- `prepare-commit-msg`が実行される
5. (1. で`-m <message>`オプションが無かったなら) `.git/COMMIT_EDITMSG`の編集が開始される
6. --- `commit-msg`が実行される
7. コミットが生成される
8. --- `post-commit`が実行される

![Hook-Commit.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540894/38ed9f96-52f5-d26e-618e-666e0a479dfe.png)

`pre-commit` `prepare-commit-msg` `commit-msg`において、`exit 1`等のように、0 以外の返り値が発生した場合、その時点でコミットの動作が中止 (reject) されます。
`commit-msg`は、コミット生成の直前に呼び出されますので、入力されたコミットメッセージが指定の書式に沿っているかのチェックに応用できます。
メッセージの書式が正しく無い場合は、コミットを生成できない、というようにすることが可能です。

`post-commit`は、コミットが正常に完了したら呼び出されますので、通知等に応用できます。
`post-commit`が実行された段階では、既にコミットは完了していますので、返り値が何であろうと中止はされません。

```sh
#!/bin/sh
# /.git/hooks/prepare-commit と /.git/hooks/commit-msg で同じ結果が得られる

echo $1  # 結果例: .git/COMMIT_EDITMSG
cat $1   # 結果例: any commit message
exit 0
```

`prepare-commit-msg` `commit-msg`が呼び出される際、第一引数には、コミットメッセージの書かれたファイル`COMMIT_EDITMSG`へのパスが入力されます。

`pre-commit` `post-commit`では、引数は入力されません。
`git-log`や`git rev-parse`コマンド等で、リポジトリの情報を取得して、何らかの動作をさせます。

`commit-msg`について、実装例を作ってみました:

https://qiita.com/aKuad/items/f345331baa37c605e08a

# push 受け取り関係 (サーバサイド)

push に関する Git Hooks は、全部で 4つあります。
Git でのコミット生成の動作と合わせて説明すると、以下のように呼び出されます。

1. `git push`が実行される
2. --- `pre-receive`が実行される
4. --- `update`が、更新されたブランチ全てに実行される
5. push が適用される
6. --- `post-receive`が実行される

![Hook-Post.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540894/860f1e64-8299-793d-a581-de2ead7dcbc8.png)

`pre-receive` `update`において、0 以外の返り値が発生した場合、その時点で push 受け取りが中止 (reject) されます。
`post-receive`は、push が正常に適用されたら呼び出されますので、通知等に応用できます。

```sh
#!/bin/sh
# /hooks/pre-receive と /hooks/post-receive で同じ結果が得られる

read branch hash_old hash_new
echo $branch    # 結果例: refs/heads/master
echo $hash_old  # 結果例: aca07435522b6e5ed63481aada5733e421fabbb5
echo $hash_new  # 結果例: 90746b18c099aec161434e0c35238db219f8e76e
exit 0
```

```sh
#!/bin/sh
# /hooks/update

echo $1  # 結果例: refs/heads/master
echo $2  # 結果例: aca07435522b6e5ed63481aada5733e421fabbb5
echo $3  # 結果例: 90746b18c099aec161434e0c35238db219f8e76e
exit 0
```

`update`では、引数に値が入力されますが、`pre-receive`と`post-receive`では (なぜか) 標準入力 (`read`) で入力が来ます。
1つ目の値は、更新前時点で最新のコミットのハッシュが、2つ目の値は、更新後で最新のコミットのハッシュ、3つ目の値は操作が行われたブランチが入力されます。

`pre-receive`と`update`とでは、`pre-receive`は 1度の push で 1度だけ実行される点、`update`は、更新のかかったブランチ全てに実行される点が異なります。
例えば、`branch1`と`branch2`を更新した後、`git push --all`とコマンドを実行した場合を考えます。
`pre-receive`では、その時点での操作先ブランチについての値が入力され、実行されます。
`update`では、`refs/heads/branch1` `refs/heads/branch2`それぞれについて値が入力され、更新されたブランチの数実行されます。

# GitHooks に使えそうなコマンド集

Git コマンドから UNIX 標準コマンドまで紹介。
(詳しい説明は省略。メモ程度にご覧ください。)

`sed` `grep`
テキストの置換や検索等、テキスト処理色々。
(一応書いておくと、) 加えて`if then`や`test`等、各種シェルスクリプトでよく使われるコマンドも必須です。

`git log -n 1`
コミット履歴のうち、最新のコミット 1つのみを表示。
1をその他の数字にすれば、その数字の数だけコミットを表示。

`git log -n 1 <HASH>`
コミット履歴のうち、`<HASH>`と一致するハッシュを持つコミットの情報を表示。

`git status -s`
`git status`の結果をシンプル表示します。
Add や Modified 等を示す英数字とファイル名を各 1行ずつ表示。
行数をカウントすれば、変更のあったファイル数を得られます。

`git rev-parse --short <HASH>`
Git のコミットのハッシュ`<HASH>`を短縮したものを表示。

`rev-parse`コマンドは、他にも色々な情報の取得に使えます。

https://qiita.com/karupanerura/items/721962bb7da3e34187e1

https://qiita.com/Alice1017/items/ad061f33c8d42d52e079