| name | semantic-commit |
| description | 現在の Git 作業ツリーの変更を、意味のある単位に分割してセマンティックなコミットにする。大きな差分や複数目的が混在した差分のコミット分割、scope なしを原則とする semantic commit 形式のメッセージ作成、コミット計画のレビューを行うときに使う。既存の Git 状態を尊重する。 |
セマンティックコミット
目的
現在の Git 差分を、小さくレビューしやすいコミット列に分割する。各コミットは 1 つの論理的な変更理由を表し、scope なしの semantic commit 形式を基本に、必要に応じて個別に revert できる粒度にする。
手順
- ステージングする前に、リポジトリの状態を確認する。
git status --short
git diff HEAD --stat
git diff --cached --stat
git log --oneline -20
- コミット計画を作る。
- まず機能境界、次に変更種別、最後に依存関係でグループ化する。
- 実装と、それを直接検証するテストは、レビューしやすい場合は同じコミットに含める。
- ドキュメント、CI、生成物、機械的な整形は、実装コミットに必須でなければ別コミットに分ける。
- 1 つのコミットメッセージで変更全体を自然に説明できる粒度にする。
- ユーザーが明示的に一気通貫の実行を求めていない限り、コミット前に計画を提示する。
- 提案するコミットメッセージを含める。
- 各コミットに含めるファイルまたは hunk を示す。
- 既にステージングされている変更があれば明示する。
- ユーザーが計画の確認だけを求めている場合は、ステージングやコミットを行わない。
- 実際に分割コミットする前に、全差分を一度チェックポイントコミットとして保存する。
git add -A
git diff --cached --stat
git diff --cached
git commit -m "chore: semantic commit checkpoint"
checkpoint=$(git rev-parse HEAD)
base=$(git rev-parse "${checkpoint}^")
git reset --mixed "$base"
- このチェックポイントは、分割前の最終状態を記録するためだけに作る。
- GPG 署名や hook は通常のコミットと同じように通す。署名に失敗した場合は停止し、ユーザーに署名環境の対応を求める。
git reset --mixed で戻すのは、この手順で作成したチェックポイントコミットだけにする。ユーザーが元から持っていたコミットや変更は戻さない。
- hook がファイルを変更した場合は、チェックポイントコミットに含まれた内容を基準とする。
- グループごとにステージングしてコミットする。
git add <paths>
git diff --cached --stat
git diff --cached
git commit -m "<type>: <subject>"
- 1 ファイル内に複数目的の変更が混在する場合は、
git add -p または git apply --cached による一時パッチで分割する。
- GPG 署名を無効化したり、hook を迂回したりしない。署名に失敗した場合は停止し、ユーザーに署名環境の対応を求める。
- ユーザーが求めていない限り、push、ブランチ作成、チェックポイントを戻す以外の履歴操作は行わない。
- 各コミット後に残りの状態を確認する。
git status --short
git diff HEAD --stat
- 分割コミットが終わったら、最初のチェックポイントコミットと最終状態を比較する。
git diff --stat "$checkpoint" HEAD
git diff --exit-code "$checkpoint" HEAD
- 差分がなければ、分割前後で最終成果物が一致している。
- 差分がある場合は、分割漏れ、hook による追加変更、誤った hunk 分割の可能性がある。追加コミットで埋め合わせず、差分内容を確認してユーザーに報告する。
- 比較は commit hash 同士で行い、作業ツリーの偶然の状態に依存しない。
分割ルール
以下のいずれかに当てはまる場合は、コミット分割を検討する。
- 変更ファイルが 5 つ以上ある
- 変更行数が 100 行以上ある
- 複数の機能領域にまたがっている
feat、fix、docs、test、ci など複数の変更種別が混在している
- 1 ファイル内に無関係な hunk が混在している
優先するグループ化の手がかり:
- 同じ機能またはドメインのディレクトリ
- 実装と、それに必要な型・スキーマ変更
- 1 つの実装グループだけを検証するテスト
- 1 つの実装グループに必要な設定や依存関係の変更
- 1 つの実装グループを説明するドキュメント
避けるべき弱いグループ化:
- 目的を共有しない「新規ファイル全部」
- 目的を共有しない「修正ファイル全部」
- 後続の実装コミットに依存するテストのみのコミット
- 何でも入れた巨大な
chore コミット
コミットメッセージ
semantic commit 形式を使う。scope は原則使わない。
<type>: <subject>
[optional body]
[optional footer(s)]
scope を付けるのは、ユーザーが明示的に求めた場合、または既存の作業指示で必須とされている場合に限る。迷ったら scope なしにする。
よく使う type:
feat: ユーザーに見える機能追加
fix: バグ修正
docs: ドキュメントのみの変更
test: テストの追加・修正
refactor: 振る舞いを変えないコード変更
style: フォーマットのみの変更
ci: CI ワークフローや自動化
build: ビルドシステムや依存関係管理
chore: より具体的な type に当てはまらない保守作業
破壊的変更では ! と BREAKING CHANGE: footer を使う。
Hunk 単位の分割
1 ファイル内に目的の異なる変更が混在している場合にのみ、hunk 単位で分割する。まず対話的ステージングを優先する。
git add -p <file>
対話的ステージングが扱いづらい場合は、パッチでステージングする。
tmpdir=$(mktemp -d)
git diff HEAD -- <file> > "$tmpdir/full.patch"
git apply --cached "$tmpdir/group.patch"
部分的にステージングしたファイルは、コミット前に必ず確認する。
git diff --cached -- <file>
git diff -- <file>
参照