こんにちは、 開発担当 の松本です。
Sleipnir 3 for Windows 開発では Redmine と Mercurial (Tortoise HG) を利用しています。
チケット駆動開発のように、Redmine チケットを利用した開発では修正をコミットしてから Redmine チケットをクローズすることはよくあるので、Redmine のチケットと Tortoise HG を連動させたくなります。
Tortoise HG から Redmine のチケットを参照するのは以前に紹介した TurtleMine で行えますので、ここからさらにチケットをクローズする処理を自動化しようというのが今回の話です。
処理の概要
Mercurial ではコミットなどの特定のタイミングで外部プログラムを呼び出すことができる仕組みがあります。今回はこれを利用して Redmine チケットのクローズ処理を自動化します。
具体的には次の手順で行います。
- 1. Redmine で API アクセスキーを作成し、
- 2. そのキーを使った Python スクリプトを作成し、
- 3. そのスクリプトを使って Mercurial のフックに登録し、
- 4. フックの処理を書く。
1. Redmine API アクセスキーの作成
まず、Redmine の API アクセスキーを作成します。Redmine にログインしてから右上の個人設定をクリックし、API アクセスキーのリセットをクリック。API アクセスキーが作成されるので、表示をクリックすると API アクセスキーが表示されます。
2. Python での Redmine API の利用
先ほどの API アクセスキーを X-Redmine-API-Key として HTTP ヘッダに入れることで Redmine API を利用することができます。この API を利用してチケットの内容を更新させる Python スクリプトを書いてみます。
#!/usr/bin/python import urllib, httplib, re REDMINE_HOST = 'redmine.example.com' REDMINE_API_KEY = '5425f89fd7ed4ec15bd75f9a7c85e7787bc26e11' def update_issue(issue_id, body): headers = {'Content-type': 'application/json', 'X-Redmine-API-Key': REDMINE_API_KEY} http = httplib.HTTPConnection(REDMINE_HOST) url = '/issues/%s.json' % (issue_id,) http.request('PUT', url, body, headers) response = http.getresponse()
REDMINE_API_KEY に先ほど取得したキーの値を入れます。このスクリプトを用いて、チケット 758 のステータスと進捗 % を変更したいときは次のように使います。
DONE = '{"issue":{"status_id":"8", "done_ratio":"100"}}' update_issue('758', DONE)
文字列 DONE はステータスを解決に変更し、進捗 % を 100 に変更する JSON です (status_id = 8 は Fenrir の Redmine では「解決」の意味)。このあたりの詳細は Redmine API を参照してください。
これを利用して push したときには、Redmine の該当チケットが次のように更新されます。
3. hgrc へのフック登録
作成したスクリプトが Mercurial の特定の条件で発動するように結びつけるためにフックを利用します。
まず、.hgrc に次の2行を追加します。
[hooks] outgoing.redmine = python:c:\path\to\redmine.py:hook
これは、outgoing (pushなどを行ったとき) のタイミングにファイル c:\path\to\redmine.py 内の関数 hook が呼ばれるようにすることを意味します。
Sleipnir 3 for Windows のリポジトリには中央リポジトリが用意されており、commit 時だとローカルでの変更になってしまうので outgoing にしています。開発体制によっては commit 時のタイミングでもよいかもしれません。
4. フック関数の作成
先ほど作ったスクリプトに関数 hook を追加します。Python を利用すると MercurialApi を利用して簡単に書くことができます。
def get_pushed_revs(node): ret = [node.rev()] ch = node.children() while len(ch) == 1: node = ch[0] ch = node.children() ret.insert(node.rev()) return None if len(ch) == 2 else ret def hook(ui, repo, **opts): if opts['source'] != 'push': return base_id = opts['node'] base = repo.changectx(base_id) for rev in get_pushed_revs(base): ctx = repo.changectx(rev) if ctx.user() == HG_USER: pat = re.match('^FIX: #([0-9]+)', ctx.description()) if pat: issue_id = pat.group(1) update_issue(issue_id, DONE)
outgoing フックは rebase などの他のタイミングでも呼ばれる可能性があります。opts[‘source’] でそれが区別できますので、関数 hook の先頭では push 以外でフックが呼ばれたときは無視するようにしています。
opts[‘node’] には送信する先頭のチェンジセット ID が含まれており、関数 get_pushed_revs ではそれを利用して、そのチェンジセットから push したチェンジセットのリストを取得します (この関数の処理はかなりいい加減なのでよい方法があれば教えてください)。そして、自分がコミットしており、FIX: 758 のような記述をしているチェンジセットがあるなら、その数字の部分を Redmine のチケット ID とみなしてチケットのクローズ処理をします。
個人ではさらに Growl Notification を利用しており、チケット更新時には次のような通知が表示されるようにしています。
Redmine API や Mercurial Hook は様々なことに利用できますのでぜひとも利用してみてください。