こんにちは、エンジニアの王です。今回は、Git初心者を悩ませるリベースについて解説してみたいと思います。
- リベースが初耳
- リベースを聞いたことはあるけど、使っていない
- 不安を抱えながらも、リベースをなんとなく使っている
上記に当てはまる方は、ぜひ読んでくださいね。
目次
リベースで何ができる?
コミットが綺麗になる! 以上です! この一言に尽きる!
具体的にどのように綺麗になるかというと……
- コミット履歴がわかりやすくなる
- コミットメッセージを後から変える
- コミットの順序を後から変える
- 2つ以上のコミットを1個に統合する
- 一度コミットした内容を編集する
といった具合でしょうか? 整理整頓が好きな方は、ぜひリベースを使いこなしていただきたいと思います!
マージとリベース
2つのブランチの変更点を統合するとき、Gitの最も一般的なやり方は、マージとリベースを使うことです。マージは初回で説明したので、今回は「リベース」を掘り下げていきましょう。
仮に下記のようなコミット履歴を持っているレポジトリがあるとします。
masterをメインブランチにして、コミットCの時点で、「topic」ブランチを切って、masterと平行して、新機能を開発する、というシチュエーションにしましょう。
マージの場合
checkout master && git merge topic
topicブランチのほうで新機能開発が完了して、マージを使って、masterブランチに取り込むと履歴図はこうなります。
リベースの場合
一方、リベースを使うとこうなります。
checkout topic && git rebase master
履歴図を見てわかる通り、ここでは、最終的には全く同じ統合結果となります。しかし、履歴の残り方が違いますよね。リベースはマージと比べて、もっと単純明快な直線的な履歴になるので、開発履歴がとても読みやすくなるというメリットがあります!
上記の例だと極めて単純化しているので、特にメリットを感じてもらえないかもしれませんが、複数人で同時開発するときに力を発揮します!
cherry-pick
リベースを理解する前に、「cherry-pick」というリベースの簡易版のようなコマンドを知っておくと理解しやすくなると思いますので、先にcherry-pickというコマンドを覗いてみましょう。
「cherrypick」の元々の意味は、熟したサクランボとそうでないものを選んでわけることで、そこから転じて、「いいとこ取り」「気に入ったものだけを選ぶ」という意味になっています。
Gitでは、サクランボではなく特定のコミットだけをピックアップして、現在いるところ(HEAD)に適用するという意味になります。
以下の例を見てみましょう。
HEADが[F]にいる状態で、git cherry-pick E
を実行すると、コミットEのコピーが作られ(仮にE`と記述しましょう)……
[F]のすぐ後に追加することが可能です。
もし、topicの全ての変更点をmasterに取り込もうと思うなら、git cherry-pick D E J
と打てば、上図のようになります。
用済みになったtopicブランチをgit branch -D topic
で削除すると、あたかもtopicというブランチが存在しなかったかのように履歴が一直線になって、スッキリします。
リベース
さて、上記例の作業をおこなうとき、リベースを使うともっとパワフルかつ簡単にできるのです!
と言うのも、HEADがtopicにいることを確認してgit rebase master
と打てば、git cherry-pick D E J
と同等の結果が得られます。
※HEADがtopicにいないなら、git checkout topic でチェックアウトしておくこと。
リベースで何が起きた?
では、git rebase master
で一体何が起きたのか、おこなわれた処理を順に見てみましょう。
- 現在のブランチ(D,E,Jコミット)でおこなわれた変更を一時的に保存
- 移行先のブランチ(master)にリセットする(
git reset --hard master
) - 1. で一時的に保存したコミットを順番に適用していく(親が違うので、コミットIDも変わる)
つまり、リベース(rebase)とは、「re+base」でベースとなるブランチを変えるということですね。
git checkout master && git merge topic
最後に、masterのほうでfast-forwardマージをおこなえば、綺麗な履歴となります。
※いわゆる「fast-forwardマージ」というのは、マージコミットを作らずに、HEADを移動するだけでのマージのことです。
リベースコマンドの基本形
git rebase BaseBranch
<BaseBranch>
に、現在いるブランチでおこなった全てのコミットを適用します。
※BaseBranchを指定してない場合、branch.<name>.remote
と branch.<name>.merge
の値を使ってください。
git rebase BaseBranch Branch
<Branch>
が指定されている場合、まずブランチにcheckoutをおこなってからリベースします。
つまり、以下のコードと同等の意味になります。
git checkout <Branch> && git rebase <BaseBranch>
git rebase --onto
基本形は以上で紹介しましたが、もうちょっとトリッキーな事例を考えてみましょう。
subtopicブランチでおこなったコミット(H,I,G)をmasterに取り入れたら下図のようになりますよね。
これを実現するためには --onto
オプションを使います:
git rebase --onto master topic subtopic
どういう意味かというと、
- subtopic ブランチにチェックアウト。
- topicとsubtopicの共通祖先(D)までのコミット(H,I,G)を一時保存。
- masterブランチにリセットする。
- 2. で一時保存したコミットを順番に適用。
ということになりますね。
ここで間違えて、git rebase master subtopic
と実行すると、コミットDまで含まれることになっちゃうので、気をつけましょう。
リベースのinteractiveモード
オプションをつけると、いわゆる「interactiveモード」なリベースがおこなえます。
rebase --interactive
or rebase -i
このinteractiveモードを使うと、いろんなことが簡単にできちゃいます!
「rebase -i」で何ができる?
実例で説明しましょう。
現在のコミット履歴は上図のとおり。git rebase -i <commit>
で、現在のブランチにある<commit>以降のコミット(コミットとマージコミットを含まない)を取り上げて、エディタが立ち上がります。
git rebase -i 1e2df67
を実行したすると、画面が上図のようになります。
1行目に「pick faf09e9 B」とあるように、コミットIDの前にコマンドが書かれています。デフォルトでは「pick」コマンドになっていますよね。コメントのところにある説明を見てもわかるように、以下のコマンドが使えます。
- pick:コミットを採用
- reword:コミットを採用するがコミットメッセージを変更
- edit:コミットを採用するがファイルを修正する
- squash:一個前のコミットと合体させる
- fixup:コミットメッセージを変更しない点以外squashと同じ
- exec:shellでコマンドを実行する
reword:コミットメッセージを変える
ご存知のとおり、直前のコミットメッセージを変えるのにはgit commit --amend
が使えます。ただ、あくまでも「直前のコミット」という条件付きのみに有効です。
3個くらい前のコミットメッセージを変えたい場合は、リベースを使います。
コミットメッセージを変更したいコミットの先頭をpickから「reword」あるいは「r」に変えます。
保存するとまたエディタが立ち上がるので、ここで先ほど選んだコミットのメッセージを編集します。
logしてみて、結果を確認します。
squash:一個前のコミットと合体させる
例えば、直近2つのコミットを1個のコミットにしたい場合、git reset --soft HEAD~1 && git commit -m "合体コミット!"
とreset --soft
を使って実現することができますよね。
しかし、rebase -i
を使うと、もっとわかりやすい、かつ柔軟で簡単にコミットを統合できます!
今度はコミットの先頭をsquash(s)にしてみます。squashが付けられたコミットは、上図が示すように、1つ前のコミットと結合するようになります。
保存したら、コミットメッセージの確認画面が立ち上がりますので、そこでコミットメッセージを変えられます。
終わってlogで確認すると、ちゃんと統合されているのがわかります。
うまくリベースできないときに使うコマンド
リベースで衝突が起きたりすると、リベース作業が一旦中止されます。このとき、下記のコマンドが使えるようになります。
git rebase --continue
:衝突などを解決したあとに実行してリベースを続行するgit rebase --skip
:エラーを無視するgit rebase --abort
:リベースをやめる
リベースを使う上での注意点
では、最後に注意点だけご説明しておきたいと思います。
パブリックレポジトリのコミットはリベースしちゃだめ!
パブリックレポジトリにプッシュしたコミットをリベースしてはいけません!
1人で作業する場合はともかく、複数人での作業の場合、リモートにあるレポジトリを無理矢理リベースしたら、全く同じコミットが重複する可能性大です。
リベースは特定のコミットをどこか別のコミットの後ろに移動するだけの作業に見えるかもしれませんが、実際は移動ではなく、新規コミットを作っているので、当然コミットIDも変わります。したがって、他の開発者のローカルにあるコミットをリベースしちゃうと、コミットIDが変わり、すでに存在している同じコミットと区別がつかなくなり、重複してしまいますよね。
ですから、他人のコミットを決してリベースしてはいけないのです。それでも、どうしてもリベースしたい場合は、事前に相手と相談しておくようにしましょう!
リベース操作を元に戻す方法
リベースに限った話ではありませんが、Gitでおこなったあらゆる操作をundoできる、いわばタイムマシンみたいな万能の機構「reflog」があります。Gitでおこなったあらゆる操作を履歴に残してくれているので、リベースだろうがリセットだろうが「reflog」を使えばどんなコマンドでも確実に戻せます。万が一リベースでしくじっても、怖くないのだ!
git reflog
で操作歴を見て、戻したいポイントを見つけたらgit reset --hard
でリセット! これでおしまいです。
※reflogが導入されるまでgit reset --hard ORIG_HEAD
を使って元に戻すのが一般的でした。ただし、ORIG_HEADはリベースコマンドの直前を指しているため、リベースのあとでさらにコミットすると使えなくなります。
まとめ
以上、Gitのリベースについて、ざっと説明しました!
コード潔癖症じゃなくても、共同開発のときに他の開発メンバーが履歴を辿りやすいように、履歴は綺麗に残したいものですよね。マージも便利ですが、場合に応じてリベースを活用するとぐっと履歴が読みやすくなりますよ。
次回は1週間後、リセットについて解説していきます。お楽しみに!
LIGではエンジニアを募集しています!
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。