6
3

Git の Interactive rebase で自在にコミット履歴を操る

Last updated at Posted at 2023-12-06

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
Rebasing (1/3)
                  A'--B'--C' topic
                 /
    D---E---F---G master
                 \
                 A'' HEAD
Rebasing (2/3)
                  A'--B'--C' topic
                 /
    D---E---F---G master
                 \
                 A''-B'' HEAD
Rebasing (3/3)
                  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 した場合や、どこかでコンフリクトした場合、editbreak で止めた場合等、git rebase コマンドが Rebase 途中で止まることがあります。その場合、どうすればいいかは git status でだいたい確認できますが、だいたい git rebase --continue でまかり通ります。

一方で、Rebase を中断して全部やり直したい場合は以下のコマンドを実行すると元通りになります:

git rebase --abort

「ん? コンフリクトが多すぎるな やっぱこの変更したくねえ」ってなったときなんかに便利です!

1. コミットを入れ替える

Todo リストで行を入れ替えると、コミットの履歴上の位置も入れ替えることができます。

Diff
+pick 90abcdef B
 pick 12345678 A
-pick 90abcdef B
 pick 01234567 C

ただし、入れ替えたコミットが別のコミットの変更に依存していた場合(例えば今回の場合、 A で作ったファイルを B が変更している場合)、コンフリクトの嵐となってつらいことになります。

2. コミットを消す

行自体を消すとそのコミットを無かったことにできます。

Diff
 pick 12345678 A
-pick 90abcdef B
 pick 01234567 C

3. コミットをいじる

edit コマンドを使うと、コミットの内容をいじることができるようになります。

Diff
 pick 12345678 A
-pick 90abcdef B
+edit 90abcdef B
 pick 01234567 C

こうすると、B が履歴に追加された後に git rebase コマンドが一旦終了し、好きにコミットを変更できるようになります。この状態で git commit --amend 等をしてコミットの中身自体を編集することができます!変更が終わったら、git rebase --continue でまた Rebase の続きを始めることができます。

4. コミットを分ける

履歴の奥底にあるコミットを分けることを考えてみます。

Diff
 pick 12345678 A
-pick 90abcdef B
+edit 90abcdef B
 pick 01234567 C

こうするとまた B が再度コミットされた直後にコマンドが一旦終了します。ここで、ひとつ前のコミットに git reset します:

$ git reset --soft HEAD~

すると、B でしていた変更がワーキングツリーに入り、HEAD がそのひとつ前の A になります! この状態で、git addgit app -p などを振り回して、git commit でコミットを追加することで、(実質的に) コミット B を分割するということができます!
ふたたび、git rebase --continue を用いることで新しく追加したコミットの上で Rebase の続きをすることができます。

5. コミットとコミットをくっつける

たとえば、(先程の例のような) コミット B の変更内容がコミット A の変更内容の細かい訂正でしか無いような場合、squashfixup を用いてコミットを融合させることができます。

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 の場合

fixupsquash と同じですが、squash の一個前のコミットのコミットメッセージを自動的に使います。 なので……

 pick 12345678 A
-pick 90abcdef B
+fixup 90abcdef B
 pick 01234567 C

とすると、特に何もコミットメッセージの入力が求められずに、自動的に B がコミット A に吸収されるような形になります。

squashfixup を連続させることも可能です。その場合、コミットメッセージは……

  • 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

  1. 本文で触れるほどでは無いながらも重要だったりわりとどうでもいいわたしの考えとかを foodnotes で記述したりしています。参考にした記事等もこちらで参照しています。あまり技術記事を書いたことがないので、至らない点が多くあるかと思いますが、コメントや編集リクエスト等いただけますと嬉しいです!

  2. 今アドカレ公開日の夜で、しかも電車の中です。やばいです!!

  3. https://git-scm.com/docs/git-rebase 2 3

  4. Branch (枝) という名前をしていますが、違います! どちらかというと、"分岐" という意味としての branch が正しい気がします。

  5. 「単純な」「簡単な」「簡易な」等とも読み替えられます。たしかに、実態はコミットのハッシュが書いてあるテキストファイルなだけなので、とても簡素です

  6. あるブランチにあるコミットというのは難しいかもしれません。 master にはなくて、topic にはあるブランチを master の先頭に持ってくるというのが正しい理解な気がします。

  7. Rebase 前にユーザにどうするかの確認が入るので Interactive という名前なんだと思います!

  8. たぶんあまり正確に書きすぎて煩雑になってもしょうがないので省略していたんだと思います。でも大事なはなし

  9. Lazygit という Git を TUI 上で管理できるツールがありますが、これにも reflog を漁る機能がついています。漁る上で結構便利なので、おすすめです! (jesseduffield/lazygit)

6
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
3