Git hooksを使用してpyproject.tomlのversion情報とgit tagの更新をコミットをトリガーに自動化する

# はじめに
本記事は以下ブログ記事の転記です。

https://7rikazhexde-techlog.hatenablog.com/entry/2023/06/10/005231

記事で書かれている[poetry](https://python-poetry.org/)、[git-hook](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)、[git-tag](https://git-scm.com/docs/git-tag)の説明は割愛します。
詳細は公式ドキュメントを確認してください。

# 補足情報

## 2024/10/09

既存の不満点を改善するため、GitHub Actionsによるワークフローで更新する方法を検討しました。

【GitHub Actions】pyproject.tomlのversionキーとgit tagを同じバージョン指定で更新してreleaseを作成するworkflowについて の記事を[ブログに投稿](https://7rikazhexde-techlog.hatenablog.com/entry/2024/10/09/233349)しましたので、こちらも参考にしてください。

## 2023/09/06

しばらく運用していて、コミットごとに`git tag`するのは冗長だと感じることが増えてきました。

コミットごとに`pyproject.toml`と`git tag`を更新（インクリメント）するのであれば良いですが、毎回更新しなくてよい場合は、後述する`.pre-commit-config.yaml`の`update-pyproject`フックを一式コメントアウトしてください。

動作としては以下の通りです。
- `pyproject.toml`の`version`キーを更新する場合は、コミットすると`pyproject.toml`の`version`キーを取得して、`git tag`が実行される。
- `pyproject.toml`の`version`キーを更新しない場合は、`git tag`は実行されない。



# 要約
- `git commit`をトリガーに`pyproject.toml`の`tool.poetry`テーブルの`version`キーと`git tag`を同時に更新する
- [**git hook**](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)でバージョン更新用のスクリプトをフックに設定する
- git hookとして[**pre-commit**](https://pre-commit.com/)と**post-commit**の2つを使用する
- post-commitでは最後に`git push`を実行する

# 作成したもの
以下のスクリプトをgit hookで実行します。

#### 1. [update_pyproject_version.py](https://gist.github.com/7rikazhexde/8d2e7e2c0fce21bfbe9f858d4240f2bc)
 - pyproject.tomlのバージョンを更新するためのPythonスクリプト

#### 2. [run_git_tag_base_pyproject.py](https://gist.github.com/7rikazhexde/b0bde94be24935525afcea457f70e732)
 - `pyproject.toml`の`tool.poetry`テーブルの`version`キーを取得して`git tag`を実行するためのPythonスクリプト

#### 3. [.pre-commit-config.yaml](https://gist.github.com/7rikazhexde/fe8281184e351a3b914add3ba2f818aa)
 - git フックスクリプトをセットアップするためのコンフィグファイル

#### 4. [pre-commitフック](https://gist.github.com/7rikazhexde/6ada2a6ef3ca23938bfa62f32e3fbed8) 
 - poetryコマンド、静的解析パッケージ、`update_pyproject_version.py`を含む、pre-commitで実行するカスタムスクリプト
 - `.pre-commit-config.yaml`作成後に`poetry run pre-commit install`コマンドで作成します。

#### 5. [post-commitフック](https://gist.github.com/7rikazhexde/3d786d926fcb19edd807ebb6e9c96df1)
 - `run_git_tag_base_pyproject.py`を`git commit`後に実行するカスタムスクリプト 
 - [`create_post-commit.sh`](https://gist.github.com/7rikazhexde/89036d5fc849411b925e6da7d4986b52)を実行することで作成します。

# 実行例

以下のプロジェクトを例に上記スクリプトの実行例を示します。  

https://github.com/7rikazhexde/video-grid-merge#readme

## 1.コミット前の情報

0.2.8までバージョンが追加されている状態です。  

### 1.1. ローカルタグ情報

```bash
video-grid-merge % git tag
v0.2.1
v0.2.2
v0.2.3
v0.2.4
v0.2.5
v0.2.6
v0.2.7
v0.2.8
```

### 1.2. リモートタグ情報

```bash
% git ls-remote --tags origin
0ae7fa7be0457a3512c51c7a231f24e724570ffd        refs/tags/v0.2.1
197a04bcf528e4c0ef5c297dff061d4d34ffea2b        refs/tags/v0.2.2
01f88f943ee8330034530c14fb417bf45c5e651b        refs/tags/v0.2.3
4212bfa0733f2c2c809f9db37333096b752a451e        refs/tags/v0.2.4
52b72640df2f31edbef5c21e0fec149d67eafe04        refs/tags/v0.2.5
6de440811f4adbbd614f622ebed74450dfeaa951        refs/tags/v0.2.6
03b75ea8f4688c3e3b8e02c285093b9736a38d24        refs/tags/v0.2.7
054237a84bbe9233099c55af4ff2443145fbb4d8        refs/tags/v0.2.8
```

### 1.3. pyproject.toml

```toml
[tool.poetry]
name = "video-grid-merge"
version = "0.2.8"
description = "This project allows you to use FFmpeg to arrange video files stored in a specified folder in an NxN grid layout and generate the output."
authors = ["7rikaz_h785 <7rikaz.h785.stat2ltas41lcijad@gmail.com>"]
readme = "README.md"
license = "MIT"
packages = [{include = "video_grid_merge"}]

[tool.poetry.scripts]
#video-grid-merge = "video_grid_merge.__main__:main"
#delete-temporaliy-files = "video_grid_merge.local_code.delete_files:main"

[tool.taskipy.tasks]
vgmrun = "python video_grid_merge"
vgmrn = "python video_grid_merge/rename_files.py"
vgmrm = "python video_grid_merge/delete_files.py"
vgmtest = "pytest -s -vv --cov=video_grid_merge --cov-branch --cov-report term-missing --cov-report html"
isort = "poetry run isort video_grid_merge tests"
black = "poetry run black video_grid_merge tests"
flake8 = "poetry run flake8 video_grid_merge tests"
mypy = "poetry run mypy"

[tool.poetry.dependencies]
python = "^3.10"
mdformat = "^0.7.16"
tomlkit = "^0.11.8"

[tool.poetry.group.dev.dependencies]
flake8 = "^6.0.0"
black = "^23.3.0"
isort = "^5.12.0"
mypy = "^1.3.0"
pytest = "^7.3.1"
flake8-pyproject = "^1.2.3"
taskipy = "^1.11.0"
pytest-cov = "^4.1.0"
pytest-mock = "^3.10.0"
pre-commit = "^3.3.2"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.black]
target-version = ['py310']

[tool.isort]

[tool.flake8]
ignore = ["E402","E501","W503"]

[tool.mypy]
files = ["video_grid_merge","tests"]
python_version = "3.10"
strict = true
warn_return_any = false
ignore_missing_imports = true
scripts_are_modules = true

[tool.pytest.ini_options]
testpaths = ["tests",]
```

## 2. プロジェクトのインストール

```bash
git clone https://github.com/7rikazhexde/video-grid-merge.git
cd video-grid-merge
poetry install 
```

プロジェクトは以下のようになります。  
`run_git_tag_base_pyproject.py`と`update_pyproject_version.py`はciという名前のディレクトリに格納されています。  

```bash
video-grid-merge % tree -L 2
.
├── LICENSE
├── README.md
├── ci
│   ├── run_git_tag_base_pyproject.py
│   └── update_pyproject_version.py
├── create_post-commit.sh
├── poetry.lock
├── pyproject.toml
├── requirements-dev.txt
├── requirements.txt
├── tests
│   ├── __init__.py
│   ├── test_data
│   └── test_main.py
└── video_grid_merge
    ├── __init__.py
    ├── __main__.py
    ├── delete_files.py
    ├── local_code
    ├── media
    └── rename_files.py
```

## 3. pre-commitスクリプトとpost-commitスクリプトの作成
.git/hooks以下は下記の通りです。  

```bash
video-grid-merge % tree -L 2 .git/hooks 
.git/hooks
├── applypatch-msg.sample
├── commit-msg.sample
├── fsmonitor-watchman.sample
├── post-update.sample
├── pre-applypatch.sample
├── pre-commit.sample
├── pre-merge-commit.sample
├── pre-push.sample
├── pre-rebase.sample
├── pre-receive.sample
├── prepare-commit-msg.sample
├── push-to-checkout.sample
└── update.sample
```

poetry install後はpre-commitスクリプトとpost-commitスクリプトは存在しないため、下記コマンドを実行してインストールします。(一部記載を環境変数に置き換えています。)  

### 3.1. pre-commit作成
```bash
video-grid-merge % poetry run pre-commit install
Configuration file exists at $HOME/Library/Preferences/pypoetry, reusing this directory.

Consider moving TOML configuration files to $HOME/Library/Application Support/pypoetry, as support for the legacy directory will be removed in an upcoming release.
pre-commit installed at .git/hooks/pre-commit
```

### 3.2. post-commit作成
```bash
video-grid-merge % chmod +x ./create_post-commit.sh
video-grid-merge % ./create_post-commit.sh 
[video-grid-mergeのディレクトリパス]/.git/hooks/post-commit created with execution permission.
```

再度.git/hooks以下を確認するとpre-commitとpost-commitが追加されていることを確認できます。

```bash
video-grid-merge % tree -L 2 .git/hooks 
.git/hooks
├── applypatch-msg.sample
├── commit-msg.sample
├── fsmonitor-watchman.sample
├── post-commit # 追加
├── post-update.sample
├── pre-applypatch.sample
├── pre-commit # 追加
├── pre-commit.sample
├── pre-merge-commit.sample
├── pre-push.sample
├── pre-rebase.sample
├── pre-receive.sample
├── prepare-commit-msg.sample
├── push-to-checkout.sample
└── update.sample
```

## 4. git commit例

### 4.1. 失敗例
作成したgit hookは`git commit`をトリガーに実行されますが、`pre-commit`では`.pre-commit-config.yaml`のプラグインオプションである`fail_fast`により、各hook処理が成功(=Passed)しない場合には実行を停止させています。これは**update-pyprojectフック**(`update_pyproject_version.py`)で失敗した情報を取得できないため、エラーの場合も実行させないことで、バージョンの更新もさせないようにしています。  

次に失敗時の例として、`.pre-commit-config.yaml`ファイルで改行を増やして、`git add`後に`git commit`した場合、**trailing-whitespaceフック**により指摘箇所は修正されますが、`pre-commitフック`の実行は**停止**されます。  

```bash
> git -c user.useConfigOnly=true commit --quiet
trim trailing whitespace.................................................Failed
- hook id: trailing-whitespace
- exit code: 1
- files were modified by this hook

Fixing README.md
```

### 4.2. 成功例
差分は下記の通りです。  
![git diff.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1832768/372e3ef2-1240-bf07-ea7b-8e598949e45b.png)

ここで再度、`git commit`を実行すると`pyproject.toml`と`git tag`を**0.2.9**で更新して`git push`していることが確認できます。(実行結果の表示について、vscodeのコミットボタン押下でも問題なく動作しますが、デフォルトではログ出力しないため、git commitコマンドを実行しています。)

```bash
% git commit -m "docs(README): Changed installation description"
trim trailing whitespace.................................................Passed
fix end of files.........................................................Passed
mixed line ending........................................................Passed
check toml...........................................(no files to check)Skipped
check yaml...........................................(no files to check)Skipped
poetry-check.........................................(no files to check)Skipped
- hook id: poetry-check
poetry-lock..............................................................Passed
- hook id: poetry-lock
- duration: 2.39s

Configuration file exists at $HOME/Library/Preferences/pypoetry, reusing this directory.

Consider moving TOML configuration files to $HOME/Library/Application Support/pypoetry, as support for the legacy directory will be removed in an upcoming release.
Updating dependencies
Resolving dependencies... (0.9s)

poetry-export........................................(no files to check)Skipped
- hook id: poetry-export
poetry-export........................................(no files to check)Skipped
- hook id: poetry-export
isort................................................(no files to check)Skipped
black................................................(no files to check)Skipped
flake8...............................................(no files to check)Skipped
mypy.................................................(no files to check)Skipped
mdformat.................................................................Passed
Update pyproject.toml version............................................Passed
Configuration file exists at $HOME/Library/Preferences/pypoetry, reusing this directory.

Consider moving TOML configuration files to $HOME/Library/Application Support/pypoetry, as support for the legacy directory will be removed in an upcoming release.
v0.2.1
v0.2.2
v0.2.3
v0.2.4
v0.2.5
v0.2.6
v0.2.7
v0.2.8
v0.2.9
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 4 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (4/4), 451 bytes | 451.00 KiB/s, done.
Total 4 (delta 3), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (3/3), completed with 3 local objects.
To https://github.com/7rikazhexde/video-grid-merge.git
   054237a..c6f792c  main -> main
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/7rikazhexde/video-grid-merge.git
 * [new tag]         v0.2.9 -> v0.2.9
.git/hooks/post-commit end!!!
[main c6f792c] docs(README): Changed installation description
 2 files changed, 5 insertions(+), 5 deletions(-)
```

### 4.3. git log(コミット後)

```bash
video-grid-merge % git log --stat
commit c6f792c39cebb9ee6c21c6ae7c040573f4ddd995 (HEAD -> main, tag: v0.2.9, origin/main, origin/HEAD)
Author: 7rikaz_h785 <7rikaz.h785.stat2ltas41lcijad@gmail.com>
Date:   Fri Jun 9 21:53:03 2023 +0900

    docs(README): Changed installation description

 README.md      | 8 ++++----
 pyproject.toml | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)
```

### 4.4. ローカルタグ情報(コミット後)

```bash
video-grid-merge % git tag
v0.2.1
v0.2.2
v0.2.3
v0.2.4
v0.2.5
v0.2.6
v0.2.7
v0.2.8
v0.2.9 # 追加
```

### 4.5. リモートタグ情報(コミット後)
```bash
% git ls-remote --tags origin
0ae7fa7be0457a3512c51c7a231f24e724570ffd        refs/tags/v0.2.1
197a04bcf528e4c0ef5c297dff061d4d34ffea2b        refs/tags/v0.2.2
01f88f943ee8330034530c14fb417bf45c5e651b        refs/tags/v0.2.3
4212bfa0733f2c2c809f9db37333096b752a451e        refs/tags/v0.2.4
52b72640df2f31edbef5c21e0fec149d67eafe04        refs/tags/v0.2.5
6de440811f4adbbd614f622ebed74450dfeaa951        refs/tags/v0.2.6
03b75ea8f4688c3e3b8e02c285093b9736a38d24        refs/tags/v0.2.7
054237a84bbe9233099c55af4ff2443145fbb4d8        refs/tags/v0.2.8
c6f792c39cebb9ee6c21c6ae7c040573f4ddd995        refs/tags/v0.2.9 # 追加
```

# 注意事項
`git commit`時にコミットキャンセルした場合は`pre-commit`は正常終了していますので、**update-pyprojectフック**(`update_pyproject_version.py`)は動作します。

結果、`pyproject.toml`の`tool.poetryテーブル`の`versionキー`は更新されますので、その状態で再度コミットするとさらに**0.0.1分加算**します。なので、この場合は`version`部分を手動で変更前の値に戻す必要があります。現状はコミットキャンセル処理を考慮していないのでこのようになっています。  

また、`update_pyproject_version.py`では`v[major].[minor].[pathch]`でバージョン定義していますが、`major`は**0以上**、`[minor]と[pathch]`は**0から999**にしています。もし、**0.1.0から0.2.0にminorバージョンを更新するためには0.1.999とする必要があります。**  

それぞれ、今後改善したい部分ですが、対応は未定です。  

# コード補足
コードは[Gist](https://gist.github.com/7rikazhexde)にコミットしていますが、下記リポジトリにもコミットしています。
試験用のため更新は非定期で、将来的にはリポジトリを変更するかもしれませんが、利用したい方は**自己責任**の範囲でフォークやDLをして使用してください。

https://github.com/7rikazhexde/trial-test#readme


# まとめ

本記事では、git hookでpythonプロジェクトのバージョン管理を自動化する記事を紹介しました。

pre-commitではバージョン更新用のフック以外にも静的解析ツールのフックも利用しています。ツールに信頼を置くことが前提になりますが、コードの品質を上げるためには必要だと思います。  

一方でツールが増えると管理が複雑になるため、一元管理し、人による操作はなるべく減らすことが必要だと感じています。DockerやGitHub Action等、便利なツールは他にもありますが、個人的には使用経験が浅いので、今後はgit hook以外にもツールを活用して、より効率的なコーディングができるようにしていきたいと思っています。

最後に、本記事がどなたかの参考になれば幸いです。

# 参考
以下の記事を参考にさせていただきました。

- [オレオレPython開発環境](https://zenn.dev/jdbtisk/articles/e6ed54b38b6a45)

- [Pythonレポジトリ用のpre-commit環境を整える](https://rcmdnk.com/blog/2023/02/07/computer-python/)

以上です。