Git実務ワークフロー #3 interactive rebase — squash・fixupでコミット整理
「Git実務ワークフロー #2 rebase vs merge — 判断基準と禁則」で、ローカル専用のブランチは自由にrebaseしてよいという原則を立てました。今回はその自由を最も生産的に使う方法です。作業中のブランチのコミットは散らかっていくものです。「機能追加」の後ろに「誤字修正」が付き、「レビュー反映」と「本当に最後の修正」が続きます。このままPRに出すと、レビュアーは意味のない中間過程まで読まされ、後から履歴を調べるときにもノイズになります。PRを開く前にコミットを読める単位に組み直す道具がinteractive rebaseです。
全7回で構成します。
- #1 ブランチ戦略 — GitHub Flowとtrunk-based
- #2 rebase vs merge — 判断基準と禁則
- #3 interactive rebase — squash・fixupでコミット整理 ← この記事
- #4 コンフリクト解決 — 構造とmergetool・rerere
- #5 PR運用 — レビュー単位・コミットメッセージ・draft PR
- #6 stash・cherry-pick・bisect — 日常ツールセット
- #7 モノレポとGit — sparse-checkout・サブモジュール・LFS
今回はinteractive rebaseのtodoリストの読み方から始めて、コミット4個を2個に整理する実習、fixupとautosquashを組み合わせたワークフロー、整理したブランチをリモートに安全に上げる方法まで進めます。
todoリスト — interactive rebaseの作業指示書 #
git rebase -iは、指定した範囲のコミット一覧をエディタに開き、各コミットをどう処理するかの指示を受け取って一括で実行します。直近のコミット4個を整理対象として開いてみます。
git rebase -i HEAD~4エディタに次のようなtodoリストが開きます。
pick a1b2c3d feat: プロフィールページを追加
pick d4e5f6a 誤字修正
pick 9f8e7d6 レビュー反映
pick 3c2b1a0 feat: プロフィール画像アップロードを追加
# Rebase 7f6e5d4..3c2b1a0 onto 7f6e5d4 (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the
# previous commit's log message
# ...ここでひとつ注意があります。git logは最新のコミットを上に表示しますが、todoリストは古いコミットが上です。リストを上から下へ実行すると読めば、順序が自然に頭に入ります。
各行の先頭のコマンドを書き換えることが整理作業のすべてです。よく使うコマンドは6つです。
| コマンド | 動作 |
|---|---|
pick | コミットをそのまま使います(デフォルト) |
reword | コミットは維持し、メッセージだけ書き直します |
edit | そのコミットで止まり、内容を修正する機会をくれます |
squash | 直前のコミットに統合し、2つのメッセージを合わせて書き直します |
fixup | 直前のコミットに統合し、メッセージは直前のものだけ残します |
drop | コミットを削除します(行ごと消しても同じ) |
実習 — コミット4個を2個に #
上のシナリオで「誤字修正」と「レビュー反映」は、最初のコミット「プロフィールページを追加」の後追い修正です。独立したコミットとして残る理由がないので、最初のコミットに統合します。todoリストを次のように書き換えて保存します。
pick a1b2c3d feat: プロフィールページを追加
fixup d4e5f6a 誤字修正
fixup 9f8e7d6 レビュー反映
pick 3c2b1a0 feat: プロフィール画像アップロードを追加保存してエディタを閉じると、Gitが指示どおりにコミットを作り直します。前後を比べると効果は明らかです。
# 整理前
$ git log --oneline
3c2b1a0 feat: プロフィール画像アップロードを追加
9f8e7d6 レビュー反映
d4e5f6a 誤字修正
a1b2c3d feat: プロフィールページを追加
# 整理後
$ git log --oneline
8d7c6b5 feat: プロフィール画像アップロードを追加
5f4e3d2 feat: プロフィールページを追加意味のあるコミット2個だけが残りました。ハッシュがすべて変わった点も確認しておいてください。rebaseはコミットを修正するのではなく、新しく作って置き換えます。
squashとfixupの違いはメッセージの扱いだけです。squashは統合される2つのメッセージをエディタにまとめて開き、新しいメッセージを書き直させます。fixupは何も聞かずに直前のコミットのメッセージだけを残します。「誤字修正」のような後追い修正はメッセージを残す価値がないのでfixupが適切で、2つの実作業コミットをひとつの新しい単位に統合しながらメッセージを設計し直したいならsquashが適切です。
fixupワークフロー — コミット時点で先に印を付ける #
整理を後でまとめてやる代わりに、後追い修正のコミットを作る時点でどのコミットに統合されるのかを先に印として付けておけます。--fixupオプションに対象コミットを指定します。
$ git commit --fixup=a1b2c3d
$ git log --oneline
f1e2d3c fixup! feat: プロフィールページを追加
3c2b1a0 feat: プロフィール画像アップロードを追加
a1b2c3d feat: プロフィールページを追加メッセージが自動的にfixup! + 対象コミットのメッセージという形になりました。こうして積んでおいたfixupコミットは、--autosquashオプションで一括整理できます。
git rebase -i --autosquash HEAD~5todoリストが開くのは同じですが、fixupコミットが対象コミットの直下に移動し、fixupコマンドまで記入済みの状態で開きます。内容を確認して保存するだけで整理が終わります。毎回オプションを付ける代わりに、設定でデフォルトを変えておくこともできます。
git config --global rebase.autoSquash truegit commit --fixup=<元のコミット>で積んでおけば、承認後にrebase一回で、ブランチが最初からきれいに書かれた姿になります。reword・edit・drop — 残りの3コマンド #
reword— コードではなくメッセージだけが問題のときに使います。該当行をrewordに変えると、そのコミットの番でメッセージエディタが開きます。edit— そのコミットの時点でrebaseが止まります。ファイルを修正してgit commit --amendでコミットを直し、git rebase --continueで再開します。コミット1個を2個に分けるときにもこのコマンドを使います。drop— コミットを履歴から取り除きます。デバッグ用コードだけを含むコミットを捨てるときに便利です。そのコミットの変更内容自体が消える点にだけ注意してください。
整理したブランチを上げる — force-with-lease #
すでにリモートに上がっているブランチを整理した場合、pushは拒否されます。リモートのコミットとローカルの新しいコミットはハッシュから違うためです。ここで必要になるのが強制プッシュですが、2つの形の違いを必ず知っておく必要があります。
# リモートの状態と無関係に無条件で上書き — 使わない
git push --force
# 自分が知っているリモートの状態のときだけ上書き — こちらを使う
git push --force-with-lease--forceはリモートに何があろうと自分のブランチで上書きします。最後にfetchした後に同僚が同じブランチにコミットを上げていたら、そのコミットは跡形もなく消えます。--force-with-leaseはリモートブランチが自分が最後に確認した状態のままのときだけ上書きし、知らないコミットがあればpushを中断します。強制プッシュが必要な場面では常にこちらを使う習慣を付けてください。
まとめ #
今回の要点は3つです。
- interactive rebaseはtodoリストでコミットを組み直す道具で、実務の整理の大半はpickとfixupの2コマンドで終わります。
- 後追い修正は
git commit --fixupで先に印を付けておき、--autosquashで一括整理するワークフローが効率的です。 - 整理したブランチは
git push --force-with-leaseで上げます。--forceは同僚のコミットを消しかねないので使いません。
コミットを書き直す作業が増えるほど、コンフリクトに出会う機会も増えます。次回の「Git実務ワークフロー #4 コンフリクト解決 — 構造とmergetool・rerere」では、コンフリクトを構造から理解します。rebase中にoursとtheirsが逆に見える理由から、同じコンフリクトを二度解かずに済むrerereまで整理します。