Gitの中でもよく使うコマンドの一つに「git rebase」があります。git rebaseは指定したブランチのコミットをとってきたり、既にあるコミットメッセージを変更したり削除したりすることもできる万能なコマンドです。
ただし、何が起こっているかを理解していないとコンフリクトが発生したり、事故ったりします。
ここではgit rebaseとは何か?どんな処理をしているのか?や実際の使い方、コンフリクトが発生したときの対処法についてまとめてます。
git rebaseとは何か?
git rebaseはマージではなく、コミット履歴を移動したり修正、削除したりするコマンドです。
マージ前にコミット履歴をきれいにする目的で使用するのが一般的です。(例えば、git rebaseをしてから、プルリクを出すなど)
ただし、他のブランチから分岐した以降のコミットをとってくることができるので、マージと似たような処理をすることになります。
git rebaseをすると、分岐している他のブランチのコミットを分岐した時点からとってきて、自分のコミットの前に付け足すことができます。
枝分かれの枝を根本からポッキっと追って、本流にくっつけるイメージです。
なお、rebaseとは土台を完全に移行するという意味です。このため元のブランチの内容を根っこから、指定したブランチのコミットログの先端にとりつける処理を指しています。
git rebaseのイメージ図
例えば以下のようにmasterブランチと、分岐したserverブランチがあるとします。
ここで、serverブランチよりもmasterブランチのコミットがだいぶ先に進んでいます。
serverブランチgit rebaseを使うと、masterから分岐したC2以降のコミット(C5~C9’)を取り込むことができます。
git rebase master
serverブランチのコミット履歴の前に「C5~C9’」までが取り込まれました。そして、serverブランチのコミットがその先端にくっついています。
masterブランチはserverブランチよりも遅れることになるので、masterブランチにserverブランチの内容を取り込むためには、git rebaseを行うか、mergeが必要です。
通常はserverブランチからmasterブランチにプルリクを出して、git mergeします。
git rebaseの使い方
git rebaseを使ってコミットをとってくる
git rebaseを使ってコミットをとってくる場合のコードは以下のようになります。
git rebase <とってきたいコミットを持つブランチ名>
例えば以下のようなコミット履歴があるとします。
A test
/
D---E---F---G aa
testブランチにはaaブランチのコミット「F」と「G」が取り込まれていません。
testブランチでgit rebase aaを実行すると、 aaブランチのコミット「F」と「G」をとってきて、自分の分岐後のコミット「A」を「A’」としてその先頭につけます。
test
D---E---F---G---A'
aa
実例
aaブランチから分岐したtestブランチのコミット履歴が以下のようになっているとします。
$ git log --oneline --graph
* 1a097d6 (HEAD -> test) [F]add div
* 6adca49 [A]destroyメソッド & Modal追加
* 61d3b4b [A]ClientEdit.vue
[F]add divというコミットメッセージの最新のコミットが「1a097d6」であることに注目してください。
aaをrebaseすると以下のようになります。
$ git rebase aa
Successfully rebased and updated refs/heads/test.
▼git logでコミット履歴を確認。
$ git log --oneline --graph
* 46fc789 (HEAD -> test) [F]add div
* 0701d9d (origin/aa, aa) [U]docker-compose add webpack port 3035
* a90d4ef [U]content-security-policy(CSP) enable webpack-dev-server
* 6adca49 [A]destroyメソッド & Modal追加
* 61d3b4b [A]ClientEdit.vue
コミット履歴を確認すると、aaブランチの最新のコミット「0701d9d」の上にコミットメッセージ「[F]add div」のコミットがきています。
コミット履歴が変わったことで、コミット番号が「1a097d6」から「46fc789」に変わっていることがポイントです。
git rebaseを使ってコミット履歴を綺麗にしてから統合する
git rebaseではコミット履歴を取り込んだにすぎず、現在のブランチの内容をマージしたわけではありません。git rebaseを使ってコミット履歴を綺麗にしてから統合する手順は以下のようになります。
(1) トピックブランチに移動する。
(2) rebaseして本流ブランチ(masterやmain)の内容を取り込む。
(3) プルリクを出す or 本流ブランチに移動する
(4) プルリクを承認する or 本流ブランチでトピックブランチをmergeする。
rebaseしてからmergeするイメージ
まずは以下のように分岐したトピックブランチと本流のmasterブランチがあるとします。
A---B---C topic
/
D---E---F---G master
topicブランチでmasterをrebaseすると以下のようになります。
topic
D---E---F---G---A'---B'---C'
master
最後に、masterでmergeを実行すれば、コミットを指すポインタが「C’」に移動します。
D---E---F---G---A'---B'---C'
master
topic
実例
rebase前のtopicブランチの履歴は下記のようになっているとします。topicブランチの履歴の中にmasterブランチはありません(分岐しているため)。
$ git log --graph --oneline
* 51328ce (HEAD -> topic) create main.js
* 0547564 edit css
* 3915358 add h5
rebaseを実行するとtopicブランチがmasterブランチの前に移動します。
#rebase実行
$ git rebase master
First, rewinding head to replay your work on top of it...
Applying: create main.js
#ログ確認
$ git log --graph --oneline
* 84df830 (HEAD -> topic) create main.js
* f35df23 (master) edit css
* 0547564 edit css
* 3915358 add h5
mergeで統合すると、masterブランチがtopicブランチの位置に移動します。分岐がないため、綺麗なコミット履歴にすることができます。
#mergeを実行するためmasterブランチに移動
$ git checkout master
Switched to branch 'master'
#merge実行
$ git merge -m "merge rabased branch" topic
Updating f35df23..84df830
Fast-forward (no commit created; -m option ignored)
main.js | 0
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 main.js
#ログを確認
$ git log --graph --oneline
* 84df830 (HEAD -> master, topic) create main.js
* f35df23 edit css
* 0547564 edit css
* 3915358 add h5
仕上げに不要になったtopicブランチを削除して完了です。
$ git branch -d topic
Deleted branch topic (was 84df830).
git rebaseでサブトピックブランチの統合 (rebase –onto)
rebaseの応用編として、サブトピックブランチのみを本流のブランチに統合することもできます。
サブトピックブランチとは、枝分かれから更に枝分かれしたブランチです。これのみを本流に移行する作業です。
–ontoオプションを使って3つのブランチを以下のように指定します。
git rebase --onto <統合先のブランチ名> <トピックブランチ名> <サブトピックブランチ名>
git rebase –ontoのイメージ
git rebase –ontoによる処理のイメージは以下のようになります。
まず、rebase前のコミット履歴は以下のようになっているとします。
H---I---J subtopic
/
E---F---G topic
/
A---B---C---D master
rebase –ontoを使って、サブトピックブランチをmasterの先端にrebaseすると以下のようになります。
E---F---G topic
/
A---B---C---D---H'---I'---J'
master subtopic
–ontoの処理内容
git rebase --onto master topic subtopic
コマンドを実行したときの裏側の処理は以下のようになっています。
- topicとsubtopicの共通祖先(G)までのコミットを一時保存。
- 保存したコミット(H-I-J)を、順にmasterに付け替える。
付け替えるとき、中身は同じでも、過去のコミット履歴の流れが変わるので、新たなコミットが付与されます(ハッシュ値が変わります)。
git rebaseでコミット履歴やメッセージを変更・削除する
git rebaseでコミット履歴やメッセージを変更・削除する方法
git rebaseでは「-i」(または「–interactive」)オプションを使うと、コミット履歴やメッセージを対話型で変更することができます。
interactiveとは対話形モードのことです。
git rebase -i <コミット>
実例
以下のようなログのコメントを変更する場合の例を紹介します。
$ git log --oneline --graph
* 46fc789 (HEAD -> test) [F]add div
* 0701d9d (origin/aa, aa) [U]docker-compose add webpack port 3035
* a90d4ef [U]content-security-policy(CSP) enable webpack-dev-server
* 6adca49 [A]destroyメソッド & Modal追加
* 61d3b4b [A]ClientEdit.vue
* 00093d1 [F]ClientNewからClientForm.vueを切り出し
* a84cfc5 (tag: v2.1) [A]ClientNew.vue
タグ名「v2.1」が付いているコミットより後で作成したコミットを編集したい場合は、以下のコマンドを実行します。(※タグ名ではなく、a84cfc5や@^^^^^^を指定した場合も同じ処理になります)
#-iオプションでコミットを指定
$ git rebase -i v2.1
するとvimエディタが立ち上がり、以下のような内容が表示されます。(VSCodeを使っている場合は、新しいファイルが表示されます)
pick 00093d1 [F]ClientNewからClientForm.vueを切り出し
pick 61d3b4b [A]ClientEdit.vue
pick 6adca49 [A]destroyメソッド & Modal追加
pick a90d4ef [U]content-security-policy(CSP) enable webpack-dev-server
pick 0701d9d [U]docker-compose add webpack port 3035
pick 46fc789 [F]add div
# Rebase a84cfc5..46fc789 onto a84cfc5 (6 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
#
# 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」という文字が表示されているのに注目してください。
これが現在採用しているコミットメッセージという意味です。
これらの内容に対して、コメントアウト(#)がある行以降で表示されているオプションの中から実行したい処理を選びます。
rebase -iのオプション一覧
オプション | 省略表記 | 内容 |
---|---|---|
pick | p | 現在のまま(変更なし) |
reword | r | コミットメッセージを編集 |
edit | e | 指定したコミットまで残す(以降を消す) |
squach | s | 直前のコミットに統合する |
fixed | f | 直前のコミットに統合する。その際このコミットメッセージを消す |
exec | x | shellでコマンドを実行する |
break | b | 処理を中断する |
drop | d | コミットを削除する |
例えば、コミットメッセージを変更する場合は「pick」を「r(reword)」にし、コミットメッセージを編集します。コミットを削q除する場合はd(drop)に書き換えます。
ここでは最新のコミットを編集する指定にして、2つ目と3つ目のコミットを削除します。
pick 00093d1 [F]ClientNewからClientForm.vueを切り出し
pick 61d3b4b [A]ClientEdit.vue
pick 6adca49 [A]destroyメソッド & Modal追加
d a90d4ef [U]content-security-policy(CSP) enable webpack-dev-server
d 0701d9d [U]docker-compose add webpack port 3035
r 46fc789 [F]add div
保存してエディタを閉じます。
すると、「r」に変更したコミットを編集するためのエディタが起動します。(「r」なしで「d」のみの場合はこの処理はありません。)
[F]add div
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date: Wed Jan 26 15:54:37 2022 +0900
#
# interactive rebase in progress; onto a84cfc5
# Last commands done (4 commands done):
# pick 6adca49 [A]destroyメソッド & Modal追加
# reword d1f95ff コミットを変更しました。
# No commands remaining.
# You are currently editing a commit while rebasing branch 'test' on 'a84cfc5'.
#
# Changes to be committed:
# modified: app/javascript/components/Header.vue
#
# Untracked files:
# debug.log
#
以下のコメントアウト(#)より上の部分のメッセージを編集します。
コミットを編集しました。
# Please enter the commit message for your changes. Lines starting
(省略)
保存してエディタを閉じます。
以下のように表示されれば完了です。
$ git rebase -i v2.1
[detached HEAD 9e5ebb1] コミットを編集しました。
Date: Wed Jan 26 15:54:37 2022 +0900
1 file changed, 1 insertion(+)
Successfully rebased and updated refs/heads/test.
ログを確認すると以下のようになっています。
9e5ebb1 (HEAD -> test) コミットを編集しました。
6adca49 [A]destroyメソッド & Modal追加
61d3b4b [A]ClientEdit.vue
00093d1 [F]ClientNewからClientForm.vueを切り出し
a84cfc5 (tag: v2.1) [A]ClientNew.vue
最新のコミットのコミットメッセージが指定した内容に変更され、2番目と3番目のコミット「a90d4ef [U]content-security-policy(CSP) enable webpack-dev-server」と「0701d9d [U]docker-compose add webpack port 3035」が削除されていることがわかります。
また、過去のコミット履歴が変わっているので、最新のコミット番号も「46fc789」から「9e5ebb1」に変更されている点もポイントです。
git conflictが発生した場合の対処法
git rebaseやmergeなど他のブランチの情報を取り込む際にコンフリクト(conflict)が発生して処理を実行できない場合があります。
例えば次のようなエラーが発生します。
エラーの内容
git rebaseでエラーが発生した場合次のようなエラーが内容されます。
$ git rebase origin/master
First, rewinding head to replay your work on top of it...
Applying: [A]article show
Using index info to reconstruct a base tree...
M db/schema.rb
Falling back to patching base and 3-way merge...
Auto-merging db/schema.rb
CONFLICT (content): Merge conflict in db/schema.rb
error: Failed to merge in the changes.
Patch failed at 0001 [A]article show
hint: Use 'git am --show-current-patch' to see the failed patch
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
エラーの内容はやたらと長くて複雑に見えますが、重要な部分は次の2つです。
CONFLICT (content): Merge conflict in db/schema.rb
error: Failed to merge in the changes.
マージする際に、db/schema.rbというファイルでコンフリクトが発生したという内容です。
Merge conflict in <ファイル名>
のファイル名に注目してください。
次に重要なのは最後の4行です。ここにコンフリクトの解決方法が3つ記されています。
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
詳しくは次の章で解説します。
コンフリクトの3つの対処方法
上記3つの対処方法は何をするかによって変わります。
- 自分でコードをいじってコンフリクトを解決する。
- 今回のリベースをスキップする。
- 今回のリベースをキャンセルする。
自分でコードを編集してコンフリクトを解決する
最も王道の解決方法は「自分でコードを編集してコンフリクトを解決する」ことです。
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
実行手順
手順は次の3ステップです。
- コンフリクトが発生しているファイルの内容を修正する。
git add <ファイル名>
で対象のファイルをステージングする。git rebase --continue
を実行する。
対象ファイルの修正
まずはコンフリクトが発生しているファイルを開きます。VSCodeを使っている場合はわかりやすくカラーで何が衝突しているかを教えてくれます。
<<<<<<と====と>>>>>>の間に囲まれている部分がポイントです。
<<<<<<<< HEAD (Current Change)
現在取り込もうとしている内容
=====
自分の変更内容
>>>>>>>> <コミットメッセージ> (Incoming Change)
上側のCurrent Changeの部分が取り込もうとしている内容です。自分のローカルとは違ってレポジトリの内容になるためCurrent Change(現在の変更内容)と言われています。
下側が自分のコミットです。新たに入れ込もうとしているのでIncomming Changeとなっています。
Current Changeの方が正しければ、下側や<<<<<<<といった不要な記号を消して、Current Changeの内容にします。Incomming Changeの場合はCurrent Changeを消します。
両方とも必要な場合はどちらも残します。
要は、編集を加えて最終形にすればOKです。
上記の例ではレポジトリの内容(上側)が正しいので、Current Changeを残して他を削除します。
VSCodeの便利オプション
VSCodeではコンフリクトが発生した時に便利なオプションが用意されています。
コンフリクトしている内容の上に表示されているオプションを選択すると、コードを自動編集してくれます。
オプション | 内容 |
---|---|
Accept Current Change | Current Changeの内容を残す(Incomming Changeや余計な記号は消す) |
Accept Incoming Change | Incomming Changeの内容を残す(Current Changeや余計な記号は消す) |
Accept Both Change | 両方の内容を残す(余計な記号は消す) |
Compare Changes | 左右のウィンドウで変更点を比較する |
それぞれ選択すると次のようになります。
コンフリクト中のコード
<<<<<<< HEAD
ActiveRecord::Schema.define(version: 2021_15_08_103381) do
=======
ActiveRecord::Schema.define(version: 2021_15_08_103372) do
>>>>>>> [A]article events
Accept Current Change
ActiveRecord::Schema.define(version: 2021_15_08_103381) do
Accept Incomming Change
ActiveRecord::Schema.define(version: 2021_15_08_103372) do
Accept Incomming Change
ActiveRecord::Schema.define(version: 2021_15_08_103381) do
ActiveRecord::Schema.define(version: 2021_15_08_103372) do
Comapre Changes
右側のウィンドウを閉じると、元のコンフリクト状態の画面に戻ります。
変更後のファイルをステージングする
ファイルの内容が正しく修正できたら、git add <ファイルパス>
を行います。
$ git add db/schema.rb
この状態でgit status
で状態を確認するとrebaseが進行中であることが表示されます。
$ git st
rebase in progress; onto a170a57c2
You are currently rebasing branch 'test' on 'a170a57c2'.
(all conflicts fixed: run "git rebase --continue")
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
ステージングしたファイルの一覧がずらっと並ぶ
git rebase –continue を実行する
最後にgit rebase --continue
を実行して完了となります。
$ git rebase --continue
Applying: [A]article events
Applying: [WIP]article yearly pv
Applying: [F]article_years model and job
git log でログを確認すれば、まだ取り込んでいなかったコミットが追加されていることが確認できます。
リモートレポジトリにプッシュできない場合
コンフリクトを修正した後に、リモートレポジトリにプッシュしようとすると、エラーメッセージが表示されてpushできない場合があります。
▼エラーメッセージの例
$ git push origin test
To github.com:xxx/yyy.git
! [rejected] test -> test (non-fast-forward)
error: failed to push some refs to 'git@github.com:xxx/yyy.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
これは既にリモートレポジトリに過去のファイルをプッシュしてしまった場合に発生します。
※基本的にコンフリクトを注意されるのは、リモートレポジトリにプッシュした後なので、上記のメッセージがでます。
対処法は、現在ローカルで行った修正内容が正しいので -f オプションを使って強制的にプッシュします。
$ git push origin test -f
Enumerating objects: 65, done.
Counting objects: 100% (65/65), done.
Delta compression using up to 12 threads
Compressing objects: 100% (50/50), done.
Writing objects: 100% (50/50), 7.32 KiB | 1.46 MiB/s, done.
Total 50 (delta 41), reused 0 (delta 0)
remote: Resolving deltas: 100% (41/41), completed with 15 local objects.
To github.com:xxx/yyy.git
+ 69751d9b1...6e98e6c92 test -> test (forced update)
以上で完了です。
最後にgit log --oneline
でコミットログを確認すると、自分のローカルとリモートレポジトリが、取り込もうとしていたブランチ(以下では origin/master)より上に来ていることがわかります。
$ git log --oneline
6e98e6c92 (HEAD -> test, origin/test) [F]article_years model and job
8dbb9dcb5 [WIP]article year pv
11ee776ea [A]article events
a170a57c2 (origin/master) Merge pull request #1422 from xxxx/zzz
今回のリベースをスキップする
コンフリクトした際に表示される2つ目のオプションはgit rebase --skip
です。
これを使うとコミットをスキップできます。
You can instead skip this commit: run "git rebase --skip".
あまり使うことがないオプションかなと思います。
今回のリベースをキャンセルする
コンフリクトした際に表示される3つ目のオプションはgit rebase --abort
です。
git rebase自体を無かったことにできる便利コマンドです。コンフリクトの内容や修正方法がわからない場合はとりあえずgit rebase --abort
をしておくと、無難にコンフリクトなしの状態に戻せます。
To abort and get back to the state before "git rebase", run "git rebase --abort".
比較的よく使うオプションです。