彷徨えるフジワラ

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

追加したファイルを、履歴を遡って改名する

2013/12/16: 適用済みパッチに対する hg rebase に関する追記あり
事の発端は以下のツイート:

これに id:hokorobi 氏が以下のように返信:

というタイムラインを目にしたのだけれど、最初は qimport と sed と hg add が頭の中で全然結びつかなくて、頭の中が『?????』な状況だった(笑)。

考える事暫し……… あぁ!そーゆーことか!?

例えば、MQ で複数のパッチを作成した際に:

  • 最初のパッチで新規ファイルを追加
  • 2つ目以降のパッチで、追加したファイルに改変を実施

という状況だとして、ここで『新規ファイル』を改名しようとすると:

  1. 最初のパッチを qpush
  2. 新規追加ファイルを rename
  3. qrefresh でパッチに rename 実施結果を反映
  4. 次のパッチを qpush
  5. 改変対象ファイルが既に rename 済みなので、パッチ適用対象無しエラー
  6. rename 先ファイルに手動でパッチを適用
  7. 以後、全てのパッチに対して、rename 先ファイルへの改変を手動適用する必要あり

みたいな、イヤ〜ンな状況になってしまう。
で、しようがないので、パッチヘッダに書かれたファイル名を、sed を使ってゴリゴリと書き換えてやる必要がある、というストーリーだと理解。

# あるいは、既存の同等リビジョン群を MQ で改変しようとしたケース

あぁ、これ、MQ の改善ネタとして、パッチ投函の虫が疼く話だなぁ(笑)。

パッチ適用の際に、パッチ中の rename 挙動を状態キャッシュで保持しておいて、以後のパッチにおいて改名元に対する改変があれば、改名先ファイルに適用する、みたいな感じ?

パッチヘッダの適用先ファイル名の書き換えをどのタイミングで実施するか(qpush ? qrefresh ?)とか、複製の場合に適用先をどうするか?というのは悩ましいけれど、なかなか面白そうなネタだ。

で、それはさておき。

順当な手段としては、hokorobi 氏が返信したように、『filemap 指定付き convert』が一番綺麗且つ簡単な筈。

追記@2013/12/16: ここから >>>>

本エントリでは、「適用パッチ途中に改名操作を追加」するようなケースを想定しています。

しかし、以下のような改名のケースであれば、パッチ適用状態からの hg rebase の方が簡単です(詳細は id:troter さんの "MQの適用済みパッチをrebaseする" 参照)。

  • リビジョン A に対してパッチ適用中だが、
  • 適用対象リビジョンを B に変更したい、且つ
  • リビジョン A 〜 B の間に、パッチ適用対象ファイルが改名済み

なお、適用済みパッチに対する hg rebase が完了した時点で、hg qrefresh 相当が実施されます。

MQ のパッチ管理領域を Mercurial 管理しているようなケースでは、hg rebase による移動の都度、「パッチ改変」扱いになりますので注意してください。

まぁ、パッチファイルを sed で書き換えたり、hg convert で移動するようなケースなら、そんな事を心配する必要も無いとは思いますが……

追記@2013/12/16: ここまで <<<<

当然、履歴の改変が含まれるので、push 済みの履歴には使えないけどね: あぁ、早く obsolete 機能が完全公開されないかなぁ〜

さて、『filemap 指定付き convert』でファイル名を変えてしまうとして、変換対象リビジョン以前の履歴が膨大な場合、履歴全体の変換ではなく、改名対象ファイルが絡むリビジョンだけを変換したい筈。

というわけで、ファイル改名におけるリビジョン変換を最小化する実験をしてみる。

とりあえず、変換元リポジトリを作成。

hg init src
echo a > src/a
hg -R src commit -Am '#a0'
echo 1 >> src/a
hg -R src commit -m '#a1'
echo 2 >> src/a
hg -R src commit -m '#a2'

echo b > src/b
hg -R src commit -Am '#b0'
echo 1 >> src/b
hg -R src commit -m '#b1'
echo 2 >> src/b
hg -R src commit -m '#b2'

まずは、ファイル b を追加したリビジョンの親リビジョンまで (= 変換の不要なリビジョン群) を抽出する。

# 対象ファイルが追加されたリビジョンを特定:
ADDEDREV=`hg -R src log -r "min(filelog('re:b'))" --template '{node}'`
# "re:b" にしているのは、デフォルトの glob 合致だと
# 『現ディレクトリ相対』なので、この例だと "src/b"
# と書く必要があるため。

# ADDEDREV の直接の親リビジョンを導出
PARENTREV=`hg -R src log -r "${ADDEDREV}^1" --template '{node}'`

hg clone -U -r ${PARENTREV} src dst

ファイル b の追加以降のリビジョンに、重要なリビジョンが無いことを仮定しているので注意。

後は、ファイル b を c に改名しつつ、変換結果リビジョンを dst に取り込めば良い。

# ファイル b を c に改名する設定
cat > filemap <rename b c
EOF

hg convert -v \
    --config convert.hg.startrev=${PARENTREV} \
    --filemap filemap \
    src dst

ADDEDREV が "#b0"、PARENTREV が "#a2" になることから、"hg convert" における "convert.hg.startrev=${PARENTREV}" 指定によって、変換対象は "#a2" 〜 "#b2" に限定される筈。

なぜ変換開始リビジョンとして、"#b0" に相当するリビジョンを指定しないかというと、"#b0" の親である "#a2" を "hg convert" に認識させることで、履歴を綺麗に繋げてくれるだろう、ということを目論んでのこと。

いざ変換を実施……:

a を追加登録中
ブランチ default へ更新中
ファイルの更新数 1、 マージ数 0、 削除数 0、 衝突未解消数 0
b を追加登録中
変換元リポジトリの走査中...
並べ替え中...
変換中...
3 #a2
変換元: 14c7ad37280fea4106f9c139d12c85976516b54f
a
2 #b0
変換元: cb2026dcd405baa92baf040416c10ab46654b1ec
c
1 #b1
変換元: 7fb7404cee9a047ba888d3b19622605db79234a1
c
0 #b2
変換元: d093a7188513aecb5debca239a0b08e0bf19504e
c

おぉ、変換は無事完了したなぁ。では、変換済みリビジョン(= dst から src への outgoing なリビジョン)の按配は……:

$ hg -R dst outgoing src
src と比較中
変更点を探索中
チェンジセット:   3:71694769aee1
親:               -1:000000000000
ユーザ:           FUJIWARA Katsunori 
日付:             Wed Oct 10 23:18:15 2012 +0900
要約:             #a2

チェンジセット:   4:7c61f5d62eb6
ユーザ:           FUJIWARA Katsunori 
日付:             Wed Oct 10 23:18:17 2012 +0900
要約:             #b0

チェンジセット:   5:f944837f2599
ユーザ:           FUJIWARA Katsunori 
日付:             Wed Oct 10 23:18:17 2012 +0900
要約:             #b1

チェンジセット:   6:41fb87dc5903
タグ:             tip
ユーザ:           FUJIWARA Katsunori 
日付:             Wed Oct 10 23:18:18 2012 +0900
要約:             #b2

おぉ、なんか大丈夫……………じゃねぇぇ!!! > "#a2" の親リビジョン

改めて見てみると、"hg convert" 実施時の出力にも、変換の必要が無い "#a2" の変換過程の出力が見て取れる。

やっぱり、src の "#a2" と dst の "#a2" が同一である、ということは、ハッシュ値の同一性だけではなく、明示的に教えてやる必要があるっぽい…… orz

しょうがないので、変換処理の前に、以下のような前準備を実施することに。

# PARENTREV を『変換済み』扱いするため、
# shamap ファイルに対応関係を書き出す
echo "${PARENTREV} ${PARENTREV}" >> dst/.hg/shamap

hg convert -v \
    --config convert.hg.startrev="${PARENTREV}" \
    --filemap filemap \
    src dst

実行& "hg outgoing" 確認してみると……やった!ちゃんと変換できてる!

ちなみに、『変換開始』リビジョン指定に ADDEDREV を指定すると、変換後の "#b0" リビジョンの親が -1 になってしまうので、当初の PARENTREV (= "#a2") を指定する方針は、奇しくも正しかった事に。

内部ロジックを推測するに:

変換対象に含まれて居ないリビジョンは、たとえハッシュ値が .hg/shamap に含まれていても、変換先には存在していないことにする

という事なのかな?

でも、『インクリメンタルな変換』というコンセプト的には、『前回変換したリビジョンは対象に含めない』ことにしたい筈だから、ちょっと挙動的には腑に落ちない感じ。

後で実装を確認した上で、場合によっては修正パッチを投げることになるのかな?