2014年2月14日金曜日

Gitで今作ったコミットを以前のコミットに半自動的に融合させる(rebase autosquash)

よく「トピックブランチにおけるコミットは、トピックに関連した内容のみ、論理的単位で小さく作れ」といわれますが、実際にコードを書いているときは最初から完璧だと思える一連のコミットをつくり上げるのは非常に困難です。

たとえば、「あー、この変更はさっきのコミットに含めておくべきだったな」とか。



Gitの場合あとから rebase -i で表示されるTODOリストの順番を並べ替えたうえ、squashやfixupを指定すれば、前のコミットに融合できますが、コミットの時点でfixup先のコミットがわかっている場合、autosquashを使うと、その作業を半自動化できます。

たとえば
% git log --oneline master..
4ce35ee 新機能: ほげほげを追加
40a7ef4 ほげほげ機能追加のための準備
のように2つのコミットがあった場合、マージ前にレビューしたところ、40a7ef4と4e35eeそれぞれに追加しておくべき修正があったとします。

このとき追加のコミットのメッセージを "fixup! ほげほげ機能追加のための準備" にしておくことで、あとからrebase -iする際に--autosquashオプションをつければGitが並び替えとfixup指定を自動的に行ってくれます。ふたつの追加のコミットを行った状態が↓です。
% git log --oneline master..
d2c6767 fixup! 新機能
0920db2 fixup! ほげ
4ce35ee 新機能: ほげほげを追加
40a7ef4 ほげほげ機能追加のための準備
ここでは2つコミットをそれぞれ「fixup! ...」というコミットメッセージで追加しています。fixup先のコミットメッセージは完全に同一のものを指定する必要はなく、先頭から区別できるだけ指定してあげればOKです。

rebase時にコミットメッセージを編集したい場合は fixup! のかわりに squash!を指定します。

作業の区切りの良いところで、rebase -i --autosquash masterを実行すると、
pick 40a7ef4 ほげほげ機能追加のための準備
fixup 0920db2 fixup! ほげ
pick 4ce35ee 新機能: ほげほげを追加
fixup d2c6767 fixup! 新機能

# Rebase a274d9e..d2c6767 onto a274d9e
#
# 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
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
のように、TODOリストの自動的な並び替えとアクションの編集を行ってくれます。問題なければそのままセーブして終了、間違っていれば内容を修正するか、全消ししてrebaseをキャンセルできます。

ざっと試した限りでは fixup! の後ろには、コミットメッセージ(の先頭部分)、コミットハッシュ、タグを指定可能なようです。また、git-commit自体のオプションでも --fixup=<コミット> または --squash=<コミット> を指定することで、このようなコミットメッセージを自動生成可能です。

autosquashを使うと宣言的な開発スタイルを実行するのも楽になります。例えば、新しいトピックブランチを作ったら、とりあえず

  1. テスト
  2. 機能実装
  3. ドキュメンテーション
  4. その他

の4つの空のコミットを(--allow-emptyで)作っておいて、コミット単位は気にせずざっくり編集、適切な単位でステージ、fixup!で融合先を指定してコミット、最後にrebase -i --autosquash --keep-empty (2014/03/15: --keep-emptyを追記)で組み立て(4.その他はその後別ブランチへ)、といった感じです。

何もかもを最初から見通すのは困難ですが、Gitは最初から何もかも見通していたかのような一連のコミットを後から作る(でっちあげる)のが楽な気がします。

補足:
  •  git config --global rebase.autosquash true でrebase -i時のautosquashをデフォルトで有効にできます。autosquashしたくない場合は --no-autosquash で。
  • fixup先を間違えると悲惨なことになるので、rebase前のコミットにバックアップのブランチ名かタグをつけておくと楽です(diffで差分がないことを確認する意味でも)。もちろんreflogでも見つけて戻れます。

0 件のコメント:

コメントを投稿