彷徨えるフジワラ

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

TortoiseHg の独自機能 〜 履歴の圧縮

このエントリは、Mercurial Advent Calendar 2013 の5日目です。

MercurialGUI」として知られている TortoiseHg ですが、単純にユーザインタフェースをグラフィカルにしただけではありません。Mercurial には無い独自機能も提供しています。

このエントリでは、そんな TortoiseHg の独自機能の1つである「履歴の圧縮」について説明しようと思います。

履歴の圧縮

TortoiseHg における「履歴の圧縮」では、最初に2つのリビジョンを選択します。以下の画面では、stable ブランチのみを表示する設定になっているため、リビジョン番号が飛び飛びになっていますが、気にしないでください。

「連続していない2要素の選択」は、Ctrl キーを押しながらのマウスの左クリック*1で行います。

選択された2つのリビジョンが、履歴ツリー上で「祖先」と「子孫」の関係にある場合は、マウスの右クリック*2で表示されるコンテキストメニューから、「履歴を圧縮」が選択できます。

選択した2つのリビジョンが「祖先」と「子孫」の関係に無い場合や、選択リビジョン数が2つ以外の場合などは、「履歴を圧縮」は選択できませんので注意してください。

「履歴を圧縮」を選択すると、「圧縮」ダイアログが表示されます。

「履歴の圧縮」は以下のような処理に相当します。

選択対象の子孫側(上記例ではリビジョン 20164)時点の内容を持つリビジョンを、祖先側(リビジョン 20158)の子リビジョンとして新規に作成する

新規作成されるリビジョン=圧縮結果リビジョンは、祖先側選択対象リビジョンの子リビジョンとして作成されますので、祖先側の選択対象リビジョンにおける変更内容は、「履歴の圧縮」対象に含まれません

拙著「入門 TortoiseHg+Mercurial」中のコラム(第13章、p247)では「祖先側リビジョンが含まれる」旨の記述がありますが、書籍側での記述は間違いですのでご注意ください。

「圧縮」ダイアログでの「圧縮」ボタン押下を契機に、圧縮処理が開始されます。

圧縮処理が完了したなら、「コミット」ボタンを押して、圧縮結果をコミットします。

表示される「コミット」ダイアログは、ワークベンチで表示されるコミット用ペイン部分を切り出したものです。「コミット」ボタンの位置の違いや、「Close」ボタンが表示される点を除けば、特に操作上の問題は無いでしょう。

コミットログ入力欄には、圧縮対象となる全てのリビジョンのコミットログが、"* * *" で区切られた状態で列挙されますので、適宜修正した上で、「コミット」ボタンでコミットしてください。

「コミット」し終えたなら、「Close」でダイアログを閉じます。コミット前に何らかの手動作業が必要な場合は、「コミット」しなくても「Close」可能ですが、後からコミットする際には、コミットログは自前で何とかする必要があります。

コミット用ダイアログから、「圧縮」ダイアログに戻りますので、「閉じる」を押して圧縮処理は終了です。

「圧縮」ダイアログにおける「古い履歴はなにも変更ありません」という表示は、ワークベンチの履歴ツリー表示で確認できます。

圧縮対象となっていたリビジョン 20161 〜 20164 はそのままに、リビジョン 20158 の子リビジョンとして、新たにリビジョン 20162 が作成されているのがわかるでしょうか?

この新規リビジョン 20162 には、リビジョン 20161 〜 20164 での変更内容の全てが含まれて居ます。

後は、リビジョン 20161 〜 20164 の履歴情報を破棄した上で、共有リポジトリなりにリビジョン 20162 を反映すれば、途中の作業過程を無視して、作業成果のみを共有できます。

「履歴の圧縮」の舞台裏

「履歴の圧縮」の際に選択されるリビジョンを、A (ancestor:祖先側) 〜 D (descendant:子孫側) の間のリビジョンとした場合、TortoiseHg の「履歴の圧縮」機能は、コマンドライン上での以下の実行に相当します。

  1. hg update -C A : リビジョン A で作業領域を更新
  2. hg revert --all --rev D : D 時点の内容を作業領域に展開
  3. hg commit : 作業領域の内容(= D 時点の内容)をコミット

2番目の手順では、作業領域の内容が D 時点の内容になりますが、hg revert での実行ですので、「作業領域の親リビジョン」は A のままです。

そのため、3番目の手順で作成される新規リビジョンの親は A のままとなります。

UNIX 系環境であれば、祖先〜子孫性の確認から、コミット直前までの操作を、以下のようなシェルスクリプト(B-shell 向け)で自動化できるでしょう。

# USAGE: compress ANCESTOR DESCENDANT
if test -z `hg log -q -r "first(($1)::($2))"`; then
    echo "$1 is not ancestor of $2" 1>&2
    exit 1
fi

hg update "$1" || exit $?
hg revert --all -r "$2" || exit $?
echo "compression completed. don't forget to commit."
echo "description of target revisions:"
echo "========"
hg log -r "($1)::($2)" --template "{desc}\n========\n"

"A::D" 記述は、「Aの子孫で且つDの祖先」に相当するリビジョンを抽出するためのもので、A と D が祖先〜子孫関係にない場合は空になります。

hg log への -q オプション指定や、revsets 記述での "first(...)" は、理論上は必要ないのですが、hg log 出力が一定量以上になる(=正常系)と、バッククォートによる置換でエラーがでてしまうため、それを防止するために記述しています。

なお、上記スクリプトでは、引数不足等の基本的なチェックは省いていますので、実際に使用される際はご注意ください。

既存の「履歴の圧縮」機能との比較

Mercurial における「履歴の圧縮」と聞いて、一般的に思い付く機能は、おそらく以下のようなものでしょう。

基本的にこれらにおける「履歴の圧縮」では、以下のような手順で機能を実現しています。

  1. 複数のリビジョンにおける変更内容を内包する、新規リビジョンを作成
  2. 新規リビジョンに変更内容が内包される既存のリビジョンを、全て破棄

既存のリビジョンが2つ目の手順において破棄されることで、あたかも「1つのリビジョンに履歴が圧縮された」ように見えるわけです。

TortoiseHg の「履歴の圧縮」も、「圧縮対象リビジョンの破棄」まで含まれていれば、既存の「履歴の圧縮」機能と同様に、「1つのリビジョンに履歴が圧縮された」ように見えた筈です。

ここからは私個人の推測ですが:

  • 既存のリビジョンの破棄実施を、実行の都度確認させるのは案外面倒
    • 利用者も、デフォルト設定を「破棄する」にしてしまいがち
  • 誤操作で想定外のリビジョンを破棄してしまうと、復旧操作はそこそこ面倒
    • hg strip 操作の取り消しと同様に、バンドルファイルの取り込みが必要

といった事情を考えた場合、GUI の持つ「操作容易性」と Mercurialの持つ「安全性」を保つには、既存のリビジョンはそのまま維持した方が妥当、という結論に至ったのではないかと思われます。

まぁ、単純に「そこまで頑張るのが面倒だった」のかもしれませんが(笑)

なお、Mercurialにおける「リビジョンの破棄」は、現状の実装では「履歴データの削除」そのものですが、今後は obsolete マーキングによって「(通常は)不可視」化する方向に進む予定です。

「不可視」化であれば、履歴情報が損なわれることがありませんから、TortoiseHg の「履歴の圧縮」でも「既存リビジョンの破棄」が行われるようになるかもしれませんね。

*1:WindowsUNIX 系 OS でのウィンドウ環境の場合。MacOS 環境では Command キーを押しながらになります。

*2:WindowsUNIX 系 OS でのウィンドウ環境の場合。MacOS 環境では Ctrl を押しながらのクリックか、「副ボタン」相当の操作になります。