gitで誤ったブランチに対して行った変更を正しいブランチへ移す(cherry-pick編)


2010年 12月 30日

gitでは様々な方法でコミットログを書き換えることができます。
その一例として誤ったブランチに対して行った変更を正しいブランチへ移す方法を紹介します。

問題

これまで「新機能Xを追加する」という設定で以下のトピックについて解説していました:

これにはまず

$ git branch topic-x master
$ git checkout topic-x

としてこの作業用のトピックブランチを作成してそちらで作業を行うのが普通です。

しかし git branch を実行したところで安心してしまい、
git checkout を忘れて全く違うブランチで作業を行ってしまう
というミスは時々やってしまいます
(git checkout -b という方法もありますがここではそれも忘れていたとしましょう)。
例えば以下のような状況だったとしましょう:

  $ git branch
  master
  topic-x
* topic-y

$ git log --oneline --decorate --graph
* c000022 (HEAD, topic-y) Update to use X
* c000012 Refactor - Sort using statements
* b000001 Add a neat feature X into the library
* 0100002 Update to use Y
* 0100001 Add a great feature Y in to the library
* 0000000 (master, topic-x) Initial import

日本語に訳すと以下の通りです:

  1. 新機能Yを追加するためのトピックブランチtopic-yをmasterから作成し、そこで作業を行った。
    この時点でmasterは0000000を、topic-yは0100002を指していた。
  2. 新機能Xを追加するためのトピックブランチtopic-xをmasterから作成したが、
    ブランチの切り替えを忘れていたため、
    topic-xに対して行う変更をtopic-y上で行ってしまった。
    結果としてtopic-yは0100002からc000022を指すようになった。

本来ならば以下のような形なっているところです:

$ git branch
master
  • topic-x
    topic-y $ git log –oneline –decorate –graph master topic-x topic-y
  • d000022 (HEAD, topic-x) Update to use X
  • d000012 Refactor – Sort using statements
  • d000001 Add a neat feature X into the library
    | * 0100002 (topic-y) Update to use Y
    | * 0100001 Add a great feature Y in to the library
    |/
  • 0000000 (master) Initial import

まだまだtopic-xの作業内容はどこにも公開されていないので、
いまのうちにコミットログを綺麗な形に書き換えるとしましょう。
しかし具体的にはどうすればよいでしょうか。

解決方法

今回の場合、作業内容は既にtopic-yに対してコミットしていますので、以下の手順で修正することにします:

  1. topic-xに本来行いたかったコミットを移す。
  2. topic-yから意図せず行ってしまったコミットを取り除く。

topic-xに本来行いたかったコミットを移す

git cherry-pick
というコマンドがあり、これを使うと既存のコミットで行った変更と同じことを行うことができます。
今回の場合は以下のコマンドでtopic-yで行ってしまった変更をtopic-xに「コピー」することができます:

$ git checkout topic-x

$ git cherry-pick b000001
Finished one cherry-pick.
[topic-x d000001] Add a neat feature X into the library
1 files changed, 21 insertions(+), 0 deletions(-)

$ git cherry-pick c000012
Finished one cherry-pick.
[topic-x d000012] Refactor - Sort using statements
1 files changed, 5 insertions(+), 3 deletions(-)

$ git cherry-pick c000022
Finished one cherry-pick.
[topic-x d000022] Update to use X
2 files changed, 10 insertions(+), 5 deletions(-)

$ git log --oneline --decorate --graph master topic-x topic-y
* d000022 (HEAD, topic-x) Update to use X
* d000012 Refactor - Sort using statements
* d000001 Add a neat feature X into the library
| * c000022 (topic-y) Update to use X
| * c000012 Refactor - Sort using statements
| * b000001 Add a neat feature X into the library
| * 0100002 Update to use Y
| * 0100001 Add a great feature Y in to the library
|/
* 0000000 (master) Initial import

git cherry-pick は「コピー」元のコミットのIDを引数に取りますが、
いちいち各コミットのIDを指定するのは面倒ですから、
以下のようにしてブランチを起点に「コピー」するコミットを指定することもできます:

$ git cherry-pick topic-y~2 # topic-yの2個前のコミット。この場合はb000001と同じ。
$ git cherry-pick topic-y~1 # topic-yの1個前のコミット。この場合はc000012と同じ。
$ git cherry-pick topic-y~0 # topic-yの0個前のコミット。topic-yと同じ。
$ git cherry-pick topic-y # topic-yの指すコミット。この場合はc000022と同じ。

topic-yから意図せず行ってしまったコミットを取り除く

gitで1つのコミットを複数のコミットに分割する
でも紹介した
git reset
コマンドを使えば可能です。
今回の場合は以下のコマンドでtopic-yから意図せず行ってしまったコミットを取り除くことができます:

$ git checkout topic-y

$ git reset --hard HEAD~3
HEAD is now at 0100002 Update to use Y

$ git log --oneline --decorate --graph master topic-x topic-y
* d000022 (topic-x) Update to use X
* d000012 Refactor - Sort using statements
* d000001 Add a neat feature X into the library
| * 0100002 (HEAD, topic-y) Update to use Y
| * 0100001 Add a great feature Y in to the library
|/
* 0000000 (master) Initial import

コミットの分割の例では作業ディレクトリの内容はそのまま残しておきたかったので
git reset HEAD~1 などを使いましたが、
今回の場合は作業ディレクトリの内容も含めて全て捨て去りたいので
git reset --hard HEAD~3 のように --hard オプションが必要です。

これで理想的な形にコミットログを書き換えることができました。
やりましたね。

(続く)