APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

Git でrebaseを迷わず活用できるようになる

はじめに

こんにちは、 Cloud Native Group の野本です。

Git のrebaseとは、簡潔に言うと「ブランチの基点(base)を変える」操作のことです。難しいとか、mergeとの使い分けがわからないとか、慣れないうちは避け気味ですが、活用できるようになると Git がとても便利になります。本記事ではこのrebaseの方法について、多くの状況で使える手順を解説します。

利用例

例えば develop ブランチから作業ブランチを切って変更しているうちに、他の変更がマージされて develop が進むことがあります。このまま作業しても問題ないことは多いですが、一方で作業ブランチを最新の develop から「生やし直したい」こともあるでしょう。このようなときにrebaseが使えます。

また、rebaseでは「interactiveモード」を使うと、コミットの取捨選択・統合・並べ替え・コメント変更などの整理全般もできます。そのため、一旦こまめにコミットしておいて、リモートリポジトリにpushする前に「ブランチを整理する」という使い方もできます。

注意事項

リモートリポジトリに存在するブランチは、チーム全員の同意を得てからrebaseしてください。ルール無用で勝手に行うと、rebase前後のコミットが両方残り、かえって履歴が汚くなる恐れがあります。

rebaseに関わる Git の仕組みを理解する

rebaseを迷わず使うには、 Git の仕組みを多少知っておく必要があります。また仕組みを知ると、rebase前の状態に復旧するのが簡単なこともわかり、安心して実行できるようになります。

コミットについて

あらゆる過去のコミットの修正は「代わりの新しいコミットを作る」ことで実現されます。不要になったコミットは、ブランチを消すことで参照困難にして擬似的に消します。

rebaseに必要な知識に関してもっと具体的に見ていきます。

  • それぞれの「コミット」は、直前の状態からの「変更差分」を表します
    • また、最古のコミットから順に辿れば「その時点のバージョン」を表します
  • コミットには全世界で一意のID(ハッシュ値)が付いています
    • 全く同じ変更であっても、時刻や編集者などの付加情報によってIDは変わります
  • 他の人とコミットを共有するには、pushやfetch/pullを実行してローカルリポジトリをリモートリポジトリと同期させます
    • リモートリポジトリにpushしていないコミットは、他の人は参照できません
    • したがって共有したくないコミットは(それが属するブランチを)pushしなければいいです
  • 一度作ったコミットは基本的に消えません
    • コミットIDさえ覚えていれば後からまた参照できます
    • とはいえ、どのブランチにも属していなければ簡単には参照できません

ブランチについて

ブランチは実際に見た方が早いので、コミット履歴をグラフ表示できるようにしておくと良いでしょう。GUIツールなら大抵グラフ表示がありますし、ターミナル上でもコマンドで表示できます。(以下のようにエイリアスを設定すると楽です)

# `git tree` というコマンドでグラフ表示できるように設定する
git config --global alias.tree "log --graph --pretty='format:%C(yellow)%h%Creset%Cred%d%Creset %s%n'"

# 全てのブランチをグラフ表示する
git tree --all

例えば libgit2 のコミット履歴をグラフ表示すると以下のようになりました。

これを見ると、まずコミットによる履歴は一本道でなく分岐できることがわかります。

  • その分岐=「ブランチ」には名前を付けて管理できます
  • checkoutすることで、手元のファイルを管理する履歴を切り替えられます
  • コミットするとブランチが伸びます
  • 分岐するだけでなく、後でブランチ同士を併合(マージ)もできます

その中で、作業中のブランチは HEAD という特別なブランチです。

  • 普段 HEAD は何かしらのローカルブランチを指しています
    • コミットなどすれば指しているブランチも更新されます
  • HEAD がローカルブランチを指していない状態(detached)にもできます
    • この状態でもコミットなどは問題なくできます
    • 他のブランチをcheckoutすると、元のブランチは(名前が無いので)参照できなくなります

実際にrebaseしてみる

rebaseの前準備をする

コミット履歴をグラフ表示したうえで、「自分はこれからどんな履歴を作りたいか」思い描きます。慣れないうちは紙などに実際に描いた方がいいでしょう。

  • どのコミットを基点に新しい履歴を作る(ブランチを伸ばす)か
  • 何番目のコミットではどういう変更を加えているか
    • 既存のコミットのどれをひとまとめにしたらいいか
  • コミットのコメントを変更するか

基本的なコマンドの流れを覚える

とりあえずこの方法だけ覚えておけば大抵なんとかなります。 git rebase の際に -i オプションを付けてinteractiveモードにするのがポイントです。

#--- rebaseしたいブランチをcheckoutする ---#
git checkout ${branch}
git branch ${branch}_backup  # 復元を簡単にするため作成

#--- interactiveモードでrebase開始する ---#
git rebase -i ${new_base}  # ${new_base} は新しい基点(コミットIDやブランチ名)
# エディタで編集する(次節)

#--- conflictなどで止まったら、画面の指示に従う ---#
...
git status  # rebaseを継続できる状態になったか確認する
git rebase --continue

#--- 成功したら、復元用ブランチは消していい ---#
git branch -D ${branch}_backup

conflictなどで止まった場合、画面には何が起きたか・どんな選択肢があり何をすればいいかメッセージが出ます。落ち着いて読んで、目的に合った操作を見つけてください。

rebase内容をエディタで編集する

interactiveモードで実行すれば、エディタが起動して、各コミットを編集する方法を指定できます。思い描いた履歴になるよう、デフォルトの記述を書き換えていきます。

  1. 不要なコミットの行を消します
    • どのコミットを適用するかは自動計算されるので、自分が望んでいないコミットまでrebase対象になっている可能性があります
    • 「デバッグ目的で一時変更」などのコミットもきっと消していいでしょう
  2. コミットを適用したい順に並べ替えます
  3. 各コミットについて編集方法を指定します(エディタの下部にヘルプが書いてあるので暗記不要です)
    • [p]ick: そのまま適用する
    • [r]eword: コメントを編集する
    • [e]dit: 適用前にrebaseを一時停止して、好きに追加編集できるようにする
    • [s]quash: 直前のコミットに混ぜる(コメントは見比べて編集できる)
    • [f]ixup: 直前のコミットに混ぜる(コメントは直前のもののまま)
    • なお、各行のコミットのコメントは書き換えても意味がありません

指定し終わったら保存して(←重要)エディタを閉じると、その通りにrebaseが始まります。

失敗時の復旧方法を知る

予想外にconflictが多かったり、コマンドを打ち間違えていたりと、rebaseがうまくいかず元に戻したくなることもあります。状況により手順は異なりますが、 Git には過去のコミットが残っているためすぐ復旧できます。

rebaseの途中の場合、 git rebase --abort でrebaseをやめて元に戻れます。( git status でも案内が出ているはずです)

rebase完了後の場合、rebase前にいた場所(復元用に作ったブランチ、またはコミットIDそのもの)がわかるなら、それをcheckoutして、rebaseしたブランチを消せばいいです。

git checkout ${restore_point}  # HEAD を移動させ、
git branch   -D ${branch}  # rebaseしたものは消して、
git checkout -b ${branch}  # 同名でブランチを作り直す

# 一撃で戻す方法もある
# git reset --hard ${restore_point}

復元場所がわからないなら、それを調べる必要があります。 git reflog を実行して、一覧から元いた場所を探し出してください。

参考:rebaseをしなくても対処可能なケース

git rebase -i は様々な状況に使える分、複雑でもあります。特定の状況ではもっと簡単な方法があります。

  • 直前のコミットを編集する場合、 git commit --amend を実行します
    • そのまま使えば、コメントなどを書き換えられます
    • 新しい変更を git add してから使えば、その分を統合できます(rebaseでのsquashと同じ)
  • 任意のコミットを現在のブランチに追加する場合、 git cherry-pick ${commit_id} を実行します

※これらの簡単な変更でも、やはり既存のコミットを書き換えたり消したりはせず、新しいコミットが作られます。

おわりに

本記事では Git のrebaseの方法を解説しました。rebaseはブランチのコミットを整理することに使えます。コミット履歴が綺麗で読みやすいと、コードレビューや将来の履歴調査がしやすくなります。rebaseを苦手としている方の参考になれば幸いです。