Python製のツールpre-commitでGitのpre-commit hookを楽々管理!!

はじめに

サーバーレス開発部@大阪の岩田です。 現在開発中のプロジェクトでソースコードの静的解析を強化すべくGitのpre-commit hookを便利・かつ簡単に管理する方法について調べていました。

色々調べたところ、Python製のpre-commitというツールが良さげだったので使い方を簡単にご紹介します。

pre-commitとは?

Gitのpre-commitフックスクリプトを管理・メンテナンスするためのフレームワーク(位置付け的にツールの方がしっくり来るので、この記事ではツールと呼びます)です。 pre-commit自体はPython製ですが、Python以外の言語で書かれたプロジェクトに導入したり、Python以外の言語で書かれたhookスクリプトを管理することもできます。

公式サイトでは以下のように説明されています。

A framework for managing and maintaining multi-language pre-commit hooks.

やってみる

実際に使ってみます。

pre-commitのインストール

まずpre-commitをインストールします。 現在開発中のプロジェクトではpipenvでライブラリを管理しているので、pipenvを使ってpre-commitを導入します。

pipenv install --dev  pre-commit

設定ファイルの作成

インストールできたらpre-commitの設定ファイルを作成します。 設定ファイルは.pre-commit-config.yamlというファイル名で作成します。 sample-configというサブコマンドでサンプルの設定が出力できるので、まずはこれを使ってみます。

pre-commit sample-config > .pre-commit-config.yaml

作成された設定ファイルはこんな感じです。

# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v2.0.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
    -   id: check-yaml
    -   id: check-added-large-files

詳細をみていきましょう。

repos

hookスクリプトが存在するリポジトリのリストを指定します。 GitHubで公開されているリポジトリを指定することで車輪の再発明が不要になります。

rev

reposで指定されたリポジトリのリビジョンもしくはタグを指定します。 reposで指定したリポジトリから、このセクションで指定されたバージョンのソースコードを取得してhookスクリプトとして利用します。

hooks

実際に実行するhookスクリプトをリストで指定します。

id

各hookスクリプトのIDを指定します。 リポジトリ内に存在するhookスクリプトのファイル名と一致(.pyは不要)するよう設定します。

Gitのhookスクリプトを導入

設定ファイルの準備ができたので、Gitのhookスクリプトを導入します。

pre-commit install

pre-commit installed at .git/hooks/pre-commitという出力が表示されれば導入完了です。

適当にコミットしてみる

hookスクリプトが導入できたので、試しに何かコミットしてみます。

[INFO] Initializing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Installing environment for https://github.com/pre-commit/pre-commit-hooks.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...........................................(no files to check)Skipped
Check for added large files..............................................Passed

設定ファイルの記述に沿ってhookスクリプトの取得と実行が行われているのが分かります。

設定のカスタマイズ

サンプルが正しく動作していることが確認できたので、pre-commitで実行するスクリプトを追加してみます。 今回はLizardでソースコードのサイクロマチック数を計測し、閾値を超えていた場合はコミットさせないような設定を追加してみます。

まずLizardをインストールします

pipenv install --dev  lizard

pre-commitの設定ファイル内に以下の記述を追加します。

-   repo: local
    hooks:
    -   id: lizard
        name: lizard
        entry: lizard -C 10
        language: python_venv
        types: [file, python]
        exclude: ^test_.*$

repo

hookスクリプトが存在するリポジトリを指定します。 サンプルではGitHubのリポジトリが指定されていましたが、localと指定することで、hookスクリプトを実行する自身のGitリポジトリを指定できます。 この機能を使うことでソースコードと自作のhookスクリプトをまとめて管理することが可能です。

name

hookスクリプト実行中に標準出力に表示される文字列を指定します。 repoにGitHubのリポジトリを指定するサンプルの設定では特にnameが指定されていませんでしたが、 repolocalを指定している場合はid,name,language,entryに加えてfilesもしくはtypesの指定が必須となります。

entry

pre-commitのhookで実行するスクリプトを指定します。 今回はlizardの -C オプションでサイクロマチック数の閾値を10に設定しました

language

hookスクリプトを実行するための言語を指定します。 pythonpython_venv以外にもrubynodeも指定可能です。

exclude

hookスクリプトの実行対象から除外するファイルを正規表現で指定します。

types

hookスクリプトのチェック対象を指定します。 [file, python]という指定することでコミット対象からpythonのファイルのみが抽出され、hookスクリプトの引数に渡ります。

例えばコミット対象が

  • a.txt
  • b.py
  • c.py

の場合、実際に実行されるhookスクリプトは lizard -C 10 b.py c.py となります。

設定のテスト

設定がカスタマイズできたので、試しに滅茶苦茶なソースコードのコミットを試みます。

ソースコードを準備します。

def hoge():
    if 1 == 1:
        print('hoge')
    elif 1 == 2:
        pass
    elif 1 == 3:
        pass
    elif 1 == 4:
        pass
    elif 1 == 5:
        pass
    elif 1 == 6:
        pass
    elif 1 == 7:
        pass
    elif 1 == 8:
        pass
    elif 1 == 9:
        pass
    elif 1 == 10:
        pass

このファイルのコミットを試みます。

Trim Trailing Whitespace.................................................Passed
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Check for added large files..............................................Passed
lizard...................................................................Failed
hookid: lizard

================================================
  NLOC    CCN   token  PARAM  length  location
------------------------------------------------
      21     11     67      0      21 hoge@1-21@hoge.py
1 file analyzed.
==============================================================
NLOC    Avg.NLOC  AvgCCN  Avg.token  function_cnt    file
--------------------------------------------------------------
     21      21.0    11.0       67.0         1     hoge.py

=========================================================================================
!!!! Warnings (cyclomatic_complexity > 10 or length > 1000 or parameter_count > 100) !!!!
================================================
  NLOC    CCN   token  PARAM  length  location
------------------------------------------------
      21     11     67      0      21 hoge@1-21@hoge.py
==========================================================================================
Total nloc   Avg.NLOC  AvgCCN  Avg.token   Fun Cnt  Warning cnt   Fun Rt   nloc Rt
------------------------------------------------------------------------------------------
        21      21.0    11.0       67.0        1            1      1.00    1.00

無事にコミットに失敗しました。 これで保守性の低いコードの混入を防止出来そうです。

まとめ

pre-commitの使い方について簡単にご紹介しました。 Gitのhookスクリプトは便利な仕組みではありますが、スクリプトを.gitディレクトリ以下に配置する必要があるため、バージョン管理やPJメンバーへの配布をどのように行うのかが課題になります。 pre-commitを利用することでこのような問題を解決し、より快適なGitライフが送れそうです。 hookスクリプトの管理に悩んでいる方は是非利用を検討してみて下さい。