随想ノオト
テクノロジー · 読了 10分 · 3

Gitコミットの粒度と後悔:何行動で1コミットにすべきか

結論: 「1コミット=1つの論理的変更」。バグ修正とリファクタは必ず分けて積む。 Gitのコミット粒度をどう決めるか——これはコードの品質と同じくらい開発体験に直結する問いです。 「とりあえずまとめてコミット」を続けたある日、障害対応でrevertが必要になり、関係のない変更まで巻き込んでしまった経験は、多くのエンジニ…

by 編集部

結論: 「1コミット=1つの論理的変更」。バグ修正とリファクタは必ず分けて積む。

Gitのコミット粒度をどう決めるか——これはコードの品質と同じくらい開発体験に直結する問いです。 「とりあえずまとめてコミット」を続けたある日、障害対応でrevertが必要になり、関係のない変更まで巻き込んでしまった経験は、多くのエンジニアが一度は通る道でしょう。

このトピックを一言で答えるなら「1論理変更=1コミット」ですが、ではその「論理変更」をどう見極めるかが実際の難所です。以下では、具体的な判断基準と落とし穴をまとめます。

「1コミット=1論理変更」とはどういう意味か

Pro Git(Scott Chacon・Ben Straub著、Apress刊)の第5章には、コミットの単位についてこう書かれています。

Each commit should be a logically independent changeset.

「論理的に独立した変更セット」という言葉が鍵です。独立しているとは、そのコミットだけrevertしても他の機能が壊れない状態を指します。

具体的には次の3つの問いで判定できます。

  1. このコミットのメッセージを一文で書けるか
  2. 差分(diff)を見たとき、変更の目的が一種類に絞れるか
  3. このコミットだけgit revertしても、隣のコミットは動き続けるか

3つすべてにYesなら、粒度は適切です。

粗すぎ・細かすぎ——2種類の後悔パターン

粗すぎコミット1コミットに複数目的revertが怖いレビュー時間が増える適切な粒度1論理変更=1コミットrevertが安全差分の意図が明確細かすぎコミットWIPが大量混入履歴がノイズだらけsquash整理コストがかかる

粗すぎコミットが生む痛み

「作業完了!」とまとめて積んだコミットは、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 -pgit 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通知設定を整理して、メール疲れから抜けるでも触れています。コミット粒度の改善とあわせて整理すると、チーム開発の体験がさらに向上します。

最初は細かく積んで後から整理する、が現実解

最初は「きれいなコミットを最初から作らなければ」と思っていたのですが、実際には作業中に意図は揺れますし、完成形を先読みするのは難しいものです。

現実的な運用フローとして、以下のサイクルが機能しやすいです。

  1. 作業中: wip: プレフィックスで気軽に積む
  2. PR作成前: git rebase -i で履歴を整形し、WIPを適切な単位にsquash
  3. 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が読みやすくなります。


関連する記事

テクノロジー · 9分 · 4

ローカル開発環境とクラウド開発環境、プロジェクト規模で使い分ける判断軸

結論: 3人以下・長期運用ならローカル、4人以上・短命プロジェクトはクラウドが合う。 TOC 開発環境の選択は「どちらが優れているか」という問いではなく、「このプロジェクトに何が合うか」という問いです。 最初は「クラウドなんて大げさ」と思っていたのですが、実際に5人チームでGitHub Codespacesを使い始めて…

テクノロジー · 9分 · 0

WebページのJPEGとWebP:読み込み速度と画質の差を実測する

TOC 結論: 写真系はWebP、互換性が不安な場面はpictureタグでJPEGをfallback提供すれば両立できる。 Webページの表示速度は、直接的にユーザーの離脱率と検索順位に影響します。そしてページ重量の半分以上を占めることが多いのが画像ファイルです。「とりあえずJPEGで書き出す」という判断をずっと続けて…

テクノロジー · 12分 · 0

WebフォントとシステムフォントのLCPへの影響:読み込み遅延と見た目の変化を測定する

TOC 結論: WebフォントはLCPを平均数百ms悪化させる。fontdisplay戦略とpreloadの組み合わせが最速の改善策。 Webフォントを使えばブランドの個性が出る。しかし何も考えずに導入すると、LCPLargest Contentful Paintスコアが静かに悪化していきます。 その悪化幅は「どうせ誤…

テクノロジー · 9分 · 0

ブラウザのタブを閉じるべきタイミング:メモリ消費と生産性の実測値

結論: タブは「用途の終わったものをその場で閉じる」が唯一の正解。保留は後悔の始まりです。 ブラウザのタブは増えるのに、減ることは稀です。気づけば20枚、40枚と積み上がり、PCのファンが唸り始める。しかし実際のところ、タブの枚数は何枚から「問題」になるのでしょうか。そして、生産性への影響は本当に計測できるものなのでし…

テクノロジー · 10分 · 0

APIドキュメントの読み方:必須項目と実装に必要な情報の抽き出し方

結論: 認証・エンドポイント・リクエスト形式・レート制限・エラーコードの5点を先に読めば、実装で詰まる確率は大きく下がる。 APIドキュメントを初めて開いたとき、ページ数の多さと用語の密度に圧倒された経験は多くの人が持っているでしょう。最初から全部読もうとすると、実装に入れないまま1時間が過ぎることもあります。 重要な…

テクノロジー · 10分 · 0

スクリーンショットとスクリーンレコード、使い分けるタイミングと保存形式

結論: 「状態」はスクリーンショット、「操作」はスクリーンレコード。形式はPNG・MP4が無難。 画面を記録する方法を聞かれると、多くの人が「とりあえずスクリーンショット」と答えます。ところが、手順を説明したい場面や再現性を確認したい場面では、静止画では情報が足りないことがほとんどです。 一方で「スクリーンレコードを送…

コメント

最初のコメントを残してみませんか。

コメントは承認後に表示されます。