← 解説「rebase — なぜ履歴が「書き換わる」のか」に戻る
サンプルコードで身につける: rebase — なぜ履歴が「書き換わる」のか
解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1rebaseでハッシュが変わる瞬間(実機の記録)
実際にrebaseして前後を記録した出力です。変更内容(notes.txt を1行追加)は全く同じなのに、parentが変わったためコミットのIDが別物になっています。これがrebaseの本質です。
# rebase する前
$ git rev-parse HEAD
834a495cf6979b968683d61dc22f2a654f2214c6
$ git rev-parse HEAD^
dd2818b6f87da96757d1cd70f43e293660aece49 ← 親は dd2818b
$ git rebase main
# rebase した後
$ git rev-parse HEAD
bacb6891d489f7fbf0c0c81ca4464a4b77539306 ← 別のコミットになった
$ git rev-parse HEAD^
459f52836739f6ed2cd608b98559f1662b75aa61 ← 親が付け替わった
# 変更内容は同じ
$ git show --stat --format='' HEAD
notes.txt | 1 +
# → 差分は同じでも、parentが違うのでハッシュが変わる
2なぜハッシュを維持できないのか
commitのIDは parent 行を含むテキスト全体から計算されます。だから親を付け替えると、必ず別のIDになります。「変えないでほしい」と願っても原理的に無理だと計算で確認できます。
Python
31つ書き換えると、その先が全部作り直される
3つ連なったコミットの真ん中を書き換えると、それ以降も芋づる式に変わります。rebase -i で古いコミットを編集したときに「そこから先が全部別IDになる」理由がこれです。
Python
4古いコミットは消えていない
rebase後も、元のコミットはオブジェクトDBに残っています。参照されなくなっただけです。reflogでハッシュを見つければ、いつでも元の状態に戻せます。
# rebase後、古いコミットはどうなったか
$ git cat-file -t 834a495cf6979b968683d61dc22f2a654f2214c6
commit ← まだDBに存在する
# ただし、どのブランチからも辿れなくなっている
$ git branch --contains 834a495
(何も出ない)
# reflog を見れば、HEADが過去に指した先が分かる
$ git reflog
bacb689 rebase (finish): returning to refs/heads/topic
bacb689 rebase (pick): メモを追加
459f528 rebase (start): checkout main
834a495 commit: メモを追加 ← rebase前の状態
# 元に戻したいとき
$ git reset --hard 834a495
5mergeとrebaseで履歴の形がどう変わるか
同じ作業をmergeした場合とrebaseした場合で、履歴の見え方を並べます。どちらが正しいという話ではなく、「既存コミットを残すか、作り直すか」という性質の違いです。
# ── merge した場合 ─────────────────
$ git log --graph --oneline
* 9f3c1a2 Merge branch 'feature'
|\
| * e4d92c8 ログイン処理を追加 ← IDは変わらない
* | 0361ab2 ログアウト処理を追加
|/
* dd2818b READMEに使い方を追加
# 分岐と合流が事実として残る。既存コミットのIDは不変
# ── rebase した場合 ────────────────
$ git log --graph --oneline
* 7b2e5f1 ログイン処理を追加 ← e4d92c8 から作り直された
* 0361ab2 ログアウト処理を追加
* dd2818b READMEに使い方を追加
# 一直線で読みやすい。ただしIDは別物になっている
# → pushしていない自分だけのブランチなら rebase して良い
# 共有済みブランチは merge にする