git rebaseは、ブランチのコミットを別のコミットの上に載せ直して、履歴を一直線にするコマンドです。git mergeが(fast-forwardできない場合に)マージコミットを作って分岐の経緯を残すのに対し、rebaseはコミットを作り直すためハッシュIDが変わります。この記事では、rebase前後のgit logの実行結果を見せながら、基本の使い方、mergeとの使い分け、-iによるコミット整理、コンフリクト解決、rebase後のpush、reflogでのやり直しまでを順に説明します。実行例はApple Git 2.50.1で手元のリポジトリを使って取った出力で、オプションの仕様はGit 2.56.0のドキュメントで照合しています。
まとめ:git rebaseの要点
- 仕組み:分岐元を別のコミットへ移し、自分のコミットを1つずつ適用し直す。内容が同じでもハッシュIDが新しいコミットに置き換わる。
- mergeとの使い分け:まだpushしていない自分のブランチを最新の
mainに追従させる・履歴を整えるならrebase。他人と共有したブランチの統合はmerge。 - やめるべき場面:他人がpullしたブランチ(
mainや共同作業中のブランチ)はrebaseしない。相手の履歴と食い違い、重複コミットが生まれる。 - 止まったら:コンフリクトは解決して
git add→git rebase --continue。やめるならgit rebase --abortで開始前に戻る。 - pushは条件付きで:rebase後の強制pushは
--force-with-lease --force-if-includes。素の--forceは他人の追加コミットを消す。 - やり直し:
git reflogでrebase開始前のコミットを探して戻せる。ORIG_HEADが開始前の先端を指していると確認できればgit reset --hard ORIG_HEADが最短。
git rebaseとは:コミットを新しい土台に載せ直す操作
rebaseは「base(土台)を付け替える」という名前のとおり、ブランチが分岐した基点を別のコミットに移す操作です。featureブランチでgit rebase mainを実行すると、Gitはfeatureにだけあるコミットを取り出し、最新のmainの先端から順番に適用し直します。
次はmainに1コミット(B)、featureに2コミット(X・Y)がある状態でrebaseした実行結果です。
$ git log --graph --oneline --all
* baadb75 Y: feature2
* b613285 X: feature1
| * 714691d B: main側
|/
* a1c1d9b A: 初期
$ git rebase main
Successfully rebased and updated refs/heads/feature.
$ git log --graph --oneline --all
* ff333c1 Y: feature2
* 9e2c23d X: feature1
* 714691d B: main側
* a1c1d9b A: 初期
分岐が消えて一直線になりました。ここで見てほしいのはハッシュIDです。XとYは変更内容もメッセージも同じなのに、b613285→9e2c23d、baadb75→ff333c1に変わっています。Xは親がAからBに、Yは親が元のXから新しいXに変わったため、どちらも別のコミットとして作り直されたからです。元のXとYはどのブランチからもたどれなくなり、一定期間を過ぎるとガベージコレクションで消えます。rebaseが「履歴の書き換え」と呼ばれ、共有ブランチで禁止される理由はここにあります。
git rebaseとgit mergeの違いと使い分け
分岐した2つのブランチをmergeすると、統合のためのマージコミットが新しく作られ、分岐と合流の経緯がそのまま残ります(片方が他方の祖先ならfast-forwardでブランチの位置が進むだけです)。既存のコミットには手を付けません。rebaseはマージコミットを作らず、自分のコミットを作り直して一直線に並べます。コンフリクトがない単純な例なら最終的なファイルの中身は同じになりますが、rebaseではコミットごとに解決や--skipをするため、結果が変わることがあります。rebase後はgit diffやテストで中身を確かめてください。
| 観点 | git merge | git rebase |
|---|---|---|
| 履歴の形 | 分岐と合流が残る | 一直線になる |
| マージコミット | 作る(fast-forward時を除く) | 作らない |
| 既存コミットのID | 変わらない | 自分側のコミットが変わる |
| コンフリクトの解決 | まとめて1回 | コミットごとに発生しうる |
| 共有済みブランチ | 安全 | 使わない |
rebaseを使う場面
使う場面は、まだpushしていない(または自分しか使っていない)作業ブランチに限られます。典型は2つです。1つ目は、プルリクエストを出す前に作業ブランチを最新のmainへ追従させること。レビューする側はmainとの差分だけを読めばよくなります。2つ目は、「typo修正」「レビュー対応」のような細かいコミットを-iでまとめて、意味のある単位に整えることです。
rebaseをやめるべき場面
main・developのように他人がpullしているブランチはrebaseしないでください。Pro Git(git-scm.comの公式書籍)も「公開リポジトリにプッシュしたコミットをリベースしてはいけない」を原則に挙げています。もう1つ避けたいのが、コミットが数十個あり同じファイルを何度も触っているブランチの追従です。rebaseはコンフリクトをコミット単位で解かせるので、同じ箇所を何度も直すことになります。この場合はgit merge mainで1回にまとめるほうが早く、解決ミスも減ります。マージコミットを含むブランチも、既定のrebaseではマージコミットが消えます(後述の--rebase-merges)。
git rebaseの基本的な使い方
作業ブランチを最新のmainへ追従させる手順
基本の書式はgit rebase <upstream> [<branch>]です。<upstream>が載せ直す先、<branch>が動かすブランチで、<branch>を省くと今いるブランチが対象になります。
git switch feature
git fetch origin
git rebase origin/main
リモートの最新を取り込むなら、ローカルのmainではなくfetch直後のorigin/mainを指定します。<upstream>自体を省略すると、ブランチに設定された追跡先(branch.<name>.merge)が使われます。
--ontoによる載せ替え先と対象範囲の指定
分岐の一部だけを別の基点へ移すときは--ontoを使います。git rebase --onto <新しい基点> <ここより後を移す> <ブランチ>という読み方です。
git rebase --onto main develop feature
これは「developから分岐したfeatureのうち、developにないコミットだけをmainの上へ移す」指定です。developの未リリースのコミットを持ち込まずにfeatureだけを先に出したいときに使います。親ブランチがスカッシュマージされた後の派生ブランチの付け替えも同じ形で、手順はgit squashとは?コミットが多すぎる履歴をまとめる手順とスカッシュマージの注意点で実行例つきで扱っています。
git pull --rebaseとpull.rebaseの設定
git pullはfetchのあとに統合を行います。git-pullのドキュメント(2.56.0)では既定は--ff-onlyで、手元とリモートが分岐していると失敗します。統合方法は--rebase(rebase)・--no-rebase(merge)などで選びます。mergeを選ぶとMerge branch 'main' of ...のようなマージコミットが増えますが、git pull --rebaseなら手元の未pushコミットを取り込み後の先端に載せ直すので増えません。毎回付けるのが面倒なら設定で既定にします。
git config --global pull.rebase true
作業ツリーに未コミットの変更があるとrebaseは開始を拒否します。git rebase --autostash(またはrebase.autoStash設定)を使うと、開始前に一時的にstashし、終了後に戻します。ドキュメントは、戻すときにコンフリクトが起きうる点に注意を促しています。
インタラクティブリベース(-i)によるコミット整理
git rebase -i(interactive)は、対象のコミット列をtodoリストとしてエディタで開き、1行ずつ操作を指定して履歴を編集する機能です。直近3コミットならgit rebase -i HEAD~3、mainから分岐した後の全コミットならgit rebase -i mainと指定します。
todoリストの読み方とコマンド
エディタには次のようなtodoリストが開きます(Git 2.50.1の実物。行頭のコマンドを書き換えて保存すると、上から順に実行されます)。
pick 9e2c23d # X: feature1
pick ff333c1 # Y: feature2
fixup ce5abcf # fixup! Y: feature2
pick a336e32 # Z: 修正
# Rebase 714691d..ce5abcf onto 714691d (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
...
# If you remove a line here THAT COMMIT WILL BE LOST.
| コマンド | 動作 |
|---|---|
| pick | そのまま採用(行の並べ替えで順序も変わる) |
| reword | メッセージだけ書き直す |
| edit | そこで止まり、修正やコミット分割をする |
| squash | 直前に統合し、両方のメッセージを編集 |
| fixup | 直前に統合し、自分のメッセージは捨てる |
| drop | コミットを削除(行を消しても同じ) |
| exec | その位置でシェルコマンドを実行 |
| break | その位置で一時停止 |
行を消すとそのコミットは失われる、という警告がリストの末尾に出ます。逆に全行を消して保存すると、rebaseは中止されて何も変わりません。編集を始めてから迷ったら、全行を消して閉じるのが安全な抜け方です。
--autosquashとfixupコミット
上のtodoリストで、3行目のfixup ce5abcfが修正対象のYの直後に並んでいるのは--autosquashの効果です。修正をコミットするときにgit commit --fixup=<対象>を使うと「fixup! 対象のメッセージ」という件名になり、git rebase -i --autosquashで対象の直後へ自動で移されてfixupが付きます。
git add f.txt # 修正内容をステージ
git commit --fixup=ff333c1
git rebase -i --autosquash main
squashとfixupの使い分け、コミットをまとめた後の扱いはgit squashの記事で詳しく説明しています。
execによるコミット単位のテスト実行
-x(--exec)を付けると、各コミットの適用後にコマンドを実行し、失敗したところでrebaseが止まります。整理した後の各コミットが単体でもビルド・テストを通るかを確かめたいときに使います。
git rebase -i --exec "npm test" main
途中のコミットでテストが壊れていると、git bisectで原因を探すときに誤判定が起きます。コミットを並べ替えた後に一度通しておけば、テストが失敗する中間コミットをその場で特定できます。CIで同じ検査を自動化する場合は、弊社のDevOps・CI/CD導入支援でも相談を受けています。
--update-refsによる積み上げたブランチの一括更新
part1の上にpart2を作るように、ブランチを積み上げて小さなプルリクエストに分けていると、mainが進んだときに両方を付け替える必要があります。Git 2.38で追加された--update-refsを使うと、先端のpart2をrebaseするだけで、途中を指しているpart1も新しいコミットへ動きます。
$ git switch part2
$ git rebase -i --update-refs main
pick e5c3ae3 # P1
update-ref refs/heads/part1
pick 91db6e7 # P2
Successfully rebased and updated refs/heads/part2.
Updated the following refs with --update-refs:
refs/heads/part1
todoリストにupdate-ref refs/heads/part1の行が入り、rebaseの最後にpart1が新しいP1を指すよう更新されます。実行後はpart1がpart2の祖先のまま保たれていました。-iなしのgit rebase --update-refs mainでも同じように動きます。毎回付けるならgit config --global rebase.updateRefs trueで既定にできます。
ただしgit-rebaseのドキュメントのとおり、別のworktreeでチェックアウト中のブランチは更新されません。git worktreeでpart1を開いたままだと、part1だけ古い位置に残ります。更新した各ブランチをすでにpushしていた場合は、それぞれ強制pushが必要です(未pushのブランチは通常のpushで足ります)。
rebase中のコンフリクト解決(continue・skip・abort)
自分のコミットと載せ直す先が同じ箇所を変更していると、そのコミットの適用でrebaseが止まります。実行結果は次のとおりです。
$ git rebase main
Auto-merging a.txt
CONFLICT (content): Merge conflict in a.txt
error: could not apply fc4d624... C: aを書き換え
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
メッセージが示すとおり、選択肢は3つです。
git add a.txt # 解決したファイルを登録
git rebase --continue # 次のコミットへ進む
git rebase --skip # このコミットを捨てて進む
git rebase --abort # rebase開始前の状態に戻す
rebaseはコミットを1つずつ適用するので、コンフリクトもコミットの数だけ繰り返し起きる可能性があります。注意点として、rebase中のoursは載せ直す先(mainに、そこまで適用済みの自分のコミットを加えた状態)、theirsはいま適用しようとしている自分のコミットを指し、mergeのときと逆になります。git checkout --oursで自分の変更を残すつもりがmain側を採っていた、という取り違えが起きやすいところです。解決の手順そのものはコンフリクトとは?Gitで変更が衝突する仕組みと解消手順・予防設計を実装視点で解説にまとめています。
同じコンフリクトを何度も解かされるなら、解決内容を記録して次回から自動で当てるrerereを有効にします。
git config --global rerere.enabled true
マージコミットを含むブランチと--rebase-mergesによるマージの再作成
既定のrebaseは、対象範囲のマージコミットをtodoリストから落とし、一本の直線に並べ直します。トピックブランチt1を--no-ffでマージ済みのブランチをmainへrebaseした結果を比べると、違いがはっきり出ます。
$ git rebase main # 既定
$ git log --oneline --graph main..rm
* b356567 T1
$ git reset --hard ORIG_HEAD # 開始前の状態に戻してから比べる
$ git rebase --rebase-merges main # マージコミットを作り直す
$ git log --oneline --graph main..rm
* 6f70c18 Merge t1
* b356567 T1
既定では「Merge t1」が消えています。マージの単位で履歴を読みたいブランチでは--rebase-mergesを付けてください。ドキュメントによれば、元のマージコミットで手作業で解決したコンフリクトや修正は、作り直すときに改めて解決が必要です。
rebase後のpush:--force-with-leaseと--force-if-includes
push済みのブランチをrebaseすると、リモートの履歴と食い違うので通常のgit pushは! [rejected]で拒否されます。通すには強制pushが要りますが、素の--forceは自分がrebaseしている間に他人が追加したコミットまで消します。
そこで使うのが--force-with-leaseです。「手元が最後に見たリモートの位置から動いていなければ上書きする」条件付きの強制pushで、他人のpushを見ていなければ拒否されます。
$ git push --force-with-lease origin feature
! [rejected] feature -> feature (stale info)
ところが、pushの前にgit fetchすると、この保護は効かなくなります。fetchで「最後に見た位置」が他人のコミットに更新されるため、条件を満たしてしまいます。手元のリポジトリで試すと、fetch後の--force-with-leaseは(forced update)で通り、他人のコミットが消えました。Git 2.30で入った--force-if-includesを併用すると、リモートの先端を取り込まないまま上書きしようとしていることを検出して拒否します。
$ git fetch
$ git push --force-with-lease --force-if-includes origin feature
! [rejected] feature -> feature (remote ref updated since checkout)
エディタやツールが裏で定期的にfetchしている環境では、--force-with-leaseだけでは足りません。git config --global push.useForceIfIncludes trueで既定にしておくのが確実です。
共有ブランチをrebaseされたときの対処
自分が作業の土台にしていたブランチを誰かがrebaseして強制pushした場合、mergeで統合すると(git pull --no-rebaseなど)、書き換え前と後の同じ内容のコミットが二重に履歴に残ります。Pro Gitのリベースの章は、この場合はgit pull --rebaseを使うよう勧めています。Gitはコミットの差分から計算した「パッチID」で同じ内容のコミットを見分け、相手側で作り直されたものを除いて、自分にしかないコミットだけを載せ直します。
git fetch
git rebase origin/feature # または git pull --rebase
相手のrebaseで内容まで変わったコミットはパッチIDが一致しないため、自動では除かれません。その場合はgit rebase -i origin/featureで、自分のものでない行をdropします。根本的な対策は、共有ブランチをrebaseしない運用を決めておくことです。
GitHubの「Rebase and merge」とgit rebaseの違い
GitHubのプルリクエストには、マージ方式として「Create a merge commit」「Squash and merge」「Rebase and merge」の3つがあります。Rebase and mergeは、プルリクエストのコミットをマージコミットなしで1つずつベースブランチへ追加します。ただしGitHubのドキュメントによると、手元のgit rebaseとは次の点が違います。
- コミッター情報とSHAが常に変わる:
git rebaseは祖先の上に載せ直すだけならコミッター情報を変えませんが、GitHubは毎回コミッター情報を更新し、新しいSHAのコミットを作ります。 - 最初から空のコミットは落とす:
git commit --allow-emptyで作ったコミットは追加されません。手元のgit rebaseは既定で残します。 - 署名は検証済みにならない:GitHubは作成者の秘密鍵を持たないため、追加されたコミットに署名できません。GitHubのドキュメントは回避策として手元でrebaseしてマージし、ベースブランチへpushする方法を挙げています。作り直したコミットは
git rebase -S(またはcommit.gpgSignの設定)で署名し直さないと署名されない点に注意してください。
GitHubが自動でrebaseできないコンフリクトがあるときは、ボタンが使えません。手元でgit rebaseしてコンフリクトを解き、強制pushしてから改めてマージします。3つの方式の選び方とリポジトリ側の設定はGitHubのPull Requestの作り方:作成手順とマージ方式・レビュー必須化の決め方で解説しています。
ORIG_HEADとreflogによるrebaseのやり直し
rebaseを完了した後で戻したくなっても、元のコミットはすぐには消えません。冒頭の例では、rebase後のORIG_HEADがrebase前の先端baadb75を指していました。ただしORIG_HEADはreset・mergeなどでも上書きされ、editで止めたrebase中にgit resetを使った場合も書き換わります。使う前にgit log -1 ORIG_HEADで中身を見て、下のreflogのrebase開始前の行と同じコミットか照合してください。
git log -1 --oneline ORIG_HEAD # rebase前の先端か確認
git reset --hard ORIG_HEAD
一致しないときや他の操作をした後は、git reflogでrebase開始前の行を探します。
$ git reflog -n 6
ff333c1 HEAD@{0}: rebase (finish): returning to refs/heads/feature
ff333c1 HEAD@{1}: rebase (pick): Y: feature2
9e2c23d HEAD@{2}: rebase (pick): X: feature1
714691d HEAD@{3}: rebase (start): checkout main
baadb75 HEAD@{4}: commit: Y: feature2
b613285 HEAD@{5}: commit: X: feature1
$ git reset --hard HEAD@{4}
戻り先はrebase (start)の1つ下の行(ここではHEAD@{4})です。reset --hardは作業ツリーの未コミットの変更も消すので、実行前にgit statusで確認してください。
reflogにも期限があります。git-configのgc設定では、通常のエントリはgc.reflogExpireの既定90日、現在の先端からたどれないエントリはgc.reflogExpireUnreachableの既定30日で整理されます。rebaseで置き換えられた元のコミットは後者に当たります。ただし30日は保証期間ではなく、エントリの日時や設定、git gcの実行状況で早く消えることも残ることもあります。確実に残したい履歴は、rebase前にgit branch backup/featureのようにブランチを作っておくのが安全です。mergeで取り込んだ変更を取り消す方法はgit mergeの取り消し|abort・reset・revert -mの使い分けで扱っています。
git history:rebase -iを使わずに履歴を直す新コマンド
Git 2.54.0で、実験的なgit historyコマンドが追加されました。git rebase -iでtodoリストを編集しなくても、1つのコミットに対する書き換えをサブコマンド1つで行えます。2.56.0時点のサブコマンドは次の4つです。
| サブコマンド | 動作 | 追加版 |
|---|---|---|
| reword | メッセージの書き直し | 2.54.0 |
| split | コミットの分割 | 2.54.0 |
| fixup | ステージした変更を指定コミットへ統合 | 2.55.0 |
| drop | コミットの削除 | 2.56.0 |
git-historyのドキュメントによると、rebaseとの大きな違いは、フックを実行しないこと、既定で対象コミットの子孫にあたるブランチをすべて新しいコミットへ付け替えることです。別の土台への載せ替えやコミット範囲の並べ替えは、引き続きgit rebaseの役割です。「EXPERIMENTAL」と明記されていて挙動が変わりうるので、チームの手順書に組み込むのはまだ早いと判断しています。手元のGitの版の確認と更新方法はGitの最新バージョンは2.56.0|確認・アップデート方法とGit 3.0の変更点【2026年10月】を参照してください。
よくある質問
git rebaseとgit mergeはどちらを使うべきですか?
まだpushしていない自分のブランチを最新のmainに追従させる・コミットを整えるならrebase、他人と共有したブランチの統合や分岐の経緯を残したいならmergeです。共有済みのブランチにはrebaseを使いません。
rebaseを途中でやめる・完了後に取り消すには?
途中ならgit rebase --abortで開始前に戻ります。完了後はgit reflogでrebase (start)より前の行を探してgit reset --hard HEAD@{n}で戻します。ORIG_HEADがrebase前の先端を指しているとgit log -1 ORIG_HEADで確認できた場合は、git reset --hard ORIG_HEADでも戻せます。
rebase後にpushがrejectedになるのはなぜですか?
rebaseでコミットIDが変わり、リモートの履歴と食い違うためです。git push --force-with-lease --force-if-includesで条件付きの強制pushをします。素の--forceは他人の追加コミットを消すので避けます。
git rebase -iのsquashとfixupの違いは?
どちらも直前のコミットへ統合します。squashは両方のメッセージをまとめて編集でき、fixupは自分のメッセージを捨てて直前のメッセージをそのまま使います。
git pull --rebaseとは何ですか?
リモートの更新を取り込むときにマージコミットを作らず、手元の未pushコミットを取り込み後の先端へ載せ直すpullです。git config --global pull.rebase trueで既定にできます。