彷徨えるフジワラ

年がら年中さまよってます

中間リビジョンの事後導入

Emacs の shell バッファ上における "hg qrefresh -i" 実行に関して追記@2012/02/08
TokyoMercurial #1 で話題になった件の詳細シリーズ - その4。

TokyoMercurial #1 当日に、参加者から以下のような質問が:

MQ を使って修正作業を行う際に、想定以上に修正内容が膨らんでしまった。
中間リビジョンを後付けで作りたいのだが、どういった手法が良いだろうか?

ちなみに、通常の "hg commit" で記録したリビジョンでも、"hg qimport -r tip -n パッチ名" で MQ 配下に取り込んで事後改変することが出来るので、『MQ を使っている場合限定の話』と言う訳でもない。

もちろん、"hg push" で共有リポジトリに公開してしまったリビジョンに関しては、手元でいくら改変しようが意味は無いのだけどね ....

さて、例えば、現行の 100 箇所の修正の中から:

  • 最初のパッチで 10 箇所
  • 次のパッチで 15 箇所
  • ....

といった具合に、『修正箇所の単純なピックアップ』だけで済むケースであれば、標準添付の Record エクステンション を有効にしておいて、"hg qrefresh" を -i/--interactive オプション付きで起動すれば、修正 hunk 単位で取捨選択ができる。
"hg qrefresh -i" による選択対象となるのは、まだパッチに取り込まれていない修正だけなので、既に qrefresh により取り込まれた修正を含めて取捨選択したい場合は、絶対に存在しないファイル名を指定して "hg qrefresh" を実行することで、パッチに取り込まれている全ての変更内容を作業領域に戻してやればよい。

なお、作業領域中の変更を元にした『新規パッチを作成』あるいは『新規リビジョンのコミット』において修正箇所を hunk 単位で取捨選択したい場合は、それぞれ "hg qrecord" (ないし "hg qnew -i") あるいは "hg record" コマンドを使用。

ちなみに、Emacs の shell バッファ等からの起動のような『tty と関連付けられていない』場合は、『対話モードで稼動できない』旨で処理が中断されてしまう。

※ 2012/02/08 追記 >>>> ここから

Meadow + cygwin bash の場合は、tty との関連付けが無いが、Linux/Unix 系 OS 上の Emacs であれば、tty と関連付けされるので、"hg qrefresh -i" の実施が可能。

単純に Emacs 〜 shell は pipe 連携だと思っていたけれど、考えてみれば Ctrl-C とか Ctrl-Z などのジョブ制御系の機能は、pipe 連携 (= データ入出力) だけでは実現できる筈が無いのだから、tty 関連付けが無い訳が無いのだよな .... orz

っつーか、だから処理の中断とかがきちんと機能しないのだな > Meadow + cygwin bash

※ 2012/02/08 追記 <<<< ここまで

どうせ Emacs を使うなら、diff-mode の機能を使用して:

  1. "hg qdiff" 結果をファイルに保存
  2. "hg qpop" で一旦パッチの適用を解除
  3. 保存しておいた差分ファイルを開く
  4. "hg qnew" で新規パッチを作成
  5. M-x diff-apply-hunk (C-c C-a) で hunk を適用
  6. 適当なところで "hg qrefresh" による確定
  7. 全ての修正を取り込むまで (4) からの繰り返し

といった方法が使えなくも無い。但し、hunk 適用が前後すると、差分コンテキストとか行数指定がアレしてしまう可能性もあるし、個々の hunk の適用状況を把握する必要があるから、そこは規模とか手法の好みで適当に。

さて、猛烈に脱線した話を元に戻す。

元々の質問意図は、Record エクステンションで対応できるような、単純な変更の選択ではなく、もうちょっと『途中経過状態を新規に作成する』的な感じ。

少々乱暴なたとえをするなら:

  1. 元の内容は "this is a pen"
  2. パッチ適用後は "this is a pencil"

という状況から:

  1. 元の内容は "this is a pen"
  2. パッチ #1 適用後は "this is a mechanical pencil"
  3. パッチ #2 適用後は "this is a pencil"

という状況を作り出す、と言う感じ。

リファクタリングを一気にやってしまったけれど、実施種別毎に別パッチにしたい

みたいなケースで、同一箇所に複数のリファクタリングが適用されているようなものが、これに近いイメージになるかも?

で、解決策としては、以下のような手順が考えられる。

  1. 修正を一括実施する現行のパッチ (以下 "PATCH_A") を適用
  2. PATCH_A から、新規に導入したい『途中状態』への改変を行うパッチ (以下 "PATCH_B") を追加
  3. 複数リビジョン指定を使用して、リビジョン間差分をパッチとして取り込む:
    生成するパッチは、qparent (パッチ適用元リビジョン) ⇒ PATCH_B (以下 "parent2B")と、PATCH_B ⇒ PATCH_A (以下 "B2A") の2つ
  4. 一旦全てのパッチ適用を解除
  5. パッチ parent2B と B2A の順で選択的に適用
  6. parent2B と B2A の内容に問題が無ければ、PATCH-A と PATCH-B を破棄

先の例で言えば、PATCH_A は "pen" ⇒ "pencil"、PATCH_B は "pencil" ⇒ "mechanical pencil" に相当する。

上記の (2) の段階で "pen" ⇒ "pencil" ⇒ "mechanical pencil" に相当するパッチ構成だったものから、"pen" ⇒ "mechanical pencil" (parent2B) と "mechanical pencil" ⇒ "pencil" (B2A) に再構築されるという按配。

というか、当日はこの辺の話を、ブワーっと一気に喋ったので、ちょっと分かりづらかったかも?

TokyoMercurial #1 に参加した Kouichi Akatsuka 氏によるエントリでも、この辺の手順に関する言及アリ。