Git HooksはPython(など)で書いてPythonで管理しよう


結局のところGitフックは

  • どうやってチームメンバーと共有するの
  • 誰がメンテすんの

あたりが最終的な課題になってくると思うのですが、
Python製のツールpre-commitでGitのpre-commit hookを楽々管理!!
こちらの記事で紹介されているpre-commitを弊チームでも導入したので補足記事を書きたいと思います。

なおあくまでも補足なのでまずは上記記事を一読されることをおすすめします。

環境

windows 10
python 3.7.2
pre-commit 1.16.1

ざっくり仕組み紹介

  1. リポジトリのルートでpre-commit installすると.git/hooksにpythonで書かれたpre-commitが生まれる
  2. このpre-commitは同じくルートにある.pre-commit-config.yamlを参照し、記載されたプログラムを上から順に実行する
  3. 全てのプログラムが0を返せばコミットが実行される

というのが基本です。
その他おさえておきたい特徴はこのあたり。

  • pre-commit installにオプションをつけることによってpre-commitのほか以下のフックを作成できる(公式ドキュメント)
    • pre-push
    • prepare-commit-msg
    • commit-msg
  • ローカルに用意したプログラムだけでなくGitHubなどの公開リポジトリを利用できる
  • 使用できる言語は以下の通り(公式ドキュメント)
    • docker
    • docker_image
    • fail
    • golang
    • node
    • python
    • python_venv
    • ruby
    • rust
    • swift
    • pcre
    • pygrep
    • script
    • system

プロジェクトと同じ言語で書けたら楽ですよね。

実演

さて、実際にpre-commitを使用してGitHubに置いた自作のPythonスクリプトをcommit-msgで動かしてみます。

公開リポジトリの準備

Pythonの場合ディレクトリ構成はこうなります。
パッケージングして.pre-commit-hooks.yamlを置けってことですね。

.
├── commit_msg(任意)
│   ├── __init__.py
│   └── issue_num.py(任意)
├── .pre-commit-hooks.yaml
├── setup.cfg
└── setup.py

まずはフックスクリプトの用意です。
今回はブランチ名からイシュー番号を拾ってコミットメッセージに付与するだけの簡単なものを作ります。

issue_num
import sys
import io
import pathlib
import re


def main():
    """
    return
    1 ... Failed
    0 ... Succeed
    """
    isError = False
    encoding = 'utf-8'
    git_dir = commit_msg_file = pathlib.Path('./.git/')
    # イシュー番号取得
    head = pathlib.Path(git_dir/'HEAD').read_text(encoding)
    issue_num_match = re.search(r'#\d{1,}', head)
    # コミットメッセージ書き換え
    if issue_num_match:
        commit_msg_file = pathlib.Path(git_dir/'COMMIT_EDITMSG')
        commit_msg = commit_msg_file.read_text(encoding)
        commit_msg_file.write_text(f'{issue_num_match.group()} {commit_msg}', encoding)
    else:
        # これがないと出力が文字化けします
        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding)
        print('ブランチ名にイシュー番号が存在しません')
        isError = True
    return isError


if __name__ == '__main__':
    sys.exit(main())

次にsetupを書きます。

setup.py
from setuptools import setup
setup()
setup.cfg
[metadata]
name = my_hooks
version = 1.0.0
description = hooks for Python tool "pre-commit".

[options]
packages = commit_msg

[options.entry_points]
console_scripts =
    issue_num = commit_msg.issue_num:main

必要最低限なので好きに追加してください。
最後に.pre-commit-hooks.yamlです。

.pre-commit-hooks.yaml
-   id: issue_num
    name: issue_num
    description: Get issue number from branch name and add to commit message.
    entry: issue_num
    language: python
    stages: [commit-msg]

id及びnameは自由ですが、entryには必ずsetupで書いたentry_pointsのそれを書いてください。
stagesはpre-commitとか他のフックが存在する際にどのフックで起動するかを指定できます。
詳しくはこちら

ここまで出来たら最終コミットのハッシュ値をメモっておいてください。
GitHubで言うとここのやつ↓

タグでも結構です。
これで準備が整いました。

フックを適用したいリポジトリの設定

ディレクトリ構成
.
├── .git
│   ├── hooks
│   ┊   └── hoges
│   └── hoge
├── .pre-commit-config.yaml
├── hoge
┊
└── hoge

まずルートでターミナルを立ち上げて

pre-commit install -t commit-msg

します。
.git/hooks内にcommit-msgが作成されたことを確認してください。
あとは.pre-commit-config.yamlを書いたら完了です。

.pre-commit-config.yaml
repos:
-   repo: リポジトリのURL
    rev: メモったハッシュ値 or タグ
    hooks:
    -   id: issue_num(.pre-commit-hooks.yamlに登録したid)

成功例

この状態でコミットしてみましょう。
初回はインストールなりなんなり(ユーザー/.cache/pre-commit内にリポジトリごとの環境を構築するので)
で時間がかかりますが、2回目以降は大丈夫です。

自作のissue_num.pyをPassしたことが出力されています。

終わりに

参照しているのはGitHubなので直接共有する必要がなく、またプロジェクトと同じ言語で書いているので誰もがメンテナンス可能です(ここは賛否あると思いますが…)。
また公式が用意しているスクリプトも有用なものが多いので、併せて使えばよりレビューが楽になるでしょう。

良いフックスクリプトのご公開をお待ちしています。かしこ