彷徨えるフジワラ

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

(特に Git 併用ユーザには是非読んでおいて欲しい) Mercurial における『ブランチ』の概念 〜 その6

長々続いたこのシリーズも、今回で一応終了。

最終回の今回は、『名前付きブランチ』に関する、ちょっと踏み込んだ話題を扱う。

普通に運用する場合には、それほど必要となる局面は無いかも知れないけれど、知っていればちょっとは便利になるかもしれない事柄などを。

以下、各回で説明する主なトピック:

  • その1: 名前無しブランチ
  • その2: 名前付きブランチ
  • その3: ブックマーク
  • その4: 構造的ブランチ
  • その5: 名前付きブランチ運用で必要なトピック
  • その6: 名前付きブランチに関するちょっと踏み込んだ話(本エントリ)


構造制約を持たない名前付きブランチ

その2でも述べたように、『名前付き』ブランチは、各リビジョン毎に持っている『ブランチ名』属性を元にした、一種のリビジョングループと言える。

情報管理上、『ブックマーク』や Git の『ブランチ』のように、単一の名称に対して『ヘッド』が一つ、といった制約が無いので、『名前付きブランチ』内部で『ブランチヘッド』を複数持つことも可能である。


また、『名前付きブランチ』への所属の判定は、各リビジョンが持つ所属グループ名情報を元にしているだけなので:

  • どこが『枝分かれ』の始まりなのか?
  • どこが『枝分かれ』の先端(= 『ブランチヘッド』)なのか?

といった構造的な情報は、どこにも管理されていない: 厳密には、『名前付きブランチ』の『ブランチヘッド』情報はキャッシュ情報としてファイルに記録されるが、あくまで高速化のための一時的なキャッシュであり、リポジトリ間を伝播する履歴情報上は記録されていない。

この『管理されていない』ことを利用して:

上記のように、同一『名前付きブランチ』内部で、複数の『根』(= 同一『名前付きブランチ』内に親を持たないリビジョン)を持つことすら可能となる。

まぁ、このような運用が必要なケースは滅多に無いとは思うけれど:

  • 特定の作業は、同じ『名前付きブランチ』内部で閉じるようにしたい
  • 作業の際の元リビジョンは、定期的に変動する

みたいなケースを『ブックマーク』や Git の『ブランチ』で運用しようと思ったら、通し番号なり日付なりを付けた名前をつかって、擬似的に運用する以外に手は無い筈。

名前付きブランチの改名

この話の元ネタは、id:kyon_mm 氏による『Mercurialでブランチ名を変更する方法』で、このエントリに対してコメントした内容を、再構築してまとめたもの:

『名前付きブランチ』を作成して作業を開始したは良いが、ブランチに付けた名前を変更したくなった場合、共有リポジトリ等へ当該リビジョンを反映させる前であれば、以下の機能を使用してブランチ名を改名することができる。

  • MQ
  • graft (+ strip)
  • rebase

なお、『名前付きブランチ』内に『名前無しブランチ』による『枝分かれ』が生じている場合は、たとえ現時点で『ブランチヘッド』が単一であっても、直線的な履歴しか扱えない MQ/graft では、履歴ツリー構造を保ったままでのブランチ改名はできないので注意すること。

MQ で実施する場合は:

  1. "hg update" で『枝分かれ』リビジョンに移動
  2. "hg qimport -r 'branch(旧ブランチ)'" で旧ブランチの全リビジョンの取り込み
  3. "hg qpop -a" でパッチ適用を解除
  4. "hg branch 新ブランチ" でブランチ名を設定
  5. "hg qpush -a" でパッチを全適用
  6. "hg qfinish -a" で全パッチを通常リビジョン化

graft (+ stirp) で実施する場合は:

  1. "hg update" で『枝分かれ』リビジョンに移動
  2. "hg branch 新ブランチ" でブランチ名を設定
  3. "hg graft 'branch(旧ブランチ)'" でリビジョンを移植
  4. "hg strip 'branch(旧ブランチ)'" で旧ブランチを破棄

"hg strip" は MQ の機能なので MQ の有効化を忘れずに。

rebase で実施する場合は:

  1. "hg update" で『枝分かれ』リビジョンに移動
  2. "hg branch 新ブランチ" でブランチ名を設定
  3. "hg rebase -s 'min(branch(旧ブランチ)' -d ." でリビジョンを移動

"'min(branch(旧ブランチ)'" は、移動元リビジョンとして、旧ブランチ全体を指定するためのもの: 詳細は "hg help revsets"(HTML 版)を参照のこと。

いずれの手順も、使用している機能における:

新規リビジョン生成で使用するブランチ名情報は、元リビジョン上のものではなく作業領域のブランチ名情報が使用される

という性質を利用したもの。

transplant でも同様なことを実現できなくはないのだが:

上記のような改名を実施しようとした場合、transplant 自体は『ブランチ名』の差異を意識していないので:

同一親の上での移植は、移植前後で何も変わらないから、移植の必要は無いと思われる

という判断から、リビジョンの移植は実行されない: graft や rebase を『柔軟』と評すべきなのか『大雑把』と評すべきなのかは微妙なところ(笑)。

『新ブランチ』の移植先リビジョンが、『旧ブランチ』の『枝分かれ』元リビジョンと異なる状況であれば、 transplant もこの用途に使用することができる:上記の図で言えば、P 以外を親する移植であれば問題ない。

なお、共有リポジトリ等へ push 済みの『名前付きブランチ』の改名に関しては、上記の手順でリビジョンを移植した上で、古い『名前付きブランチ』を『閉鎖』すれば良い。

但し、『共有リポジトリ等へ push 済み』であることから、古い『名前付きブランチ』上のリビジョンを破棄するわけにはいかないので、MQ や rebase を用いる場合は注意が必要: graft は単に strip の実施を止めれば良い。

MQ の場合は、既存リビジョンの破棄に繋がる "hg qimport -r 'branch(旧ブランチ名)'" ではなく、以下のような方法での取り込みが必要(以下は sh/bash スクリプトでの例):

hg log -r 'sort(branch(旧ブランチ名), -rev)' --template '{rev}\n' |
while read rev
do
    hg export ${rev} | hg qimport -n "${rev}" -
done

while ループを回す際に、入力となる "hg log" の対象リビジョン指定で、"sort(..., -rev)" を用いているのは、"hg qimport" されたパッチが "First In(import), Last Out(pushed)" なスタック管理なので、取り込みをリビジョン降順にする必要があるため。

直接 "hg qimport" に対象リビジョンを指定する場合は、"hg qimport" が内部的に降順に整列してくれるので、明示的な順序指定は必要ないのだ。

rebase の場合は、"--keep" オプションにより、移動元リビジョンの破棄を抑止すること。

また、id:troter 氏による『mercurialでpush済みの名前付きブランチをリネームする三つの方法』も参考になる筈。

ちなみに、少々先の話でもよければ、2.3 版で追加された obsolete 機能を使うことで、改名後リビジョンの作成/push 済みリビジョンの破棄を伝播させることで、結果として『名前付きブランチ』の改名が実現できるようになる筈。