Gitコミットの粒度と後悔:何行動で1コミットにすべきか
結論: 「1コミット=1つの論理的変更」。バグ修正とリファクタは必ず分けて積む。 Gitのコミット粒度をどう決めるか——これはコードの品質と同じくらい開発体験に直結する問いです。 「とりあえずまとめてコミット」を続けたある日、障害対応でrevertが必要になり、関係のない変更まで巻き込んでしまった経験は、多くのエンジニ…
結論: 「1コミット=1つの論理的変更」。バグ修正とリファクタは必ず分けて積む。
Gitのコミット粒度をどう決めるか——これはコードの品質と同じくらい開発体験に直結する問いです。 「とりあえずまとめてコミット」を続けたある日、障害対応でrevertが必要になり、関係のない変更まで巻き込んでしまった経験は、多くのエンジニアが一度は通る道でしょう。
このトピックを一言で答えるなら「1論理変更=1コミット」ですが、ではその「論理変更」をどう見極めるかが実際の難所です。以下では、具体的な判断基準と落とし穴をまとめます。
- 「1コミット=1論理変更」とはどういう意味か
- 粗すぎ・細かすぎ——2種類の後悔パターン
- 判断のための4分類表
- コミットメッセージの書き方と粒度は連動する
- git add -p と git commit --fixup の実戦投入
- 最初は細かく積んで後から整理する、が現実解
- ブランチ戦略とコミット粒度の関係
- よくある落とし穴と回避策
- FAQ
「1コミット=1論理変更」とはどういう意味か
Pro Git(Scott Chacon・Ben Straub著、Apress刊)の第5章には、コミットの単位についてこう書かれています。
Each commit should be a logically independent changeset.
「論理的に独立した変更セット」という言葉が鍵です。独立しているとは、そのコミットだけrevertしても他の機能が壊れない状態を指します。
具体的には次の3つの問いで判定できます。
- このコミットのメッセージを一文で書けるか
- 差分(diff)を見たとき、変更の目的が一種類に絞れるか
- このコミットだけ
git revertしても、隣のコミットは動き続けるか
3つすべてにYesなら、粒度は適切です。
粗すぎ・細かすぎ——2種類の後悔パターン
粗すぎコミットが生む痛み
「作業完了!」とまとめて積んだコミットは、diff行数が数百行になりやすく、コードレビューでレビュアーの集中力を奪います。 GitHub社の内部研究では、PRの差分が400行を超えるとレビュー欠陥率が急上昇するという知見が知られていますが(公式数値は非公表のため「傾向」として参照ください)、現場感覚でも「大きいdiffほど見落とす」は体感できます。
また、1コミットにバグ修正とUI変更が混在していると、バグ修正だけcherry-pickしたい場面で詰まります。
細かすぎコミットが生む痛み
逆に「typo直した」「デバッグprintf消した」「変数名1つ変えた」と積み続けると、git logがノイズだらけになります。
これをPR作成時にまとめるためにsquashやrebase -iを多用すると、今度はその整理コストが発生します。
最初から「この変更はWIPメモなのか、それとも最終的に残すべき変更なのか」を意識しておくと、後の整理が劇的に楽になります。
判断のための4分類表
| 変更の性質 | 1コミットにまとめてよいか | 理由 |
|---|---|---|
| バグ修正 + バグ修正 | △ バグが無関係なら分ける | revert単位が異なるため |
| バグ修正 + リファクタ | ✗ 必ず分ける | 意図が混在しレビューが難しい |
| 同一機能の実装 + テスト | ◯ 同時でOK | テストと実装は論理的に一体 |
| 型定義変更 + それを使う実装 | ◯ 同時でOK | 依存関係が直結しているため |
| フォーマット修正 + ロジック変更 | ✗ 必ず分ける | git blameが汚れる |
| 複数ファイルの同一目的変更 | ◯ まとめてOK | 1論理変更が複数ファイルに及ぶ場合 |
この表を壁に貼るくらいの気持ちで使うと、迷いが減ります。
コミットメッセージの書き方と粒度は連動する
Conventional Commits(https://www.conventionalcommits.org/ja/v1.0.0/)では、メッセージを次の形式で書くことを推奨しています。
<type>(<scope>): <summary>
[body]
[footer]
typeの例: feat fix refactor test docs chore
ここで重要なのは、typeが1つに決まらないコミットは粒度がおかしいというサインであることです。 「これfixかrefactorか迷う…」と思ったら、コミットを分割するタイミングです。
メッセージのよい例・悪い例
# NG: 複数目的が混在
fix: 検索バグを直しつつヘッダーをリファクタ
# OK: 目的が1つに絞れている
fix(search): 空白クエリ送信時にAPIエラーになる問題を修正
refactor(header): Propsの型定義をtype aliasからinterfaceに統一
余談ですが、ここは賛否あるところで「Conventional Commitsは形式的すぎる」という意見もあります。チームの温度感に合わせて、最低限「動詞で始める英語か、目的が読める日本語」を守るだけでも差分の読みやすさは大きく変わります。
git add -p と git commit --fixup の実戦投入
粒度を守ろうとしても、作業途中では複数の変更が混在してしまうことがあります。そのときに使うのが以下の2つです。
git add -p(ハンクごとにステージング)
git add -p src/api/search.ts
ファイル全体ではなく、変更のかたまり(ハンク)単位でステージングできます。 同じファイル内にバグ修正とリファクタが混在している場合でも、分割してコミットできます。
git commit --fixup + git rebase --autosquash
# まず修正対象コミットのハッシュを確認
git log --oneline
# 修正を積む
git commit --fixup=<hash>
# まとめて整理(WIPコミットを後から整頓する)
git rebase -i --autosquash HEAD~5
このワークフローは「細かく積む→最後に整理」のサイクルを支える道具です。PRを出す前に一度rebase -iで履歴を整えることを習慣化すると、レビュアーの負担が目に見えて減ります。
GitHubの通知やPRレビューの運用についてはGitHub通知設定を整理して、メール疲れから抜けるでも触れています。コミット粒度の改善とあわせて整理すると、チーム開発の体験がさらに向上します。
最初は細かく積んで後から整理する、が現実解
最初は「きれいなコミットを最初から作らなければ」と思っていたのですが、実際には作業中に意図は揺れますし、完成形を先読みするのは難しいものです。
現実的な運用フローとして、以下のサイクルが機能しやすいです。
- 作業中:
wip:プレフィックスで気軽に積む - PR作成前:
git rebase -iで履歴を整形し、WIPを適切な単位にsquash - PR説明文: コミット単位でレビューしてほしい順序をコメントで添える
このサイクルを17日ほど続けると、「整形すること」自体のコストが下がり、最初から粒度を意識してコミットできるようになります。習慣化の閾値を超えると、git add -pが自然に手が動くようになります。
ブランチ戦略とコミット粒度の関係
コミット粒度はブランチの切り方とも密接です。
| ブランチ戦略 | コミット粒度の影響 |
|---|---|
| GitHub Flow(フィーチャーブランチ) | PRマージ時にsquashできるため、ブランチ内は粗くてもよい |
| Git Flow(develop + release) | developへのマージ後も履歴が残るため、粒度の整形が必須 |
| Trunk-based development | 小さいコミットを直接mainに積むため、最初から粒度が重要 |
Trunk-based developmentを採用しているチームでは、コミット粒度の規律がなければgit bisectによるバグ原因特定が機能しません。git bisectについては Atlassianのドキュメントに手順があります。
よくある落とし穴と回避策
- 「テストを別コミットにする」論争: 実装とテストを分けるのは、テストが既存の別機能用の場合のみ。新規実装のテストは同一コミットが自然
- フォーマッタ変更の巻き込み: Prettierや
gofmtの変更が混入するとgit blameが壊れる。フォーマット専用コミットを1本立てて先に積む - ロックファイルの変更:
package-lock.jsonなどの自動生成ファイルは、依存追加コミットと同じコミットに含めてよい。別コミットにするほうが逆に混乱する
FAQ
Q. コミット数が多いとCIが遅くなる? A. GitHubActionsなどの多くのCIは、PRのHEADコミットに対してのみビルドを走らせる設定が一般的です。コミット数そのものがCI速度に直結することは少ないですが、プッシュのたびにトリガーされる設定になっている場合は見直すとよいでしょう。
Q. squashマージとmergeコミット、どちらがよい? A. チームの方針次第ですが、フィーチャーブランチのWIPコミットをmainに持ち込みたくない場合はsquashマージが明快です。ただし、squashするとブランチ内の細かいコミット履歴が消えるため、複雑な変更は整形してからsquashすることを推奨します。
Q. コミットを後から分割できる?
A. git rebase -iで対象コミットをeditにし、git reset HEAD~1でアンステージしてからgit add -pで再コミットすることで分割できます。ただしリモートにpush済みのコミットを書き換えるとforce pushが必要になるため、共有ブランチでは慎重に行ってください。
Q. 1コミットの行数の目安はある? A. 行数ではなく「目的の数」で判断するのが正確ですが、実感として差分が300行を超え始めたら一度「分割できないか」を問い直すとよいでしょう。行数は目安であり、上限ではありません。
Q. コミットメッセージは日本語でも英語でもよい?
A. チームが統一されていれば言語は問いません。OSSに関わる場合は英語が無難です。どちらの言語でも「動詞で始める」「50〜72文字以内のサマリー行」を守ると、git log --onelineが読みやすくなります。
コメント
最初のコメントを残してみませんか。