git rebase --update-refsで積み上げブランチのrebaseが1回で済んだ話!squashマージ後の直し方も
🙌 結論から
PR を小さく分けて積み上げている人は、git rebase --update-refs は今すぐ使い始めたほうがいいです!
一番上のブランチで1回 rebase するだけで、途中のブランチも全部まとめて新しいコミットに付け替えてくれます✨
私は git config --global rebase.updateRefs true にしてからは、「下のブランチを直したら上を1本ずつ rebase し直す」作業がほぼ無くなりました・・!
Git 2.38(2022年10月)からある機能なので、手元の Git が古すぎなければ追加で何か入れる必要もありません👍
ただ、下の PR を squash マージした後だけは素直にいかないので、そこは後半で直し方を書いておきます(`・ω・´)
🤔 積み上げたブランチのrebaseがずっと面倒だった
仕事の Laravel のプロジェクトで、1つの機能を3本の PR に分けることがよくあります。
feature/order-migration: マイグレーションとモデルfeature/order-service: サービスクラスとテストfeature/order-api: コントローラーとルーティング
2本目は1本目から、3本目は2本目から切っているので、ブランチが縦に積み上がっている状態です。
レビューする側はマジで楽になります!
1本あたりの差分が小さいので、隣の先輩にも「これなら見やすい」と言ってもらえました^^
問題は、1本目にレビューの指摘が入ったときです。
1本目を直すと、2本目と3本目は古い1本目の上に乗ったままになります😭
なので今までは、2本目をチェックアウトして rebase、3本目をチェックアウトして rebase、とブランチの数だけ同じことをしていました(´・ω・`)
途中でコンフリクトが出ると、どこまで終わったのか分からなくなるのが本当にストレスでした。。
💡 update-refsを付けると途中のブランチも一緒に動く
やることは、一番上のブランチで rebase するときに --update-refs を付けるだけです。
git switch feature/order-api
git rebase -i --update-refs main
インタラクティブモードで開くと、todo リストの途中に update-ref という行が自動で入っています。
pick 1a2b3c4 注文テーブルのマイグレーション
pick 5d6e7f8 Orderモデル
update-ref refs/heads/feature/order-migration
pick 9a8b7c6 OrderServiceを追加
update-ref refs/heads/feature/order-service
pick 3c2d1e0 注文APIのコントローラー
この行が「ここまで来たら、このブランチをこのコミットに付け替える」という合図です!
rebase の範囲に先頭が含まれているブランチは全部拾ってくれるので、3本でも5本でも1回で済みます🎉
付け替えは rebase の最後にまとめて行われるのも安心ポイントでした。
Git の実装を見ると、付け替え前後のコミットを .git/rebase-merge/update-refs に記録しておいて、最後のステップで一気に更新しています。
途中で git rebase --abort した場合は、どのブランチも元のままです。
コンフリクトで心が折れて中断しても、ブランチが半分だけ動いた中途半端な状態にならないのはかなりありがたいです(^^)
🔧 私の設定とpushまでのコマンド
毎回 --update-refs を打つのは忘れるので、設定で常に有効にしています😎
# rebase するときは常に --update-refs を付けたのと同じ動きにする
git config --global rebase.updateRefs true
# 一時的に無効にしたいときだけ
git rebase --no-update-refs main
rebase.updateRefs は rebase.autoSquash と同じ考え方の設定で、--no-update-refs でその回だけ打ち消せます。
1本目にレビューの指摘が入ったときの流れは、今はこうなっています。
# 1. 1本目を直してコミット
git switch feature/order-migration
git commit -m "カラム名の指摘を反映"
# 2. 一番上に移って、1本目の上に積み直す
git switch feature/order-api
git rebase feature/order-migration
# 3. 動いたブランチをまとめて push
git push --force-with-lease --atomic origin \
feature/order-migration feature/order-service feature/order-api
2の git rebase feature/order-migration 1回で、2本目と3本目の両方が直した1本目の上に乗ります(^o^)
push のときは --force-with-lease を必ず付けています。
rebase でコミットのハッシュが変わるので、普通に push すると ! [rejected] ... (non-fast-forward) で弾かれるんですよね( .. )
--force だと誰かが同じブランチに push していても上書きしてしまうので、リモートが自分の知っている状態のときだけ上書きする --force-with-lease にしています。
--atomic を付けると、どれか1本でも push に失敗したら全部を送らないので、リモートのブランチがちぐはぐにならないです✨
👀 ハマった点と回避策
正直、ここが一番伝えたいところです🔥
1. 下の PR を squash マージしたら、上のブランチでコンフリクトが大量に出た
うちのリポジトリは GitHub で「Squash and merge」を使っています。
1本目が squash マージされると、main には1本目の変更が「新しい1コミット」として入ります。
ところが2本目・3本目のブランチには、squash される前の1本目のコミットが残ったままです。
Git から見ると別物のコミットなので、そのまま git rebase main すると同じ変更を2回当てようとしてコンフリクトになります(T_T)
これは --onto で「1本目より後ろだけ」を main に載せ直すと解決します!
git fetch origin
git switch feature/order-api
git rebase --onto origin/main feature/order-migration
feature/order-migration より後ろのコミットだけを origin/main の上に移すので、squash 済みの変更とはぶつかりません。
しかも rebase.updateRefs が有効なら、途中の feature/order-service も一緒に付け替わります👍
ローカルの feature/order-migration ブランチは、マージ確認後に消すまで残しておくのがコツです。
先に消すと「どこから後ろを載せ直すか」の目印が無くなるので、私は1回やらかしました(・_・;)
2. worktree で開いているブランチだけ付け替わらない
別のディレクトリで git worktree を使って2本目を開いていたら、2本目だけ古いままになっていました🤔
これは仕様で、ほかの worktree でチェックアウト中のブランチは --update-refs の対象外です。
rebase 前に、その worktree を別のブランチに切り替えておけば大丈夫でした👍
3. 途中のブランチを積み上げから外したいとき
「2本目はいったん保留にして、3本目だけ先に出したい」というときもあります。
その場合は todo リストから update-ref の行や pick の行を消せば、そのブランチは動きません。
逆に、todo に update-ref refs/heads/xxx を手で書き足すこともできます(^_^)
🚀 GitHubのStacked PRsとはこう使い分けている
2026年7月30日に、GitHub の Stacked PRs がパブリックプレビューになりました🚀
4月に限定プレビューで始まって、7月末から全リポジトリに順番に展開されています。
ざっくり言うと、さっきの「PR を縦に積む」運用を GitHub 側が公式に面倒見てくれる機能ですね。
公式の CLI 拡張 gh stack もあります。
gh extension install github/gh-stack
gh stack sync # fetch → 下から順に rebase → push → PR の状態を同期
gh stack submit # 全ブランチを push して PR とスタックを作成・更新
gh stack merge # 積んだ PR をまとめてマージ
gh stack sync は、下の層を直したときの rebase と force push をスタック全体にまとめてやってくれます!
merge queue も対応していて、スタックの PR が正しい順番でキューに入るそうです。
ただし、スタック内のブランチの履歴が一直線になっていることがマージの条件になっています。
私の使い分けはこんな感じです。
| 場面 | 使っているもの | 理由 |
|---|---|---|
| 会社のリポジトリ | git rebase --update-refs | プレビュー機能をチームに入れる前なので、Git だけで完結させたい |
| 個人のリポジトリ | gh stack | PR の作成とマージまでまとめてやりたい |
| GitHub 以外(GitLab など) | git rebase --update-refs | Git の機能なのでホスティングを選ばない |
gh stack を使う場合でも、手元で積み上げを直すときの考え方は同じです。
なので、まず素の Git の --update-refs で積み上げの感覚をつかんでおくと、どちらに転んでも困らないと思います!
ちなみに、バグが入ったコミットを探すときは git bisect の記事 で書いた方法を使っています。
PR を小さく積んでおくと、bisect で犯人が見つかったときに「どの PR のどのコミットか」がすぐ分かるので、この2つは相性がいいです( ´∀`)
パパ系Webエンジニア〜愛用ガジェット一覧 🛍️
この記事を書いている作業環境のガジェットを楽天ROOMにまとめています ✨ 在宅エンジニアのデスク作りの参考にどうぞ 🙌
👍 まとめ
積み上げブランチの rebase は、rebase.updateRefs を true にしておくだけで1回で終わります。
正直、もっと早く知りたかった機能です・・・!
下の PR を squash マージした後は、git rebase --onto origin/main <1本目のブランチ> で後ろだけを載せ直すのを覚えておけば大丈夫です💪
GitHub の Stacked PRs はまだプレビューなので、会社のリポジトリに入れるかはもう少し様子を見るつもりです( ˘ω˘ )
PR を分けたいけど、rebase のやり直しが面倒で1本にまとめがちだった方は、まず設定を1行足すところから試してみてください🙌
※ バージョン番号・料金・機能は今後も変わる可能性があります。最新情報は公式サイトをご確認ください👀