ありゃ?日付が変わってる。。。

以前、TracでSubversion側からチケット更新を行った場合にメールを飛ばそうとしていたんですが、 細かく整理するとやりたいことは以下のようなことになります。

  1. Trac上でチケットの編集 ⇒こりゃ既存の機能っすね
  2. Subversion上でコミット ⇒これだけで考えれば、そんなに難しくないと思う
  3. Subversion上でコミットしてコミットメッセージ内にチケットを操作するコマンドを埋め込む
この3に関しては、何も考えずに1と2を用意すると チケットの更新メール + Subversionのコミットメール の2通が飛んでくる。。。 内容は同じなのにそりゃ邪魔だろということで、 3のパターンのときにはTracサイドのチケット更新メールにSubversionのコミット情報を埋め込む方向でモジュールを作り始めた。 せっかくなので、2についてもtracパッケージで用意されているパッケージ群を使って送れるとおもしろいかなぁ~と思って、早速作ってみた!
# -*- coding: utf-8 -*-

# Copyright (C) 2003-2006 Edgewall Software
# Copyright (C) 2003-2005 Daniel Lundin 
# Copyright (C) 2005-2006 Emmanuel Blot 
# All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at http://trac.edgewall.org/wiki/TracLicense.
#
# This software consists of voluntary contributions made by many
# individuals. For the exact contribution history, see the revision
# history and logs, available at http://trac.edgewall.org/log/.
#
# Author:がら
#

from trac import __version__
from trac.config import *
from trac.core import *
from trac.notification import NotifyEmail
from trac.util.datefmt import format_date
from trac.util.text import CRLF, wrap
from trac.versioncontrol.api import NoSuchChangeset
from trac.versioncontrol.diff import unified_diff
import md5



_kindmap = {'dir':u"ディレクトリ", 'file':u"ファイル"}
_actionmap = {'add': u"追加", 'copy': u"コピー",
           'delete': u"削除", 'edit': u"更新",
           'move': u"移動"}

logfile = "postcommit_notification.log"
LOG = True

if LOG:
 f = open (logfile, "w")
 f.write("Begin Log\n")
 f.close()
 def log (s, *params):
     f = open (logfile, "a")
     f.write(s % params)
     f.write("\n")
     f.close()
else:
 def log (s, *params):
     pass

class PostCommitNotificationSystem(Component):

 always_notify_owner = BoolOption('notification', 'always_notify_owner',
                                  'false',
     u"""チケットの担当者に常に通知メールを送信するかを設定します (''0.9 以降'') 。""")

 always_notify_reporter = BoolOption('notification', 'always_notify_reporter',
                                     'false',
     u"""''報告者'' フィールドにあるアドレスに常に通知メールを
     送信するかを設定します。""")

 always_notify_updater = BoolOption('notification', 'always_notify_updater',
                                    'true',
     u"""チケットの属性の変更者に常に通知メールを
     送信するかを設定します。""")


class PostCommitNotifyEmail(NotifyEmail):
 """Subversionのコミット情報をメール送信"""

 template_name = "postcommit_notify_email.cs"
 from_email = 'trac+ticket@localhost'
 COLS = 75

 def __init__(self, env):
     self.env = env
     self.repos = self.env.get_repository()
     self.repos.sync()
     NotifyEmail.__init__(self, env)
     self.prev_cc = []

 def notify(self, rev):
     self.rev = rev
     try:
         chgset = self.repos.get_changeset(rev)
     except NoSuchChangeset:
         return # キャッシュされているチェンジセットの値からはみ出ている
     self.chgset = chgset
     self.message = wrap(self.chgset.message,
                                      self.COLS, initial_indent=' ',
                                      subsequent_indent=' ', linesep=CRLF)
     self.reporter = ''
     self.owner = ''
     self.hdf.set_unescaped('email.commit_body_hdr', self.format_hdr())
     subject = self.format_subj()
     link = self.env.abs_href.changeset(self.rev)
     self.hdf.set_unescaped('email.subject', subject)
     self.hdf.set_unescaped('email.commit.author', self.chgset.author)
     self.hdf.set_unescaped('email.commit.date', format_date(self.chgset.date))
     self.hdf.set_unescaped('email.commit.change_paths', self.change_paths())
     self.hdf.set_unescaped('email.commit.diff', self.unified_diff())
     self.hdf.set_unescaped('email.commit.message', self.message)
  
     self.link = link
     NotifyEmail.notify(self, self.rev, subject)

 def format_hdr(self):
     txt = ""
     for set in self.chgset.get_changes():
         lst = list(set)
         path = list(set)[0]
         kind = list(set)[1]
         change = list(set)[2]
         base_path = list(set)[3]
         base_rev = list(set)[4]
         txt += "[%s]: %s" % (self.rev, wrap(path,
                              self.COLS, linesep=CRLF))
     return txt

 def get_recipients(self, rev):
     notify_reporter = self.config.getbool('notification',
                                           'always_notify_reporter')
     notify_owner = self.config.getbool('notification',
                                        'always_notify_owner')
     notify_updater = self.config.getbool('notification',
                                          'always_notify_updater')

     ccrecipients = self.prev_cc
     torecipients = []

     return (torecipients, ccrecipients)

 def format_subj(self):
     prefix = self.config.get('notification', 'smtp_subject_prefix')
     if prefix == '__default__':
         prefix = '[%s]' % self.config.get('project', 'name')
     if prefix:
         return '%s [%s]' % (prefix, self.rev)
     else:
         return '[%s]' % (self.rev)

 def get_message_id(self, rcpt, modtime=0):
     """Generate a predictable, but sufficiently unique message ID."""
     s = '%s.%08d.%d.%s' % (self.config.get('project', 'url'),
                            int(self.rev), self.chgset.date,
                            rcpt.encode('ascii', 'ignore'))
     dig = md5.new(s).hexdigest()
     host = self.from_email[self.from_email.find('@') + 1:]
     msgid = '<%03d.%s@%s>' % (len(s), dig, host)
     return msgid

 def send(self, torcpts, ccrcpts):
     dest = self.reporter or 'anonymous'
     hdrs = {}
     hdrs['X-svn-changeset-ID'] = str(self.rev)
     hdrs['X-svn-changeset-URL'] = self.link
     msgid = self.get_message_id(dest)
     hdrs['In-Reply-To'] = msgid
     hdrs['References'] = msgid
     NotifyEmail.send(self, torcpts, ccrcpts, hdrs)

 def unified_diff(self):
     txt = ""
     try:
         chgset = self.repos.get_changeset(self.rev)
     except NoSuchChangeset:
          return # out of scope changesets are not cached
     for set in chgset.get_changes():
         lst = list(set)
         path = list(set)[0]
         kind = list(set)[1]
         change = list(set)[2]
         base_path = list(set)[3]
         base_rev = list(set)[4]
         if kind != u"file":
             continue
         if change == u"add":
             continue
         elif change == u"delete":
             continue
         elif change == u"move":
             continue
         elif change == u"copy":
             continue
         new_node = self.repos.get_node(path, chgset.rev)
         old_node = self.repos.get_node(base_path, base_rev)
         new_content = new_node.get_content().read()
         old_content = old_node.get_content().read()
         txt = wrap("", self.COLS, linesep=CRLF)
         txt += u"diff: %s" % wrap(path, self.COLS, linesep=CRLF)
         txt += wrap(u"------------------------", self.COLS, linesep=CRLF)
         txt += wrap(u"--- %s (リビジョン %s)" % (base_path, base_rev), self.COLS, linesep=CRLF)
         txt += wrap(u"+++ %s (リビジョン %s)" % (path, chgset.rev), self.COLS, linesep=CRLF)
         context = 3
         options = ['-U%d' % -1]
         options.append('-B')
         options.append('-i')
         options.append('-b')
         for option in options:
             if option.startswith('-U'):
                 context = int(option[2:])
                 break
         for line in unified_diff(old_content.splitlines(),
                               new_content.splitlines(), context,
                                   ignore_blank_lines='-B' in options,
                                   ignore_case='-i' in options,
                                   ignore_space_changes='-b' in options):
             txt += wrap(line, self.COLS, linesep=CRLF)

     return txt

 def change_paths(self):
     txt = ""
     try:
         chgset = self.repos.get_changeset(self.rev)
     except NoSuchChangeset:
          return # out of scope changesets are not cached
     for set in chgset.get_changes():
         lst = list(set)
         path = list(set)[0]
         kind = list(set)[1]
         change = list(set)[2]
         base_path = list(set)[3]
         base_rev = list(set)[4]
         line = "%s %s %s" % (_actionmap.get(change), _kindmap.get(kind), path)
         txt += wrap(line, self.COLS, linesep=CRLF)
      
     return txt
うーん、正直まだ未完成。。。 叩き台ができたってところかな? 基本的にTracの設定に準拠して動かそうと思っているのでこんな感じかな?と思っています。 しかし、Pythonってなかなかデバックしにくい。。。 そう思うのは僕だけかな?

Posted in ラベル: , , |

0 コメント: