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

【インフォシーク】メンテナンスのお知らせ

というわけで、同じ方が夜でもアッサム: 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!

2009-07-26

Backlogのコミットフックスクリプトを書いてみたよ

22:40 | Backlogのコミットフックスクリプトを書いてみたよ - ikikko.py を含むブックマーク はてなブックマーク - Backlogのコミットフックスクリプトを書いてみたよ - ikikko.py Backlogのコミットフックスクリプトを書いてみたよ - ikikko.py のブックマークコメント

ちょっとネタを思いついたので、今週かけてちょこちょこ検証しながらやってました。久しぶりのPython

元ネタは、trac-post-commit-hookです。日本語での説明は、[Think IT] 第4回:チケットとソースコードを連携せよ! (1/3)とかが分かりやすいかな。

概要

Backlogで外部Subversionを使用しているときに、コミットログで課題キーが指定されると自動でBacklogの該当課題にコメントを登録します。これは、内部ではBacklog APIを呼び出して実現しています。例えば、コミット時のログに

refs [[DEMO-1]], fixes [[DEMO-2]], done [[DEMO-3]]
コミットフックスクリプトのテストです。

とか書くと、

ln(r1) refs [[DEMO-1]], fixes [[DEMO-2]], done [[DEMO-3]]
コミットフックスクリプトのテストです。

Subversionのりビジョン(例:r1)を含んだコメントが各関連課題に登録され、課題:DEMO-2には同時に完了(完了理由:対応済み)に、課題:DEMO-3は処理済に持って行けます。

f:id:ikikko:20090726220029p:image

コミットログの記法

記法は、trac-post-commit-hookで同様の形式が使えます*1。「課題キー」の部分は「[[課題キー]]」でも構いません。

  • command 課題キー
  • command 課題キー, 課題キー
  • command 課題キー & 課題キー
  • command 課題キー and 課題キー

使えるコマンドは、↓の通りです。

処理コマンド備考
コメント登録close, closed, fix, fixed, fixes
コメント登録&完了references, refs, addresses, re, see
コメント登録&処理済doneオリジナルで追加

tracフックスクリプトとの違い

tracのそれと違うところは、下記の点です。

  1. 自動コメントの登録者は固定
  2. 課題更新者が参加しているプロジェクト全てが、更新対象
  3. 「done」コマンドで、状態を処理済へ変更するコマンドを追加

1は、Backlog APIを使う制限上、コメントの登録者は固定となってしまいます*2。なので課題更新用のユーザを作るのが一番分かりやすいでしょう。上記の画面イメージでは、「SVNフック君」というユーザを追加しています。

2は、SVNリポジトリと対応していない他のプロジェクトに対しても、(同一スペース内で)課題リンクさえ記載されていれば更新対象となります。というのは、プロジェクトからはリポジトリが一意に定められるけど、リポジトリからはプロジェクトを一意に定められないためです。

3は、新たに追加したコマンドです。小さいプロジェクトやタスクではコミット即完了とすることもあるでしょうが、ある程度の規模になるとコミットした後に責任者が確認してその後に完了とすることもあるかと思います。その際には、追加された「done」コマンドを使うと、状態を完了とはせずに処理済にすることができます。

事前設定

処理の流れ

  1. ソースコードをコミットする
  2. コミット後、post-commitが起動される
  3. post-commitから、課題更新を行うbacklog-post-commit-hook.pyが起動される
    1. svnlookコマンドを使用して、コミットしたてのコミットログを取得
    2. コミットログから、課題キーと対応するコマンド(refs, fixes等)を抽出
    3. 各課題に対して、コマンドに応じたBacklog APIで課題を更新

注意事項

(言い訳ですが)全部の検証をまともにやったわけではありません。特に、BacklogではHTTP/HTTPSで接続できるリポジトリにしか外部Subversionを設定できないのですが、そういった環境を用意できなかったので、完全な連携をとった形で試してはいません><

実際に試したのは、Backlogとの関連がないSubversionリポジトリにフックスクリプトを設置して、そのリポジトリにコミットしたときに適切にBacklog APIが飛んでいてコメントが登録されているよねというレベルです。正しいリポジトリと連携されていないので、コメントに記載されているSubversionリンクをクリックしても、思うようなページへジャンプはしてくれません。。。

なお、リポジトリは下記の場所に用意して試してみました。

*1:全部は検証し切れていませんが大体大丈夫です、多分。。。

*2Tracでは、コミット者の名前がチケット更新者に入る

2009-03-16

Pygmentsの自作字句解析(入門)

01:32 |  Pygmentsの自作字句解析(入門) - ikikko.py を含むブックマーク はてなブックマーク -  Pygmentsの自作字句解析(入門) - ikikko.py  Pygmentsの自作字句解析(入門) - ikikko.py のブックマークコメント

(追記:構文解析と字句解析を間違えていたので、タイトルその他を修正)


最近、読書の割合が多くなってきて、あまりアウトプットができていない。もちろん読書(インプット)も大事だけど、やっぱりアウトプットしてこそなんぼ…てのはあるので、もっと意識してやっていかんと!


下記のプラグインの追加

(中略)

  • pygment(シンタックスハイライティング)
Trac Lightning2.1alpha1リリース - almost nearly dead

上記のように、Trac Lightning2.1からシンタックスハイライトのPygmentsが追加されています。ですが、Pygmentsには今仕事で扱っているABAP言語には対応していません。

大丈夫、ご安心ください。Pygmentsでは、字句解析プログラムを自作して拡張することができます。というわけで、PygmentsをABAPでもハイライトできるようにしてみましょう*1

なお、ここではABAPを取り上げていますが、どの言語に対応させるときでも基本は一緒なはずです。


サンプルコード

対象とするコードは、SAP Help Portalを使います。highlight関数を用いて「abap.txt」のソースコードを「abap.html」にハイライトした形で出力できます。

from pygments import highlight
from pygments.lexers import PythonLexer
from pygments.formatters import HtmlFormatter


if __name__ == '__main__':
    code = ''
    for line in file('abap.txt', 'r').readlines():
        code += line

    highlight(code, AbapLexer(), HtmlFormatter(full=True), file('abap.html', 'w'))

何もハイライトをしてないデフォルトの状態は↓な感じです。

f:id:ikikko:20090317005230p:image

第1ステップ:lexerクラスの作成

さぁ、ここから自作のlexerを作っていきましょう♪

import re

from pygments.lexer import RegexLexer, bygroups
from pygments.token import *


class AbapLexer(RegexLexer):
    name = 'ABAP'

    flags = re.IGNORECASE | re.MULTILINE
    tokens = {
        'root': [
            (r'\s+', Text),
            (r'.', Text),
        ],
    }
  • RegexLexerクラスを継承して、正規表現で字句解析を行うことができるようにします。
  • name属性は無くても構いません。設定しておくと、組み込みのlexer機能側で何かいいことをしてくれるみたいですw
  • ABAPでは大文字小文字の区別が無いので、正規表現のフラグ「re.IGNORECASE」をセットしておきます。
  • tokensでは「root」の上から順にマッチングをしていきます。まずは、空白をText形式で表示するようにしておきます。で、最初はとりあえず全てText形式で表示するようにしておきましょう。

これだけでは結局何もハイライトはされませんが、うまくコンパイルできていることは確認できると思います。

第2ステップ:コメントのハイライト

ABAPでのコメントは、行頭が「*」で始まる行か「"」以降の文字列です*2。サンプルコードには「*」が出てきているので、とりあえずそちらに対応してみましょう。

    tokens = {
        'root': [
            (r'\s+', Text),
            (r'^\*(.*)$', Comment),
            (r'.', Text),
        ],
    }
  • 先ほどの「root」中に、「*」で始まる行はCommentとみなすように設定します。

結果は↓な感じ。ちょっと色が出ました。

f:id:ikikko:20090317005232p:image

第3ステップ:キーワード・文字列リテラルのハイライト

さぁ、この調子でどんどん色をつけていきましょう。続いては、キーワードと文字列リテラルのハイライト。

    tokens = {
        'root': [
            (r'\s+', Text),
            (r'^\*(.*)$', Comment),
            (r'(call screen|default|parameters|position|report|skip to line|write)', Keyword),
            (r"'[^']*'", String),
            (r'.', Text),
        ],
    }
  • サンプル中に出てくるキーワードを列挙して「Keyword」とします*3
  • 「'」で囲まれた文字列はリテラルとみなします。

最終的な結果は↓な感じ。キーワードが緑色に、リテラルが赤色にハイライトされています。とりあえず、サンプルコードに対しては、こんなものでしょう。

f:id:ikikko:20090317005233p:image


で、当初の目的である「ABAPのPygments対応」ですが、実は404 Not Foundにまんまあります。知らない内に誰か作ってくれてたみたい…orz

けど、前ぐぐった時にはこんなの見つからなかったよな?って見てみると、2009/02/25にチケット発行か。一応、

pygmentのABAP拡張できないかな?ちょっと調べてみよう。

Shibuya.trac新年会 感想 - @ikikko のはてなブログ

と、2009/01/15@Shibuya.trac新年会でアイデアは頭にあったので、さっさと行動しておけば。くやしいです・・・><


今回の教訓:考えるだけじゃ意味がない、すぐに行動に移そう。

*1:念のため補足しておくと、仕事でTracを使える可能性は限りなく0に近いですが、興味をそそられたのでまずはやってみることに。エンジニアとして、好奇心は大事なことですよねw

*2:残念ながら、ABAPにはマルチラインコメントはありません

*3:もちろん、ABAPのキーワードはこれだけではないので、まじめにやるならば全てを列挙しておく必要があります

GeorgesGeorges2012/02/21 05:27Please keep throniwg these posts up they help tons.

JasonerumpJasonerump2017/01/25 04:26книга на заказ http://wkrolik.com.ua/products/konverty

2009-02-19

Tracプラグイン作成でお世話になったサイト

22:27 |  Tracプラグイン作成でお世話になったサイト - ikikko.py を含むブックマーク はてなブックマーク -  Tracプラグイン作成でお世話になったサイト - ikikko.py  Tracプラグイン作成でお世話になったサイト - ikikko.py のブックマークコメント

自分用の覚書程度ですが、まとめておきます。参照先の皆さんに感謝します♪


プラグイン開発環境

まずは開発環境の整備から。

TracDoc/PyDevWithTrac – HirobeのHack倉庫 – Trac
PyDevでTracプラグインを作る際の環境設定。
EclipseでTracプラグイン開発 - 現場のためのソフトウェア開発プロセス - たかのり日記
上記サイトの補足。

チケット更新と同期したアクション

ITicketChangeListenerというInterfaceを実装すれば、自動的にチケット登録・更新・削除のタイミングで処理を拡張できる。

ITicketChangeListenerでチケット更新時に任意の処理を実行: 気の向くままに・・・
サンプル例。
no title
やりたいことは違うけど、処理フローは大体一緒。ソースコードが見せてもらえれば、もっと楽に作れたかもしれない・・・><
TracDoc/Interface – HirobeのHack倉庫 – Trac
ITicketChangeListenerを含めた、各種Interfaceの説明。
CreatePluginScript ? Trac Hacks - Plugins Macros etc.
テンプレートを作成するためのスクリプト。残念ながら、使いませんでしたが。

trac.iniのGUI設定

チケット変更履歴の文字色を変えるプラグインその2: 気の向くままに・・・
IniAdminPluginを使って、GUItrac.iniを設定可能にする方法を紹介。

開発中に詰まったところ

VersioningSystemBackend ? The Trac Project
Subversion以外のバージョン管理システムと連携する方法。最初はこの方法を用いようとしました。が、思いのほか難しそうなので、SAPの対象オブジェクトをSubversionにコミットするという、ある種遠回りな方式を採用することに><
404 Not Found
TracSAPSubversionと3つのコンポーネントをまたがるので、その連結部分の文字コードは結構ひっかかりました。
404 Not Found
SAPにアクセスする際にActiveXを利用していますが、連続してアクセスを行おうとするとエラーを吐く問題の対処法。
Re: Not possible to save a ticket from the ITicketChangeListener.ticket_changed method ? - Farialima - com.googlegroups.trac-dev - MarkMail
Subversionコミット結果をチケットにコメント追記で反映する部分。単純にやろうとすると、本来のコメントと追記コメントが同時刻でのDBコミットとなって重複登録エラーとなります。仕方ないので、1秒スリープをかませることに。

AuthAuth2012/06/08 12:11Thanks for that! It's just the aswner I needed.

2009-02-18

setup.pyのスペルミスにはまる

01:55 |  setup.pyのスペルミスにはまる - ikikko.py を含むブックマーク はてなブックマーク -  setup.pyのスペルミスにはまる - ikikko.py  setup.pyのスペルミスにはまる - ikikko.py のブックマークコメント

前のエントリは12月か、結構空いてますね。

Sap2SvnPluginリリース! - @ikikko のはてなブログtracのプラグインをリリースしました。今日は、その際の配布モジュールを作る際にはまった話を。(参考:404 Not Found

ま、一言で言えば単なるスペルミスなのですがw


trac/plugindev - Shibuya.trac Wiki - Shibuya.trac - OSDNらへんを参考にしながらプラグインを作り、動作検証もあらかた終えて、さぁリリースしようと思ったときの出来事。試しに「python setup.py bdist_egg」でeggファイルを作ろうとしても、何故かモジュールがeggファイルに入ってきません。よくよく出力ログを見てみると、以下のような警告が。

C:\TracLight\python\Lib\distutils\dist.py:263: UserWarning: Unknown distribution option: 'package'
  warnings.warn(msg)

(中略)

warning: install_lib: 'build\lib' does not exist -- no Python modules to install

最初、後者の「build\lib」ディレクトリがねーよという警告ばかりチェックして、手動でディレクトリ作成とかしてみたりしました。一応、それでeggが作られるようにはなったのですが、何だか気持ち悪いなと。で、前者の警告を見落としていたので、こっちから追っていってみました。

すると、setup.pyが

from setuptools import find_packages, setup

setup(

    ### 中略 ###

    package=find_packages(exclude=["*.tests*"]),

    ### 中略 ###

)

となっているではないですか!?これはもしや?と思い、↓のように変更して再度トライ。

    packages=find_packages(exclude=["*.tests*"]),

普通にうまくいくじゃないですか…orz


教訓:サンプルコードは手でタイプするな、コピペしろ*1

*1:ただし、コピペより実際にタイプした方が理解は進むんですよね・・・