git rebase — コミットを積み直す
2本に分かれたブランチをつなぐ方法は2つあります。前の回の merge は、親が2つのマージコミットで合流させました。もう1つの git rebase は、自分のコミットを、相手のブランチの先端の上に作り直して並べ直すコマンドです。歴史は一本道になりますが、作り直したコミットは ID が変わります。merge との使い分け、--autostash、2台運用で追いつく git pull --rebase、コンフリクトしたときの --continue・--abort・--skip を1コマンドずつ確かめます。
- 上のターミナルは、実際に打ったときと同じ出力です(Git は英語で答えます)
- 下の中で起きていることは、Git の中の何が変わったかを図にしたものです。丸がコミット、黒い札がブランチ、ピンクの札が HEAD です。rebase で置き去りになった元のコミットは薄く残ります
- リモートのある区間では、上が GitHub 側、下が手元のグラフです。点線の札
origin/mainは手元に貼ってある GitHub の写しです - 右の説明をスクロールして読み進めると、左のターミナルでコマンドが1つずつ打たれます。
→キーでも1つずつ進みます(←で1つ戻ります。スマホでは「次へ」ボタン)。最後まで進んでから→を押すと次の区間へ移ります。黄色く光っているのが、そのコマンドで変わった場所です
1. git rebase — 自分のコミットを相手の先へ作り直す
feature で作業しているあいだに、main にも別のコミットが入った場面です。feature に乗ったまま git rebase main を打つと、Git は次の3つを順に行います。
featureにだけあるコミットを、古い順に書き出す- HEAD を
mainの先端へ移し、その変更を1つずつ当てて新しいコミットを作る featureの付箋を、最後に作ったコミットへ付け替える
git rebase — feature のコミットを main の先へ積み直す
featureで「パンを追加」「バターを追加」を積んでいるあいだに、mainには「メモを追加」が入りました。2本は「卵を追加」から枝分かれしています。$ git log --oneline --all 9ae3d51 (main) メモを追加 71d9b3e (HEAD -> feature) バターを追加 e5c0a62 パンを追加 8b41d07 卵を追加 3f2a9c1 牛乳を追加git rebase mainは、featureにだけあるコミットを、mainの先端の上に1つずつ 作り直します。featureの付箋は作り直したコミットへ移り、元の2つは付箋が外れてグラフに薄く残りました。$ git rebase main Successfully rebased and updated refs/heads/feature.「パンを追加」は
e5c0a62から4be41b1に、「バターを追加」は71d9b3eから5034a3dに変わりました。変更の中身は同じでも、親が変わったので 別のコミット です。元の2つは付箋が外れたので、--allでも出ません。$ git log --oneline --all 5034a3d (HEAD -> feature) バターを追加 4be41b1 パンを追加 9ae3d51 (main) メモを追加 8b41d07 卵を追加 3f2a9c1 牛乳を追加もう一度打つと
Current branch feature is up to date.(もうmainの先にある)と答えて、何もしません。$ git rebase main Current branch feature is up to date.reflog には rebase の足取りが残っています。
rebase (start)でmainの先端に移り、rebase (pick)で1つずつ作り直し、rebase (finish)でfeatureの付箋を付け替えました。置き去りになった元のコミットの取り戻し方は、3-4 reflog の回で見ます。$ git reflog -4 5034a3d (HEAD -> feature) HEAD@{0}: rebase (finish): returning to refs/heads/feature 5034a3d HEAD@{1}: rebase (pick): バターを追加 4be41b1 HEAD@{2}: rebase (pick): パンを追加 9ae3d51 HEAD@{3}: rebase (start): checkout mainmainに戻ります。いまのfeatureは、mainの先端からまっすぐ伸びています。$ git switch main Switched to branch 'main'一本道なので、merge は付箋を前に進めるだけの fast-forward(
Fast-forward)で済みます。マージコミットはできません。$ git merge feature Updating 9ae3d51..5034a3d Fast-forward todo.txt | 2 ++ 1 file changed, 2 insertions(+)歴史は分かれ目のない一本道になりました。
mainとfeatureは同じ5034a3dを指しています。$ git log --oneline 5034a3d (HEAD -> main, feature) バターを追加 4be41b1 パンを追加 9ae3d51 メモを追加 8b41d07 卵を追加 3f2a9c1 牛乳を追加
ID が変わるのは、コミットの ID が「中身・メッセージ・親・日時」から計算されるからです。変更の中身が同じでも、親が「卵を追加」から「メモを追加」に変わったので、rebase したコミットは元のコミットとは別物になります。元の e5c0a62 と 71d9b3e はどの付箋からも辿れなくなりますが、reflog に記録が残っているあいだは Git の中にあり、取り戻せます(3-4 reflog で取り戻す)。
rebase したあとの feature は main の先端からまっすぐ伸びているので、取り込む merge は fast-forward(早送り。付箋を前に進めるだけ)で済みます。「rebase で並べ直してから、fast-forward で取り込む」が、一本道の歴史を作る定番の流れです。
2. merge との違いと使い分け
| git merge | git rebase | |
|---|---|---|
| グラフの形 | 枝分かれが残り、マージコミットで合流する | 一本道になる |
| 新しくできるコミット | マージコミット1つ(親が2つ) | 自分のコミットの作り直し |
| 元のコミットの ID | 変わらない | 自分の側だけ変わる |
| コンフリクト | まとめて1回 | 作り直すコミットごとに起きうる |
| 向いている場面 | push 済みのブランチ・共有しているブランチをつなぐ | まだ push していない自分のコミットを整える |
使い分けの基準は1つです。rebase してよいのは、まだ push していない自分のコミットだけです。push 済みのコミットを rebase すると、手元は GitHub 側と別の ID のコミットを持つことになり、次の git push は non-fast-forward で拒否されます。通すには --force が要り、それが GitHub 側の何を消しうるのかは 3-3 force pushで見ます。
3. 書きかけがあると止まる — --autostash
rebase はコミットを作り直すたびに作業ツリーを書き換えるので、コミットしていない書きかけがあると始める前に止まります。このとき使えるのが --autostash です。1-7 stashで見た stash と pop を、rebase の前後で自動でやってくれます。
書きかけがあると止まる — git rebase --autostash
featureで作業している途中です。まだコミットしない1行をtodo.txtに書き足しました。$ echo "バターを買う" >> todo.txtrebase は
cannot rebase: You have unstaged changes.(コミットしていない変更がある)で止まります。作り直すあいだに作業ツリーを書き換えるので、書きかけを巻き込まないよう 始める前に 止まります。グラフは何も変わっていません。$ git rebase main error: cannot rebase: You have unstaged changes. error: Please commit or stash them.--autostashを付けると、始める前に書きかけを stash し(Created autostash)、積み直したあとで戻します(Applied autostash.)。featureはmainの先へ移りました。$ git rebase --autostash main Created autostash: c7a9f7c Applied autostash. Successfully rebased and updated refs/heads/feature.modified: todo.txtのとおり、書きかけはコミットされずに、作業ツリーの変更として戻っています。$ git status On branch feature Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: todo.txt no changes added to commit (use "git add" and/or "git commit -a")最後の「バターを買う」が書きかけの1行です。積み直した「パンを追加」の上に、そのまま残っています。
$ cat todo.txt 牛乳を買う 卵を買う パンを買う バターを買う退避の棚は空です。autostash が使った退避は、戻した時点で片付けられます。
$ git stash list
Created autostash が stash、Applied autostash. が pop にあたります。戻すときに書きかけと積み直した中身がぶつかった場合は、Git は書きかけを捨てずに退避の棚に残し、Your changes are safe in the stash. と知らせます。そのときは git stash list で確かめて、git stash pop で戻します。
Claude Code が打った Git のコマンド延べ約14,000回のうち、rebase は41回で、うち22回が git rebase --autostash でした。
4. git pull --rebase — 2台運用で、マージコミットを作らずに追いつく
2-3 fetch と pullでは、Windows のノート PC と Mac で別々にコミットすると diverged(枝分かれ)になり、git pull --ff-only も git push も止まるのを見ました。git pull --rebase は、fetch してから、手元のまだ push していないコミットを origin/main の先へ積み直すコマンドです。中身は git fetch と git rebase origin/main を続けて打つのと同じです。
git pull --rebase — マージコミットを作らずに追いつく
Windows のノート PC で、
todo.txtに1行書き足します。$ echo "バターを買う" >> todo.txt手元の
mainが「バターを追加」に進みました。まだ push していません。$ git commit -am "バターを追加" [main 15e1410] バターを追加 1 file changed, 1 insertion(+)同じころ、Mac で「メモを追加」を push しました。GitHub 側の
mainは手元とは別の方向へ進み、2台のコミットが枝分かれします。さらに、まだコミットしない書きかけを1行足しました。
$ echo "チーズを買う" >> todo.txtgit pull --rebaseは、fetch してから手元のコミットを origin/main の先へ積み直すコマンドです。書きかけがあるとcannot pull with rebaseで止まります。Fromの行が出ていないとおり、fetch もしていません。$ git pull --rebase error: cannot pull with rebase: You have unstaged changes. error: Please commit or stash them.--autostashを付けると通ります。fetch で origin/main が9ae3d51へ進み、書きかけを退避して「バターを追加」を「メモを追加」の先へ作り直し、書きかけを戻しました。$ git pull --rebase --autostash From https://github.com/you/memo-app c43e6a8..9ae3d51 main -> origin/main Created autostash: e6d8520 Applied autostash. Successfully rebased and updated refs/heads/main.「バターを追加」は
4646058に作り直されて、origin/main の上にまっすぐ乗っています。マージコミットはありません。$ git log --oneline 4646058 (HEAD -> main) バターを追加 9ae3d51 (origin/main, origin/HEAD) メモを追加 c43e6a8 卵を追加 3f2a9c1 牛乳を追加ahead of 'origin/main' by 1 commitです。枝分かれが解けて、1つ先に進んでいるだけになりました。modified: todo.txtは戻ってきた書きかけです。$ git status On branch main Your branch is ahead of 'origin/main' by 1 commit. (use "git push" to publish your local commits) Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: todo.txt no changes added to commit (use "git add" and/or "git commit -a")今度の push は通ります(
9ae3d51..4646058)。GitHub 側のmainも一本道のまま進みました。$ git push To https://github.com/you/memo-app.git 9ae3d51..4646058 main -> main
ふつうの git pull で枝分かれをつなぐと、手元にマージコミットができ、2台で毎日作業すると合流の印が何本も並びます。pull --rebase なら、Mac のコミットの後ろに Windows のコミットが並ぶだけで、push もそのまま通ります。
書きかけがあると、git pull --rebase は fetch する前に止まります。From https://github.com/… の行が出ていなければ、origin/main もまだ動いていません。git config pull.rebase true と git config rebase.autoStash true を設定しておくと、オプション無しの git pull がこの区間の --rebase --autostash と同じ動きになります。
Claude Code は、追いつくときにまず git pull --ff-only を打ちます(pull 51回のうち24回)。早送りで済めばそれで終わり、枝分かれで止まったら、そこで初めて rebase か merge を選ぶ順番です。
5. 積み直しの途中でコンフリクトしたとき — --continue
自分のコミットと相手のコミットが同じ行を別々に変えていると、作り直す途中でコンフリクト(変更のぶつかり)が起きて止まります。merge の回と同じ「卵を2つ買う」と「卵を6つ買う」の場面ですが、rebase では止まっているあいだ HEAD が detached HEAD(どのブランチにも乗っていない状態)になり、プロンプトが (feature|REBASE 1/2) になります。コミットを1つずつ作り直すので、コミットごとに止まりうるのも merge との違いです。
積み直しの途中でコンフリクトしたとき — git rebase --continue
featureの「卵を2つに」とmainの「卵を6つに」は、同じ2行目を別々に変えています。1つ目を作り直すところでCONFLICTになり、could not apply a4e81f2... 卵を2つにで止まりました。プロンプトのREBASE 1/2は「2つのうち1つ目で止まっている」という意味です。$ git rebase main Auto-merging todo.txt CONFLICT (content): Merge conflict in todo.txt error: could not apply a4e81f2... 卵を2つに 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". hint: Disable this message with "git config set advice.mergeConflict false" Could not apply a4e81f2... # 卵を2つにinteractive rebase in progress; onto c3c3c3cは「c3c3c3cの上への積み直しが途中」という意味です。Last command doneが当てようとして止まったコミット、Next command to doがまだ残っているコミットです。$ git status interactive rebase in progress; onto c3c3c3c Last command done (1 command done): pick a4e81f2 # 卵を2つに Next command to do (1 remaining command): pick d07c5b9 # メモを追加 (use "git rebase --edit-todo" to view and edit) You are currently rebasing branch 'feature' on 'c3c3c3c'. (fix conflicts and then run "git rebase --continue") (use "git rebase --skip" to skip this patch) (use "git rebase --abort" to check out the original branch) Unmerged paths: (use "git restore --staged <file>..." to unstage) (use "git add <file>..." to mark resolution) both modified: todo.txt no changes added to commit (use "git add" and/or "git commit -a")* (no branch, rebasing feature)のとおり、途中のあいだ HEAD はどのブランチにも乗っていない detached HEAD です。featureの付箋は、終わるまで元の場所に残っています。$ git branch * (no branch, rebasing feature) feature main印の上側の
HEADは、積み直す先のmainの内容(卵を6つ)です。下側のa4e81f2 (卵を2つに)が、いま当てようとしている 自分のコミット です。rebase は相手の先端に乗ってから自分のコミットを当てるので、merge のときと上下が逆に見えます。$ cat todo.txt 牛乳を買う <<<<<<< HEAD 卵を6つ買う ======= 卵を2つ買う >>>>>>> a4e81f2 (卵を2つに)どちらを残すかを決めて、印を消した中身でファイルを丸ごと書き直します。ここでは自分の「卵を2つ買う」を残します。
$ printf '牛乳を買う\n卵を2つ買う\n' > todo.txtgit addで「このファイルは解決した」と印を付けます。$ git add todo.txtgit rebase --continueで、止まっていた1つ目を作り直し、残りの「メモを追加」も当てて最後まで進みます。[detached HEAD 995677b]のとおり作り直しはブランチの外で行い、最後にfeatureの付箋を付け替えます。$ git rebase --continue [detached HEAD 995677b] 卵を2つに 1 file changed, 1 insertion(+), 1 deletion(-) Successfully rebased and updated refs/heads/feature.featureはmainの先へ一本道で並びました。直した中身は、作り直した「卵を2つに」995677bに入っています。$ git log --oneline --all 69ba3a1 (HEAD -> feature) メモを追加 995677b 卵を2つに c3c3c3c (main) 卵を6つに 8b41d07 卵を追加 3f2a9c1 牛乳を追加
いちばん間違えやすいのは印の上下です。merge では、上側の HEAD が自分のブランチ、下側が取り込む相手でした。rebase では、相手の先端に HEAD を移してから自分のコミットを当てるので、上側の HEAD が相手、下側の >>>>>>> a4e81f2 (卵を2つに) が自分です。下側の印にはコミットの ID とメッセージが出るので、どちらが自分かはそこで確かめられます。
直し方は merge と同じで、印を消した中身に書き直して git add します。最後だけ、git commit ではなく git rebase --continue を打ちます。本物の Git では、このときコミットメッセージの編集画面が開きます。そのまま保存して閉じれば続きますが、編集画面を操作できないところから打つなら GIT_EDITOR=true git rebase --continue と書くと、開かずに元のメッセージで進みます。
なお interactive rebase in progress は、-i を付けていなくても出ます。いまの Git は、ふつうの rebase も内部で同じしくみで動かしているからです。
6. 引き返す --abort、その1つを飛ばす --skip
止まったときに取れる道は3つです。直して進む --continue、rebase そのものをやめて始める前に戻る --abort、止まっているコミットだけを捨てて先へ進む --skip です。
止まったら戻る・その1つを飛ばす — git rebase --abort と --skip
前の区間と同じ場面です。1つ目の「卵を2つに」でコンフリクトして止まりました。
$ git rebase main Auto-merging todo.txt CONFLICT (content): Merge conflict in todo.txt error: could not apply a4e81f2... 卵を2つに 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". hint: Disable this message with "git config set advice.mergeConflict false" Could not apply a4e81f2... # 卵を2つに止まっているあいだは、ふつうの commit も
Committing is not possible because you have unmerged files.で止まります。直して--continueで進めるか、引き返すかのどちらかです。$ git commit -m "とりあえず" error: Committing is not possible because you have unmerged files. hint: Fix them up in the work tree, and then use 'git add/rm <file>' hint: as appropriate to mark resolution and make a commit. fatal: Exiting because of an unresolved conflict. U todo.txtgit rebase --abortは、rebase を始める前の状態にすべて戻します。HEAD はfeatureに戻り、作業ツリーの印も消えました。$ git rebase --abortfeatureは元のd07c5b9を指したままで、ID も変わっていません。始める前に戻るだけなので、何も失いません。$ git log --oneline --all c3c3c3c (main) 卵を6つに d07c5b9 (HEAD -> feature) メモを追加 a4e81f2 卵を2つに 8b41d07 卵を追加 3f2a9c1 牛乳を追加もう一度 rebase して、同じところで止めます。
$ git rebase main Auto-merging todo.txt CONFLICT (content): Merge conflict in todo.txt error: could not apply a4e81f2... 卵を2つに 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". hint: Disable this message with "git config set advice.mergeConflict false" Could not apply a4e81f2... # 卵を2つにgit rebase --skipは、止まっているコミット「卵を2つに」を 作り直さずに飛ばし、残りの「メモを追加」だけを積み直します。$ git rebase --skip Successfully rebased and updated refs/heads/feature.作り直した
featureには「卵を2つに」がありません。元のa4e81f2は付箋が外れて、グラフに薄く残っているだけです。$ git log --oneline --all 7e61229 (HEAD -> feature) メモを追加 c3c3c3c (main) 卵を6つに 8b41d07 卵を追加 3f2a9c1 牛乳を追加todo.txtはmainの「卵を6つ買う」のままです。skip は自分のコミットを捨てるので、直して進めるつもりなら--continueを打ちます。$ cat todo.txt 牛乳を買う 卵を6つ買う
迷ったら --abort です。始める前の状態に戻るだけなので、何も失いません。--skip は、自分の変更が相手側にすでに入っているときなど、そのコミットが要らないとはっきりしているときだけ使います。
7. Claude Code の許可画面で見かけたら
この回で見たコマンドを、「何が消えうるか」で並べるとこうなります。
| コマンド | 何が起きるか | 消えうるもの |
|---|---|---|
git rebase main(まだ push していないコミット) | 自分のコミットを main の先へ作り直し、付箋を付け替える | なし(元のコミットは置き去りになるが、reflog で戻せる) |
git rebase --autostash main | 書きかけを stash してから積み直し、終わったら戻す | なし(戻すときにぶつかれば、書きかけは退避の棚に残る) |
git rebase --continue | 直した中身で作り直し、続きを当てる | なし(編集画面が開くことがある) |
git rebase --abort | rebase を始める前の状態に戻す | 注意 コンフリクトを直しかけた中身(コミットは何も失わない) |
git rebase --skip | 止まっているコミットを飛ばして続きを当てる | 注意 飛ばしたコミット(reflog で戻せる) |
git pull --rebase(--autostash 付きも) | fetch して、まだ push していない手元のコミットを origin/main の先へ積み直す | なし(書きかけがあれば fetch する前に止まる) |
push 済みのブランチの git rebase | GitHub 側と ID の違うコミットになる | 注意 次の push に --force が要る(3-3) |
見分け方は、積み直すコミットがまだ手元にしかないかどうかです。git status が ahead と答えている分だけなら、どこにも影響しません。rebase のあとで Claude Code が git push --force を打とうとしたら、push 済みのコミットを作り直したしるしなので、いったん止めてください。
まとめ
git rebase mainは、自分のブランチにだけあるコミットをmainの先端の上に作り直して並べ直す。ID が変わり、元のコミットは置き去りになる(reflog で戻せる)- merge はマージコミットで合流させ、rebase は一本道にする。rebase のあとの merge は fast-forward で済む
- rebase してよいのは、まだ push していない自分のコミットだけ。push 済みを rebase すると次の push が拒否される
- 書きかけがあると止まる。
--autostashは stash と pop を前後で自動でやる git pull --rebaseは fetch してから手元のコミットを origin/main の先へ積み直す。書きかけがあると fetch する前に止まる- コンフリクトで止まると HEAD は detached、プロンプトは
REBASE 1/2。印の上側のHEADが相手、下側が自分。直してgit add→--continue、迷ったら--abort
次の回では、この回で何度か名前の出た git push --force を扱います。rebase で歴史を書き換えたあとの push を force push がどう押し通し、そのあいだにもう1台の PC が push していたコミットがどうなるのか。--force-with-lease が何を確かめて止めてくれるのかまで、GitHub 側と手元のグラフで見ます。