リポジトリの歴史を改変できるコマンド。
- 基本機能としてはブランチの付け替えができる。
- その他に複数のコミットを一つにまとめたりコミットメッセージを変更したり、もう少し高級なことができる。
改変できると何が嬉しい? => コミットログを整理できる。
以下のような歴史を考える。
(1)
develop: A---B-------C
\
feature: D---E---F
これをそのままマージすると以下のようになる。
(2)
develop: A---B-------C---G
\ /
feature: D---E---F
B 時点から D, E, F の変更を行ったという歴史になっているが、ログを見たときに C の変更と D, E, F の変更が混在して分かりにくさがある。(特にブランチがもっと増えた場合に)
このとき、もし D, E, F の変更を C のあとに導入したのでも問題ないのであれば以下のようにできて、よりスッキリしたログになる。
(3)
develop: A---B---C-----------G
\ /
feature: D---E---F
Git Rebase を使うとこれができる。
より正しくは、
(4)
develop: A---B-------C
\
feature: D---E---F
この状態で以下のコマンドを実行すると、
(5)
# feature ブランチをチェックアウト
git checkout feature
# 現在の ブランチを develop ブランチの最新の位置に付け替える。
git rebase develop
このように変換できる。
(6)
develop: A---B---C
\
feature: D---E---F
なので、このあとで以下のように develop ブランチ上でマージ処理を実行すれば (3) の状態が実現できる。
(7)
git checkout develop
git merge --no-ff feature
(注: ここで C に付け替えられた D, E, F のコミットは、元の D, E, F のコミットに対して内容が同じでコミット ID が異なる新たなコミットになる)
以下のような歴史を考える
(8)
develop: A---B-----------C
\
feature: D---E---F---G
ここで D, E, F, G の変更内容は以下のようになっていて、ライブラリに関する変更とクラスに関する変更が混在している。
(9)
D use lib XXX
E impl class YYȲ
F use lib ZZZ instead of XXX
G minor fixes of class YYY
これをそのまま develop ブランチに取り込むと、後でログを見返すときに分かりにくい。 以下のように整理できると嬉しい。
- D と F の変更をまとめたい
- XXX を導入してうまくいかなかったという経験は重要だが、 D の内容をコミットとして残しておく必要がないという場合を想定している。
- もし D の後ですぐに F の作業をやっていたなら
git commit --amend
で直前のコミットの歴史を改変できたのだが、間に E が入っているので今回はそれができない。
- E と G の変更をまとめたい
- G の変更は軽微なものなので、コミットとして残す必要はあまりなくて、 E にその内容を取り込んでしまいたい。
- E のコミットメッセージに Typo があるので直したい
- D-F の変更は C とは無関係なので、 develop ブランチの最新状態から feature ブランチを分岐させたい
つまり以下のような歴史になると嬉しい
(10)
develop: A---B---C
\
feature: D---E
この状態での feature ブランチのコミットメッセージは次の通り
(11)
D use lib ZZZ
E impl class YYY
Git Rebase の Interactive モードを使うとこれができる。
Git Rebase の Interactive モードは、一連のコミットを別のブランチに付け替える作業を設定ファイルで細かく制御できる機能。コミット列の順番を変更したり複数のコミットを一つにまとめたりもできる。
Interactive モードで rebase を行うには、以下のように git rebase コマンドに -i (--interactive) オプションを付ける。
(12)
git checkout feature
git rebase -i develop
これを実行すると vim (あるいは事前に設定されたエディター)が開いて、以下のようにコミットの履歴が表示される。 この画面はどのように歴史を改変するかを指定する設定ファイルになっていて、このファイルを更新してエディターを閉じることで、指定した設定にもとづいて歴史を改変する処理が実行される。
(13)
pick 8a32bd2 D: use dayjs
pick 202d9a1 E: impl MyAppp
pick 94b81f9 F: use date-fns instead of dayjs
pick 2fd18d1 G: minor fixes of MyApp
# Rebase 618988d..2fd18d1 onto 618988d (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# create a merge commit using the original merge commit's
# message (or the oneline, if no original merge commit was
# specified); use -c <commit> to reword the commit message
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
# to this position in the new commits. The <ref> is
# updated at the end of the rebase
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
各行には先頭に pick
や fixup
のようなコマンドを指定する。これによってどのコミットをどのように適用するかを制御する。
主に使うコマンドの意味は以下の通り。
(14)
コマンド | 意味 |
---|---|
pick | そのコミットを使用する |
drop | そのコミットを無視する(なかったことにする) |
reword | コミットメッセージを編集する |
edit | コミットの適用直前の状態で処理を中断し、コミット内容を編集できるようにする |
squash | このコミットの内容を直前のコミットに取り込む。コミットメッセージを新たに編集できる。 |
fixup | このコミットの内容を直前のコミットに取り込む。直前のコミットのコミットメッセージがそのまま使用される。 |
例えばこのようになっている歴史に対して
(15)
pick 8a32bd2 D: use dayjs
pick 202d9a1 E: impl MyAppp
pick 94b81f9 F: use date-fns instead of dayjs
pick 2fd18d1 G: minor fixes of MyApp
E と F の順番を入れ替えて設定ファイルを保存すれば、
(16)
pick 8a32bd2 D: use dayjs
pick 94b81f9 F: use date-fns instead of dayjs
pick 202d9a1 E: impl MyAppp
pick 2fd18d1 G: minor fixes of MyApp
F のあとに E が繋がるように歴史が改変される。 (さらに git rebase コマンド実行時に develop ブランチから繋がるように指定してるので)改変された歴史はこのようになる。
(17)
develop: A---B---C
\
feature: D---F---E---G
(実際のデモ)
- 汚いコミットログを整理できる。
- 順番がくずれていたり Typo があったりするコミットログが remote の履歴に残るのに抵抗がある人にとって嬉しい。
- 細かい単位でコミットを行いやすくなる
- あとで整理できるという心理的安全性
- ある作業の途中で細かい問題に気づいたとき、作業がコンフリクトしなさそうであれば細かい問題を修正してコミットしてしまえる。(あとで順番や内容が整理できるので)
Git Rebase は、一連のコミットに対して連続して cherry-pick を行うような動作をする。なので、 rebase されたコミットは、内容が同じで ID が異なる新たなコミットになる。
つまり、下のような歴史で feature ブランチを develop に付け替えたとき、
(18)
develop: A---B-------C
\
feature: D---E---F
下のように D, E, F と同じ内容でコミット ID が異なるコミットが作られることになる。
(19)
develop: A---B---C
\
feature: D'---E'---F'
これを remote に push してしまうと、他の開発者からは、以前に pull してきたはずのコミットが無くなって、同じ内容の別 ID のコミットが remote 上に存在するように見えて混乱の元になる。
あくまで自分の手元にある push 前のコミットのログを整理するために使用するのが良い。
歴史を改変するときに、それぞれのコミットで編集箇所が被っていると rebase 作業中にコンフリクトが発生する。これを解消するのは結構手間になることが多い
なのであまり複雑になる場合は、諦めることも大事。