Hatena::Groupfukuoka-py

ikikko.py このページをアンテナに追加 RSSフィード

2009-07-27

Backlogフックスクリプトの技術的よもやま話

23:48 |  Backlogフックスクリプトの技術的よもやま話 - ikikko.py を含むブックマーク はてなブックマーク -  Backlogフックスクリプトの技術的よもやま話 - ikikko.py  Backlogフックスクリプトの技術的よもやま話 - ikikko.py のブックマークコメント

Backlogのコミットフックスクリプトを書いてみたよ - ikikko.py - fukuoka.pyのはてなグループでは機能的な概要の説明が主だったので、こっちではやってるときに詰まったところとかを中心に。少しでも後学の人の参考になればと思いつつ。


基本的な構造

基本的な構造は、trac-post-commit-hookと変わりません。かなり参考にさせてもらったから。違うのは、

  • trac-post-commit-hook
    • コミットログ抽出:TracチケットAPI
    • チケット変更:TracチケットAPI
  • backlog-post-commit-hook
    • コミットログ抽出:svnlook
    • 課題変更:Backlog API

という点かな。コミットログにsvnlookを使うところは、当初は些細な違いだとは思っていたけど意外に大きく影響してきました。

コミットログ抽出

フックスクリプトが起動するのは(いわゆるDBトランザクション的な意味での)コミット後なので*1、svnlookでコミットログを取れます。が、ログの中の日本語の扱いについてかなりはまりました。

Tortoise SVNではcommit logに日本語を使う事ができます。この日本語はUTF-8でリポジトリに 格納されるようです。 日本語commit logをメールで送るためには、UTF-8ISO-2022-JP(JIS)変換をしてやれば良い事になります。

commit logはsvnlook logで出力できますが、UTF-8そのままで出力されるわけではなく、表示できない文字が"?\xxx"という形式で出力されます。

例:% svnlook log /repo/path -r 11 ?\229?\191?\152?\227?\130?\140?\227?\129?\166?\227?\129?\132?\227?\129?\159?\227?\128?\130

【インフォシーク】Infoseek : 楽天が運営するポータルサイト

というわけで、同じ方が夜でもアッサム: subversionで、commitしたファイルでコミットメールの宛先を変えるPython版フックスクリプトを作られていたので、それを参考に。「?\xxx」から「?\」を取り除いてやって、残った数字部分「xxx」をchr型に変換してやっているみたいです。ただ、このように書くとどうしてうまく_replメソッドが呼び出されるのかが、いまいち分からなかったのですが・・・orz

# コミットログ置換('?\048' -> '0')
def _repl(self, m):
  return chr(int(m.group(0)[2:])) # remove '?\'

~~~

log = re.sub(r'\?\\\d\d\d', CommitHook()._repl, log)

あと、Windows環境のリポジトリだとコミットログがShift-jisで返ってくる(?)みたい。なぜかは分からないけど。なので、こっちは(ちょっと気持ち悪いけど)Windows決めうちでユニコード変換をかけてやりました。

if sys.platform.find('win') >= 0:
  log = unicode(log, 'mbcs')

課題変更

これは、むしろtracより簡単だった部分。Backlog APIXML-RPCとして定義されていたので、それを呼び出すだけ。

一点だけテストしててがっくりきたのが、同じ課題を参照した場合に参照回数分だけBacklog APIが発行されたこと。

ref DEMO-1, DEMO-1, DEMO-1

とかやると、DEMO-1に対して3回APIが発行されました…orz

これも解決方法は単純。起動すべきコマンドと対象の課題を格納している部分で、リストを使っている箇所

for issue in issue_re.findall(isus):
  func = getattr(self, funcname)
  issues.setdefault(issue, []).append(func)

を、Set型を使うようにしてやれば、同一課題・コマンドは一つしか格納されなくなり、複数回APIが発行されることが無くなります。

for issue in issue_re.findall(isus):
  func = getattr(self, funcname)
  issues.setdefault(issue, set([])).add(func)

サポートコマンドの追加

今回、課題の状態を処理済に変更するコマンドを追加したように、コマンドを新たに追加したい場合は

  • _supported_cmdsを追加
  • 対応するコマンドのメソッドを記述

すればできます。まあ、ソースコードを追えばそこまで難しくないでしょう。


ソースコード

最後に、両方のソースコードを貼り付けておきます。

post-commit

#!/bin/sh

##########   Setting   ##########
PYTHON="XXXXXXXX"  # Pythonのパス
SCRIPT="YYYYYYYY"  # backlog-post-commit-hook.pyを置いた場所
SVNLOOK="ZZZZZZZZ" # svnlookのパス

SPACE_ID="xxxxxxxx" # スペースID
USER="yyyyyyyy"     # ユーザ
PASSWORD="zzzzzzzz" # パスワード
#################################

REPOS="$1"
REV="$2"

LOG=`$SVNLOOK log -r $REV "$REPOS"`

$PYTHON $SCRIPT \
  "$REPOS"    "$REV"  "$LOG"      \
  "$SPACE_ID" "$USER" "$PASSWORD"

backlog-post-commit-hook.py

#!/usr/bin/env/ python
# -*- coding: utf-8 -*-

import re
import subprocess
import sys
import xmlrpclib

class CommitHook(object):

    # サポートされているコマンド:
    # コマンドを追加する際は、この変数に追加して同名のメソッドを定義すること
    _supported_cmds = {'close':      '_cmdClose',
                       'closed':     '_cmdClose',
                       'closes':     '_cmdClose',
                       'fix':        '_cmdClose',
                       'fixed':      '_cmdClose',
                       'fixes':      '_cmdClose',
                       'addresses':  '_cmdRefs',
                       're':         '_cmdRefs',
                       'references': '_cmdRefs',
                       'refs':       '_cmdRefs',
                       'see':        '_cmdRefs',
                       'done':       '_cmdDone',}

    _status_id     = {'none':   1,
                      'doing':  2,
                      'done':   3,
                      'closed': 4}

    _resolution_id = {'fixed':      0,
                      'wontfix':    1,
                      'invalid':    2,
                      'duplicate':  3,
                      'worksforme': 4}

    # コミットログ置換('?\048' -> '0')
    def _repl(self, m):
        return chr(int(m.group(0)[2:])) # remove '?\'

    # Backlog API実行
    def execCmd(self, repos, rev, log, spaceId, user, password):

        # ログ変換
        if sys.platform.find('win') >= 0:
            log = unicode(log, 'mbcs')
        log = re.sub(r'\?\\\d\d\d', CommitHook()._repl, log)

        # XML-RPCエンドポイントインスタンス生成
        server = xmlrpclib.ServerProxy('https://%s:%s@%s.backlog.jp/XML-RPC' %
                                       (user, password, spaceId))

        # 所属しているプロジェクト全てに対して、課題の検索
        for project in server.backlog.getProjects():

            # 課題検索用正規表現パターン
            issue_prefix = '(?:\[{0,2})'
            issue_suffix = '(?:]{0,2})'
            issue_reference = issue_prefix + '%s-\d+' % project['key'] + issue_suffix
            issue_command = (r'(?P<action>[A-Za-z]*).?'
                             '(?P<issue>%s(?:(?:[, &]*|[ ]?and[ ]?)%s)*)' %
                             (issue_reference, issue_reference))
            command_re = re.compile(issue_command)
            issue_re = re.compile(issue_prefix + '(%s-\d+)' % project['key'] + issue_suffix)

            # 各課題に対するコマンド設定
            issues = {}
            for cmd, isus in command_re.findall(log):
                funcname = CommitHook._supported_cmds.get(cmd.lower(), '')
                if funcname:
                    for issue in issue_re.findall(isus):
                        func = getattr(self, funcname)
                        issues.setdefault(issue, set([])).add(func)

            # 設定されたコマンドに応じたBacklog API起動
            for issue, cmds in issues.iteritems():
                try:
                    for cmd in cmds:
                        cmd(server, issue, '(ln #rev(%s)) %s' % (rev, log))
                except Exception, e:
                    print>>sys.stderr, 'Unexpected error while processing issue ' \
                                       'ID %s: %s' % (issue, e)

    # 課題完了用Backlog API
    def _cmdClose(self, server, issue, comment):
        server.backlog.switchStatus({'key'         : issue,
                                     'statusId'    : CommitHook._status_id['closed'],
                                     'resolutionId': CommitHook._resolution_id['fixed'],
                                     'comment'     : comment})

    # 課題参照用Backlog API
    def _cmdRefs(self,  server, issue, comment):
        server.backlog.updateIssue({'key'    : issue,
                                    'comment': comment})

    # 課題対応用Backlog API
    def _cmdDone(self, server, issue, comment):
        server.backlog.switchStatus({'key'     : issue,
                                     'statusId': CommitHook._status_id['done'],
                                     'comment' : comment})

if __name__ == '__main__':
    (repos, rev, log, spaceId, user, password) = sys.argv[1:]
    CommitHook().execCmd(repos, rev, log, spaceId, user, password)

*1:なので、フックスクリプトがこけてもコミット自体に影響は与えない

MoisesMoises2012/02/21 02:14Got it! Thanks a lot again for hpenlig me out!