未来エンジニア養成所Blog

プログラミングを皆に楽しんでもらうための情報をお届けします。

【Git&GitHub】リベースで変更履歴を修正する(rebaseコマンド)

title


リベースする

リベースというのは、変更を統合するときに、履歴をきれいに整えるために使うものになります。


今まで変更を統合する、つまり、変更を取り込むというのはmergeコマンドを使ってきました。

実は、変更を取り込むやり方にはマージ以外にもあって、そのやり方がリベースになります。


では、リベースのコマンドを見ていきましょう。

コマンドは

$ git rebase <ブランチ名>

です。


このコマンドをすると何が起こるかというと、ブランチの起点となるコミットを別のコミットに移動します。


イメージで確認していきましょう。


今、状況として「コミット1」からmainブランチとfeatureブランチが分岐していて、mainブランチの方のコミットは「コミット2」、featureブランチの方のコミットは「コミット3」とコミットが分岐している状態を想定します。

rebase1


そして、今自分はfeatureブランチにいるとします。

featureブランチにいて、featureブランチの方に最新のmainブランチの内容を取り込みたいという状況だとします。


そうした場合、今まではmainブランチの変更分を取り込むために「git merge main」というコマンドを使って、マージすることで変更分を取り込んでいました。

今回はリベースを使うことで、この変更分を取り込んでみます。


そのためのコマンドは

$ git rebase main

になります。

すると、featureブランチがmainブランチの後ろに移動します。

rebase2


これは何が起きているのかというと、「コミット3」の親コミットが今まで「コミット1」だったのですが、それが「コミット2」に変わりました。

つまり、featureブランチの親となるコミット(起点となっていたコミット)が「コミット1」から「コミット2」に変わったということです。


これは何を表しているかというと、mainブランチの変更分、つまり「コミット2」の変更分がfeatureブランチに取り込まれたと言う事になります。


もう一度ここでよく図を見てください。

「コミット3」は薄くなっていて、これは消えているという意味です。

コミットグラフをよく見ると、「コミット3’」、「コミット2」、「コミット1」という順につながっています。

つまり「コミット3’」の親コミットが「コミット2」になっています。

「コミット3’」の親コミットが「コミット2」になることで、「コミット2」の変更分というのをfeatureブランチに取り込んでいます。

しかも、マージコマンドと違うのは、ただmainブランチの変更分を取り込んでいるのではなく、コミットの履歴を一直線にしています。

「コミット3’」、「コミット2」、「コミット1」とコミットの履歴が一直線に綺麗に整理されています。

つまりrebaseコマンドを使うことで、他のブランチへの変更分を自分の中に取り込みながら、しかもこのように履歴を綺麗に整えるということができているのです。


リベースの作業

それではリベースの一連の作業の流れを見てみましょう。

まず状況として、今featureブランチとmainブランチの2つがあるとします。

リベースの作業1


自分はmainブランチにいて何か作業をしてコミットしたとします。

リベースの作業2


すると、「コミット2」ができます。

その状況でfeatureブランチの方に自分のいるブランチを戻して、作業をしてコミットします。

リベースの作業3


すると「コミット3」というコミットができました。


この状況でmainブランチの方にfeatureブランチの内容を取り込みたいとします。

今まででしたら

$ git merge feature

をすれば良かったです。

ですが今回は、ただ内容を取り込むだけではなく、履歴も綺麗にしたいので、rebaseコマンドを使った場合をみてみましょう。


その場合どのようにすれば良いのかというと、まずfeatureブランチに移動します。

$ git checkout feature

その上で

$ git rebase main

コマンドを実行します。


リベースの作業4


すると「コミット3」が移動して、「コミット2」の方を指し示すように移動します。

これは「git rebase main」コマンドを打つことで、「コミット3」の親コミットが「コミット1」から「コミット2」に変わったということです。

その結果として「コミット3’」という新しいコミットができて、今まで存在していた「コミット3」は消えました。


featureブランチは「コミット3’」の方を指し示しています。

この状況にした上で、次はmainブランチの方に今自分がいるブランチを切り替えます。

$ git checkout main

です。

そして、その上で

$ git  merge feature

をします。


リベースの作業5


すると、mainブランチが指し示すコミットが「コミット3’」の方に移動します。

今回の状況だと、マージしても新しいマージコミットは生まれません。


なぜかというと、featureブランチが指し示す「コミット3’」というのが「コミット2」の直接の子供のコミットだったので、Fast-forwardが起こっています。

Fast-forwardはマージのところで解説しています。

Fast-forwardは自分のコミットのブランチの変更分を取り込む時、マージコミットを作る必要がないので、自分のブランチが指し示すコミットを、そのコミットの方に移動させることでした。

今回はFast-forwardが起こって、マージコミットは作成されずにmainブランチが指し示すコミットが「コミット2」から「コミット3’」に移動しています。


全体図を見て頂くと「コミット3’」、「コミット2」、「コミット1」というのが一直線のコミットになっています。

しかもmainブランチにはfeatureブランチの変更分であった「コミット3’」の内容も取り込まれています。


このようにリベースを使うことで、履歴を一直線にした上で、他のブランチの変更分を取り込むことができています。


rebaseコマンドとmergeコマンドの違い

rebaseコマンドとmergeコマンドの違いについて見ていきます。


まずrebaseコマンドは今まで見てきたとおりです。


リベースコマンド


これは先ほどの1個前の図をそのまま貼り付けたものになります。

ちなみに、なぜリベースというかというと、featureブランチの親となるコミット、つまりベース(起点)となるコミットをリベース(新しく)しているからリベースコマンドと呼びます。


では、これとmergeコマンドはどのように違うのか。

mergeコマンドの場合は、今mainブランチとfeatureブランチがあって枝分かれしているとします。


マージコマンド


その状況でmainブランチの方にfeatureブランチの変更内容を取り込んでいきます。

そのためにはまず

$ git checkout main

でmainブランチに移動します。

そして

$ git merge feature

を使うことで、featureブランチの内容をマージしていきます。

すると、何が起きるのか。


mergeコマンドの場合は「コミット4」というマージコミットが作成されます。


リベースコマンド


この「コミット4」は「コミット2」と「コミット3」を親コミットとして指し示しています。


ここで、rebaseコマンドとmergeコマンドの結果を見比べて欲しいのですが、共通している点はどちらのmainブランチもfeatureブランチの内容を取り込んでいると言うことです。

その上で違いは、rebaseコマンドの方が履歴が一直線になっているのに対して、mergeコマンドの方は履歴が枝分かれしているという点です。

リベースとマージの違いというのは、履歴が一直線なのか、履歴が枝分かれしているかという点です。

ですので、他のブランチの内容を取り込みたくて、しかもその時に履歴を綺麗に整えたい時はrebaseコマンドを使うようにしてみてください。


実際にやってみよう

それではターミナルで実際に作業をしてみましょう。

いつも使っている「git_tutorial」ディレクトリに移動します。


今いる自分の状況を確認してみましょう。

$ git status
On branch main
nothing to commit, working tree clean

なにも変更が無いことを確認します。


今いる自分のブランチも確認しておきます。

$ git branch
* main

するとmainブランチしか無いことが確認できました。


ではこの状況でfeatureブランチを作ってみましょう。

$ git branch feature

これでfeatrureブランチが作られました。


では作られたか確認してみます。

$ git branch
  feature
* main

featureブランチが作られています。


では、今回mainブランチとfeatureブランチそれぞれで作業して枝分かれさせて上で、rebaseコマンドを使ってfeatureブランチの変更分をmainブランチの方に取り込んでいくと言うことをやってみましょう。


mainブランチの方で新規ファイルを作成してみます。

ファイルには「<p>main 2</p>」と記載します。

ファイル名はmain2.htmlとします。

作成できたら、この変更分を記録していきます。

$ git add .

$ git commit -m 'main2を新規作成'
[main b638345] main2を新規作成
 1 file changed, 1 insertion(+)
 create mode 100644 main2.html

コミットされました。


この状況でfeatureブランチに切り替えて、また作業してコミットしていきましょう。

$ git checkout feature
Switched to branch 'feature'

こちらで作業していきます。


新規ファイルを作成します。

ファイルには「<p>feature 2</p>」と記載します。

ファイル名はfeature2.htmlとします。

作成できたら、この変更分を記録していきます。

$ git add .

$ git commit -m 'feature2を新規作成'
[feature 5920d1e] feature2を新規作成
 1 file changed, 1 insertion(+)
 create mode 100644 feature2.html

コミットされました。


ここまででmainブランチとfeatureブランチそれぞれで別々の作業をしてコミットして、コミットが枝分かれした状態になりました。


ではここからmainブランチの方にfeatureブランチの内容をrebaseコマンドを使って取り込んでいきましょう。

そのためにまずfeatureブランチの方にmainブランチの内容を取り込んでいきます。

今自分のいるブランチはfeatureブランチのはずですので、ブランチの移動はしなくて大丈夫です。

$ git rebase main
Successfully rebased and updated refs/heads/feature.

するとリベースしたよと言う内容が表示されます。


リベースできたのか、実際に確認してみましょう。

$ git log --oneline
966020f (HEAD -> feature) feature2を新規作成
b638345 (main) main2を新規作成
c378922 (origin/main) Merge pull request #2 from piketa/github_flow
b3864bb (origin/github_flow) GitHub Flowを追記
52fd079 Merge pull request #1 from piketa/pull_request
1a878bc (origin/pull_request) pull requestを追記
     :
     :

すると今自分がいるfeatureブランチの最新コミットというのは「feature2を新規作成」というコミットでした。

そのコミットの親コミット(2個目のコミット)のコミットメッセージが「main2を新規作成」となっています。

これはmainブランチで作業した分の変更内容です。

つまり、featureブランチが指し示す親コミットというのが、mainブランチが指し示しているコミットの方に切り替わったことになります。

リベースされているということです。


このようにrebaseコマンドを使うことで、mainブランチの変更内容を取り込みながら履歴を一直線にすることができています。


ではこれでfeatureブランチの方にmainブランチの内容を取り込めたので、次にmainブランチの方にfeatureブランチの内容をFast-forwardで取り込んでいきます。


まずmainブランチの方に切り替えます。

$ git checkout main
Switched to branch 'main'

mainブランチに移動したらFast-forward(マージ)していきます。

$ git merge feature
Updating b638345..966020f
Fast-forward
 feature2.html | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 feature2.html

ここまでがリベースの一連の作業の流れになります。

このようにリベースすると変更内容を取り込みながら履歴も綺麗に保つことができます。


では最後に今回の内容を「git push」でGitHubにアップして終了しましょう。

$ git push origin main
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 8 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 582 bytes | 194.00 KiB/s, done.
Total 6 (delta 2), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (2/2), completed with 1 local object.
To github.com:piketa/git_tutorial.git
   c378922..966020f  main -> main


mainブランチの内容をGitHubにプッシュしたら、残っているfeatureブランチはmainブランチの方に取り込んでいるので、featureブランチは削除しておきましょう。


まずブランチがどのような状況か確認しておきます。

$ git branch
  feature
* main

featureブランチがあります。


ではこのブランチを削除します。

$ git branch -d feature
Deleted branch feature (was 966020f).

これでブランチを消しました。


ブランチが消えたか確認しましょう。

$ git branch
* main

確かに消えています。


これでお掃除もできたので、今回はここまでになります。


参考図書



独学で挫折しそうになったら、オンラインプログラミングスクール
未来エンジニア養成所Logo



あわせて学習したい

phoeducation.work