Gitを簡単に:rebaseで履歴を変更する—パートI
Or Kamara
2020年6月9日
0 分で読めます最近、コードのさまざまな領域に変更を加える大規模な機能の開発に取り組んでいました。この機能のPRを読みやすく保つため、すべての機能を複数のコミットに分割しました。ところが、時間が経つにつれて、それだけでは不十分だとすぐに気づきました。問題を見つけたり、CRの修正をしたりするなかで、それぞれのコミットに多くの変更を加える必要があったのです。
その時点で、私は魔法のようなgit rebaseの対話型コマンドの基本をすでに理解しており、「履歴を書き換える」ために使えることも知っていました。しかし、必要な変更は基本的な知識で対応できる範囲を超えていたため、このコマンドのオプションを詳しく調べ始めました。
このコマンドをまったくご存じない方のために、git rebase -i BRANCHを実行すると、次のように表示されます。
初めてこのコマンドを実行したときのことをよく覚えています。反応はこんな感じでした。えええ、これはどういうこと?コミットを簡単に変更できる対話型の方法だと聞いていたのに、目の前にあるのはとても複雑なインターフェース……。画面を閉じたら、それまでの作業がすべて失われるかもしれないと思い、各オプションについて調べるのにかなり時間をかけました(#ScaredDeveloper)。
画面を閉じてすべてを取り消そうとしても、簡単にはいきませんでした……。やり方をGoogleで検索したところ、コミットの行をすべて削除すれば変更されないとわかりました。簡単ですが、なんとも不格好です。最初に思ったのは「でも、Googleに書かれていることを何でも信用できるわけじゃない。そうしたら作業が全部消えてしまうんじゃないか?」でした。
そこで、ほかに選択肢はないと考え、各コマンドについて調べ始めました。
この記事では、複数のコミットを含むPRに取り組む際に、開発者が解決しなければならないさまざまな問題について説明します。コマンドの細かな仕組みは解説しません。内部で何が起きているのかを説明する記事がいくつもあります。その代わりに、gitを使うときに誰もが遭遇しうるシナリオをいくつか紹介し、適切なgit rebase -iの解決方法を提案します。
かなり幅広いテーマなので、この記事は2部構成にします。前編ではRebaseの基本と、よくあるシナリオを2つ取り上げます。後編では、さらに4つのよくあるシナリオとまとめを紹介します。
それでは、さっそく前編を見ていきましょう。
Rebaseの概要
まず、次のシナリオを見てみましょう。あなたと友人が、わくわくするような新しいプロジェクトに取り組んでいます。リポジトリを作成し、最初のコミットAを済ませ、いよいよコーディングです。効率よく進めるために、作業を分担することにしました。あなたはメイン機能を担当し、友人はアプリのフレームワークを構築します。
2人とも、同じAコミットから作業を始めます。5時間集中して作業した後、変更をmasterにマージすることにしました。友人が先にマージし、Aの上に3つの新しいコミットB + C + Dを作成しました。次はあなたの番です。FeatureBranchで行った作業、つまりコミットE + F + Gをすべてmasterにマージします。図にすると、次のようになります。
作成した機能をテストするには、フレームワークの関数、具体的にはDコミットの関数を使う必要があります。つまり、最新の状態からブランチを作成した場合と同じように、ブランチのベースをAコミットからDコミットに変更する必要があります。
これが、git rebaseが必要な理由です。コミットの一連の流れを、新しいベースコミットに移動します。内部では、gitが新しいコミットを作成し、指定した新しいベースに適用します。
具体的には、この記事では対話型モードについて説明します。
-iフラグを付けてrebaseを実行すると、基本的なrebaseの処理の中で個々のコミットを変更できます。ここから魔法が始まります。対話型モードでは、各コミットに対する操作(統合、分割、名前変更、並べ替えなど)を選べます。
git rebaseが役立つ一般的なシナリオを取り上げながら、それぞれのオプションについて見ていきましょう。
よくあるシナリオと対処方法
コードの履歴を変更したいときによくあるシナリオと、Gitを使った対処方法を見てみましょう。
1. 古いコミットメッセージを変更する(reword)
コードの変更をいくつか作成し、コミットごとにきれいに分けたものの、コミットメッセージに誤字があることに気づきました。
おそらくおわかりのように、2番目のコミットのメッセージが3nd commitになっています(うっかり!)。修正しましょう。
誤字が最後のコミットにあれば、git commit --amendコマンドを使えます。しかし、誤字がgitのコミットグラフの途中にあるため、もう少し複雑な方法が必要です。
この場合は、git rebase -i masterコマンドを使い(masterは別のブランチでも構いません)、2番目のコミット(a21bd1d)にrewordを指定します。
ファイルを保存すると、新しいエディターが開き、コミットメッセージを編集できます。
次に、最初の行を新しいコミットメッセージfeat: 2nd commitに変更して、もう一度保存します。
これで、新しいgitのコミットグラフが修正されました。
2. 古いコミットの内容を変更する(squash、fixup)
仲間からCRのコメントを受け取り、新しいファイルを追加したり、既存のファイルの内容を変更したりする必要が出てきました。
ご存じのように、各コミットは単なるファイルの変更ではなく、ひとまとまりのロジックを表すべきです。避けたいことの1つは、今回のようなケースでCR fixesや、さらにはreally small CR fixesといったコミットメッセージを付けることです。望ましいのは、すべてのコミットをsquashして、最終的に各コミットにCR修正を含む関連変更がすべて含まれるようにすることです。
では、どうすればよいでしょうか。ここでsquash / fixupフラグが役立ちます。
まず、必要な変更を含む一時的なコミットを作成します。
上の例では、新しいコミットを2つ作成しました。
5641a86—1つ目のコミットで作成したファイルの内容を変更17a73c1—2つ目のコミットに新しいファイルを追加
しかし、このようなgitのコミットグラフのままにしたくはありません。5641a86を271c66eに、17a73c1をe2952e7にsquashしたいのです。そのためにgit rebase -i masterコマンドを実行します。前回と同様に、最初に表示される画面は次のとおりです。
次に、コミットを希望する順序に並べ替え、コミット5641a86と17a73c1にsquashオプションを指定します。
この変更を行うと、gitはコミット(271c66eからfdb5f71まで)を実行し、squashが指定されたコミットの内容を直前のコミットに統合します。これでgitのコミットグラフは、思いどおりの形になります。
squashの代わりにfixupを使うこともできます。どちらもほぼ同じ動作をしますが、fixupではコミットのログメッセージが破棄されます。
重要な注意点として、コミットをsquashする際にコードの競合が発生することがあります。gitが競合を検出すると、競合を解決して変更をステージし、git rebase --continueを実行してrebaseを続けるよう求められます。どうすればよいかわからない場合は、いつでもgit rebase --abortを実行できます。このコマンドはrebaseを中止し、リポジトリの状態をgit rebase -iの実行前の状態に戻します。
まとめ
Rebaseを使ってgitの履歴を変更する方法の前編は以上です。後編では、さらに4つのよくあるシナリオを取り上げ、対処方法を紹介します。
Capture the Flagを始めよう
オンデマンドのバーチャル入門ワークショップを見て、Capture the Flagの課題の解き方を学びましょう。



