Git入門:バージョン管理のきほん

rebase で履歴を整える

生田 陸人
LuaGate エンジニア / 現役エンジニア
編集 LuaGate編集部

このレッスンで分かること

  • rebase が「コミットを別の土台に載せ直す」操作であること
  • 自分の作業ブランチを最新の main に追いつかせる手順
  • 共有済みのブランチを rebase してはいけない理由と、事故ったときの戻し方

rebase は土台を差し替える操作

main から feature を切って作業している間に、main にも別の人のコミットが入りました。履歴はこの形です。

プレーンテキスト

A---B---C feature / o---o---D---E main

このまま feature を進めると、main からどんどん離れていきます。最新の main の上で動くか確かめるには、いま main の内容を取り込む必要があります。

merge を使うと合流点のコミットが増えて、履歴は網目になります。rebase は違う考え方をします。A・B・C を作り直して、E の後ろにつなぎ直すのです。

プレーンテキスト

A'---B'---C' feature / o---o---D---E main

A' は A と同じ変更内容ですが、親が変わったので別のコミットです。ハッシュも変わります。ここが rebase の本質で、後で述べる危険もすべてここから来ます。名前の通り、base (土台) を re (付け替える) 操作です。

なぜ内容が同じなのにハッシュが変わるのか。コミットのハッシュは、変更内容だけでなく親のコミット、作者、日時、メッセージをまとめて計算した結果だからです。親が D から E に変われば、計算結果も当然変わります。Git にとって A と A' は無関係な別物で、「同じコミットを移動した」という記録はどこにも残りません。

merge と rebase の違いを整理しておきます。

観点mergerebase
コミット合流点を 1 つ足す既存のコミットを作り直す
ハッシュ変わらない全部変わる
履歴の形枝と合流が残る一直線になる
共有済みブランチ使ってよい使ってはいけない

「履歴を事実の記録として残したいなら merge、読みやすい物語として整えたいなら rebase」と考えると選びやすくなります。

自分の作業ブランチを最新の main に載せ直す

用途はこれ 1 つに絞ってください。手順は 3 段階です。

まず main を最新にします。origin はリモートリポジトリ (GitHub 上の本体) につけられた既定の呼び名です。

ターミナル

$ git switch main Switched to branch 'main' $ git pull Updating 4c2b70e..7e2d804 Fast-forward README.md | 5 +++++ 1 file changed, 5 insertions(+)

次に作業ブランチへ戻り、main の上に載せ直します。

ターミナル

$ git switch feature Switched to branch 'feature' $ git rebase main Successfully rebased and updated refs/heads/feature.

最後に形を確かめます。

ターミナル

$ git log --graph --oneline -5 * 9c4e71a ログインのバリデーションを追加 * 2f8b06d ログインフォームの雛形を追加 * 7e2d804 READMEに開発手順を追記 * 5b1c93f フッターの著作権表記を更新 * 4c2b70e 初期コミット

枝分かれの跡がなく、一直線に並んでいます。この状態で main にマージすれば必ず fast-forward になり、main の履歴もきれいなまま保てます。

やる前に、作業ツリーがきれいであることを必ず確かめてください。コミットしていない編集があると rebase は始まりません。

ターミナル

$ git rebase main error: cannot rebase: You have unstaged changes. error: Please commit or stash them.

コミットするか git stash で退避してから、もう一度実行します。

rebase をいつ行うかにも目安があります。プルリクエストを出す直前が最も安全なタイミングです。まだ誰も自分のブランチを見ていないので、履歴を作り直しても誰の手元とも食い違いません。逆に、レビューが始まってからの rebase は、レビュアーが見ていたコミットが消えてコメントの位置が分からなくなるので避けます。

git rebase main は「main の上に載せ直す」であって、main を書き換えるわけではありません。動くのは自分が立っているブランチだけです。方向を勘違いして main に立ったまま git rebase feature と打つと、main の側が作り直されます。これはやってはいけない操作です。

共有済みのブランチは絶対に rebase しない

ここがこのレッスンでいちばん大事な部分です。

rebase はコミットを作り直します。内容が同じでも、ハッシュが変わるので Git にとっては別物です。そのブランチをすでに push していて、他の人が手元に持っている場合、履歴が食い違います

相手の手元には古い A・B・C があり、あなたの手元には新しい A'・B'・C' があります。相手が git pull すると、Git は両方を別々のコミットとして扱い、同じ変更が二重に入った履歴や、大量のコンフリクトが生まれます。それを直そうとして誰かが push --force すると、今度は相手の作業が丸ごと消えます。

守るべき線引きは単純です。

ブランチの状態rebase してよいか
まだ push していない自分だけのブランチしてよい
push 済みだが自分しか触っていない作業ブランチ状況次第。push は --force-with-lease が要る
maindevelop などの共有ブランチ絶対にしない
他の人が作業中のブランチ絶対にしない

覚え方はこうです。他人が持っているかもしれない歴史は書き換えない。共有済みの履歴を戻したいときは、rebase ではなく打ち消しコミットを作る revert を使います。

push 済みの自分の作業ブランチを rebase した場合、そのままでは push が拒否されます。

ターミナル

$ git push ! [rejected] feature -> feature (non-fast-forward) error: failed to push some refs to 'https://github.com/example/app.git'

このときは --force-with-lease を使います。素の --force と違い、リモートが自分の知らない状態に進んでいたら拒否してくれるので、他人の作業を消す事故を減らせます。

ターミナル

$ git push --force-with-lease

それでも、そのブランチをレビュー中の人がいるなら一声かけてください。

もう 1 つ、間接的に共有履歴を書き換えてしまう経路があります。git pull の既定動作を rebase に変える設定です。

ターミナル

$ git pull --rebase

これ自体は、自分のローカルのコミットを最新のリモートの上に載せ直すだけなので安全です。まだ push していないコミットが対象だからです。ただし、すでに push 済みのコミットが混ざっている状態で使うと、それらも作り直されます。push した後は pull --rebase を打たないと覚えておけば事故りません。

コンフリクトが出たときの進め方

rebase はコミットを 1 つずつ載せ直すので、途中で止まることがあります。

ターミナル

$ git rebase main Auto-merging index.html CONFLICT (content): Merge conflict in index.html error: could not apply 2f8b06d... ログインフォームの雛形を追加 Resolve all conflicts manually, mark them as resolved with "git add/rm <conflicted_files>", then run "git rebase --continue".

マーカーの読み方と直し方は コンフリクトを解決する と同じです。ただし完了のコマンドが違います。git commit ではなく git rebase --continue です。

ターミナル

$ git add index.html $ git rebase --continue Successfully rebased and updated refs/heads/feature.

コミットが 3 つあれば、最悪 3 回この解決が発生します。merge なら 1 回で済んだ衝突を複数回さばくことになるので、コミットが多いブランチの rebase は覚悟が要ります。

途中でやめたくなったら、開始前の状態に完全に戻せます。

ターミナル

$ git rebase --abort

--abort で戻る先は rebase を始める前です。作業は 1 つも失われません。判断に迷ったらまず abort してください。

コマンド使う場面
git rebase main作業ブランチを最新の main の上に載せ直す
git rebase --continueコンフリクトを直して git add した後、続きを進める
git rebase --abort開始前の状態に戻して撤退する
git rebase --skipいま載せようとしているコミットを捨てて次へ進む
git pull --rebasepull のときに merge ではなく rebase で取り込む

--skip はコミットを 1 つ丸ごと捨てるので、意味を理解していないうちは使わないでください。

いま何番目を処理しているかは git status に出ます。

ターミナル

$ git status interactive rebase in progress; onto 7e2d804 Last command done (1 command done): pick 2f8b06d ログインフォームの雛形を追加 Next command to do (1 remaining command): pick 8f3a91c ログインのバリデーションを追加

残りが何個あるか分かるので、あと何回コンフリクトに向き合うのかの見当がつきます。3 回以上残っていて内容も重いなら、いったん --abort して merge に切り替えるほうが早いこともあります。手段は目的ではないので、こだわらずに乗り換えてください。

コンフリクト解決中の --continuegit commit と打ち間違えると、rebase の途中のまま別のコミットが増えて状況が複雑になります。rebase 中は完了コマンドが違う、と意識しておいてください。

やってしまったときの戻し方

rebase が完了したあとで「元に戻したい」と気づく失敗はよくあります。作り直す前のコミットも、しばらくはリポジトリに残っています。git reflogHEAD が動いた履歴をすべて記録しているので、そこから rebase 直前の位置を探します。

ターミナル

$ git reflog 9c4e71a HEAD@{0}: rebase (finish): returning to refs/heads/feature 2f8b06d HEAD@{1}: rebase (pick): ログインフォームの雛形を追加 7e2d804 HEAD@{2}: rebase (start): checkout main 8f3a91c HEAD@{3}: commit: ログインのバリデーションを追加

rebase (start) の 1 つ下、HEAD@{3} が rebase を始める直前の位置です。そこへ戻します。

ターミナル

$ git reset --hard HEAD@{3} HEAD is now at 8f3a91c ログインのバリデーションを追加

rebase の直前位置は ORIG_HEAD にも自動で記録されるので、git reset --hard ORIG_HEAD でも同じ結果になります。ただし ORIG_HEAD は他の操作でも上書きされるため、確実なのは reflog です。詳しい使い方は 消したはずの作業を取り戻す(reflog) にあります。

--hard は作業ツリーごと戻すので、コミットしていない編集は消えます。実行前に git status がきれいなことを確かめてください。

この章のポイント
  • rebase は「コミットを作り直して別の土台に載せ直す」操作。ハッシュは必ず変わる
  • 用途は自分の作業ブランチを最新の main に追いつかせることに絞る
  • 他人が持っている履歴は書き換えない。main や共有ブランチの rebase は禁止
  • 迷ったら git rebase --abort、完了後の後悔は git reflog から reset --hard で戻す