はじめに
こちらの記事では、今まで GitHub Desktop に頼り切っており、最近漸く Git のコマンドに手を出した私が、rebase について勉強したこと・実際に rebase を実施した結果をメモとして残します。
rebase とは
Git の歴史を改変するコマンド
- 改変例
- コミットメッセージを変更(reword)
- コミットを修正(edit)
- コミットを前のコミットとまとめて、コミットメッセージを変更(squash)
メリット
- Git の歴史が綺麗になる
-
main
ブランチの最新の内容を取り込んだ上で開発を進めることができる - 参考:あなたはmerge派?rebase派?綺麗なGitログで実感したメリット
rebase コマンドのオプションの一例
$ rebase -i
- rebase を開始
- 各コミットに対して、どのコマンド(pick、reword、edit、squash など)を実行するか選択
$ rebase --continue
- rebase を継続
- 使用例
- rebase 時にコンフリクトが発生し、rebase が中断
- コンフリクトを解決
- add と commit を実行し、次に作るべきコミットを生成
-
$ rebase --continue
を実行し、rebase を再開
$ rebase --abort
- rebase を中止
-
$ rebase -i
の実行前に戻る
rebase するときの注意点
push 済みのブランチをリベースしない(特に、複数人で1つのブランチを共有して開発を進めている場合)
- push(リモートの
main
を fast-forward merge で更新)できなくなり、force push を強いられる- 参考:なぜrebase後に強制プッシュをする必要があるのか
- force push により、他人のコミットを吹き飛ばす可能性がある
- ローカルの
origin/main
が指すコミットが、リモートのmain
に存在しなくなり、他の人が困る
試しに rebase してみる
こちらの記事がとても腹落ちしたので、記事の内容を自分で再現してみます。
rabase するための準備
準備の完成形
各ブランチとコミットの作成
main
ブランチ
-
root
コミットを作成$ echo "# rebase_test" > README.md
- push する
main
から feat/1/機能A
ブランチを作成
-
c_1
コミットを作成$ echo "func_A_1" > feat_A.md
-
c_2
コミットを作成$ echo "call feat_A" > call.md
- push する
main
から feat/2/機能B
ブランチを作成
-
c_3
コミットを作成$ echo "func_B_1" > feat_B.md
$ echo "call feat_B" > call.md
- push する
feat/1/機能A
を main
にマージ
-
c_4
コミットを作成-
feat/1/機能A
からプルリクエストを作成し、main
にマージ
-
rabase してみる
-
feat/2/機能B
に移動し、$ git fetch
と$ git rebase -i main
を実行- 今回は
pick
を選択して終了
- 今回は
-
HEAD
とfeat/2/機能B
でコンフリクトが発生-
call.md
を修正 - 今回は
call feat_A
とcall feat_B
の両方を残す
-
-
c_5
コミットを作成- ここまでで、何かに失敗してしまっていたら、
$ git rebase --abort
を実行して1に戻る
- ここまでで、何かに失敗してしまっていたら、
-
git rebase --continue
を実行して rebase を再開し、rebase を完了させる -
1 ~ 4
の結果を確認
テスト(今回はスキップ)
実際の開発では、機能Aが存在する状態で機能Bが正常に動作するか、確認する必要があると思います。
完成した feat/2/機能B
をマージしてもらう
push で失敗してみる
feat/2/機能B
を push したいところですが、実際にはエラーが発生すると思います。
これは、push 済みのブランチをリベースしているからです。
このエラーが発生する原因について、もう少し説明します。
まず、現状では、下の画像のようにリモートの feat/2/機能B
は、ローカルの origin/feat/2/機能B
と同じコミットを指しています。
また、push では、リモートの feat/2/機能B
を fast-forward merge で更新します。
しかし、下の画像から分かるように、リモートの feat/2/機能B
が指すコミット(今回は、ローカルの origin/feat/2/機能B
が指すコミットと同義)から、新しく追加したい feat/2/機能B
が指すコミットまでは、親→子の順で辿ることができません。そのため、push に失敗します。
このような状況では、force push を実行する必要があります。
force push してみる
push に --force-with-lease
を追加して実行します。
下の画像のように、ローカルの feat/2/機能B
と ローカルの origin/feat/2/機能B
が指すコミットが一致すると思います。
feat/2/機能B
からプルリクエストを作成してマージしてもらう
c_6
コミットを作成すると、下の画像のような結果になると思います。
確かに歴史が綺麗。
実際に rebase するときに気になったこと
rebase により発生したコンフリクトを解決した後の $ git diff
の表示結果
予想していた表示結果
- リベース先のブランチのインデックスと、コンフリクト解決後のワーキングツリーの差分を表示
diff --git a/XXXXX.xxx b/XXXXX.xxx
実際の表示結果
- コンフリクトを解決するために適用した全ての修正を表示
-
diff --cc XXXXX.xxx
-
-cc
とは?
-
調べて分かったこと
- rebase によるマージを実行したとき、このような表示になる
- 参考:Combined diff format
-
-cc
は$ git diff
でマージを表示するときのデフォルトのオプション - 結合された差分を生成
-
- 参考:Combined diff format
-
リベース先のブランチの最新のコミット(インデックス)と、コンフリクト解決後の結果(ワーキングツリー)の差分を表示
- 上を実現するためには、
$ git diff HEAD^ HEAD
を実行
- 上を実現するためには、
おわりに
この記事では、最近漸く Git のコマンドに手を出した私が、rebase について勉強したこと・実際に rebase を実施した結果をメモとして残しました。
間違い等があれば、ご指摘いだけると助かります。
初めて git rebase してみる人の参考になればと思います。