エクステンションの作り方
Mercurial には、コマンドを追加するためにエクステンション機構が用意されています。
エクステンションを使えば、まるで組み込みコマンドのように hg コマンドから直接実行可能な機能を作ることができます。 エクステンションからは全ての MercurialApi にアクセス可能ですが、 API changes に注意してください。
Mercurial の内部 API を使うなら、十中八九あなたのコードは Mercurial のライセンスの影響を受けるでしょう。 始める前に、まず License ページを確認してください。
Contents
1. ファイル構造
エクステンションは、たいてい単なる Python モジュールです。 大きめのエクステンションなら、単一パッケージの複数モジュールへ分割した方が良いでしょう(参考: ConvertExtension)。 その場合、パッケージのルートモジュールがエクステンションの名前になり、次に説明する cmdtable と任意のコールバックを提供します。
2. コマンドテーブル
エクステンションを作る際に、個々のコマンドを記した辞書を cmdtable という名前で Python モジュールに追加することができます。
2.1. cmdtable 辞書
cmdtable は、コマンド名をキーに取り、値に以下の tuple を取る辞書(dict)です:
- コマンド実行時に呼び出す関数。
- コマンドが受け取る引数の一覧(list)。
- コマンドラインの概要(関数の docstring がコマンドのヘルプになります)。
2.2. 引数の一覧
コマンドフラグ引数の説明は、全て mercurial/fancyopts.py のソースにあります。
引数の一覧は、以下を含む tuple の list です:
短い引数文字か、それが無いことを示す '' (例: o は-o 引数を意味)。
長い引数名 (例: option は --option 引数を意味)。
- 引数のデフォルト値。
- 引数のヘルプ ("hg newcommand" の部分は省略可能で、引数名とパラメーター文字列のみ必要)。
2.3. cmdtable の例
cmdtable = { # "command-name": (function-call, options-list, help-string) "print-parents": (printparents, [('s', 'short', None, 'print short form'), ('l', 'long', None, 'print long form')], "hg print-parents [options] node") }
3. コマンド関数のシグネチャ
新しいコマンドを実装する関数は、常に ui と(たいていの場合) repo を引数に取ります。 この引数の使い方については MercurialApi を参照してください。 残りの引数には、コマンドラインで指定されたハイフン始まりではない項目が順番に入ります。 引数リストにデフォルト値が指定されていなければ、その引数は必須です。
コマンドにリポジトリが関係せず repo が不要であるなら、 commands を mercurial から import して commands.norepo に名前を追加してください。 例えば:
from mercurial import commands ... commands.norepo += " mycommand"
norepo の例は RcpathExtension (RcpathExtension/rcpath.py のソースへの直リンク)または ConvertExtension (convert のソースへの直リンク)にあります。
4. コマンド関数の docstrings
関数の docstring は hg help mycommand で表示されるヘルプの本文になります。 docstring は reStructuredText の単純なサブセット形式で書いて下さい。 サポートされている構造は:
段落:
This is a paragraph. Paragraphs are separated by blank lines.
コロン 2 つに続く字下げされたブロックはそのまま表示されます。 二重のコロンは、コロン 1 つとして表示されます:
Some text:: verbatim text !!
フィールドリスト:
:key1: value1 :key2: value2
リスト:
- foo - bar
順序づきリスト:
1. foo 2. bar
インラインマークアップ: *太字*, ``等幅`` 。 Mercurial のコマンドを :hg:`command` で括ると、適切なドキュメントへリンクを張ってくれます。 新しい構造が追加された際に、それをサポートするのに大した手間はかからないでしょう。
5. ユーザとのやり取り
MercurialApi に挙げた(ui.write(*msg) や ui.prompt(msg, default="y") などの) ui メソッド以外に、エクステンション自体や個々のコマンドについてヘルプを書くことも大切です。
モジュールの docstring は hg help extensionname を実行した時に表示され、同様に、コマンドのヘルプとその関数の docstring は hg help command で表示されます。
6. コールバックの設定
Mercurial はエクステンションを段階的にロードします。 全てのエクステンションに対してある段階の処理が行われた後、次の段階へ進みます。 最初の段階では、エクステンションモジュールを全てロードし、 Mercurial に登録します。 つまり、以下に示す段階では、 extensions.find を使って有効化されたエクステンションを知ることができます。
6.1. ui setup
uisetup は任意のコールバックです。 エクステンションを初めてロードした際に、 ui オブジェクトを引数に uisetup を呼び出します:
def uisetup(ui): # ...
6.2. Extension setup
extsetup は任意のコールバックです。 エクステンションを全てロードした後に呼び出され、エクステンションが他のエクステンションに依存しているケースで役に立ちます。 シグネチャ:
def extsetup(): # ...
Mercurial version 8e6019b16a7d 以降(1.3.1 以降)では、 extsetup は ui 引数を取ることができます:
def extsetup(ui): # ...
6.3. Command table setup
extsetup の後、 cmdtable を Mercurial のグローバルコマンドテーブルへコピーします。
6.4. Repository setup
reposetup は任意のコールバックです。 Mercurial リポジトリの初期化が終わった後に呼び出され、エクステンションで必要なローカルステートを設定する時に使えます。
コマンド関数と同様に、 ui オブジェクトと repo オブジェクトを受け取ります(追加の引数はありません):
def reposetup(ui, repo): #do initialization here.
reposetup 関数が受け取る ui オブジェクトは、 uisetup や extsetup 関数が受け取るものと異なります。 この点を考慮することが大切です。 特に、(以下の節で説明しますが)フックを設定する際には、全てのフックが同一の ui オブジェクトを使うわけではないので、フックによって異なるセットアップ関数で設定しなければなりません。
7. フックの設定
エクステンションはフックを使う必要に迫られることがあります。 ユーザーが手で hgrc の [hooks] セクションを書き換え、必要なフックを設定することもできますが、 ui.setconfig('hooks', ...) 関数を先ほど説明したセットアップ関数から呼び出すことで、自動的に設定することも可能です。
hgrc の hooks セクションを手で書き換えることと ui.setconfig() を使うことの主な違いは、 ui.setconfig() では実際のフック関数オブジェクトにアクセス可能なので、関数自体を ui.setconfig() へ渡せるという点です。 一方、 hgrc ファイルの hooks セクションでは、フック関数を "python:modulename.functioname" 表記(例: "python:hgext.notify.hook")で参照する必要があります。
例:
# Define hooks -- note that the actual function name it irrelevant. def preupdatehook(ui, repo, **kwargs): print("Pre-update hook triggered") def updatehook(ui, repo, **kwargs): print("Update hook triggered") def uisetup(ui): # When pre-<cmd> and post-<cmd> hooks are configured by means of # the ui.setconfig() function, you must use the ui object passed # to uisetup or extsetup. ui.setconfig("hooks", "pre-update.myextension", preupdatehook) def reposetup(ui, repo): # Repository-specific hooks can be configured here. These include # the update hook. ui.setconfig("hooks", "update.myextension", updatehook)
フックによって異なるセットアップ関数で設定する必要があることにご注意を。 この例から、 update フックは reposetup 関数で設定しなければならず、一方で pre-update は uisetup か extsetup 関数で設定しなければならいことが分かるでしょう。
8. 互換バージョン表記
互換性が確認されている Mercurial リリースを testedwith 変数に表記すべきです。 開発者やユーザーが問題の出処を調査する上で役に立ちます。
1 testedwith = '2.0 2.0.1 2.1 2.1.1 2.1.2'
サードパーティーエクステンションで internal マークを使ってはいけません。もし見かけたら、そのエクステンションが記されたバグレポートは一切読みませんよ。
同様に、エクステンションの不具合報告の方法については buglink 変数で指定してください。 エクステンションでエラーが発生した際に、このリンクがエラーメッセージに記載されます。
1 buglink = 'https://bitbucket.org/USER/REPO/issues'
9. サンプルエクステンション
1 """printparents
2
3 Prints the parents of a given revision.
4 """
5 from mercurial import util
6
7 # Every command must take ui and and repo as arguments.
8 # opts is a dict where you can find other command line flags.
9 #
10 # Other parameters are taken in order from items on the command line that
11 # don't start with a dash. If no default value is given in the parameter list,
12 # they are required.
13 #
14 # For experimenting with Mercurial in the python interpreter:
15 # Getting the repository of the current dir:
16 # >>> from mercurial import hg, ui
17 # >>> repo = hg.repository(ui.ui(), path = ".")
18
19
20 def printparents(ui, repo, node, **opts):
21 # The doc string below will show up in hg help.
22 """Print parent information."""
23 # repo can be indexed based on tags, an sha1, or a revision number.
24 ctx = repo[node]
25 parents = ctx.parents()
26
27 try:
28 if opts['short']:
29 # The string representation of a context returns a smaller portion
30 # of the sha1.
31 ui.write('short %s %s\n' % (parents[0], parents[1]))
32 elif opts['long']:
33 # The hex representation of a context returns the full sha1.
34 ui.write('long %s %s\n' % (parents[0].hex(), parents[1].hex()))
35 else:
36 ui.write('default %s %s\n' % (parents[0], parents[1]))
37 except IndexError:
38 # Raise an Abort exception if the node has only one parent.
39 raise util.Abort('revision %s has only one parent' % node)
40
41
42 cmdtable = {
43 # cmd name function call
44 'print-parents': (printparents,
45 # See mercurial/fancyopts.py for all of the command flag options.
46 [('s', 'short', None, 'print short form'),
47 ('l', 'long', None, 'print long form')],
48 '[options] REV')
49 }
50
51 testedwith = '2.2'
cmdtable や reposetup なしでもエクステンションは機能します。 つまり、エクステンションは「無言で」機能し、コマンドラインインターフェースから直接見える機能を追加しなくても構わないということです。
10. サンプルエクステンションのテスト
上記のサンプルエクステンションのテストです:
Test printparents extension. Activate the printparents extension: $ echo "[extensions]" >> $HGRCPATH $ echo "printparents=" >> $HGRCPATH Create a new repo: $ hg init r $ cd r Add two new files and commit them separately: $ echo c1 > f1 $ hg commit -Am 0 adding f1 $ echo c2 > f2 $ hg commit -Am 1 adding f2 Update to revision 0. Add and commit a third file creating a new head: $ hg up 0 0 files updated, 0 files merged, 1 files removed, 0 files unresolved $ echo c3 > f3 $ hg commit -Am 2 adding f3 created new head Merge the two heads and commit: $ hg merge 1 files updated, 0 files merged, 0 files removed, 0 files unresolved (branch merge, don't forget to commit) $ hg commit -m 3 Test printparents with the (merged) tip: $ hg print-parents tip default 33960aadc16f c3adabd1a5f4 Testing printparents with revision 2 will fail (because there is only one parent): $ hg print-parents 2 abort: revision 2 has only one parent [255]
Mercurial のテストについて詳細は: WritingTests
11. まとめ: 何をどこに書くべき?
Mercurial にバンドルされたエクステンションのセットアップを元に、よくある作業をリストアップしました。
11.1. uisetup
ui.__class__ の変更。 コマンド実行で使われる ui オブジェクトはまだ生成されていない。 ここでの変更は今後生成される ui オブジェクト、特に runcommand へ渡す ui に影響する
コマンドのラップ (extensions.wrapcommand)
他のエクステンションに見せる必要がある変更: エクステンションは段階的に初期化される(uisetup を全て実行した後 extsetup が実行される)ので、ここでの変更は他のエクステンションの extsetup から見える
dispatch モジュールメンバーのモンキーパッチや関数ラップ(extensions.wrapfunction)
- pre-* post-* フックの設定
pushkey の設定
11.2. extsetup
他のエクステンションの状態に依存する変更。 (if extensions.find('mq'))
- 全コマンドに対するグローバルオプションの追加
- revsets の拡張
11.3. reposetup
- pre-* post-* 以外のフック
- 設定値の変更
repo.__class__ や repo.dirstate.__class__ の変更
12. エクステンションの公開
あなたのエクステンションが汎用的で、良くできていて、他の人が興味を持ちそうであれば、 PublishingExtensions を参照してください。