git flow feature finishした後にリモート更新で気づくと悲しい問題への対処法

7/21のSCM Boot Camp in Tokyo 3にGitの講師役として参加してきた。その時に id:ToMmY さんとgit-flow(もしくはgit merge --no-ff)した後にリモートが更新されていると悲しいという話をしてたらgit-flowのオプションあるから使うべきという耳寄り情報をゲット*1したのでまとめてみる。

何が問題か?

git-flowで開発をしている場合feature startでフィーチャーブランチを作成して作業を進め、フィーチャーの実装が完了したらfeature finishでdevelopブランチにマージといった流れになる。
この時のコマンドの流れは以下

$(develop) git flow feature start cool-feature
$(feature/cool-feature) # イケてる機能の実装&コミットコミット
$(feature/cool-feature) git flow feature finish cool-feature
$(develop) git push origin develop # リモートにプッシュしておしまい

ここでのリビジョングラフ*2はこんな感じ

*   2e4abd0 (HEAD, develop) Merge branch 'feature/cool-feature' into develop cynipe
|\
| * ba85b12 (feature/cool-feature) イケてる機能のUIを実装 cynipe
| * 3e9851b イケてる機能のロジックを実装 cynipe
|/
* 8a95a92 (master) Initial commit O

git-flowのfeature finishコマンドはマージ完了後にフィーチャーブランチの削除が行われる。マージ後のフィーチャーブランチは不要なのでこの動き自体は間違っていない。ただしリモートに更新がない場合に限って。

リモートに更新があった場合以下のようになる

$(develop) git flow feature start cool-feature
$(feature/cool-feature) # イケてる機能の実装&コミットコミット
$(feature/cool-feature) git flow feature finish cool-feature
$(develop) git push origin develop # リモートにプッシュするが更新があるので失敗
# リモートの更新を取得
$(develop) git pull --rebase origin develop

もしリモートに更新があった場合は通常これをrebaseなどするが、そんなことをしてしまうと折角--no-ffでマージしてできたリビジョングラフが以下のように一直線にされてしまう。

* ba85b12 (HEAD, develop) イケてる機能のUIを実装 cynipe
* 3e9851b イケてる機能のロジックを実装 cynipe
*   ae86a43 (origin/develop) Merge branch 'feature/nice-feature' into develop someone
|\
| * 455966e ナイスな機能のUIを実装 someone
| * 427d7ef ナイスな機能のロジックを実装 someone
|/
* 8a95a92 (origin/master) Initial commit

このとき、思い描いていた理想のリビジョングラフをもう一度取り戻すためにマージのやり直しをしたくなる。しかし、やり直すためのフィーチャーブランチはもう削除されてしまっていて作成したリビジョングラフは戻ってこない。結果、同じリビジョングラフを再現するためには、developの状態をフィーチャー開始前の状態にreset --hardし、同名のフィーチャーブランチを作成して、reflogとcherry-pickを駆使してそのフィーチャーブランチで行なっていたコミットを再現しなおす以外にない。

どうすればいいか?

つまりフィーチャーブランチが削除されなければいい。
削除されなければもう一度mergeをやり直せばいいだけで、消えてしまったフィーチャーブランチの再現なんていうアホな事はしなくて良くなる。

フィーチャーブランチを消さずに残しておくにはfeature finishコマンドにオプションをつけて以下のようにすればいい。

$(feature/cool-feature) git flow feature finish -k cool-feature
$(develop) git push origin develop # リモートにプッシュするが更新があるので失敗
# リモートの更新を取得
$(develop) git pull --rebase origin develop

当然この時点ではrebaseをかけてるので前述と同じ状態になる。ただし、今回はマージ済みではあるもののフィーチャーブランチは残っているので、これをやり直すことができる。

* ba85b12 (HEAD, develop) イケてる機能のUIを実装 cynipe
* 3e9851b イケてる機能のロジックを実装 cynipe
*   ae86a43 (origin/develop) Merge branch 'feature/nice-feature' into develop someone
|\
| * 455966e ナイスな機能のUIを実装 someone
| * 427d7ef ナイスな機能のロジックを実装 someone
|/
* 8a95a92 (origin/master) Initial commit

まずdevelopブランチを元の状態に戻す

$(develop) git reset --hard HEAD~2

フィーチャブランチでの作業はなくなり、純粋にリモートの更新を取り込んだ状態になる。

*   ae86a43 (origin/develop) Merge branch 'feature/nice-feature' into develop someone
|\
| * 455966e ナイスな機能のUIを実装 someone
| * 427d7ef ナイスな機能のロジックを実装 someone
|/
* 8a95a92 (origin/master) Initial commit

次にフィーチャーブランチをもう一度finishするが、この時kオプションと合わせてrオプションをつけてfinishする。

$(develop) git flow feature finish -kr cool-feature

rオプションをつけてfinishすると対象となるフィーチャーブランチ上でrebaseを行い、その後でdevelopに対して--no-ffなマージを行なってくれる。あとはpushをすれば作業は完了!

*   2e4abd0 (HEAD, develop) Merge branch 'feature/cool-feature' into develop cynipe
|\
| * ba85b12 (feature/cool-feature) イケてる機能のUIを実装 cynipe
| * 3e9851b イケてる機能のロジックを実装 cynipe
|/
*   ae86a43 (origin/develop) Merge branch 'feature/nice-feature' into develop cynipe
|\
| * 455966e ナイスな機能のUIを実装 cynipe
| * 427d7ef ナイスな機能のロジックを実装 cynipe
|/
* 8a95a92 (origin/master) Initial commit

まとめ

  • git flow feature finishする時は-kするとフィーチャーブランチが消えない
  • git flow feature finishする時に-rするとフィーチャーブランチ上でrebaseした上で--no-ffなマージを行ってくれる
  • この2つを覚えておけばフィーチャーブランチの再現をしなくて済んでハッピー

Gitポケットリファレンス

Gitポケットリファレンス


入門Git

入門Git

*1:すみません、どなたに教えていただいたか失念しましたorz

*2:なんていうのが正しいんだろ?コミットグラフ?