daily dayflower

2010-02-12

textwrap でマルチバイト文字列を fold する

Mercurial に日本語メッセージカタログが同梱されるようになって久しいんですが、Mercurial 1.4 以降で日本語ヘルプメッセージが一部文字化けするようになってしまいました。

化け方からすると text wrapping (folding) 処理まわりで化けているみたい(文字のバイト境界を無視して folding している)。ということでちょっと Mercurial のコードを浅追いしてみたところ、minirst.pytextwrap を呼び出していて textwrap がマルチバイトに対応していないからではないかと思いました。

なので、とりあえずマルチバイト文字列に対応した textwrap を書いてみようと思いました。

import sys, textwrap
from unicodedata import east_asian_width

__all__ = ['MBTextWrapper', 'wrap', 'fill']

def _mb_width(c):
    eaw = east_asian_width(c)
    if eaw == 'F':
        return 2
    elif eaw == 'W':
        return 2
    elif eaw == 'A':
        return 2

    return 1

class MBTextWrapper(textwrap.TextWrapper):
    def __init__(self,
                 width=70,
                 initial_indent="",
                 subsequent_indent="",
                 expand_tabs=True,
                 replace_whitespace=True,
                 fix_sentence_endings=False,
                 break_long_words=True,
                 encoding=None):
        textwrap.TextWrapper.__init__(self, width, initial_indent,
                                      subsequent_indent, expand_tabs,
                                      replace_whitespace,
                                      fix_sentence_endings,
                                      break_long_words)

        self.encoding = encoding or sys.stdout.encoding

    def _mb_len(self, str):
        if not isinstance(str, unicode):
            str = unicode(str, self.encoding)

        return sum(_mb_width(c) for c in str)

    def _mb_cut(self, str, max_len):
        want_unicode = isinstance(str, unicode)
        if not want_unicode:
            str = unicode(str, self.encoding)

        tok1, tok2 = '', ''
        l = 0
        for c in str:
            w = _mb_width(c)

            if l + w <= max_len:
                tok1 += c
            else:
                tok2 += c
            l += w

        if want_unicode:
            return tok1, tok2

        return tok1.encode(self.encoding), tok2.encode(self.encoding)

    def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
        space_left = max(width - cur_len, 1)

        if self.break_long_words:
            cut, res = self._mb_cut(reversed_chunks[-1], space_left)
            cur_line.append(cut)
            reversed_chunks[-1] = res
        elif not cur_line:
            cur_line.append(reversed_chunks.pop())

    def _wrap_chunks(self, chunks):
        lines = []
        if self.width <= 0:
            raise ValueError("invalid width %r (must be > 0)" % self.width)

        chunks.reverse()

        while chunks:
            cur_line = []
            cur_len = 0

            if lines:
                indent = self.subsequent_indent
            else:
                indent = self.initial_indent

            width = self.width - self._mb_len(indent)

            if chunks[-1].strip() == '' and lines:
                del chunks[-1]
            while chunks:
                l = self._mb_len(chunks[-1])

                if cur_len + l <= width:
                    cur_line.append(chunks.pop())
                    cur_len += l

                else:
                    break
            if chunks and self._mb_len(chunks[-1]) > width:
                self._handle_long_word(chunks, cur_line, cur_len, width)

            if cur_line and cur_line[-1].strip() == '':
                del cur_line[-1]

            if cur_line:
                lines.append(indent + ''.join(cur_line))

        return lines

def wrap(text, width=70, **kwargs):
    w = MBTextWrapper(width=width, **kwargs)
    return w.wrap(text)

def fill(text, width=70, **kwargs):
    w = MBTextWrapper(width=width, **kwargs)
    return w.fill(text)

コード量が多くなってしまっていますが、_handle_long_word()_wrap_chunks() はほぼ(添付の textwrap の)コピペで、len() を呼び出すところ等を置き換えているだけです。

エンコーディングデフォルト値を sys.stdout.encoding にしてるのはちょっと問題かもしんないですね。sys.getdefaultencoding() にしようかとも思ったんですが、デフォルトインストール状態だとたいがい ascii のままなのでアレだし。まぁ所詮デフォルト値ですが。Mercurial のことだけ考えるのならデフォルト値を 'utf-8' 決め打ちでいいかもしんないです。



Mercurial文字化けを改善するには、上記のコードをたとえば /usr/lib/python2.4/site-packages/mercurial/ 以下などに mbtextwrap.py などの名前で置きます。

んで、mercurial/minirst.py のコードで textwrap を import してるところを mbtextwrap に置き換えて、textwrap.fill() を呼び出しているところを mbtextwrap.fill() に置換すれば*1、一応ヘルプメッセージの文字化けはなくなりました。

えっといいわけがましいですが Python母国語でないんで、なんかこうしたほうがいいとかアドバイスがあれば是非是非。

*1:あと前述のようにエンコーディングsys.stdout.encodingデフォルトとしているので encoding='utf-8' のように引数を指定したほうがいいかもしんないです。

2009-06-23

MQ の適用済パッチのログメッセージを一括取得する extension を書いた

たとえば,

$ hg qpush
applying 248.diff
now at: 248.diff

$ hg qpush
applying 249.diff
now at: 249.diff

$ hg qseries -s
248.diff: 記事追加機能を仮実装した(うまくいっていない)
249.diff: 記事追加機能を修正した
250.diff: 記事編集機能を実装した

こんな状況の場合 hg qseries -s でだいたいの変更内容は取得できるんだけど,一つのパッチコミット)のログが複数行にわたる場合,これだけだとわかんない。

いちおう,hg log -v すると,複数行のログもとれる。

$ hg log -r qbase:qtip -v
changeset:   248:cf52d09c494d
tag:         qbase
user:        dayflower <dayflower@example.com>
date:        Tue Jun 23 14:22:08 2009 +0900
files:       lib/AddArticle.pm
description:
記事追加機能を仮実装した(うまくいっていない)


changeset:   249:94502ca09ad5
tag:         qtip
tag:         tip
user:        dayflower <dayflower@example.com>
date:        Tue Jun 23 14:41:07 2009 +0900
files:       lib/AddArticle.pm db/schema.sql
description:
記事追加機能を修正した
スキーマを変更した

だけど,なにかと冗長な情報になってしまう。

今回つくった extension を使うと,

$ hg qlog
[248:cf52d09c494d]
記事追加機能を仮実装した(うまくいっていない)

[249:94502ca09ad5]
記事追加機能を修正した
スキーマを変更した

のように,qbaseqtip(つまり applied queue)に限って,changeset ID とログメッセージを取得することができます。

これをコピペすれば,他の VCSコミットするときのコミットメッセージに使える,というわけです。


実は hg log には表示に使うテンプレートを指定する機能があるので,それ使えば同じようなことは標準のモジュールだけでできるんだけど,勉強のために extension として書いてみました。

# qlog.py

from mercurial import context, util, cmdutil, error
from mercurial.i18n import _
from mercurial.node import hex, short

class qlog_printer(cmdutil.changeset_printer):
    def _show(self, ctx, copies, prop):
        changenode = ctx.node()
        log        = self.repo.changelog
        changes    = log.read(changenode)

        self.ui.write(_("[%d:%s]\n") % (ctx.rev(), short(changenode)))

        description = changes[4].strip()
        if description:
            self.ui.write(description)
            self.ui.write("\n")

        self.ui.write("\n")

def qlog(ui, repo):
    try:
        repo.lookup('qbase')
        repo.lookup('qtip')
    except error.RepoError:
        return

    get = util.cachefunc(lambda r: repo[r].changeset())
    changeiter, matchfn = cmdutil.walkchangerevs(ui, repo, [], get,
                                                 {'rev': ['qbase:qtip']})

    patch = False       # matchfn or cmdutil.matchall(repo)

    displayer = qlog_printer(ui, repo, patch, {}, True)

    for st, rev, fns in changeiter:
        if st == 'add':
            displayer.show(context.changectx(repo, rev))
        elif st == 'iter':
            displayer.flush(rev)

cmdtable = {
    "qlog": (qlog, [], _('hg qlog')),
}

コード自体は Mercurial のコードをコピペしたので,あんまり見通しはよくないです*1

extension でコマンドを実装するには,このように cmdtable というテーブル変数関数などを登録しておけばいいみたい。


この extension のインストールのしかたは,適当な場所において,.hgrc

[extensions]
qlog=/home/dayflower/hgext/qlog.py

とかみたく,extension を置いた場所をフルパスで指定すればいいです。このへんのしくみは非常にお手軽ですね。

*1:てか Mercurial のコードは期待したほど綺麗な構造でもなかった。

2009-06-08

Mercurial MQ でバイナリファイルを扱う場合はご用心

MQ でなんの気なしにバイナリファイルを扱うと,バイナリファイル自体を lost します。これはこわい。というか実際にはまりました。

現象

まずバイナリファイルを追加。

$ hg init

$ perl -e 'print "\x00"' > bin

$ ls
bin

$ hg addremove
adding bin

$ hg ci -m "binary file added"

$ hg log
changeset:   0:90e1a39f0fe7
tag:         tip
user:        dayflower <dayflower@example.com>
date:        Mon Jun 08 11:31:12 2009 +0900
summary:     binary file added

バイナリファイルといっても NUL バイトいっこのファイルだけど。

この changeset を qimport する。

$ hg qimport -r tip

$ hg qseries -s
0.diff: binary file added

んで qpop

$ hg qpop -a
patch queue now empty

$ ls
# なにもない(問題ない)

qpop したから bin ファイルが消えるのは想定どおり。


ここで qpush すると bin ファイルが復活するはずだけど……

$ hg qpush -a
applying 0.diff
patch 0.diff is empty
now at: 0.diff

$ ls
# ファイルが追加されてない!

復活してない!

なぜ?

パッチファイルを見てみると

$ cat .hg/patches/0.diff
# HG changeset patch
# User dayflower <dayflower@example.com>
# Date 1244428471 -32400
# Node ID cabcbf570d050415463d8541b1045ed8ae497981
# Parent  0000000000000000000000000000000000000000
binary file added

diff -r 000000000000 -r cabcbf570d05 bin
Binary file bin has changed

「Binary file bin has changed」としか内容がない(バイナリ自体のデータが記載されていない)。

diff の出力をそのままパッチにしたからこうなったんですね。だから,戻しようがない。

対処するには --git オプションをつける

qimport からやりなおします。このとき --git オプションをつけると,パッチdiff)が GIT 形式になる(後述)。

$ hg qimport -r tip --git

$ hg qseries -s
0.diff: binary file added

$ hg qpop -a
patch queue now empty

$ ls
# なにもない(問題ない)

さて,qpush すると……

$ hg qpush -a
applying 0.diff
now at: 0.diff

$ ls
bin     # ちゃんと復活した!

おお,今回はちゃんと復活しましたよ!


パッチファイルを覗いてみると,

$ cat .hg/patches/0.diff
# HG changeset patch
# User dayflower <dayflower@example.com>
# Date 1244428314 -32400
# Node ID d016fd57d57065401ac2b7d732adbc36ba3089a9
# Parent  0000000000000000000000000000000000000000
binary file added

diff --git a/bin b/bin
new file mode 100644
index 0000000000000000000000000000000000000000..f76dd238ade08917e6712764a16a22005a50573d
GIT binary patch
literal 1
Ic${MZ000310RR91

今回は 1 バイト の NUL 文字のファイルだからよくわかんないけど,もっとサイズが大きい場合は,base64 みたいな(どんな形式かは忘れた)テキストによるバイナリエンコーディングの表現が載ります。なので,きちんと復活できるわけです。

バイナリファイルじゃなくても属性まわりでも同じことが

こんどは,ファイルの実行属性をいじって,それを changeset / MQ patch 化してみます。

$ ls -F
test.cgi

$ hg qnew -m 'added eXecutable bit to test.cgi' add_x

$ chmod a+x sample.cgi

$ ls -F
sample.cgi*     # 実行属性を付与した

$ hg status
M sample.cgi

$ hg qrefresh

$ hg qpop -a
patch queue now empty

$ ls -F
sample.cgi      # 実行属性が消えた(想定どおり)

これで qpush すると実行属性が再び付与されるはずだけど……

$ hg qpush -a
applying add_x
patch add_x is empty
now at: add_x

$ ls -F
sample.cgi      # 実行属性が戻らない!

やっぱりだめです。


こんどは同じように --git オプションを使います。qimport に対してじゃなくて qrefresh に対してだけど。

$ chmod a+x sample.cgi

$ hg status
M sample.cgi

$ hg qrefresh --git

$ ls -F
sample.cgi*     # 実行属性を付与した

$ hg qpop -a
patch queue now empty

$ ls -F
sample.cgi      # 実行属性が消えた(想定どおり)

$ hg qpush -a
applying add_x
now at: add_x

$ ls -F
sample.cgi*     # 実行属性がきちんと戻った!

今回はきちんと実行属性についてもハンドリングできました。


なお qnew などにも --git オプションをつけることができるけど,これはあくまで一番最初のパッチを GIT 形式にするという意味しかなくて,qrefresh の際につけわすれると,結局バイナリファイル・属性値の変更履歴は lost します。

まとめ

qimportqrefresh するときは忘れずに --git オプションをつけよう。絶対。

おわりに

いちおう下記の issue があがってます。

たしかに無言でファイルが消えたり属性が消えたりするのは怖い。--gitデフォルトにするか,GIT 形式じゃなくてもバイナリなどの変更履歴を保存するようにしてほしいです。

2009-06-02

単一の changeset を分割する(ファイル単位の場合)

いま working copy がこんな状態だとする。

$ hg status
M aaa
M bbb
M ccc

とりあえず aaa だけ commit しようと思って……

$ hg ci -m 'modified aaa'

あああパス指定するの忘れた。

$ hg status

bbb, ccc も changeset に取り込まれてしまった!

やりなおしたい!(あるある……というか,いまさっきやってしまった)


こんな時にも MQ は使えます。

MQ にとりこむべき changeset の revision number を知るために,とりあえず hg log してみる。

$ hg log
changeset:   1:e8d6debe7796
tag:         tip
user:        dayflower <dayflower@example.com>
date:        Tue Jun 02 15:43:50 2009 +0900
summary:     modified aaa

changeset:   0:f746ed49fd00
user:        dayflower <dayflower@example.com>
date:        Tue Jun 02 15:43:28 2009 +0900
summary:     initial import

ふむ。changeset 1 を指定すればよいのね。なので hg qimport -r 1 とやってもいいんだけど,今回の場合は最新の changeset だから tip キーワードを使って,

$ hg qimport -r tip

のようにすればよい。

で確認してみると,

$ hg qseries -s
1.diff: modified aaa

いまさっきの changeset が MQ のパッチスタックに変換された(そして適用済み)。


これで hg qpop して,patches/ ディレクトリのパッチを直接いじればいいのかなぁ……なんて思ってウェブを調べたら,そんなめんどいことはしなくてよかった。MqTutorial - Mercurial にきちんと書いてありました。


発想としては,1.diffパッチ内容を aaa に関するものだけに修正しよう―― refresh しよう――ということになる。なので,hg qpop をせずに話をすすめます。

んで,現在のパッチスタックのパッチにどのファイルに対する変更があるのか調べてみる。

$ hg qdiff | grep '+++'
+++ b/aaa	Tue Jun 02 15:45:05 2009 +0900
+++ b/bbb	Tue Jun 02 15:45:05 2009 +0900
+++ b/ccc	Tue Jun 02 15:45:05 2009 +0900

これ他にいいやり方ないですかね。ともかく,aaabbbccc が最新パッチに含まれていることがわかった。

なので,1.diffaaa への変更内容だけ含むように refresh する。

$ hg qrefresh aaa

これで 1.diffaaa の修正履歴「だけ」を含むように更新された*1

パッチ群をみてみると

$ hg qseries -s
1.diff: modified aaa

適用されたままだけど,

$ hg status
M bbb
M ccc

bbbcccパッチに含まれない。やったね!

なので,現在のパッチをもう一度 changeset に戻す。つまり hg qfinish するだけ。

$ hg qfinish -a

あいかわらず何もいわれないので不安になるけど,ステータスをみてみると,

$ hg status
M bbb
M ccc

きちんと?未 commit のままになってる。

んで,ログをみると

$ hg log
changeset:   1:6b8e35a13500
tag:         tip
user:        dayflower <dayflower@example.com>
date:        Tue Jun 02 15:43:50 2009 +0900
summary:     modified aaa

changeset:   0:f746ed49fd00
user:        dayflower <dayflower@example.com>
date:        Tue Jun 02 15:43:28 2009 +0900
summary:     initial import

aaa の修正履歴だけはまた changeset に戻ってる。すばらしい。


今回はファイル単位の changeset を分割したんだけど,修正内容の一部だけ……とかやる場合は,やはりパッチを手でいじくる必要がある(⇒ MqTutorial - Mercurial)。

このへんは git のほうがすぐれている気もする。でも Mercurial + MQ で十分満足。

*1:今後一部 commit のアナロジーで間違えてやっちゃいそうな syntax ですな。qrefresh の使い道からすると修復不能で困ったりはしないけど。

2009-05-20

Mercurial MQ について

巷では git の大ブームだけど,ひさしぶりに Mercurial について書きます。


Mercurial について言及されたブログとか読んでいるとき,たまに MQ という言葉を目にして気になっていた。ながらく気にはとめつつ全然調べていなかったんだけど,ちょっと利用しようかなというケースがあり,ちょこっと触ってみた。

f:id:dayflower:20090519115853p:image:w186,right

自分の理解では,MQ (Mercurial Queues) とは,誤解を恐れずにいえば Mercurial の changeset と独立して構成される修正履歴(パッチ)のスタックのようなものだ。

(なので今後 MQ の patch queues を Queues という名称と裏腹に「パッチスタック」「パッチ群」などと勝手に呼び称します)

「誤解を恐れずにいえば」と書いたけれど,この直感的な印象は MQ を使っていくうちに――大筋では変わらないものの――ちょっと変わった。それでこの文書を書こうと思った。

さいしょは具体的な利用局面を想定してこの tutorial 的なものを書こうと思ったんだけど,挫折した。挫折したのは,恥ずかしながら Mercurial 自体チームプレイで使ったことがないからだと思う。だから説得力のある例を思いつけなかった。


ということで,(確固たる目標なしに)だらだらと MQ を使いながら書いていきます。

いいわけカコワルイけど,いくつか断り書きを。

  • (当然のことながら)Mercurial 本体の知識(利用歴)があること前提で書いている。もし Mercurial の知識なしにこの文書を読むと,Mercurial ってなんて面倒なんだという感想を抱くかもしれない。でも通常の開発フローで Mercurial (本体)を使うなら,一般の分散型リビジョン管理システムと相違なく使える。これはあくまで MQ の使い方について。
  • 入門Mercurial に MQ についても記載されているらしい。Mercurial 本体も習熟したいのならこの本を買うのもいいだろう。
  • 書き終わってから気づいたけど HACKING TIPS AND TRICKS のほうが簡潔でわかりやすい。この長いだらだらした文書に眩暈がしたならこちらのほうがおすすめ。
  • 当たり前だけど,自分の手を動かすのが理解を深める一番の手段。

MQ を使い始めるその前に

MQ は(今の)Mercurial distribution についてくるんだけど,extension なので標準では有効になっていない。

~/.hgrc

[extensions]
mq=

を追加しておこう。

そして,MQ を有効にするのなら,ついでに(同様に同梱されている)color extension も追加しておこう。これがあると MQ の出力がわかりやすくなる。しかも hg statushg diff の出力も colorize されるというおまけつき。MQ を使わない人にも超オススメなのです。

イントロダクション

ある日 BOSS に呼び出された。

BOSS ちょっとある店のページを作ることになってさ,おおまかには作ったんだけど,メニューのとこだけやってよ

ある店?ページ?などいくつかの疑問が頭にわいたけど,とりあえず BOSS の Mercurial レポジトリを clone することにした。

$ hg clone /home/boss/saturn mywork
updating working directory
1 files updated, 0 files merged, 0 files removed, 0 files unresolved

$ cd mywork

ふむ,いっこしかファイルがないらしい。

$ ls -F
index.txt

$ cat index.txt
=Bar Saturn=

==STAFF==
* dayflower

==MENU==

ふむふむ。Bar Saturn*1 のページね。テキストファイル?まあいいや。この MENU ってとこに追記していけばいいのか。

はじめての MQ パッチ

さて,普段の Mercurial ならがしがし編集していって都度都度 commit という形をとるんだけど,MQ の場合「まず(これからの作業を記録する)パッチを作成し,修正するたびに更新」という形になる。

$ hg qinit

$ hg qnew add_menu

hg qinit というのがこの作業コピー用の MQ の初期化コマンドなんだけど,パッチ群のバージョン管理をおこなわない(後述)のなら実は hg qinit は必要ない*2

hg qnew で「これからの作業履歴」を記録するパッチを作成する。名前は簡潔かつわかりやすいもの(そして英数字で構成されており空白を含まないもの……すなわちファイル名みたいなの)をつけたほうがよい。

メニューを追加するのでパッチの名前は add_menu ということにした。

余談: パッチ名についてのちょっと深い話

なんで「簡潔かつわかりやすい名前」なのかって?

MQ では,例の .hg/ 下に patches/ というディレクトリを作り,そこにパッチ群を格納していく。

$ ls -F .hg/
00changelog.i  branchheads.cache  hgrc      requires  undo.branch
branch         dirstate           patches/  store/    undo.dirstate

$ ls -F .hg/patches/
add_menu  series  status

ここにいまさっき名前をつけた add_menu というファイルがある。これが「簡潔かつわかりやすい(かつ空白を含まない英数字)」名前をつけたほうがいい理由。日本語で長ったらしく説明したい,ということであれば,パッチの「メッセージ」(コミットメッセージとほぼ同義)を後からでもつけられるので,そちらを利用する。

なお,この add_menu というファイルは unified diff 形式のパッチファイルそのもの。こいつを直接編集するのはおすすめしないけど*3パッチ群をガッと upstream にメールで送りたいときは,こっからもっていってもいいと思う。

ちなみに上記 patches/ ディレクトリの中をみればわかるとおり,パッチ名として series というのと status というのはつけられない。暇なひとは hg qnew series とかやってみよう。


作業をおこなうまえに「簡潔かつわかりやすい」名前を考えるのなんて面倒。でも,パッチ名はあとからでも hg qrename で変更できるので,とりあえず hg qnew p1 とかしといても構わない。あと「作業前にあらかじめ」ってとこに抵抗感がある人もいると思うけど,じっさいはいくらでも対処法があります(後述)。

パッチの「更新」

さーて編集。

$ hg status
# まだなにも編集されていない

$ echo "* cocktail" >> index.txt

$ hg status
M index.txt
$ hg diff
diff -r 90768f851444 index.txt
--- a/index.txt Fri May 15 17:21:19 2009 +0900
+++ b/index.txt Fri May 15 17:31:42 2009 +0900
@@ -4,3 +4,4 @@
 * dayflower
 
 ==MENU==
+* cocktail

ここまでは,いつもの Mercurial と同じ。いつもならここで hg ci するところだけど,今回は MQ。

MQ 的な考えでは「add_menu パッチを,現在の編集結果を反映したパッチに更新しよう」となる。

(ちなみに「更新」する前は hg qnew したばっかなので,パッチの中身はカラ)

$ hg qrefresh

$ hg status

$ hg diff

あたかも commit したかのように,現在は最新版ってことになった。


現在パッチスタックがどのようになっているか調べるには hg qseries というコマンドを使う。

$ hg qseries
add_menu

ミョーに強調されてみえる理由は color extension を有効にしたため。まだ初期段階なのであんまり詳しくは書かないけど,とくに color extension が有効な場合,hg qseries コマンドを一番よく使うことになると思う。

余談: MQ と changeset (1)

「あたかも commit したかのように」って書いたけど,じつはパッチは changeset として生成されている。

$ hg log
changeset:   1:1e4ff0e2e48d
tag:         qtip
tag:         add_menu
tag:         tip
tag:         qbase
user:        dayflower <dayflower@example.com>
date:        Fri May 15 17:37:42 2009 +0900
summary:     [mq]: add_menu

changeset:   0:79d8edfc7bda
tag:         qparent
user:        BOSS <boss@example.com>
date:        Fri May 15 17:19:26 2009 +0900
summary:     初期インポート

いろいろ tag もついてる(後述)んだけど,このように add_menu は changeset としてみなされている。このへんの changeset まわりについては後でも触れる。いまのところあんまり気にしないで。

さらに変更して,さらに qrefresh

カクテルしかメニューにないというのもアレなので,ワインも追加してみる。

$ echo "* win" >> index.txt

$ hg status
M index.txt

$ hg qrefresh

$ hg status

$ hg qseries
add_menu

hg qrefresh で「現在のパッチadd_menu)」を「更新」したので,hg qseries ででてくるのは,あくまで add_menu だけ。


この「現在のパッチ」がどのようになっているか,確認してみよう。

$ hg diff

おっと。先ほども述べたように(疑似的に)commit された状態なので,hg diff しても何もでてこない。

現在のパッチ(スタックトップのパッチ*4)を確認するには hg qdiff というコマンドを使う*5

$ hg qdiff
diff -r 79d8edfc7bda index.txt
--- a/index.txt Fri May 15 17:19:26 2009 +0900
+++ b/index.txt Mon May 18 12:56:50 2009 +0900
@@ -4,3 +4,5 @@
 * dayflower
 
 ==MENU==
+* cocktail
+* win

カクテルとワインを追加したという,これまでの過程が,ひとつのパッチにまとまっている。

逆にいうと,それらの過程は,このように qrefresh していくだけだと後から不可分になる。さきほど「あたかも commit したかのように」と書いたけど,qrefreshhg ci の代わりに使うと痛い目を見る。

各作業過程を別個のものとして管理するには,hg qnew で新しいパッチとして新たに始めることが必要になる。んだけど後述。

余談: MQ と changeset (2)

$ hg tip
changeset:   1:fe5fb08c54a6
tag:         qtip
tag:         add_menu
tag:         tip
tag:         qbase
user:        dayflower <dayflower@example.com>
date:        Mon May 18 12:54:40 2009 +0900
summary:     [mq]: add_menu

さきほどの changeset ID をおぼえていますか?実は「1:1e4ff0e2e48d」だった。つまり(qrefresh により)今回生成された changeset と revision index はおんなじだけど,ハッシュ ID が違うということになる。

つまり hg qrefresh するたびに,内部的には「commit しなおし」ていることになる。おもしろいね*6

パッチにメッセージをつける

hg qnew で次のステップに進む前に。

いままで add_menu とかいう,人間にはちょっとわかりづらい名前でパッチを扱ってきた。でも,これだけだとどんな変更を加えた・加えているのか自分にもわかんないよね。

なので,パッチには「メッセージ」をつけることができる。hg qrefresh-m オプションをつけると現在作業中のパッチにメッセージをつけられる。

$ hg qrefresh -m "カクテルとワインを追加した"

まるでコミットログみたいだ*7


パッチメッセージは hg log でも確認できるけど,いままで使ってきた hg qseries コマンドに -s オプション(summary の略)をつけると,確認することができる。

$ hg qseries -s
add_menu: カクテルとワインを追加した

これで「いままでどんな変更を加えてきた」「いまどんな変更をしようとしている」のか,わかりやすくなった。


なお,hg qrefresh コマンドは,ファイルに変更がなくても実行することができる。パッチメッセージの変更のためだけにおこなうのも全然 OK。

新しいパッチで作業を始める

BOSS に聞いていた限りでは Bar Saturn で提供される飲み物はカクテルとワインだった。add_menu パッチはほぼ完成したと見ていいだろう。

しかし個人的にはビールも加えたほうがいいと思う。メニューにビールを加えるかどうかだが……最終的に BOSS に意向を聞く必要があるので,add_menu パッチには含めたくない。別個のパッチadd_beer)として管理しよう。

$ hg qnew -m "ビールを追加した" add_beer

hg qnew でも -m オプションをつかってあらかじめパッチのメッセージを登録できる(そしてもちろん後ほど qrefresh -m MESSAGE で変更できる)。メッセージが過去形の文章になっているのは,のちのち commit した場合のことを考えてそうした。体言止めで書く流儀の人もいるかもしれない。


さて,編集編集。

$ echo "* beer" >> index.txt

$ hg qrefresh

$ hg qseries
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

これでパッチスタックには add_menuadd_beer の二つがたまった。スタックといいつつ,より新しいものが下に記述されるようになっていることに注意。

余談: MQ と changeset (3)

ここで hg log して changeset をみてみる。

$ hg log
changeset:   2:146728d6674f
tag:         qtip
tag:         tip
tag:         add_beer
user:        dayflower <dayflower@example.com>
date:        Mon May 18 13:57:38 2009 +0900
summary:     ビールを追加した

changeset:   1:05b916013804
tag:         add_menu
tag:         qbase
user:        dayflower <dayflower@example.com>
date:        Mon May 18 12:54:40 2009 +0900
summary:     カクテルとワインを追加した

changeset:   0:79d8edfc7bda
tag:         qparent
user:        BOSS <boss@example.com>
date:        Fri May 15 17:19:26 2009 +0900
summary:     初期インポート

「適用済み」のパッチが個別の changeset として commit されているかのようにみえる。つまり MQ のパッチスタックが一つの changeset になるのではなくて,各パッチが一つの changeset となる,ということだ。

あくまで「適用済み」のみ現れるので,後述する hg qpop などでパッチスタックを遡ると,この changeset の数が増減する。

tag にいろいろ興味深いタグが出現している。

qparent
MQ を適用する元となった changeset
qbase
MQ パッチスタックで一番最初に適用された(底の)パッチ
qtip
MQ パッチスタックで適用済みのもののうち最上位

また「パッチ名」もタグになっている。

qpopqpush で作業状態を自由に移動

パッチスタックというくらいだから,スタックの push / pop はお手の物。

スタックを pop(qpop)して前のパッチ編集状況に戻してみる。

$ cat index.txt
=Bar Saturn=

==STAFF==
* dayflower

==MENU==
* cocktail
* win
* beer

$ hg qpop
now at: add_menu

$ cat index.txt
=Bar Saturn=

==STAFF==
* dayflower

==MENU==
* cocktail
* win

おお。見事に前の編集履歴まで戻ってこれた。

シェルディレクトリスタックの pushd / popd とのアナロジーからすると,pop してしまったパッチの編集内容(add_beer)は失われてしまったように感じるかもしれない。

でも心配ご無用。パッチスタックには pop したパッチもきちんと残っている。

$ hg qseries -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

color extension を有効にしていると,このようにパッチスタックのうち適用されているパッチとそうでないパッチを識別することができる(これが color extension をオススメする理由)。

color extension が有効でない場合は,hg qapplied で適用済みのパッチの一覧を取得すればよい。

$ hg qapplied -s
add_menu: カクテルとワインを追加した

ちなみに hg qunapplied という逆の(適用されていないパッチ一覧を表示する)コマンドもある。

また,現在のスタックトップは hg qtop というコマンドで確認することができる*8

$ hg qtop
add_menu

しつこいようだけど,color extension を有効にしていれば qapplied / qunapplied / qtop コマンドを使うことはあまりないと思う。


ということで,qpop で遡ったパッチスタックは qpush で安心して戻ってくることができる。

$ hg qpush
applying add_beer
now at: add_beer

$ cat index.txt
=Bar Saturn=

==STAFF==
* dayflower

==MENU==
* cocktail
* win
* beer

$ hg qapplied -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

スタック途中のパッチを修正する

と,この時点でワインのつづりが間違っていることに気づいてしまった。

add_beer パッチを適用したうえで,新しく fix_wine というパッチを作成するという考え方ももちろんアリだろう。でも,今回の例では add_menu パッチの誤りを訂正するという方向で話を進めたい。

まずは add_menu 時点の作業環境まで状況を戻すことにする。

$ hg qgoto add_menu
now at: add_menu

もちろん qpop で戻ってもいいんだけど,今回は一足飛びで指定したスタックトップに飛ぶことのできる qgoto を使ってみた(スタックの段数があまりないので意味ないけど)。

$ vi index.txt      # wine のつづりを修正

$ hg diff
diff -r 553b8522771e index.txt
--- a/index.txt Mon May 18 13:09:19 2009 +0900
+++ b/index.txt Mon May 18 13:47:50 2009 +0900
@@ -5,4 +5,4 @@
 
 ==MENU==
 * cocktail
-* win
+* wine
$ hg qrefresh

$ hg qseries -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

qrefresh によって,現在のスタックトップ(すなわち add_menu)のパッチを修正することができた。


なお,今回は add_menuqrefresh で修正したけど,ここから qnew で新しいパッチfix_wine)を「挿入」することもできる(別パッチとすべきだという方針もありうる)。

12.5.5 Pushing and popping many patches

hg qpush -a で全パッチ適用ということになると、「パッチ適用順序」は神経質になるに値する重要事項ですから、前節での疑問である「パッチスタック途中での hg qnew は、パッチスタックに対してどの位置にパッチを作成するのか?」がますます深刻なものになってきます。

というわけで、「パッチスタック途中での hg qnew は、パッチスタックに対してどの位置にパッチを作成するのか?」を確認してみました。

〜〜〜中略〜〜〜

何のことは無い、hg qnew で生成されるパッチは、最上段の「applied patch」(この場合は 1st.patch)と最下段の「not applied patch」(この場合は 2nd-1.patch)の間に生成されます。「スタック」と言い切っているのですから、確かにそれ以外に作りようが無いですよね。

〜〜〜中略〜〜〜

スタック途中で hg qnew により作成されたパッチは、間違いなくスタック途中に挿入されるようです。

no title

くわしくは自習課題ということで。


さて,qpush でビールの編集に戻ろう。

$ hg qpush
applying add_beer
patching file index.txt
Hunk #1 FAILED at 5
1 out of 1 hunks FAILED -- saving rejects to file index.txt.rej
patch failed, unable to continue (try -v)
patch failed, rejects left in working dir
errors during apply, please fix and refresh add_beer

おっと,パッチ当てに失敗してしまった。

パッチ当てに失敗したスタックを修正する

普通にパッチスタックを構築していれば,このようになることはあんまりないんだけど,せっかくだからこの失敗したパッチを修正してみる。

今どこにいるのかな。

$ hg qseries -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

$ ls
index.txt  index.txt.rej

add_beer まで当たったことになっている。そして失敗結果が index.txt.rej として残っているらしい。

編集結果をみてみよう。

$ cat index.txt
=Bar Saturn=

==STAFF==
* dayflower

==MENU==
* cocktail
* wine

$ cat index.txt.rej
--- index.txt
+++ index.txt
@@ -6,3 +6,4 @@
 ==MENU==
 * cocktail
 * win
+* beer   

ああなるほど。パッチの context が一致しないのでうまくパッチ当てがすすまなかったみたいだ。

これくらいなら手で修正可能(beer の行を付け加えるだけだもんね)なので,エディタで編集する。

$ vi index.txt

$ hg diff
diff -r 54f1717b189e index.txt
--- a/index.txt Mon May 18 13:49:15 2009 +0900
+++ b/index.txt Mon May 18 13:51:06 2009 +0900
@@ -6,3 +6,4 @@
 ==MENU==
 * cocktail
 * wine
+* beer

$ rm index.txt.rej

$ hg qrefresh

これでパッチ add_beer が現状に沿う形になった。

qdiff で確認してみる。

$ hg qdiff
diff -r 9b5a198afb12 index.txt
--- a/index.txt Mon May 18 13:48:07 2009 +0900
+++ b/index.txt Mon May 18 13:51:24 2009 +0900
@@ -6,3 +6,4 @@
 ==MENU==
 * cocktail
 * wine   
+* beer

うんうん。正しくなった。

親の修正履歴を pull で取り込む

と,ここまでやったところで BOSS に呼ばれた。

BOSS こないだのアレだけど,ちょっと修正したから内容とりこんどいて

自分の作業がすべておわってから merge するというのも手だけど,今すぐ反映して結果をみたいということもよくある。んで MQ を使うとそういうときでも迷うことがない。

まずは hg pull する前に hg in して,心の準備をする。

$ hg in
comparing with /home/dayflower/mywork
searching for changes
changeset:   1:c4f2797cb6d5
tag:         tip
user:        BOSS <boss@example.com>
date:        Mon May 18 13:53:49 2009 +0900
summary:     STAFF を MENU のあとにもってきた

なんだか大掛かりな変更っぽいな。

ここで,通常の Mercurial フローなら hg pull するところだけど,MQ を使っているのなら pull する前に qpop -a して自分の作業履歴を「リセット」しておこう。

$ hg qpop -a
patch queue now empty

$ cat index.txt
=Bar Saturn=

==STAFF==
* dayflower

==MENU==

qpop -a-a は all のことで,パッチスタック上の全パッチを未適用状態にするという意味。んで,ファイルをたしかめたところ,たしかに(ローカル的には)一番最初の状態に戻っている。

さて,いよいよ hg pull する。

$ hg pull
pulling from /home/boss/saturn
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files
(run 'hg update' to get a working copy)

qpop -a で「もともとの状態」に戻しておいたおかげで,multiple heads とはならなかったみたいだ。

なので安心して?hg update する。

$ hg update
1 files updated, 0 files merged, 0 files removed, 0 files unresolved

$ cat index.txt
=Bar Saturn=

==MENU==

==STAFF==
* dayflower

なるほど,STAFF の項目が MENU より後ろに来ているね。これくらいの修正なら,いままでのパッチはそのまま当たるかな。


さて,うれしいことに(というか当たり前なんだけど)hg pull / update しても,パッチスタックはきちんと生き残っているのだ。

$ hg qseries -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

全部まだ未適用だけど(qpop -a したから当たり前)。

では,qpop とは逆に qpush -a してパッチスタック上の全パッチ(すなわちこれまでの自分の全修正履歴)を適用してみよう。

$ hg qpush -a
applying add_menu
patching file index.txt
Hunk #1 succeeded at 2 with fuzz 1 (offset -3 lines).
applying add_beer
now at: add_beer

適用箇所でちょっと文句をいわれた*9けど,無事適用できた。

ファイルの内容をみてみると……

$ cat index.txt
=Bar Saturn=

==MENU==
* cocktail
* wine
* beer

==STAFF==
* dayflower

おお,きちんと BOSS の編集内容も反映されている(STAFF が MENU のあとにきている)し,自分の編集内容も反映されている。

余談: さきに hg pull しちゃっても大丈夫

いまの例だと,hg pull する前に hg qpop -a したけど,実は qpop する前に pull しちゃってもうまく対処できる。

qpop -a する前に hg pull するとこうなる。

$ hg pull
pulling from /home/boss/saturn
searching for changes
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files (+1 heads)
(run 'hg heads' to see heads, 'hg merge' to merge)

自分の編集履歴と独立した履歴なので multiple heads (+1 heads) になってしまった。

hg heads で確認してみると,

$ hg heads
changeset:   3:c4f2797cb6d5
tag:         tip
parent:      0:79d8edfc7bda
user:        BOSS <boss@example.com>
date:        Mon May 18 13:53:49 2009 +0900
summary:     STAFF を MENU のあとにもってきた

changeset:   2:002fd52dec0a
tag:         qtip
tag:         add_beer
user:        dayflower <dayflower@example.com>
date:        Mon May 18 13:51:13 2009 +0900
summary:     ビールを追加した

たしかに multiple heads になっている(BOSS の修正ツリーと自分の修正ツリーが別ブランチとしてみなされている)。


この時点では自分の「作業コピー」はあくまで changeset 2:002fd52dec0a のまま。ということは,ここから qpop -a しても問題なさそうだ。

$ hg qpop -a
saving bundle to /home/dayflower/mywork/.hg/strip-backup/9b5a198afb12-temp
adding branch
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files
patch queue now empty

おお,これまでと違って saving bundle ... とかいわれてるけど,問題なく qpop できた!

しかも multiple heads が解消されたっぽい。確認してみると……

$ hg heads
changeset:   1:c4f2797cb6d5
tag:         tip
user:        BOSS <boss@example.com>
date:        Mon May 18 13:53:49 2009 +0900
summary:     STAFF を MENU のあとにもってきた

single head になってる!

ではということで,現在の「作業コピー」に「BOSS の編集結果」を update してみる。

$ hg update
1 files updated, 0 files merged, 0 files removed, 0 files unresolved

$ cat index.txt
=Bar Saturn=

==MENU==

==STAFF==
* dayflower

問題なく update できた。


あとの手順はさきほどと同じ。

$ hg qpush -a
applying add_menu
patching file index.txt
Hunk #1 succeeded at 2 with fuzz 1 (offset -3 lines).
applying add_beer
now at: add_beer

$ cat index.txt
=Bar Saturn=

==MENU==
* cocktail
* wine
* beer

==STAFF==
* dayflower

めでたしめでたし。

qfinishパッチスタックによる作業を完了する

さてさて。

これまで MQ のパッチスタックで作業をおこなっていたけれど,BOSS のオッケーもでたので push したい。まずは commit しよう。

$ hg ci
abort: cannot commit over an applied mq patch

おっと。「MQ が当たっている状態だと commit できないよ」と怒られてしまった。

実際にはこれまでも見てきたように MQ のパッチも changeset として管理されているから,ここで commit しても意味はない(ある意味もう commit 済なのだ)。このメッセージは安全弁として考えればいい。


ほんとうに今やりたかったことは,今現在構成されているパッチスタックとかいうもやもやしたものを取り払って,これまでの修正履歴(パッチ)を changeset として記録したい,ということだ。

#とここまで書いてちょっと表現が分かりにくいなと思った。ともあれ実際の実行結果(下記)をみればわかると思う。

これは hg qfinish というコマンドでおこなう。

$ hg qfinish -a

-a というのは,これまでの all じゃなくて,applied の略。すなわち,今現在適用済みの patch を changeset に変換するということになる*10

hg qseriesパッチスタックの状態を見てみると……

$ hg qseries -s

なくなった。

じゃあ changeset log として見てみると……

$ hg log
changeset:   3:ff656f0e765c
tag:         tip
user:        dayflower <dayflower@example.com>
date:        Mon May 18 13:57:38 2009 +0900
summary:     ビールを追加した

changeset:   2:68ad307143ae
user:        dayflower <dayflower@example.com>
date:        Mon May 18 13:57:38 2009 +0900
summary:     カクテルとワインを追加した

changeset:   1:c4f2797cb6d5
user:        BOSS <boss@example.com>
date:        Mon May 18 13:53:49 2009 +0900
summary:     STAFF を MENU のあとにもってきた

changeset:   0:79d8edfc7bda
user:        BOSS <boss@example.com>
date:        Fri May 15 17:19:26 2009 +0900
summary:     初期インポート

おお。これまでの qなんとか とかいう tag もついていない,通常の changeset として記録された。しかも,これまで律儀に書いてきたパッチメッセージもコミットログとして記録されている。

f:id:dayflower:20090519115917p:image:w240,left

以上でひととおりの MQ ツアーはおしまい。

これまででてきたコマンドを図にまとめておきます。


qfold で複数のパッチをまとめる

人によっては add_menuadd_beer が別々の changeset として commit されたことに不満を覚えるかもしれない。手元の修正履歴はまとめて一つの changeset として commit したいんやー*11

MQ では qfold というコマンドを使うことで,他のパッチを畳む――すなわち融合する――ことができる。

パッチスタックが現在下記の状態になっているとしよう。

$ hg qseries -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

今回は add_menuadd_beer をまとめたい。なので,まずは add_menu スタックトップに移動する。

$ hg qgoto add_menu
now at: add_menu

んで,qfold に「畳みたい」未適用のパッチの名前を指定する*12

$ hg qfold add_beer

これで add_beer という修正内容が add_menu に取り込まれた*13

パッチスタックはどうなったか。

$ hg qseries -s
add_menu: カクテルとワインを追加した

add_beer が消えた。

じゃあ現在のパッチadd_menu)はどうなった?

$ hg qdiff
diff -r c4f2797cb6d5 index.txt
--- a/index.txt Mon May 18 13:53:49 2009 +0900
+++ b/index.txt Mon May 18 14:07:27 2009 +0900
@@ -1,6 +1,9 @@
 =Bar Saturn=

 ==MENU== 
+* cocktail
+* wine
+* beer

 ==STAFF==
 * dayflower

いままでの修正(add_menuadd_beer)が一つのパッチになったことがわかる。


このままだとパッチメッセージが add_menu だけのものになっているので,現況を反映したものに変えておこう :)

$ hg qrefresh -m "カクテルとワインとビールを追加した"

あとからパッチhg qnew -f 篇)

MQ の場合は「あらかじめパッチを作成して,そのパッチを『更新』していく」という作業手順になっている。

前に

「作業前にあらかじめ」ってとこに抵抗感がある人もいると思うけど,じっさいはいくらでも対処法があります(後述)。

と書いたけど,対処法のその1を書く。


MQ でパッチ管理を始める前の段階(BOSS からファイルをもらってきたところ)から始めたと思ってください。

編集をおこなう。

$ echo "* cocktail" >> index.txt

$ hg status
M index.txt

前回はこのように編集をおこなう前に hg qnew で新しいパッチを作っていたけれど,編集をおこなったあとで qnew すると,その修正内容を新しいパッチとしてつくることができる。

といっても普通に hg qnew すると,

$ hg qnew add_menu
abort: local changes found, refresh first

のように怒られてしまう。通常は「修正」→「更新(qrefresh)」のサイクルで作業をおこなっていくので,このエラーもいうなれば安全弁ですな。

今回は「意図的に」新規パッチを作成したいので,-f オプションをつけて qnew を実行する。

$ hg qnew -f add_menu

$ hg status

$ hg qseries
add_menu

これで(cocktail を追加するという内容の)add_menu という新規パッチとなる。

そうそう,ワインも追加するんだった。

$ echo "* wine" >> index.txt

$ hg qrefresh

今回の編集内容は add_menu に加えてもいいかなと思える内容なので,普通に qrefresh した。


さらに編集を続ける。

$ echo "* beer" >> index.txt

$ hg status
M index.txt

と,ここで「ビールを追加するのは add_menu に含めたくないなぁ」と思ったとする。

そこで hg qnew -f する。

$ hg qnew -f add_beer

$ hg qseries
add_menu
add_beer

いまの編集内容(ビールの追加)が add_beer という新規パッチとなった。

あとからパッチhg qimport 篇)

と,以上のように hg qnew -f するとあとからでもパッチ化することができるんだけど,(オプションの名前からすると)無理にまげてやってる感がいなめない。

なので,もっとダイレクトな方法(いままでの Mercurial 作法が通用する方法)を説明する。


まずは,いつもと同じように Mercurial で履歴をとる。

$ echo "* cocktail" >> index.txt
$ echo "* wine"     >> index.txt

$ hg ci -m "カクテルとワインを追加した"

$ echo "* beer" >> index.txt

$ hg ci -m "ビールを追加した"

通常と同じようにバリバリ hg ci してる。

ここまでの履歴をみてみると,

$ hg log
changeset:   2:a3fe0a48ea18
tag:         tip
user:        dayflower <dayflower@example.com>
date:        Mon May 18 14:16:31 2009 +0900
summary:     ビールを追加した

changeset:   1:ac74699bafab
user:        dayflower <dayflower@example.com>
date:        Mon May 18 14:16:16 2009 +0900
summary:     カクテルとワインを追加した

changeset:   0:79d8edfc7bda
user:        BOSS <boss@example.com>
date:        Fri May 15 17:19:26 2009 +0900
summary:     初期インポート

全部通常の changeset になっている(当たり前)。

んで,MQ のすごいのは,hg qimport で,通常の changeset を MQ パッチスタックに変換できるところ。

$ hg qimport -r 1:2

ここでは上記リビジョンコードの 1 と 2(カクテルとワインの追加,と,ビールの追加)を MQ 化してみた。

MQ のパッチスタックをみてみると……

$ hg qseries -s
1.diff: カクテルとワインを追加した
2.diff: ビールを追加した

1.diff とか 2.diff とか味気ないパッチ名になってるけど,これはまぎれもなくパッチスタックだ。

名前が味気ないので,いままでと同じような名前に qrename しておく(もちろん必須じゃない)。

$ hg qrename 1.diff add_menu

$ hg qrename 2.diff add_beer

$ hg qseries -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

で,当然のことながら,qpop / qpush でスタックを行き来できる。

$ hg qpop
now at: add_menu

$ cat index.txt
=Bar Saturn=

==STAFF==
* dayflower

==MENU==
* cocktail
* wine

パッチスタックの作業がおわったら hg qfinish で通常の changeset に戻すことももちろんできる。


f:id:dayflower:20090519115950p:image:w216,right

今まで「MQ はパッチスタックだ」と書いてきた。ところどころ顔を出す changeset の呪縛も感じながら。でも呪縛なんかじゃなかった。MQ は(ローカルな)changeset を自在に渡り歩くためのツールなんだ。

なんかすごくない?



しかもこれを応用すると既存の changeset をいじる(変更・削除・挿入)することすらできる。qfold と組み合わせると,複数の changeset をまとめることもできる。

くわしくは下記記事参照。

注意: MQ を利用している作業コピーを pull させてはいけない

MDC の Mercurial basics には以下のようなことが書いてある。

誰かが pull する可能性のあるレポジトリでは Mercurial Queues を使用してはいけません。

no title

これ,どういうことかなと思ったんだけど,実際確かめてみた。

いま現在 MQ のパッチスタックが下記のようになっているとする。

$ hg qseries -s
add_menu: カクテルとワインを追加した
add_beer: ビールを追加した

この状態で hg log をとると,

$ hg log
changeset:   1:ac74699bafab
tag:         qtip
tag:         add_menu
tag:         tip
tag:         qbase
user:        dayflower <dayflower@example.com>
date:        Mon May 18 14:16:16 2009 +0900
summary:     カクテルとワインを追加した

changeset:   0:79d8edfc7bda
tag:         qparent
user:        BOSS <boss@example.com>
date:        Fri May 15 17:19:26 2009 +0900
summary:     初期インポート

add_menu だけ changeset 化されていることがわかる。


んじゃ,いまこの作業コピーを clone してみよう。

$ cd ..

$ hg clone mywork work2
updating working directory
1 files updated, 0 files merged, 0 files removed, 0 files unresolved

別に問題はなさそうだ。

本当に?

clone されたレポジトリを hg log でみてみると……

$ cd work2

$ hg log
changeset:   1:ac74699bafab
tag:         tip
user:        dayflower <dayflower@example.com>
date:        Mon May 18 14:16:16 2009 +0900
summary:     カクテルとワインを追加した

changeset:   0:79d8edfc7bda
user:        BOSS <boss@example.com>
date:        Fri May 15 17:19:26 2009 +0900
summary:     初期インポート

MQ のつけていたタグは除去され,通常の changeset として記録されている。

もちろん MQ のタグが除去されているのは好ましい。ただ,問題は MQ の適用済みパッチが changeset として import されてしまっているところだ。

clone もとの親が MQ のパッチqrefresh したり qpush したりするたび,これらの changeset はかわっていく。MQ スタックをいじるたびに changeset の深さやリビジョン hash ID がかわっていく。そして pull するたびにそれは子孫に「違う changeset」として伝播する。

ということは。子孫にとって望まざる mutiple heads が発生しうるということだ。そして子孫がある changeset から修正を加えていったとしても,それは親にとってはあくまで temporalily にいじっていたファイルだったのかもしれない。


結論: MQ は末端(clone / push / pull 系譜上という意味で)の開発者が作業用コピーで十徳ナイフとして使うべきものだ。


補足。MQ が有効な場合,MQ のパッチデータも含めて clone することのできる qclone というコマンドがある。ただ後述するようにパッチをバージョン管理(レポジトリ化)してないとだめっぽいし,MQ の使われ方からすると,複数人の共同作業で使うというよりは個人のちょっとしたテストのための clone として使うものだと思う。

パッチスタックの(適用)順序を変えたい

実はパッチスタックの適用順は .hg/patches/series にかかれている。

$ cat .hg/patches/series
add_menu
add_beer

内容はシンプル。

Mercurial Definitive Guide では

Note

You may sometimes want to edit the series file by hand; for example, to change the sequence in which some patches are applied. However, manually editing the status file is almost always a bad idea, as it's easy to corrupt MQ's idea of what is happening.

Chapter?12.?Managing change with Mercurial Queues

と脅されてるけど,.hg/patches/status ファイル「は」いじるなってことかな。series のほうはまあ許容範囲っぽい。

でもちょっと注意。

6.3. Re-ordering patches

This is a very safe way to do it:

  • Execute hg qpop -a to remove all patches from the stack
  • Reorder patches in .hg/patches/series file
  • Execute hg qpush -a or hg qpush for patches that you want to re-apply
MqTutorial - Mercurial

.hg/patches/series を書き換える前に hg qpop -a しとくこと。

qrefresh したけど実は前の内容のほうが正しかった

それは今からではどうしようもない!

でもこれからのことを考えるなら,パッチ群自体も履歴管理するという方法がある。


通常 hg qinit でレポジトリの MQ 的初期化をおこなう*14けど,その際に hg qinit -c とすれば MQ パッチ群をリビジョン管理することができる。

パッチ群をリビジョン管理ってどういうことか?と思うけど,単純にいって .hg/patches/ ディレクトリを Mercurial レポジトリ化するだけのことだ*15。MQ の普段使いでは注意することはなにもない。パッチ群の「作業用コピー」をパッチとして扱うだけだから。

今現在のパッチ群の内容をとっておきたい,と思ったら hg qcommit とするとパッチ群のレポジトリが commit される。


じゃあこれまでのパッチ群の履歴(ログ)を見たいときや,パッチ群を少し前の(commit した)状態に戻したいときはどうすればいいんだろう。

実は MQ にはそのような作業に特化したコマンドは用意されていない。そのようなことをしたいのなら,.hg/patches/ レポジトリで Mercurial のコマンドを発行する必要がある。じっさい Mercurial Definitive Guide には

Finally, as a convenience to manage the patch directory, you can define the alias mq on Unix systems. For example, on Linux systems using the bash shell, you can include the following snippet in your ~/.bashrc.

alias mq=`hg -R $(hg root)/.hg/patches'
Chapter?12.?Managing change with Mercurial Queues

なんて書かれている。不便だと思うならこんな(シェル)エイリアスを定義すればいいでしょう,と。うーん。

あまつさえ no title では

Mercurial Queues を使用する場合は作業のバックアップを保存してください。hg qrefresh は古いパッチを新しいもので破壊的に置き換えます! パッチのために別のバックアップレポジトリを作成するには hg qinit -c を使用し、定期的に hg qcommit -m backup を実行してください。

no title

のように書かれてる。あくまでも「バックアップ」,ねぇ。


個人的には「パッチの履歴管理」という目的で hg qinit -c することの必要性は感じない。たしかに qrefresh でそれまでの修正履歴が一新されてしまうけど,それは普通に Mercurial を使っていてもおこりうること(hg ci する前に色々修正して,あー戻せばよかった,と思ったり)。

ただ,MQ は extension(すなわちアドオン)なので本体より動作が安定していない可能性がある。パッチスタックが深くなってくると何かの事故で失われたらどうしようと不安にもなる。それに MQ の操作に慣れていないうちはついつい気軽に qrefresh してしまうかもしれない。なので,保険として――MDC のいうようにバックアップとして―― hg qinit -c するのは悪くない。さいわいにして普通に使っている分には,パッチ群が履歴管理されていることは隠蔽されている(すなわち透過的につかえる)から。

他にどのようなコマンド/フィーチャーがある?

hg help mq とすると MQ extension で追加されるコマンドの一覧と説明が表示される。

なんと,いままでの解説で MQ に用意されているほとんどのコマンドを使ってきた。

説明していないコマンドは下記のとおり。

  • qsave / qrestore (ステート保存系)
  • qguard / qselect (ガード系)
  • strip (レポジトリ操作系)

ステート保存系(qsave / qrestore)は,現在のパッチ群の状態を保存したり戻したりするときに使うらしい。パッチ群をリビジョン管理するまでもないけど,っていうときに使うのかな?あと MQ の内容を 3-way merge するときにも使うらしい?よくわかりません。

ガード系(qguard / qselect)というのは,パッチ群のなかのいくつかのパッチをグルーピングするために使える。たとえば i386 アーキテクチャだと patch1patch2 を当てるけど patch3 は当てない,とか。こんなときは patch1patch2 に positive guard として +i386 を指定して patch3 には negative guard として -i386 を指定しておく。んで qselect i386 とすると,パッチスタックで適用するパッチを制限できる。とあたかも知ってるフリして書いてきたけど,使ったことないのでわかりません。Chapter?13.?Advanced uses of Mercurial Queues に書いてある。

strip というのは指定した changeset (とそれに連なる changeset)を(ローカル)レポジトリから削除するもの。MQ とは直接関係ないけど MQ についてる。じっさい,MQ で同じことできるしね。qimport して qpop -a し,qdelete すれば strip と同じことになります。

で,結局どういうときに使えばいいの?

で,どうなんだろう。


「これからは編集をおこなう度に qnew して,ある程度自信がついたら qfinish で changeset 化しよう」というのも一つの考え方ではある。

んが,hg qimport もあとからできるわけだし,最初からこのように張り切って使う必要もないかな,と思う。


個人的に MQ の用途としてぱっと思いついたのはこれくらい。

  • 新規機能の開発時にちょこっと試してみたい作業の記録
  • 既存機能のバグ修正
  • 複数のブランチに merge する機能の開発とテスト適用
  • 3-way merge ではない merge
  • 頻繁に更新される別レポジトリとの協調作業

だいたい内容は想像つくと思うんだけど,「3-way merge ではない merge」について補足。

通常 Mercurial では merge の際 3-way merge をおこなうんだけど,この 3-way merge がいまいち馴染まないという人もいるかと思う*16。そんなとき MQ を使うと,あくまでパッチ単位での適用可否になるのでローカルでの作業はシンプルになる(と思う)。ただし ThreeWayMerge でいうところの OTHER の修正をすべて正として採択した上で,それにパッチをおこなうということになってしまうんだけど。

なお MQ を使ってもパッチ群を 3-way merge することはできます*17


あと最後の「頻繁に更新される別レポジトリとの協調作業」だけど,たとえば今回途中で BOSS の修正内容を取り込んだように,プロジェクト初期段階でどんどん別レポジトリが修正されていく(そして実装機能が変わっていく)場合,最後に merge しようと思っても乖離が大きすぎて時既に遅しってこともあると思う。

そんなときは MQ を使っているとこまめに追従できるよってことだ(もちろんローカルでこまめに merge していってもいいんだけど)。

Mercurial は個人レベルでしか使ってないんだけど MQ 使う意味ある?

あると思います。


くだらない typo など恥ずかしい changeset を削除したい。バラバラに commit した changeset をあとからまとめたい。MQ はそのようなわがままに応えることができる。


コミット権限が無いプロジェクトに手元で修正を加えている場合,新しい版が出た際に独自の変更分の反映が面倒』というのがあるんだけど,こんなときでも MQ は便利。


ドットファイルなど設定ファイルをレポジトリにつっこんでいる場合,ローカルな環境用の記述を別 branch とかにする代わりにも MQ は使える。

おわりに

MQ は差分をあてたり戻したりという試行錯誤を簡単におこなうことのできる十徳ナイフのようなものだ。まして既存の(ローカル)レポジトリを自在にあとから変更できてしまう。便利さと裏腹に,リビジョン管理システムの存在意義を無に帰するものなのではと警戒する人もいるだろう。

たしかに,ぱっとみ醜く感じるいろいろな試行錯誤の跡もまた履歴管理の花,かもしれない。だが(ローカルなレポジトリだからこそ)「すべての」リビジョン管理を堅苦しく考える・履行する必要はない,ということだと思う。

The huge advantage of MQ

/* 中略 */

Traditional revision control tools make a permanent, irreversible record of everything that you do. While this has great value, it's also somewhat stifling. If you want to perform a wild-eyed experiment, you have to be careful in how you go about it, or you risk leaving unneeded―or worse, misleading or destabilising―traces of your missteps and errors in the permanent revision record.

By contrast, MQ's marriage of distributed revision control with patches makes it much easier to isolate your work. Your patches live on top of normal revision history, and you can make them disappear or reappear at will. If you don't like a patch, you can drop it. If a patch isn't quite as you want it to be, simply fix it―as many times as you need to, until you have refined it into the form you desire.

Chapter?12.?Managing change with Mercurial Queues
12.3 The huge advantage of MQ

この節では、(従来の)構成管理ツールは「永続的な記録が残ってしまうのが堅苦しい」と、構成管理の利点をいきなりひっくり返す記述があります。

「それを言っては身も蓋も無い」気がしますが、「上流リポジトリ」が自身の制御の及ばない状況の場合、 Mercurial のようにローカルリポジトリへの自由な commit が出来るからといって、「永続的な記録」=チェンジセットを残してしまうと、その後の「上流リポジトリ」の更新への追従の際には継続的な merge が必要とされます。そのようなことから「永続的な記録が残ってしまうのが堅苦しい」と思ってしまう気持ちはわからないでもありません。

この辺は、制御できない外部要因とのすりあわせが常に要求される OSS (に関わる)開発と、開発プロセスや品質管理/開発分担といったものが制御しやすい閉じた開発とで、意識が全然違ってくるところでしょう。

no title

参考文献

*1:ここ WEB+DB PRESS Vol.50 を読んだ人なら笑うところです。いちおう。

*2:初回の hg qnewhg qimport のときに自動的に qinit される。

*3:実際どうなんだろ。パッチをバージョン管理する場合には結局直接いじってるみたいなことになるので,問題はおこらない気もする。ただ status ファイルがあるからパッチだけいじるのは問題おこるかも。

*4:厳密にいうと,スタックトップのパッチ,ではない。スタックトップのパッチ+現在の編集内容,だ。qrefresh するとこういう内容のパッチになりますよ,ということだ。

*5:もちろん qdiff しなくても hg diff -r 0:1 しても確認できる。でも専用コマンドのほうが楽だよね。

*6:さらに付け加えると,ドキュメントに変更をまったく加えずに hg qrefresh しても changeset hash ID は変わる。

*7:じっさい後述するようにコミット時にコミットログとして使われる。

*8qtip という名前のほうが整合性がとれる気がするんだけど。

*9:そう,だから今後もこのパッチ群を利用するなら add_menuqrefresh しておいたほうがよい。

*10:だからもちろん,パッチ群の一部のみ changeset 化して,残りはパッチスタックとして管理を続けるということもできる。

*11:SVK での svk push -l のように。

*12:ここが MQ で唯一非線形にパッチスタックを適用できる部分だと思う。違ってたらすみません。

*13hg qrefresh する必要もない。というか,逆に patch -p < .hg/patches/add_beer して hg qrefresh して hg qdelete add_beer するというのをひとまとめに実行するのが hg qfold だといえるだろう。

*14:前述したように,リビジョン管理をしないなら実は必要ない。

*15:だから,あとからパッチ群を履歴管理する場合,.hg/patches/ ディレクトリで hg init すればいい。と Chapter?12.?Managing change with Mercurial Queues にも書いてある。

*16:何を隠そうわたしのことです。たぶん大規模な分散リビジョン管理開発をおこなったことがないからだと思うな。

*17no title の 12.8 Updating your patches when the underlying code changes 項参照。

2008-03-18

Mercurial 勉強中 (8) - hook を Trac と絡めて

TracMercurial を使ってインタフェースとして Trac を利用している場合*1,hook を設定するとさらに便利になります。

trac の post-commit-hook ヘルパスクリプトとは

たいていの SCM には commit 時などについでに何か(メールを送信したりスペルチェックをしたり)を実行させること(hook)ができます。trac-post-commit-hook というのは,Trac に付属している hook 用スクリプトで,commit メッセージをもとに ticket を閉じたり参照したりできるようにするものです。

たとえば(下記は Subversion ではなく Mercurial の例ですが),

内容を変更した(fixes #1)
HG: user: dayflower
HG: branch default
HG: changed index.html

のように commit メッセージを記述して push(Subversion の場合 commit)すると,

f:id:dayflower:20080318140856p:image


ticket のステータスが closed になります。

実際,ticket 側のログにも残ります。

f:id:dayflower:20080318140901p:image


trac-post-commit-hook を使わないなら Trac を使う意味がないというくらい素晴らしい機能です。Ticket Driven Development をやってみたくなりますね。


Subversion の場合,レポジトリフォルダ下の hooks/ フォルダに post-commit という名前の実行可能なものを放り込んでおくと commit 時に(trac-post-commit-hook に限らず)さまざまな hook を実行することができます。具体的な例は ページがみつかりません | Weboo! Returns. を参照してください。

Mercurial で hook

Subversion ではレポジトリフォルダ下の hooks/ フォルダでしたが,Mercurial の場合はレポジトリ下 .hg/hgrc に hook の所在を記述します。

たとえば push 時に hook を実行したいのであれば,マスタレポジトリの hgrc に下記のような設定を書き加えます。

[hooks]
incoming.trac = /var/www/repos/example/.hg/trac-post-commit.sh

incoming.trac となっていますが,ここは incoming だけでも大丈夫です。複数の hook をかけたい場合このようにサブ名称を付与しますので*2,例として & conflict を避けるため .trac を付け加えました。

push なのに incoming?という気がしますが,マスタレポジトリ側の立場からみると,外部からの push は incoming に相当するからです。

trac-post-commit-hook を invoke するスクリプトは下記のようになります。

#!/bin/sh

TRAC_ENV=/var/www/trac/example
REV=$HG_NODE

/usr/bin/python /usr/share/trac/contrib/trac-post-commit-hook -p "$TRAC_ENV" -r $REV

$HG_NODE という環境変数に push された revision が入っているんでそれを渡しているだけです。

ticket の changeset 参照を短くしたい

このままでも Subversion のときのようにうまく動くのですが,

f:id:dayflower:20080318140856p:image


ticket で参照している changeset の名前が長いですよね。

なので,invoker をシェルスクリプトではなく Python スクリプトとして書き起こしてみました。

#!/usr/bin/env python

trac_env    = '/var/www/trac/example'
hook_script = '/usr/share/trac/contrib/trac-post-commit-hook'

# style:: 'short', 'long', or 'number'
changeset_id_style = 'number'

import os

def invoke_trac_hook(trac_env, rev):
    def _make_smart_rev(trac_env, rev):
        if changeset_id_style == 'long':
            return rev
        else:
            import trac.env
            env = trac.env.open_environment(trac_env)
            # instance of mercurial.hg.repository
            repo = env.get_repository().repo
            ctx = repo.changectx(rev)
            if changeset_id_style == 'short':
                import mercurial.node
                return mercurial.node.short(ctx.node())
            else:   # 'number'
                return str(ctx.rev())

    smart_rev = _make_smart_rev(trac_env, rev)

    f = open(hook_script, 'r')
    try:
        import imp
        ext = os.path.splitext(hook_script)[1]
        m = imp.load_module('trac_hook', f, f.name, (ext, f.mode, imp.PY_SOURCE))
        m.CommitHook(project=trac_env, rev=smart_rev)
    finally:
        f.close()

invoke_trac_hook(trac_env, os.environ['HG_NODE'])

trac_envhook_script は環境にあわせて適宜書き換えてください。

changeset ID の表記についてですが,changeset_id_stylenumber を指定すると,

f:id:dayflower:20080318140857p:image


のようになります。

changeset_id_styleshort を指定すると,

f:id:dayflower:20080318140858p:image


のようになります。

ほんとうは他の Timeline 等での表記([3:9058d29b14e4])と揃えたいところなのですが,単純にこの形式を revision として指定しても Wiki が対応していないのでリンクを貼ってくれません。TracMercurial か trac-post-commit-hook を改造する*3ことになります。

おまけ

trac-post-commit-hook の改造の話がでたのでついでですが。

付属の trac-post-commit-hook は Ticket の close と refer しかできませんが,結構わかりやすいコードになので,ワークフローに沿って Ticket のステータスを変更するように改造することもできます(⇒ trac-post-commit-hookでステータスも変更: 気の向くままに・・・)。すばらしい。

*1 Mercurial 勉強中 (2) - Trac と統合 - daily dayflower

*2hgrc 参照

*3CommitHook クラスの __init__ ですべての処理を行ってしまっているので,クラスを継承して処理を修正する,ということができないんです

2008-03-17

Mercurial 勉強中 (7) - Web 経由の push と HTTP 認証

Web 経由で Mercurial のレポジトリを公開すると,デフォルトの状態では clone / pull しかできません。push するためには設定が必要になります。

なお今回は hgweb.cgimod_wsgi 経由*1等で Apache と絡めた場合の話になります。

というのは,hg serve コマンドで起動される HTTP サーバは BaseHTTPServer をもとにしているのですが,ビルトインの機能としては Authentication をサポートしておらず*2,また hgweb.server モジュールでもハンドリングしていないので認証関連の機能が実装されていないためです。

設定子

hgrc 設定ファイルで下記のものが特に関係のある設定子です。

  • web
    • allowpull
    • allow_push
    • deny_push
    • push_ssl

push 可能に設定する

[web] セクションの allow_push 設定子を指定します。

allow_push
Whether to allow pushing to the repository. If empty or not set, push is not allowed. If the special value "*", any remote user can push, including unauthenticated users. Otherwise, the remote user must have been authenticated, and the authenticated user name must be present in this list (separated by whitespace or ","). The contents of the allow_push list are examined after the deny_push list.

hgrc

レポジトリ下の .hg/hgrc ファイルに下記の内容を追加します。

[web]
allow_push = *

暫定的に「*」を指定しています。下記で説明している HTTP 認証と絡めて具体的なユーザ名を指定することもできます。

非 HTTPS(HTTP)でも push 可能に

デフォルトでは HTTPS 経由でないと push できません。イントラなど HTTP 経由で構わない場合は [web] セクションの push_ssl 設定子に false を指定します。

push_ssl
Whether to require that inbound pushes be transported over SSL to prevent password sniffing. Default is true.

hgrc

具体的にはレポジトリ下の .hg/hgrc ファイルに下記の内容を追加します。

[web]
push_ssl = false

HTTP 認証の設定を加える

Mercurial の Web インタフェースの認証関連の機能は Remote-User ヘッダを見ています。ですので,通常の Apache と同様に設定すれば OK です。

<Location path-to-repository>
AuthType Basic
AuthUserFile /foo/bar/htpasswd

Require valid-user
</Location>

これで allow_push 設定子に具体的なユーザ名を指定することができるようになります。

ちなみに上の例では Basic 認証ですが,実際には私の環境では mod_auth_ntlm_winbind で AuthType NTLM を使用しています*3

push だけ認証を要求したい

clone だけは anonymous OK だけど push できるのは認証したユーザだけにしたい,というのはよくある要件です。

アクセスログを見ると,どうやら push の際には

  • POST repository URI?cmd=unbundle&heads=HOGEHOGEHOGE

というアクセスが発行されるようです。

ですので <LocationMatch>?cmd=unbundle をひっかけてもいいんですが,ざっとみた感じメソッドが POST に限定されているようなので,Subversion と揃えて次のような設定で制限をかけるようにしました。

AuthType Basic

<LimitExcept GET PROPFIND OPTIONS REPORT>
    Require valid-user
</LimitExcept>

念のため deny_push も指定しておく

いままでの内容では allow_push に「*」を設定しました。もし手違いで Apache 側の認証関連の設定を lost してしまった場合,anonymous に push 可能になってしまいます。

あくまで認証しているユーザには push をさせたい,だけど anonymous に push されてしまうのも困る,ということを Mercurial 側に設定しておきましょう。deny_push を使うと可能になります。

deny_push
Whether to deny pushing to the repository. If empty or not set, push is not denied. If the special value "*", all remote users are denied push. Otherwise, unauthenticated users are all denied, and any authenticated user name present in this list (separated by whitespace or ",") is also denied. The contents of the deny_push list are examined before the allow_push list.

hgrc

副次的効果ですが deny_push になんらかの設定がなされている場合,REMOTE_USER が設定されていない場合に push できなくなります(コードも見て確認しました)。

ですので,ダミーですが

[web]
allow_push = *
deny_push = unauthenticated_user

のようにしておくと,万一 Apache の設定が失われても push できなくなります。

push するたびに毎度ユーザ名とパスワードを聞かれてうざい

Subversion の場合,一度 commit する際にユーザ名とパスワード入力すると,その後は認証情報を覚えてくれます。ですが,Mercurial の場合,push する度に聞かれます。

% hg push
pushing to http://localhost/hg/example
searching for changes
http authorization required
realm: Mercurial
user: dayflower
password: ********
adding changesets
adding manifests
adding file changes
added 1 changesets with 1 changes to 1 files

なんとかなりませんか。

その答え,FAQ にあります。

How can I store my HTTP login once and for all ?

You can specify the usename and password in the URL like:

http://user:password@mydomain.org

Then add a new entry in the paths section of your hgrc file.

How can I store my HTTP login once and for all ? - FAQ - Mercurial

ううう,なんかダサい。

どうでもいいけど

Unauthorized でハネられるときにも Status 200 を返すのがダサいと思いました。

*1 Mercurial のウェブインタフェースを mod_wsgi にのせてみた - daily dayflower

*2:自力で WWW-Authenticate ヘッダ等やりとりすればいけるんじゃとは思います。詳しくないので自信ないです。

*3 Apacheで統合Windows認証を使う - daily dayflower