git-cherry-pickを掘り下げる

Gitにgit-cherry-pickという、知らなくてもなんとかなるが知っていると便利なコマンドがある。このコマンドを少し掘り下げてみた。

git-cherry-pick

git-cherry-pickは、狙ったコミットの変更内容だけを現在のブランチに取り込む操作である。

例えば、つぎのような履歴を想定する。

---A---B---C [master]
        \
         \
           ---X---Y [temp]

ここで、YはCの後にコミットするほうが適切であることに気づいた。このとき、masterブランチで次のようにすると目的は達成される*1

$ git cherry-pick Y

コミットYの変更内容だけをmasterのHEADに適用する、という操作である。このときXの変更内容は適用されない点がgit-mergeとは異なる。

---A---B---C---Y' [master]
        \
         \
           ---X---Y [temp]

もはやYは必要なくなるので、git-resetしてしまう。

$ git checkout temp
$ git reset --hard HEAD^

これで期待する履歴になった。

---A---B---C---Y' [master]
        \
         \
           ---X [temp]

疑問点

当然思いつくことだと思うのだが、Yの変更内容がXに依存する場合はどうなるのか?

例えば、git-revert*2したものだった場合は?

$ (edit)
$ git commit -am 'すごいハック!'
$ git revert HEAD
---A---B---C [master]
        \
         \
           ---X---^X [temp]

ここで^Xをgit-cherry-pickした場合、^X単体で見れば変更を行っているのだが、実際はXを取り消しているだけなのでBとの差分は存在しない。つまり、git-cherry-pickには何も変更しないで欲しいわけだが…。

$ git checkout master
$ git cherry-pick ^X
Finished one cherry-pick.
# On branch master
nothing to commit (working directory clean)

変更しない!賢い!

では、これほど単純ではなくて、git-cherry-pickの対象コミットに直前のコミットの一部が含まれる場合は?

$ (edit...)
$ git commit -am 'かっこいい処理を追加した!'
$ (edit...)
$ git commit -am 'かっこいい処理にちょっとした間違いがあったから手直しした!'
---A---B---C [master]
        \
         \
           ---X---XY [temp]

どれどれ…

$ git checkout master
$ git cherry-pick XY
Automatic cherry-pick failed.  After resolving the conflicts,
mark the corrected paths with 'git add <paths>' or 'git rm <paths>' and commit the result.
When commiting, use the option '-c 007c861' to retain authorship and message.

衝突した。

ちなみに、git-mergeすると自動的にマージされるので、通常の意味での変更の衝突は無いはずである。

$ git merge XY
Auto-merging awesome.rb
Merge made by recursive.
 awesome.rb |    2 ++
 1 files changed, 2 insertions(+), 0 deletions(-)

Xを修正するコミットであるXYを、XY単体で取り込むとはどういう意味か?そんなのコマンドを叩いた人間でないとわからない。だからgit-cherry-pickはそれを問い合わせてくるのだろう。そしてこうした妙なgit-cherry-pickの要請を、内部的には"衝突"として検知しているわけだ。

ともあれ、少なくとも妙なコミットが自動的に適用される心配はしなくていいようだ。

まとめ

git-cherry-pickは単にパッチを当てるよりもずっと賢くコミットを取り込んでくれる。安全のために積極的に使っていこう。

しかしまぁなんですか、他のコミットの変更内容を引きずっているコミットをgit-cherry-pickする機会はないと思うので、おそらくこのあたりを気にする必要は無いと思う。 それなりに発生しえる事態であるそうなので、いざというときのために正しく使えるようになっておこう。コメント欄参照。id:jch2355 さん、ご指摘ありがとうございます。

*1:もちろん、変更が衝突する場合もあるので、そのときは通常の手続きで衝突を解消する。

*2:指定したコミットを取り消すコミットを作成するコマンド。