Skip to content

Instantly share code, notes, and snippets.

@hotwatermorning
Last active May 31, 2023 08:49
Show Gist options
  • Save hotwatermorning/2bbae5c9c0a4ad1535cc3017df2faa45 to your computer and use it in GitHub Desktop.
Save hotwatermorning/2bbae5c9c0a4ad1535cc3017df2faa45 to your computer and use it in GitHub Desktop.
Git Rebase 入門

Git Rebase について

Git Rebase とは

リポジトリの歴史を改変できるコマンド。

  • 基本機能としてはブランチの付け替えができる。
  • その他に複数のコミットを一つにまとめたりコミットメッセージを変更したり、もう少し高級なことができる。

改変できると何が嬉しい? => コミットログを整理できる。

Git Rebase の効果

以下のような歴史を考える。

(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 が異なる新たなコミットになる)

Git Rebase のさらなる活用 (Interactive モード)

以下のような歴史を考える

(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 モードについて

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.
#

各行には先頭に pickfixup のようなコマンドを指定する。これによってどのコミットをどのように適用するかを制御する。

主に使うコマンドの意味は以下の通り。

(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

(実際のデモ)

Git Rebase の利点

  • 汚いコミットログを整理できる。
    • 順番がくずれていたり Typo があったりするコミットログが remote の履歴に残るのに抵抗がある人にとって嬉しい。
  • 細かい単位でコミットを行いやすくなる
    • あとで整理できるという心理的安全性
    • ある作業の途中で細かい問題に気づいたとき、作業がコンフリクトしなさそうであれば細かい問題を修正してコミットしてしまえる。(あとで順番や内容が整理できるので)

Git Rebase の注意点

すでに remote に push されているコミットに対しては Rebase してはいけない。

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 作業中にコンフリクトが発生する。これを解消するのは結構手間になることが多い

なのであまり複雑になる場合は、諦めることも大事。

Git Rebase を支援するツールについて

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment