git add と git rebase のちょっと応用的な使い方(add -p, rebase -i)

Git 可愛いよ、Git

という訳で、最近Git の使い方を覚えてきたので、少しまとめておく。

書いたのは、下記の2コマンドのオプションについてです。

  • git add -p
  • git rebase -i

両方ともSVN では出来ないですので、SVN 使っている方はGit キモい 凄いと思うこと間違いなし!

コミットの選択(git add -p)

普通のコミット

例えば、下記のような(作成中の)ソースがあるとする。

#!/usr/bin/env python
# -*- coding:utf-8 -*-

def fizzbuzz(num):
    return num


if __name__ == '__main__':
    for n in range(1, 20):
        print n

これを普通にコミットする場合、add してcommit すればいい。

$ git add fizzbuzz.py
$ git commit -m "引数をそのまま返すfizzbuzzを作成"
変更箇所の選択(ハンクの選択)

次に、fizzbuzz の条件分岐を作ったとする。

#!/usr/bin/env python
# -*- coding:utf-8 -*-

def fizzbuzz(num):
    if num % 3 == 0:
        return 'Fizz'
    elif num % 5 == 0:
        return 'Buzz'

    return num


if __name__ == '__main__':
    for n in range(1, 20):
        print fizzbuzz(n)

変更箇所は下記の2点

  • 3, 5での除算の条件式を追加
  • fizzbuzz を使用しておらず、数字がそのまま出力されていたバグ修正(最後の行)

この例のように、機能追加とバグ修正を一緒にしてしまうケースは結構ある。
しかし、入門Gitにも書かれているように、コミットは分けるべきです。

論理的に関連のない複数の変更は、別々のコミットとして記録するようにするのが、お行儀の良い版管理のコツです。
(入門Git p.57, p.58)

git add には、-p オプションで、どの変更をコミットに含めるかを選択する機能がある。

$ add -p fizzbuzz.py

コマンドを実行すると、下記のように変更箇所が表示され、ハンクを選択できる。

$ git add -p fizzbuzz.py
diff --git a/fizzbuzz.py b/fizzbuzz.py
index db4a9bf..a66fef3 100644
--- a/fizzbuzz.py
+++ b/fizzbuzz.py
@@ -2,9 +2,14 @@
 # -*- coding:utf-8 -*-

 def fizzbuzz(num):
+    if num % 3 == 0:
+        return 'Fizz'
+    elif num % 5 == 0:
+        return 'Buzz'
+
     return num


 if __name__ == '__main__':
     for n in range(1, 20):
-        print n
+        print fizzbuzz(n)
Stage this hunk [y,n,q,a,d,/,s,e,?]?

-p は近い行の変更を自動的に1つのハンクとして検知する。
今回のように近い場所の変更を分けてコミットする場合は、 e (manually edit the current hunk) を選択する。
コメントで指摘を頂きました。
今回のケースだと s (split the current hunk into smaller hunks) でコミットを分ける方が適切です。

Stage this hunk [y,n,q,a,d,/,s,e,?]? s
Split into 2 hunks.
@@ -2,8 +2,13 @@
 # -*- coding:utf-8 -*-

 def fizzbuzz(num):
+    if num % 3 == 0:
+        return 'Fizz'
+    elif num % 5 == 0:
+        return 'Buzz'
+
     return num


 if __name__ == '__main__':
     for n in range(1, 20):
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? y
@@ -5,6 +10,6 @@
     return num


 if __name__ == '__main__':
     for n in range(1, 20):
-        print n
+        print fizzbuzz(n)
Stage this hunk [y,n,q,a,d,/,K,g,e,?]? n

コミットに含めるハンクで "y" , 含めないハンクでは "n" を選択します。
これで、条件分岐の部分だけをコミットできます。

$ git commit -m "3, 5での除算の条件式を追加"
もし手動でハンクを選択する場合

e (manually edit the current hunk) を選択します。

Stage this hunk [y,n,q,a,d,/,s,e,?]? e

するとエディタが起動するので、ステージングしないハンクを削除し、保存する。

  • 編集前
# Manual hunk edit mode -- see bottom for a quick guide
@@ -2,9 +2,14 @@
 # -*- coding:utf-8 -*-
 
 def fizzbuzz(num):
+    if num % 3 == 0:
+        return 'Fizz'
+    elif num % 5 == 0:
+        return 'Buzz'
+
     return num
 
 
 if __name__ == '__main__':
     for n in range(1, 20):
-        print n
+        print fizzbuzz(n)
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
#
# If the patch applies cleanly, the edited hunk will immediately be
# marked for staging. If it does not apply cleanly, you will be given
# an opportunity to edit again. If all lines of the hunk are removed,
# then the edit is aborted and the hunk is left unchanged.
  • 編集後
# Manual hunk edit mode -- see bottom for a quick guide
@@ -2,9 +2,14 @@
 # -*- coding:utf-8 -*-
 
 def fizzbuzz(num):
+    if num % 3 == 0:
+        return 'Fizz'
+    elif num % 5 == 0:
+        return 'Buzz'
+
     return num
# ---
# To remove '-' lines, make them ' ' lines (context).
# To remove '+' lines, delete them.
# Lines starting with # will be removed.
#
# If the patch applies cleanly, the edited hunk will immediately be
# marked for staging. If it does not apply cleanly, you will be given
# an opportunity to edit again. If all lines of the hunk are removed,
# then the edit is aborted and the hunk is left unchanged.

これで、上記の -s と同等のコミットになります。

歴史の書き換え(git rebase -i)

現在のコミット系譜は下記のようになっているとする。

    A---B---C master
  • A ... "引数をそのまま返すfizzbuzzを作成"
  • B ... "3, 5での除算の条件式を追加"
  • C ... "fizzbuzzを使用していないバグ修正"

この歴史を下記のように変えてみます。

      --C---D---E master
     /
    A---B---C (old)master
  • A ... "引数をそのまま返すfizzbuzzを作成"
  • C ... "fizzbuzzを使用していないバグ修正"
  • D ... "3での除算の条件式を追加"
  • E ... "5での除算の条件式を追加"

C とB の順番を変えて、B のコミットを2つ (D, E) に分けている。
つまり、"先にバグ修正をして、条件分岐を1つずつ実装していた"ことにする。

順番変え + コミット編集

歴史を変える場合、git rebase -i を使用する。
今回の場合、歴史はA から分岐するため、A のコミットを引数に指定する。

$ git rebase -i HEAD^^

すると、下記の状態でエディタが起動する。

  • 編集前
pick e28c55a 3, 5での除算の条件式を追加
pick 78ac991 fizzbuzzを使用していないバグ修正

# Rebase dadcfb3..78ac991 onto dadcfb3
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

1行目と2行目を入れ替え、編集するコミットを"edit"に書き換え、保存する。

  • 編集後
pick 78ac991 fizzbuzzを使用していないバグ修正
edit e28c55a 3, 5での除算の条件式を追加

# Rebase dadcfb3..78ac991 onto dadcfb3
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#
コミットの編集

git rebase を実行すると、B のコミットの状態に作業コピーが変わる。

まだコミットを2つに分けていないため、rebase 中はこういうイメージ

           ↓ココ
      --C---B master
     /
    A---B---C (old)master

ここでBの変更を残したまま、Cに戻す。

$ git reset HEAD^
       ↓ココ(作業コピーはBと同じ)
      --C master
     /
    A---B---C (old)master

後は、add -p を使ってコミット箇所を選択し、2回コミットする。

      --C---D---E master
     /
    A---B---C (old)master

Bのコミット編集が全て終わったら、rebase を再開する。

$ git rebase --continue

これで、歴史の書き換え完了です。

もしrebase 中にコンフリクトしたら・・・

焦らずに、まずはrebase 実行前に戻す。

$ git rebase --abort

実行前に戻せたら、コンフリクト解消用のブランチ作ったり、cherry-pickで少しずつマージして対応する。

入門Git

入門Git