Simple rebase ( -i
なしの rebase ) を取り扱った経験があるけど、interactive rebase で自分好みにコミット履歴を操ったことがない、という人向けに、Interactive Rebase ではこういうことができるぜ、というのを紹介していく記事です1。
この記事はゆめみ 24 卒アドカレの 6 日目です!2 他の方が書かれた記事もぜひご覧ください。
復習
Rebase をするにあたって知っておきたいことについてさらっとまとめています。ここに記されている内容がよくわからない場合は、事前に調べておくとよりよいかと思います!
ブランチや rebase については大丈夫だぜという方は、はじめての Interactive Rebase 節まで飛ばしていただければと思います!
ブランチ
たとえば、次のコマンドラインたちを実行して、Git のドキュメンテーション3 から引用した次のコミットツリーを作り上げたとします。
git checkout -b master
git commit --allow-empty -m "D"
git commit --allow-empty -m "E"
git checkout -b topic
git commit --allow-empty -m "A"
git commit --allow-empty -m "B"
git commit --allow-empty -m "C"
git checkout master
git commit --allow-empty -m "F"
git commit --allow-empty -m "G"
A---B---C topic
/
D---E---F---G master
ブランチというのはこの図のうちどれかというと topic
, master
ですが、じゃあその実体 (= データとしてのすがた) は何かと言うと、図における線自体やコミットの範囲ではなく4、コミットを指し示す軽量 (lightweight) な5ポインタのことをいいます。もっと簡単に言うと、ブランチは特定のコミットを指し示すためのピン📌とかとも言えるかもしれません。
詳細はこちらの記事を参照するとよく分かるかと思います!
Rebase
Rebase はコミットをブランチの先頭に作り直す (Reapply commits on top of another base tip)3 ことらしいです。といってもこれは simple rebase の話で、-i
を使って Interactive rebase を使うと既存のコミットを変更/削除したりすることができます。
Simple rebase について、簡単な Example を Git のドキュメンテーション3から引用して復習します。このようなコミットツリーがあるリポジトリで……
A---B---C topic
/
D---E---F---G master
topic
ブランチにいるときに……
$ git rebase master
を実行すると………
A'--B'--C' topic
/
D---E---F---G master
のように、topic ブランチの履歴上にあるコミットを6 master ブランチの履歴の先頭に持ってくることができます。
rebase する必要がないときに rebase する
先程のように、topic ブランチ上のコミットツリーがすでに master の先頭から生えているときでも、master の上に rebase するということも一応できます。
Simple rebase の場合は、(ほぼ) 特に何も起こりません。いえ、コミットハッシュや Commit date は変わるのですが、肝心のコードベースやコミットツリーには特に大きな影響はありません。
変な話なように聞こえますが、Simple rebase のときは確かにコミットハッシュが変わるだけで深い意味はありませんが (たぶん)、Interactive rebase のときはよくされます。
具体的にどう動く?
このようなブランチを考えます。
A'--B'--C' topic, HEAD
/
D---E---F---G master
ここで、これを実行すると……
git rebase master
A'--B'--C' topic
/
D---E---F---G master
\
A'' HEAD
A'--B'--C' topic
/
D---E---F---G master
\
A''-B'' HEAD
A'--B'--C' (旧 topic)
/
D---E---F---G master
\
A''-B''-C'' topic, HEAD
のように動作し、似たコミットツリーが誕生します。
はじめての Interactive Rebase
Interactive rebase とは、コミットを別ブランチの先頭に乗せるときに、どのように乗せるかを指定できるモードです。
A---B---C topic
/
D---E---F---G master
例えば、こちらの topic
ブランチを master
ブランチの上に Rebase するとします。すでに topic
ブランチは master
ブランチの上に乗っかっていますが……
- 🧑💻「コミット B のメッセージタイポしてもた…… てか、もっと具体的でもっと良いコミットメッセージにしたほうがええなぁ……」
- 🧑💻「コミット C も、コミット B で修正し忘れた typo の修正やから、B とひとまとめにしてまいたいな」
というように、コミット履歴をいじりたくなったときに使える rebase です。
これをやってみる
試しに、以下の変更を履歴に加えてみましょう:
- コミット B のコミットメッセージを別のものに変更する
- コミット C をコミット B に含める
0. コミットツリーを確認する
冒頭に示したスクリプトでコミットツリーを作ると、このようなコミットツリーとなっているはずです:
$ git log --oneline --graph
* 932c987 (HEAD -> master) G
* 188ff71 F
| * 5db0352 (topic) C
| * 47bfc71 B
| * 9e4be7d A
|/
* eb9faa0 E
* b3ac56b D
1. Interactive Rebase を始める
topic
ブランチにいるときに、以下のコマンドを実行すると……
$ git rebase -i master
このようなコンテンツが含まれたファイルが開かれます7:
pick 12345678 A
pick 90abcdef B
pick 01234567 C
# ... 下に多くの説明が含まれている
ここでエディタが開かなかった人は、Linux 系の場合 EDITOR
環境変数の設定が必要かもしれません。こうするとうまくいくかもしれません:
$ export EDITOR=$(which vim)
$ git rebase -i master
また、冒頭に示したコマンドでコミットツリーを作られた方は # empty
というテキストがコミットメッセージの横に出ているかと思います (単語の意味そのままで、Diff がないコミットという意味です)。
2. どのようにコミット履歴を変更するかを設定する
詳しくは後述しますが、このように変更すると上に提案したことを適用できます。
pick 12345678 A
reword 90abcdef B
fixup 01234567 C
# ... 下に多くの説明が含まれている
3. 実際に rebase を開始する
ファイルを閉じてエディタを閉じると、設定した通りの Rebase を開始してくれます。
途中、コミット B のコミットメッセージを編集するための画面が表示されます:
B
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
-m
オプションをつけずに git commit
した後の画面と同じなので、コミットメッセージを設定するいつも通りの方法でコミットメッセージを変更することができます。
4. Rebase 結果を確認する
上のエディタでコンテンツを保存して消すと、以下のようなコミットメッセージが表示されます。これで Interactive Rebase は完了です!
Successfully rebased and updated refs/heads/topic.
コミット履歴を確認すると、以下のようになっているのが分かるかと思います:
$ git log --oneline --graph --all
* 2e5c1c8 (HEAD -> topic) New B
* 1effbf9 A
* a5fea92 (master) G
* 84f96cf F
* 7d8678c E
* d384119 D
わかりにくいですが、つまりこうです:
A---New B topic
/
D---E---F---G master # A を G の横に移動すると、
# 上と似たような感じになります
- コミット B だったもののコミットメッセージが変わっていて、
- コミット C だったものが消えている
ことが分かるかと思います! (コミット C は消えたわけではなく New B
に吸収されたたので消えているように見えています。例がわかりにくい………)
Interactive Rebase でいろいろやる
本題です! Interactive Rebase でコミット履歴をいろいろいじってみます。
基本: todo リストを知る
先程 git rebase -i master
した直後に編集したファイルについてです。
先程、このようなファイルを編集しました:
pick 12345678 A
pick 90abcdef B
pick 01234567 C
# ... 下に多くの説明が含まれている
これは、このような構成をしています:
[操作] [ハッシュ] [コミットメッセージ]
[操作] [ハッシュ] [コミットメッセージ]
[操作] [ハッシュ] [コミットメッセージ]
# ... (# から始まる行はコメント)
todo リストでは、上から下 に向かって新しいコミットが来ます。git log
の順番とは逆です!
操作の定義は、コミットの一覧の下にコメントとして書いてあります。
いろいろやってみる
0. Rebase を続ける/やっぱりやめる
Rebase 中に Ctrl+C した場合や、どこかでコンフリクトした場合、edit
や break
で止めた場合等、git rebase
コマンドが Rebase 途中で止まることがあります。その場合、どうすればいいかは git status
でだいたい確認できますが、だいたい git rebase --continue
でまかり通ります。
一方で、Rebase を中断して全部やり直したい場合は以下のコマンドを実行すると元通りになります:
git rebase --abort
「ん? コンフリクトが多すぎるな やっぱこの変更したくねえ」ってなったときなんかに便利です!
1. コミットを入れ替える
Todo リストで行を入れ替えると、コミットの履歴上の位置も入れ替えることができます。
+pick 90abcdef B
pick 12345678 A
-pick 90abcdef B
pick 01234567 C
ただし、入れ替えたコミットが別のコミットの変更に依存していた場合(例えば今回の場合、 A
で作ったファイルを B
が変更している場合)、コンフリクトの嵐となってつらいことになります。
2. コミットを消す
行自体を消すとそのコミットを無かったことにできます。
pick 12345678 A
-pick 90abcdef B
pick 01234567 C
3. コミットをいじる
edit
コマンドを使うと、コミットの内容をいじることができるようになります。
pick 12345678 A
-pick 90abcdef B
+edit 90abcdef B
pick 01234567 C
こうすると、B
が履歴に追加された後に git rebase
コマンドが一旦終了し、好きにコミットを変更できるようになります。この状態で git commit --amend
等をしてコミットの中身自体を編集することができます!変更が終わったら、git rebase --continue
でまた Rebase の続きを始めることができます。
4. コミットを分ける
履歴の奥底にあるコミットを分けることを考えてみます。
pick 12345678 A
-pick 90abcdef B
+edit 90abcdef B
pick 01234567 C
こうするとまた B が再度コミットされた直後にコマンドが一旦終了します。ここで、ひとつ前のコミットに git reset
します:
$ git reset --soft HEAD~
すると、B
でしていた変更がワーキングツリーに入り、HEAD がそのひとつ前の A
になります! この状態で、git add
や git app -p
などを振り回して、git commit
でコミットを追加することで、(実質的に) コミット B を分割するということができます!
ふたたび、git rebase --continue
を用いることで新しく追加したコミットの上で Rebase の続きをすることができます。
5. コミットとコミットをくっつける
たとえば、(先程の例のような) コミット B の変更内容がコミット A の変更内容の細かい訂正でしか無いような場合、squash
や fixup
を用いてコミットを融合させることができます。
squash
の場合
squash
を使った場合は、途中コミットメッセージの設定の画面になります。
pick 12345678 A
-pick 90abcdef B
+squash 90abcdef B
pick 01234567 C
# This is a combination of 2 commits.
# This is the 1st commit message:
A
# This is the commit message #2:
B
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
この画面で、どちらかを消したり新しい適切なコミットメッセージを設定することができるようになります。
fixup
の場合
fixup
は squash
と同じですが、squash
の一個前のコミットのコミットメッセージを自動的に使います。 なので……
pick 12345678 A
-pick 90abcdef B
+fixup 90abcdef B
pick 01234567 C
とすると、特に何もコミットメッセージの入力が求められずに、自動的に B
がコミット A
に吸収されるような形になります。
squash
や fixup
を連続させることも可能です。その場合、コミットメッセージは……
-
squash
の場合は、squash
が連続しているコミットとその一個前のコミットのメッセージが、コミットメッセージ設定時に利用できます。 -
fixup
の場合は、先頭のfixup
の一個前のコミットのメッセージが使用されます。
6. コミットの情報を自動的にいじる
もっといい方法がありそうですが…… たとえば、メールアドレスを変えたなどで Committer / Author を変えたい場合、次のようなことができます:
pick 12345678 A
pick 90abcdef B
+exec git commit --allow-empty --amend --no-edit --author "Name <example@example.invalid>"
pick 01234567 C
+exec git commit --allow-empty --amend --no-edit --author "Name <example@example.invalid>"
こうすると、変更したい各コミットに対して --author
引数付きの --amend
を自動的にすることができ、各コミットを edit
するよりも人の手を必要とせずに進めることができます!
悪用はしないでください! また、このような悪用から身を守るために GPG 鍵による署名を設定しておくと安心できます。
--allow-empty
は、冒頭のスクリプトでツリーを作った場合各コミットが空なため、つけないとエラーになるために使用しています。普通はいらないはずです!
絶対もっといい方法があるはずなので、どなたかご存知でしたらご教示いただきたいです……!
おわりに
このように、Interactive Rebase を使うといろいろな形でコミット履歴を変更することができます!コミット履歴が整理できると、GitHub の Pull Request で Merge commit を作る形でコミットをしても、main ブランチのコミット履歴が大変なことになることなくマージすることができます。Rebase を積極的に、そして用法を守って活用して良きな履歴を作って、平和な Git ライフを送ることができればと思います!!
Appendix
Rebase に失敗した時
前提: Rebase してもコミットが「消滅する」わけではない
前提として、Rebase をしても元のコミットは消滅しません (が、どのブランチからも参照されない状態になるかもしれません)。例えば、先程の例だと rebase 後はこのようになると説明しました:
A'--B'--C' topic
/
D---E---F---G master
ですが元のコミットは消滅しないため、実際にはこうなっています8:
A'--B'--C' topic
/
D---E---F---G master
\
A---B---C (旧 topic)
なので、元のコミットのハッシュがわかればブランチが参照するコミットを元に戻してブランチを rebase する前の状態に戻すことができます。例えば、コミット C
のハッシュが 12345678
... であった場合、topic
ブランチ上で、
$ git reset --hard 12345678
とすると、
A'--B'--C' (旧新topic)
/
D---E---F---G master
\
A---B---C topic
とでき、topic については元の状態に戻すことができるようになります。
失敗した時用にブランチを取っておく
git branch backup
などで、rebase 前のコミットでブランチを切っておくと、万が一大失敗したときにコミットハッシュの代わりにこのブランチを使って reset することができます。
コミットハッシュを持っていない
失敗すると思っていなかった rebase で失敗したとかでハッシュを持っていなかったとしても、reflog で探せるかもしれません。
git reflog
Rebase の各ステップごとに reflog のエントリが増えるので探すのが少し大変かもしれませんが、このコマンドがあれば確実に元のコミットがどこだったかを探すことができます9。
-
本文で触れるほどでは無いながらも重要だったりわりとどうでもいいわたしの考えとかを foodnotes で記述したりしています。参考にした記事等もこちらで参照しています。あまり技術記事を書いたことがないので、至らない点が多くあるかと思いますが、コメントや編集リクエスト等いただけますと嬉しいです! ↩
-
今アドカレ公開日の夜で、しかも電車の中です。やばいです!! ↩
-
Branch (枝) という名前をしていますが、違います! どちらかというと、"分岐" という意味としての branch が正しい気がします。 ↩
-
「単純な」「簡単な」「簡易な」等とも読み替えられます。たしかに、実態はコミットのハッシュが書いてあるテキストファイルなだけなので、とても簡素です ↩
-
あるブランチにあるコミットというのは難しいかもしれません。
master
にはなくて、topic
にはあるブランチをmaster
の先頭に持ってくるというのが正しい理解な気がします。 ↩ -
Rebase 前にユーザにどうするかの確認が入るので Interactive という名前なんだと思います! ↩
-
たぶんあまり正確に書きすぎて煩雑になってもしょうがないので省略していたんだと思います。でも大事なはなし ↩
-
Lazygit という Git を TUI 上で管理できるツールがありますが、これにも reflog を漁る機能がついています。漁る上で結構便利なので、おすすめです! (jesseduffield/lazygit) ↩