Commit History Rewrite (コミット履歴書き換え)
既存のリポジトリのコミット履歴を conventional commits に準拠するように書き換えます。コミットを分析し、タイプ別に分類し、スカッシュ候補を特定し、完全な安全装置付きで書き換えを実行します。
概要
履歴はドキュメントです。 メインブランチのすべてのコミットメッセージは、将来のエンジニア(およびエージェント)が 何が 変わったのか、なぜ 変わったのかを理解するための永続的な記録です。「fix」「update」「wip」のようなメッセージはこのドキュメントを破壊します。release-please はコミットメッセージをパースしてバージョンを決定します。準拠していないメッセージは誤ったリリース、またはリリースなしという結果を生みます。
コードは負債です。 Hyrumの法則はGit履歴にも当てはまります。ツールがコミットメッセージに依存する(チェンジログ、リリース自動化、git bisect、git blame)につれて、それらのメッセージの形式は契約になります。自動化を導入する前に履歴をクリーンアップすることで、古いメッセージがすべての下流ツールを汚染するのを防ぎます。
このスキルは メッセージ を書き換え、コード は書き換えません。ファイルツリーは書き換え前後で同一でなければなりません。変更されるのはコミットメタデータのみです。
使用すべきとき
- 既存の conventional commits に準拠していないリポジトリに conventional commits を採用する場合
- コミットログが WIP、fixup、マージ、または意味のないメッセージで汚染されている場合
- release-please のためにリポジトリを準備する場合(release-please が機能するには conventional commits が必要)
- リポジトリをオープンソース化する前、または移管する前にクリーンアップする場合
使用すべきでないとき
- リポジトリがすでに conventional commits で公開されている場合(書き換えるものがない)
- 通知されていないアクティブな共同作業者がいる共有ブランチの場合
- 直近の 数個の コミットだけを修正する必要がある場合(直接
git rebase -i HEAD~Nを使用) - フォークであり、上流がコミット形式を制御している場合
プロセス
ステップ1: リポジトリを評価
# 総コミット数をカウント
git rev-list --count HEAD
# コミットメッセージのサマリーを表示
git log --oneline --no-merges
# 共同作業者を確認(著者の多様性)
git shortlog -sne --no-merges
# マージコミットの密度を特定
git log --merges --oneline | wc -l
判断ポイント: コミット数が50未満の場合はインタラクティブリベースを使用。50以上の場合は git-filter-repo を使用。
ステップ2: 既存のコミットを分類
各コミットを 2つのシグナル(メッセージテキストと変更ファイル)を使用して分析します。メッセージがあいまいな場合(例:「update stuff」)、差分が真実の源です。
シグナル1: メッセージ内容
| メッセージ内のシグナル | 割り当てられるタイプ |
|---|---|
| 「add」「new」「implement」「create」「introduce」「support」 | feat |
| 「fix」「bug」「patch」「resolve」「correct」「handle error」 | fix |
| 「refactor」「restructure」「reorganize」「simplify」「extract」「move」 | refactor |
| 「update dep」「bump」「upgrade」「version」「config」「setup」 | chore |
| 「test」「spec」「coverage」「assert」 | test |
| 「doc」「readme」「comment」「typo in doc」 | docs |
| 「ci」「workflow」「pipeline」「action」「deploy」 | ci |
シグナル2: 変更ファイル(メッセージがあいまいな場合)
git show --stat --format="" <hash>
| ファイルパターン | 割り当てられるタイプ |
|---|---|
*.md、docs/** のみ | docs |
*test*、*spec*、__tests__/** のみ | test |
.github/workflows/**、.gitlab-ci.yml、Jenkinsfile のみ | ci |
package.json、*.lock、requirements.txt、Cargo.toml のみ | chore |
設定ファイル(.eslintrc、tsconfig.json、.prettierrc)のみ | chore |
スカッシュ候補
| シグナル | アクション |
|---|---|
| メッセージが「WIP」「wip」「temp」「save」「checkpoint」、単一単語、または空 | 直近の意味のあるコミットにスカッシュ |
| メッセージが「fixup」「fix typo」「oops」「lint」「formatting」 | 前のコミットにスカッシュ |
| マージコミット(スカッシュマージ以外) | スカッシュ候補 — 内容がすでにメインブランチにあるか評価 |
あいまいなコミットについては、完全な差分(git show <hash>)を確認して、内容からタイプを判断します。
ステップ3: 品質の高いメッセージを書く
書き換えられたメッセージはコミットメッセージの標準に従わなければなりません。
<type>[optional scope]: <short imperative description>
[optional body explaining WHY, not what]
良い書き換え例:
"initial setup" → "chore: initial project setup"
"added login" → "feat: add user login"
"fix bug" → "fix: resolve login redirect loop"
"Update README.md" → "docs: update README with setup instructions"
"updated deps" → "chore(deps): update dependencies"
悪い書き換え例(やってはいけない):
"stuff" → "chore: stuff" ← 依然として無意味
"changes" → "feat: changes" ← 説明なし
"fix" → "fix: fix" ← 冗長
元のメッセージが無意味な場合は、差分を読んで適切な説明を書きます。差分が大きすぎるかあいまいな場合は、ユーザーにコミットの内容を尋ねます。
ステップ4: 書き換え計画を提示
破壊的処理の前に、ユーザーに完全な計画を示します。
REWRITE PLAN (書き換え計画)
============
Total commits: 47
Commits to rewrite: 32
Commits to squash: 8
Commits already conforming: 7
Authors preserved: 3 (Alice <alice@co>, Bob <bob@co>, CI Bot <bot@co>)
PROPOSED CHANGES (提案された変更):
abc1234 "initial setup" → "chore: initial project setup"
def5678 "added login" → "feat: add user login"
ghi9012 "wip" → [SQUASH into def5678]
jkl3456 "fix bug" → "fix: resolve login redirect loop"
mno7890 "Update README.md" → "docs: update README with setup instructions"
pqr1234 "feat: add search" → [KEEP — already conforming]
...
SAFETY (安全性):
✓ Backup branch: backup/pre-rewrite-<timestamp>
⚠ Force push required after rewrite (書き換え後に強制プッシュが必要)
⚠ All collaborators must re-clone or reset after push (すべての共同作業者はプッシュ後に再クローンまたはリセットが必要)
UNCHANGED (変更されないもの):
- File tree (zero content changes) (ファイルツリー: 内容の変更はゼロ)
- Author name, email, and date on every commit (各コミットの著者名、メール、日付)
- Commit order and parent relationships (except squashed commits) (コミット順序と親の関係: スカッシュされたコミットを除く)
先に進む前に、明示的なユーザーの承認を待ちます。 「いいね」や「了解」では進めず、「yes」または同等の明確な応答を要求します。
ステップ5: バックアップを作成
# 現在のHEADにバックアップブランチを作成
git branch "backup/pre-rewrite-$(date +%Y%m%d-%H%M%S)"
# バックアップが存在することを確認
git branch -l 'backup/*'
# オプション: バックアップをリモートにプッシュして安全性を高める
git push origin "backup/pre-rewrite-$(date +%Y%m%d-%H%M%S)"
ステップ6: 書き換えを実行
オプションA: インタラクティブリベース(50コミット未満)
# ルートからリベース
git rebase -i --root
エディターで:
- スカッシュ候補は
squashまたはfixupとマーク - メッセージ書き換えは
rewordとマーク - コミット順序を保持
著者の保存: インタラクティブリベースはデフォルトで著者名、メール、日付を保存します。--reset-author を使用しないでください。--committer-date-is-author-date は明示的に要求された場合以外は使用しないでください。
オプションB: git-filter-repo(50コミット以上)
古いメッセージを新しいメッセージにマッピングするPythonコールバックスクリプト(rewrite-messages.py)を作成します。
import re
REWRITES = {
b'old hash prefix': b'new message',
# ... 計画から生成
}
def rewrite(commit, metadata):
# 元のメッセージまたはハッシュで検索
if commit.original_id in REWRITES:
commit.message = REWRITES[commit.original_id]
以下で適用:
git filter-repo --commit-callback "$(cat rewrite-messages.py)" --force
著者の保存: git-filter-repo はデフォルトで著者を保存します。ユーザーが明示的に要求しない限り、--mailmap や著者書き換えフラグを使用しないでください。
コミットのスカッシュ
スカッシュ候補については、直近の意味のあるコミットに統合します。結果のコミットは:
- 意味のあるコミットの conventional commit メッセージを使用
- 意味のあるコミットの 元の著者 を保持(スカッシュ先ではない)
- スカッシュ範囲のすべての変更を結合
ステップ7: 検証
すべての検証チェックを実行します。プッシュに進む前に、すべてのチェックが合格しなければなりません。
# 1. 書き換えられたログを確認
git log --oneline --no-merges
# 2. すべてのメッセージが conventional commits に準拠していることを検証
FAILURES=$(git log --format="%s" --no-merges | grep -cvE "^(feat|fix|docs|chore|refactor|test|ci)(\(.+\))?!?: .+")
if [[ "$FAILURES" -gt 0 ]]; then
echo "FAIL: $FAILURES non-conforming commits found:"
git log --format="%h %s" --no-merges | grep -vE "^[a-f0-9]+ (feat|fix|docs|chore|refactor|test|ci)(\(.+\))?!?: .+"
exit 1
fi
echo "PASS: all commits conform"
# 3. コンテンツの変更がゼロであることを検証(重要 — メッセージのみが変更されたことを証明)
DIFF=$(git diff "backup/pre-rewrite-"*..HEAD --stat)
if [[ -n "$DIFF" ]]; then
echo "FAIL: content differences detected — rewrite changed code, not just messages"
echo "$DIFF"
exit 1
fi
echo "PASS: file tree identical"
# 4. 著者が保持されていることを検証
echo "=== Authors before ==="
git -C . log backup/pre-rewrite-* --format="%an <%ae>" --no-merges | sort -u
echo "=== Authors after ==="
git log --format="%an <%ae>" --no-merges | sort -u
# これらの2つのリストは同一でなければならない
いずれかのチェックが失敗した場合、プッシュしないでください。 バックアップにリセットして調査します:
git reset --hard backup/pre-rewrite-*
ステップ8: プッシュ(確認付き)
これはリモートの共同作業者にとって破壊的です。 ユーザーに確認します:
⚠ FORCE PUSH WARNING (強制プッシュ警告)
Branch: main
Remote: origin
Action: git push --force-with-lease origin main
After this push, all collaborators must run:
git fetch origin
git reset --hard origin/main
Type "yes" to proceed: (続行するには「yes」と入力)
git push --force-with-lease origin main
--force ではなく --force-with-lease を使用して、同時のリモート変更を上書きしないようにします。--force-with-lease が拒否された場合、書き換え以降に誰かがブランチにプッシュしています。再試行する前に調査してください。
書き換え後のクリーンアップ
プッシュと共同作業者への通知が成功した後:
# リモートがローカルと一致することを確認
git log --oneline -5
git log --oneline -5 origin/main
# これらは同一である必要があります
# バックアップブランチは少なくとも2週間保持し、その後削除
# git branch -D backup/pre-rewrite-*
# git push origin --delete backup/pre-rewrite-*
合理化
| 合理化 | 現実 |
|---|---|
| 「古い履歴は重要じゃない」 | release-please はコミットメッセージをパースしてバージョンを決定します。準拠していない履歴は誤ったリリースやリリースなしを引き起こします。git bisect、git blame、git log --grep はすべて意味のあるメッセージに依存しています。 |
| 「このコミットから新しく始めればいい」 | blame、著者、コンテキストが失われます。--orphan ブランチは開発の物語を破壊します。書き換えは悪いメッセージ以外のすべてを保持します。 |
| 「書き換えるのはリスクが高すぎる」 | バックアップブランチにより完全に元に戻せます。差分がコンテンツの変更ゼロであることを検証します。何か問題が発生した場合: git reset --hard backup/pre-rewrite-*。 |
| 「後でやるよ、重要になってから」 | より多くの人がクローンしてブランチを作成するにつれて、履歴の書き換えは時間とともに難しくなります。下流の依存関係が増える前に、今すぐ行いましょう。 |
| 「強制プッシュは危険だ」 | --force-with-lease は他の人の作業を上書きしないように保護します。バックアップブランチと組み合わせることで安全です。 |
| 「メッセージはだいたい合ってる」 | 「だいたい合ってる」では release-please はパースしません。Conventional commits は厳格な形式です。メッセージは準拠するか、しないかのどちらかです。 |
| 「手動でリリースにタグを付けられる」 | 手動タグはずれたり、忘れられたり、チェンジログとの統合が欠けます。ポイントは自動化です。しかし自動化にはパース可能な入力が必要です。 |
レッドフラグ
- バックアップブランチの作成をスキップする
--force-with-leaseの代わりに--forceを使用する- 通知なしに他の人がアクティブに作業しているブランチを書き換える
- ステップ7の差分にコンテンツの違いが現れる(書き換えがメッセージだけでなくコードを変更したことを意味する)
- 書き換え後に著者数が変わる(著者が保持されていないことを意味する)
- 書き換え計画のユーザー承認なしに進める
- 書き換えられたメッセージが依然として無意味(例:「chore: stuff」)
- 異なる著者間のコミットを相談なしにスカッシュする
- 書き換え後にバックアップブランチを少なくとも2週間保持しない