7rikazhexde’s tech log

技術的な興味関心、備忘録、アウトプットなどを書いています。

【Pythonバージョン管理】git hookを使用してコミットをトリガーにpyproject.tomlとgit tagを更新するスクリプトについて

経緯

私はGitHubソースコードを管理していますが、pythonで作成するプロジェクトのバージョンと依存関係管理でPoetryも使用しています。

プロジェクトのバージョンはgit tagで作成して管理することが可能です。また、poetryではpythonプロジェクトの設定ファイルであるpyproject.tomlでもtool.poetryテーブルのversionキーでバージョンを記載して管理することができます。

git tagによるタグ作成とversionキーの入力は必須ではありませんが、バージョン情報を明示的に記載することでプロジェクト管理がしやすくなります。

追加情報

20230906

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

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

動作としては以下の通りです。

  • pyproject.tomlversionキーを更新する場合は、コミットするとpyproject.tomlversionキーを取得して、git tagが実行される。
  • pyproject.tomlversionキーを更新しない場合は、git tagは実行されない。

課題

pyproject.tomlとgit tagはバージョンは合わせる方針で手動で更新していたところ、更新を忘れることがあり、何より手動での更新が手間でした。そこで同時に更新する方法がないか検討していましたが、少なくとも私が調べた範囲では同時更新するようなツールは存在しませんでした。

解決方法

git hookを使用してコミットをトリガーにpoetry.tomlgit tagを更新するpythonスクリプトを実行する方法で解決しました。
pythonスクリプトpre-commitとpost-commitで実行します。

本記事では主にスクリプトと実行例について紹介します。

作成したもの

作成したスクリプトは下記の通りです。

  1. update_pyproject_version.py: pyproject.tomlのバージョンを更新するためのPythonスクリプト

  2. run_git_tag_base_pyproject.py: pyproject.tomltool.poetryテーブルのversionキーを取得してgit tagを実行するためのPythonスクリプト

  3. .pre-commit-config.yaml: git フックスクリプトをセットアップするためのコンフィグファイル

  4. pre-commitフック: poetryコマンド、静的解析パッケージ、update_pyproject_version.pyを含む、pre-commitスクリプト

  5. create_post-commit.sh: post-commitフックを作成するシェルスクリプト

  6. post-commitフック: run_git_tag_base_pyproject.pygit commit後に実行するためのシェルスクリプト

詳細

update_pyproject_version.py

pyproject.tomlのバージョンを更新するためのPythonスクリプト

run_git_tag_base_pyproject.py

pyproject.tomlのtool.poetryテーブルのversionキーを取得してgit tagを実行するためのPythonスクリプト

.pre-commit-config.yaml

git フックスクリプトをセットアップするためのコンフィグファイル

create_post-commit.sh

poetryコマンド、静的解析パッケージ、update_pyproject_version.pyを含む、pre-commitスクリプト

post-commit

run_git_tag_base_pyproject.pyをgit commit後に実行するためのシェルスクリプト

実行例

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

github.com

コミット前の情報

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

ローカルタグ情報
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
リモートタグ情報
% 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
pyproject.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",]

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

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

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

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

pre-commitスクリプトとpost-commitスクリプトの作成

.git/hooks以下は下記の通りです。

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スクリプトは存在しないため、下記コマンドを実行してインストールします。(一部記載を環境変数に置き換えています。)

pre-commit作成
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
post-commit作成
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が追加されていることを確認できます。

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

git commit例

失敗例

作成した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フックの実行は停止されます。

> 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
成功例

差分は下記の通りです。

git diff

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

% 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(-)
git log(コミット後)
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(-)
ローカルタグ情報(コミット後)
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 # 追加
リモートタグ情報(コミット後)
% 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.tomltool.poetryテーブルversionキーは更新されますので、その状態で再度コミットするとさらに0.0.1増加します。なので、この場合はversion部分を手動で変更前の値に戻す必要があります。現状、コミットキャンセル処理は考慮していないのでこのようになっています。

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

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

コードはGistにコミットしていますが、下記リポジトリにもコミットしています。 試験用のため更新は非定期で、将来的にはリポジトリを変更するかもしれませんが、利用したい方は自己責任の範囲でフォークやDLをして使用してください。

github.com

まとめ

本記事では、git hookを使用してコミットをトリガーにpyprojetc.tomlgit tagを更新するスクリプトを紹介しました。

update_version.pycreate_git_tag.pyの2つのPythonスクリプトを作成し、pre-commitpost-commitgit hookを使用してバージョン情報の更新を自動化しました。

この方法により、バージョン管理とgit tagの更新を手動で行う必要がなくなり、ミスも減り、より効率的にプロジェクト管理ができるようになりました。

参考

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