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つを順に行います。

  1. feature にだけあるコミットを、古い順に書き出す
  2. HEAD を main の先端へ移し、その変更を1つずつ当てて新しいコミットを作る
  3. feature の付箋を、最後に作ったコミットへ付け替える

git rebase — feature のコミットを main の先へ積み直す

  1. feature で「パンを追加」「バターを追加」を積んでいるあいだに、main には「メモを追加」が入りました。2本は「卵を追加」から枝分かれしています。

    $ git log --oneline --all
    9ae3d51 (main) メモを追加
    71d9b3e (HEAD -> feature) バターを追加
    e5c0a62 パンを追加
    8b41d07 卵を追加
    3f2a9c1 牛乳を追加
  2. git rebase main は、feature にだけあるコミットを、main の先端の上に1つずつ 作り直します。feature の付箋は作り直したコミットへ移り、元の2つは付箋が外れてグラフに薄く残りました。

    $ git rebase main
    Successfully rebased and updated refs/heads/feature.
  3. 「パンを追加」は e5c0a62 から 4be41b1 に、「バターを追加」は 71d9b3e から 5034a3d に変わりました。変更の中身は同じでも、親が変わったので 別のコミット です。元の2つは付箋が外れたので、--all でも出ません。

    $ git log --oneline --all
    5034a3d (HEAD -> feature) バターを追加
    4be41b1 パンを追加
    9ae3d51 (main) メモを追加
    8b41d07 卵を追加
    3f2a9c1 牛乳を追加
  4. もう一度打つと Current branch feature is up to date.(もう main の先にある)と答えて、何もしません。

    $ git rebase main
    Current branch feature is up to date.
  5. 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 main
  6. main に戻ります。いまの feature は、main の先端からまっすぐ伸びています。

    $ git switch main
    Switched to branch 'main'
  7. 一本道なので、merge は付箋を前に進めるだけの fast-forward(Fast-forward)で済みます。マージコミットはできません。

    $ git merge feature
    Updating 9ae3d51..5034a3d
    Fast-forward
     todo.txt | 2 ++
     1 file changed, 2 insertions(+)
  8. 歴史は分かれ目のない一本道になりました。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 mergegit 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

  1. feature で作業している途中です。まだコミットしない1行を todo.txt に書き足しました。

    $ echo "バターを買う" >> todo.txt
  2. rebase は cannot rebase: You have unstaged changes.(コミットしていない変更がある)で止まります。作り直すあいだに作業ツリーを書き換えるので、書きかけを巻き込まないよう 始める前に 止まります。グラフは何も変わっていません。

    $ git rebase main
    error: cannot rebase: You have unstaged changes.
    error: Please commit or stash them.
  3. --autostash を付けると、始める前に書きかけを stash し(Created autostash)、積み直したあとで戻します(Applied autostash.)。feature は main の先へ移りました。

    $ git rebase --autostash main
    Created autostash: c7a9f7c
    Applied autostash.
    Successfully rebased and updated refs/heads/feature.
  4. 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")
  5. 最後の「バターを買う」が書きかけの1行です。積み直した「パンを追加」の上に、そのまま残っています。

    $ cat todo.txt
    牛乳を買う
    卵を買う
    パンを買う
    バターを買う
  6. 退避の棚は空です。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 — マージコミットを作らずに追いつく

  1. Windows のノート PC で、todo.txt に1行書き足します。

    $ echo "バターを買う" >> todo.txt
  2. 手元の main が「バターを追加」に進みました。まだ push していません。

    $ git commit -am "バターを追加"
    [main 15e1410] バターを追加
     1 file changed, 1 insertion(+)
  3. 同じころ、Mac で「メモを追加」を push しました。GitHub 側の main は手元とは別の方向へ進み、2台のコミットが枝分かれします。

  4. さらに、まだコミットしない書きかけを1行足しました。

    $ echo "チーズを買う" >> todo.txt
  5. git 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.
  6. --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.
  7. 「バターを追加」は 4646058 に作り直されて、origin/main の上にまっすぐ乗っています。マージコミットはありません。

    $ git log --oneline
    4646058 (HEAD -> main) バターを追加
    9ae3d51 (origin/main, origin/HEAD) メモを追加
    c43e6a8 卵を追加
    3f2a9c1 牛乳を追加
  8. 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")
  9. 今度の 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

  1. 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つに
  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")
  3. * (no branch, rebasing feature) のとおり、途中のあいだ HEAD はどのブランチにも乗っていない detached HEAD です。feature の付箋は、終わるまで元の場所に残っています。

    $ git branch
    * (no branch, rebasing feature)
      feature
      main
  4. 印の上側の HEAD は、積み直す先の main の内容(卵を6つ)です。下側の a4e81f2 (卵を2つに) が、いま当てようとしている 自分のコミット です。rebase は相手の先端に乗ってから自分のコミットを当てるので、merge のときと上下が逆に見えます。

    $ cat todo.txt
    牛乳を買う
    <<<<<<< HEAD
    卵を6つ買う
    =======
    卵を2つ買う
    >>>>>>> a4e81f2 (卵を2つに)
  5. どちらを残すかを決めて、印を消した中身でファイルを丸ごと書き直します。ここでは自分の「卵を2つ買う」を残します。

    $ printf '牛乳を買う\n卵を2つ買う\n' > todo.txt
  6. git add で「このファイルは解決した」と印を付けます。

    $ git add todo.txt
  7. git 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.
  8. 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. 前の区間と同じ場面です。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つに
  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.txt
  3. git rebase --abort は、rebase を始める前の状態にすべて戻します。HEAD は feature に戻り、作業ツリーの印も消えました。

    $ git rebase --abort
  4. feature は元の d07c5b9 を指したままで、ID も変わっていません。始める前に戻るだけなので、何も失いません。

    $ git log --oneline --all
    c3c3c3c (main) 卵を6つに
    d07c5b9 (HEAD -> feature) メモを追加
    a4e81f2 卵を2つに
    8b41d07 卵を追加
    3f2a9c1 牛乳を追加
  5. もう一度 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つに
  6. git rebase --skip は、止まっているコミット「卵を2つに」を 作り直さずに飛ばし、残りの「メモを追加」だけを積み直します。

    $ git rebase --skip
    Successfully rebased and updated refs/heads/feature.
  7. 作り直した feature には「卵を2つに」がありません。元の a4e81f2 は付箋が外れて、グラフに薄く残っているだけです。

    $ git log --oneline --all
    7e61229 (HEAD -> feature) メモを追加
    c3c3c3c (main) 卵を6つに
    8b41d07 卵を追加
    3f2a9c1 牛乳を追加
  8. 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 --abortrebase を始める前の状態に戻す注意 コンフリクトを直しかけた中身(コミットは何も失わない)
git rebase --skip止まっているコミットを飛ばして続きを当てる注意 飛ばしたコミット(reflog で戻せる)
git pull --rebase(--autostash 付きも)fetch して、まだ push していない手元のコミットを origin/main の先へ積み直すなし(書きかけがあれば fetch する前に止まる)
push 済みのブランチの git rebaseGitHub 側と 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 側と手元のグラフで見ます。