Skip to main content

Gitを簡単に:rebaseで履歴を変更する—パートI

著者
Headshot of Or Kamara

Or Kamara

2020年6月9日

0 分で読めます

最近、コードのさまざまな領域に変更を加える大規模な機能の開発に取り組んでいました。この機能のPRを読みやすく保つため、すべての機能を複数のコミットに分割しました。ところが、時間が経つにつれて、それだけでは不十分だとすぐに気づきました。問題を見つけたり、CRの修正をしたりするなかで、それぞれのコミットに多くの変更を加える必要があったのです。

その時点で、私は魔法のようなgit rebaseの対話型コマンドの基本をすでに理解しており、「履歴を書き換える」ために使えることも知っていました。しかし、必要な変更は基本的な知識で対応できる範囲を超えていたため、このコマンドのオプションを詳しく調べ始めました。

このコマンドをまったくご存じない方のために、git rebase -i BRANCHを実行すると、次のように表示されます。

pick 271c66e feat: 1st commit
pick 51588eb feat: 2nd commit
pick 4b339f2 feat: 3rd commit

# Rebase 24aa86a..4b339f2 onto 24aa86a (3 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 <commit> = like "squash", but discard this commit's log message
# 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>]
...

初めてこのコマンドを実行したときのことをよく覚えています。反応はこんな感じでした。えええ、これはどういうこと?コミットを簡単に変更できる対話型の方法だと聞いていたのに、目の前にあるのはとても複雑なインターフェース……。画面を閉じたら、それまでの作業がすべて失われるかもしれないと思い、各オプションについて調べるのにかなり時間をかけました(#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にマージします。図にすると、次のようになります。

      E----F----G FeatureBranch
     /
    A----B----C----D master

作成した機能をテストするには、フレームワークの関数、具体的にはDコミットの関数を使う必要があります。つまり、最新の状態からブランチを作成した場合と同じように、ブランチのベースをAコミットからDコミットに変更する必要があります。

                     E----F----G FeatureBranch
                    /
    A----B----C----D master

これが、git rebaseが必要な理由です。コミットの一連の流れを、新しいベースコミットに移動します。内部では、gitが新しいコミットを作成し、指定した新しいベースに適用します。

具体的には、この記事では対話型モードについて説明します。

-iフラグを付けてrebaseを実行すると、基本的なrebaseの処理の中で個々のコミットを変更できます。ここから魔法が始まります。対話型モードでは、各コミットに対する操作(統合、分割、名前変更、並べ替えなど)を選べます。

git rebaseが役立つ一般的なシナリオを取り上げながら、それぞれのオプションについて見ていきましょう。

よくあるシナリオと対処方法

コードの履歴を変更したいときによくあるシナリオと、Gitを使った対処方法を見てみましょう。

1. 古いコミットメッセージを変更する(reword)

コードの変更をいくつか作成し、コミットごとにきれいに分けたものの、コミットメッセージに誤字があることに気づきました。

* 3d320d9 (HEAD -> feat/git-history) feat: 3rd commit
* 03d03e9 feat: 3nd commit
* 271c66e feat: 1st commit
* 24aa86a (master) Init: Git History

おそらくおわかりのように、2番目のコミットのメッセージが3nd commitになっています(うっかり!)。修正しましょう。

誤字が最後のコミットにあれば、git commit --amendコマンドを使えます。しかし、誤字がgitのコミットグラフの途中にあるため、もう少し複雑な方法が必要です。

この場合は、git rebase -i masterコマンドを使い(masterは別のブランチでも構いません)、2番目のコミット(a21bd1d)にrewordを指定します。

pick 271c66e feat: 1st commit
reword 03d03e9 feat: 3nd commit
pick 3d320d9 feat: 3rd commit

ファイルを保存すると、新しいエディターが開き、コミットメッセージを編集できます。

feat: 3nd commit

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Sun May 17 16:28:13 2020 +0300
#
# interactive rebase in progress; onto 24aa86a
# Last commands done (2 commands done):
#    pick 271c66e feat: 1st commit
#    r 03d03e9 feat: 3nd commit
# Next command to do (1 remaining command):
#    pick 3d320d9 feat: 3rd commit
# You are currently editing a commit while rebasing branch 'feat/git-history' on '24aa86a'.
#
# Changes to be committed:
#       modified:   b.txt
#

次に、最初の行を新しいコミットメッセージfeat: 2nd commitに変更して、もう一度保存します。

これで、新しいgitのコミットグラフが修正されました。

* fdb5f71 (HEAD -> feat/git-history) feat: 3rd commit
* e2952e7 feat: 2nd commit
* 271c66e feat: 1st commit
* 24aa86a (master) Init: Git History

2. 古いコミットの内容を変更する(squash、fixup)

仲間からCRのコメントを受け取り、新しいファイルを追加したり、既存のファイルの内容を変更したりする必要が出てきました。

ご存じのように、各コミットは単なるファイルの変更ではなく、ひとまとまりのロジックを表すべきです。避けたいことの1つは、今回のようなケースでCR fixesや、さらにはreally small CR fixesといったコミットメッセージを付けることです。望ましいのは、すべてのコミットをsquashして、最終的に各コミットにCR修正を含む関連変更がすべて含まれるようにすることです。

では、どうすればよいでしょうか。ここでsquash / fixupフラグが役立ちます。

まず、必要な変更を含む一時的なコミットを作成します。

* 17a73c1 (HEAD -> feat/git-history) CR fix: add new file to 2nd commit
* 5641a86 CR fix: content of file in 1st commit
* fdb5f71 feat: 3rd commit
* e2952e7 feat: 2nd commit
* 271c66e feat: 1st commit
* 24aa86a (master) Init: Git History

上の例では、新しいコミットを2つ作成しました。

  • 5641a86—1つ目のコミットで作成したファイルの内容を変更

  • 17a73c1—2つ目のコミットに新しいファイルを追加

しかし、このようなgitのコミットグラフのままにしたくはありません。5641a86を271c66eに、17a73c1をe2952e7にsquashしたいのです。そのためにgit rebase -i masterコマンドを実行します。前回と同様に、最初に表示される画面は次のとおりです。

pick 271c66e feat: 1st commit
pick e2952e7 feat: 2nd commit
pick fdb5f71 feat: 3rd commit
pick 5641a86 cr fix: content of file in 1st commit
pick 17a73c1 cr fix: add new file to 2nd commit

次に、コミットを希望する順序に並べ替え、コミット5641a86と17a73c1にsquashオプションを指定します。

pick 271c66e feat: 1st commit
squash 5641a86 cr fix: content of file in 1st commit
pick e2952e7 feat: 2nd commit
squash 17a73c1 cr fix: add new file to 2nd commit
pick fdb5f71 feat: 3rd commit

この変更を行うと、gitはコミット(271c66eからfdb5f71まで)を実行し、squashが指定されたコミットの内容を直前のコミットに統合します。これでgitのコミットグラフは、思いどおりの形になります。

* 1b2f5b1 (HEAD -> feat/git-history) feat: 3rd commit
* 6871683 feat: 2nd commit
* 682c27f feat: 1st commit
* 24aa86a (master) Init: Git History

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の課題の解き方を学びましょう。

カテゴリー:

続きを読む

Blog

Stadium Summer:Snyk Connect Fan Zone Tour

SnykのFan Zone Tourでは、8都市と3回のオンラインセッションで、AIセキュリティのワークショップやネットワーキング、楽しい競技を開催しました。参加者はスキルを磨き、アイデアを共有し、共に成長しました。

Blog

Snyk VulnBench JS 1.0:LLMは同じバグを2回見つけられるか?

Snyk VulnBench JS 1.0:300回の反復スキャンで、LLMのセキュリティ検出結果は実行ごとに異なる一方、SASTとモデルはそれぞれ異なる脆弱性の見落としを検出することが明らかに。

feature insights context
Blog

お客様と築くAIセキュリティ:Evoのデザインパートナープログラムから得た5つの教訓

SnykのEvoデザインパートナープログラムから得た5つの重要な教訓をご紹介します。AIの検出、リスクインテリジェンス、ポリシーの自動化によって、チームが生成AIを保護し、拡大するAI利用を大規模に統制する方法をご覧ください。