Bazaarで歴史改変

Bazaarの場合、「メインライン」という概念があって、そこさえちゃんとしていればよしという感覚なので、フィーチャブランチなどの履歴を改変して整理するっていうのは、あまりする必要がありません。
とはいえ、それでも履歴の改変がしたい場合もあります。例えば、

  • はじめての相手にマージリクエストを出すときはかっこつけたいし、しょうもないミスは消しておきたい
  • 思った以上に履歴が長くなってしまったので、もう少し分割して個々にレビューを受けたい

みたいな。という訳で、今日は歴史改変ネタで行きたいと思います。

標準機能でがんばってみる

実は、Bazaarの標準機能だと、あまり履歴の改変に使えるコマンドがありません。
基本的には、以下の2つの組み合わせになります。

  • uncommitやpullコマンドでコミットを取り消す
  • mergeコマンドで、必要なリビジョンをチェリーピッキングする
uncommitで、最新のコミットを取り消す

# 最新のコミットを取り消します。
$ bzr uncommit
# 最新と、そのひとつ前のコミットを取り消します。uncommitでは「どれを消すか」ではなく「どこまで戻すか」を -r で指定します。
$ bzr uncommit -r -3

pullで、最新のコミットを取り消す

# 最新のコミットを取り消します。
$ bzr pull . -r -2 --overwrite
# 最新と、そのひとつ前のコミットを取り消します。
$ bzr pull . -r -3 --overwrite
※uncommitの場合、作業ツリー内のファイルはコマンド実行前のままですが、pullの場合は作業ツリー内のファイルも昔の状態に戻ります。つまり、[uncommit + revert == pull]ということになります。

mergeでチェリーピッキング

# リビジョン5の変更内容を、ブランチcolo:fooから取り込みます。
$ bzr merge colo:foo -r 4..5
※ 「リビジョン5での変更」ということで、"-r 4..5" のかわりに "-c 5" と指定することもできます。

標準機能でがんばってみる(例題)

例えばこんな履歴があるとして、

444: IWATA 2011-12-07 Reduce codes triggered b ...
443: IWATA 2011-12-07 Fix : WtCacheEntry may b ...
442: IWATA 2011-12-07 [merge] Fix small mistak ...
441: IWATA 2011-12-07 Now, we don't have to re ...
440: IWATA 2011-12-07 Fix problems about 'show ...
439: IWATA 2011-12-07 Add some tests
438: IWATA 2011-12-06 Rewrite WtCacheEntry --  ...
その1:r442〜r444をひとつにまとめる

$ bzr uncommit -r 441
$ bzr commit -m "新しいメッセージ"
これは簡単ですね。

その2:r439を抹消する

コピーを作って、元のブランチはr438まで戻す
$ bzr branch . ../copy
$ bzr pull . -r 438 --overwrite
copyから、r440〜r444をひとつずつ移植
$ bzr merge ../copy -c 440
$ bzr commit -m "Fix problems about 'show ..."
$ bzr merge ../copy -c 441
$ bzr commit -m "Now, we don't have to re ..."
・・・(以下略)・・・
これはかなり面倒です。ていうか手作業でするようなことじゃないです。
Gitならrebase -iがあるし、Mercurialならtransplantやgraftがあるのに・・・。
と長らく思ってたんですが、

rewrite_interactiveを使ってみる

最近BazaarのMLで、「Gitのrebase -iと同じことするプラグインを実装したぜ」っていう方がいました。
OrderedDict data structure in bzrlib?
すばらしいですね。ソースを公開されてますので、これを試してみましょう。

#プラグインフォルダにrewrite_interactiveという名前で配置します
$ bzr branch http://dl.dropbox.com/u/1185733/src/bzr-rewrite_interactive/ rewrite_interactive
さっきの例題をやってみます。

444: IWATA 2011-12-07 Reduce codes triggered b ...
443: IWATA 2011-12-07 Fix : WtCacheEntry may b ...
442: IWATA 2011-12-07 [merge] Fix small mistak ...
441: IWATA 2011-12-07 Now, we don't have to re ...
440: IWATA 2011-12-07 Fix problems about 'show ...
439: IWATA 2011-12-07 Add some tests
438: IWATA 2011-12-06 Rewrite WtCacheEntry --  ...
その2:r439を抹消する

コピーを作って、元のブランチはr438まで戻す
$ bzr branch . ../copy
$ bzr pull . -r 438 --overwrite
rewriteコマンドでcopyからr440〜444を移植
$ bzr rewrite ../copy -r 438..444
そうすると、エディタでこんな感じのテキストが表示されます。

pick    439  Add some tests
pick    440  Fix problems about 'show ...
pick    441  Now, we don't have to re ...
pick    442  Fix small mistakes
pick    443  Fix : WtCacheEntry may b ...
pick    444  Reduce codes triggered b ...

-------------- This line and the following will be ignored --------------

 pick = use commit
 edit = use commit, but stop for amending
 squash = use commit, but meld into previous commit

このファイルの1行目を削除して、保存してエディタをクローズします。コンフリクトが発生しなければ、それで移植完了です。
コンフリクトが発生した場合は、それを解消してから、rewrite-continueコマンドを叩きます。

$ bzr rewrite-continue
ずいぶん簡単になりました。

その3:r439〜r441をまとめる

その2と同じようにコマンドを実行して、表示されたテキストの2行目、3行目を pick → squash に書き換えます。

2行目|squash  440  Fix problems about 'show ...
3行目|squash  441  Now, we don't have to re ...

この操作だとコンフリクトは起きないので気が楽ですね。

標準のプラグインにもrewriteってのがあって(そっちにはrewriteコマンドではなくrebaseコマンドが入っている)紛らわしいんですが、最終的にはそっちに統合されることになるかもしれません。

ともあれ、作者の方に感謝です。

という訳で、Bazaarによる履歴改変方法の紹介でした。