Trac과 SVN 연동: svn revision -> trac ticket 연동 by 오리대마왕

Trac <--> SVN 연동


널리 쓰이는 이슈 트래커인 trac은 자체적으로 svn과 연동이 된다. 내장된 svn repository 탐색기도 꽤 훌륭하다. 여기에 svn 의 revision 과 특정 ticket을 연동하는 기능을 추가적으로 제공한다. 즉, 이번에 commit 된 내용이 어떤 ticket에 관련된 내용인지를 인식하게 하는 것이다
제대로 연동될 경우 trac 상에서 1) svn repository 탐색기에서 관련 ticket으로 이동, 2) ticket 상세보기에서 ticket 구현을 위해 작업한 svn revision 내역 조회가 가능하다. 이에 대해 정리된 문서는 많으나, 일단 다시 한번 정리하는 차원에서 정리해 본다. 참고로 window 기반이고, 사용한 trac은 0.10.4 버전이다. windows 에 trac 설치하는 가장 쉬운 방법은 TOW(Trac on windows)를 이용하는 것이다. 정말 쉽다.

1단계: project에 svn property 설정


svn은 cvs와 다르게 property 라는 개념이 있어서, 대상 파일의 속성등을 상세히 정의할 수 있다. trac과 같은 이슈 트래커와 연동하기 위해 svn은 bugtraq: 로 시작하는 몇개의 property 를 제공한다. 다음은 내가 설정한 내용이다. 아래 그림에서 url은 지워버렸는데, 자신의 trac 주소를 입력하면 된다.

참고로 다른 property는 내부적인 처리에 사용하는 것이고, bugtraq:label 은 commit 시 클라이언트에서 표시되는 내용이다.svn의 property 는 commit 해야 적용되기 때문에 여기까지 하고선 일단 commit 하여 property가 적용되도록 하자.

이제 property가 적용되었으므로, commit 시 client의 대화창이 약간 바뀌어야 한다. 내 경우 eclipse 의 subversive를 사용중인데, 다음과 같이 바뀐다. 위에서 bugtraq:label 에 입력한 내용이 보인다.


2단계: svn post commit hooking script 설정


이제 commit할 때 "지금 commit한 것은 어떤 issue와 연관된 것들입니다" 라는 것을 svn에 알려줬다. 이제는 svn이 이렇게 알고 있는 정보를 직접 trac 등의 이슈 트래킹 시스템에 알려줄 차례이다. 이를 위해 사용되는 것이 svn의 post commit hooking script이다. svn repository 에 보면 hooks 라는 디렉터리가 있고, 여기에 보면 몇개의 템플릿들이 미리 작성되어 있다. 우리가 할 것은 post-commit 시점에 수행되어야 할 내용을 작성하는 것이다. post-commit.bat 파일을 만들고, 다음과 같이 입력한다. 이때 주의할 것은 post-commit.bat 에서는 path로 잡아놓은 경로가 먹히질 않기 때문에 무조건 절대경로로 적어줘야 한다. trac의 post commit hooking script 는 python으로 작성되어 있으므로 아래와 같이 python binary 위치를 절대경로로 지정해야 한다. TOW로 설치했으면 아래 위치에 존재한다. 다음은 post-commit.bat 내용이다. 짧다.

C:\TOW\Python\python.exe %1\hooks\trac-post-commit-hook.py %1 %2

3단계: trac의 trac-post-commit-hook 설정


이건 좀 헷갈리는데, 원래 trac에 들어있는 것인지, 내가 직접 설정 해 준 것인지 모르겠네. 하여간, 나는 trac-post-commit-hook.py을 위의 post-commit.bat 파일과 같은 위치에 넣었다. 내용은 다음과 같다. 아래 내용 중 절대경로로 된 부분은 고쳐서 써야 한다. 대략 수행하는 내용은 "방금 commit 된 svn revision 의 내용은 이러이러하니, ticket 의 change log 에 알아서 남기도록 해라" 라는 것 같다.

import sys,os
import commands
import time

ERRORFILE = "D:\\svn_respository\\post-commit.log"
f = open( ERRORFILE, "a" )

try:
    repos = sys.argv[1]
    rev = sys.argv[2]

    log = os.popen( "E:\\dev\\tools\\VisualSVNServer\\bin\\svnlook.exe log " +
repos + " -r " + rev ).read().strip()
  
    author = os.popen( "E:\\dev\\tools\\VisualSVNServer\\bin\\svnlook.exe author "
+ repos + " -r " + rev ).read().strip()
  
    trac_env = 'C:\\TOW\\TracRepo\\Projects\\your_project_name' ## Set the path to your trac environment here

    result = os.popen( "C:\\TOW\\Python\\python C:\\TOW\\Python\\Scripts\\trac-post-commit-hook.py \
                  -p \"" + trac_env +"\" \
                  -r \"" + rev +"\"       \
                  -u \"" + author +"\"    \
                  -m \"" + log +"\"" \
             ).read().strip()

    print >> f, time.strftime( "%a, %d %b %Y %H:%M:%S +0000",
time.localtime())
    print >> f, "repos:" + repos
    print >> f, "rev:" + rev
    print >> f, "author:" + author
    print >> f, "log:" + log
    print >> f, "trac_env:" + trac_env
    print >> f, "result:" + result
  
except:
    import traceback

    type, value, tb = sys.exc_info()
    stack = traceback.extract_tb( tb )
    print >> f, time.strftime( "%a, %d %b %Y %H:%M:%S +0000",
time.localtime())
    #print >> f, rev
    print >> f, "\n".join( traceback.format_list( stack ) )
    print >> f, "Type: %s <br/>" % type
    print >> f, "Value: %s <br/>" % value
    raise

아직 끝이 아니다. 위의 소스를 보면 C:\\TOW\\Python\\Scripts\\trac-post-commit-hook.py 이라는 파일이 언급되어 있다. 이것이 진짜 알맹이다. 위 소스는 단순히 "이러이러한 내용이 있으니 ticket에 추가해 주세요" 까지 던지는 것이고, 이를 실제 추가해 줘야 할 녀식이 등장한다. 위에 언급한 C:\\TOW\\Python\\Scripts\\trac-post-commit-hook.py 파일을 생성하고 다음 내용을 넣는다.

#!/usr/bin/env python

# trac-post-commit-hook
# ----------------------------------------------------------------------------
# Copyright (c) 2004 Stephen Hansen
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
#   The above copyright notice and this permission notice shall be included in
#   all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
# ----------------------------------------------------------------------------

# This Subversion post-commit hook script is meant to interface to the
# Trac (http://www.edgewall.com/products/trac/) issue tracking/wiki/etc
# system.
#
# It should be called from the 'post-commit' script in Subversion, such as
# via:
#
# REPOS="$1"
# REV="$2"
# LOG=`/usr/bin/svnlook log -r $REV $REPOS`
# AUTHOR=`/usr/bin/svnlook author -r $REV $REPOS`
# TRAC_ENV='/somewhere/trac/project/'
# TRAC_URL='http://trac.mysite.com/project/'
#
# /usr/bin/python /usr/local/src/trac/contrib/trac-post-commit-hook \
#  -p "$TRAC_ENV"  \
#  -r "$REV"       \
#  -u "$AUTHOR"    \
#  -m "$LOG"       \
#  -s "$TRAC_URL"
#
# It searches commit messages for text in the form of:
#   command #1
#   command #1, #2
#   command #1 & #2
#   command #1 and #2
#
# You can have more then one command in a message. The following commands
# are supported. There is more then one spelling for each command, to make
# this as user-friendly as possible.
#
#   closes, fixes
#     The specified issue numbers are closed with the contents of this
#     commit message being added to it.
#   references, refs, addresses, re
#     The specified issue numbers are left in their current status, but
#     the contents of this commit message are added to their notes.
#
# A fairly complicated example of what you can do is with a commit message
# of:
#
#    Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12.
#
# This will close #10 and #12, and add a note to #12.

import re
import os
import sys
import time

from trac.env import open_environment
from trac.ticket.notification import TicketNotifyEmail
from trac.ticket import Ticket
from trac.ticket.web_ui import TicketModule
# TODO: move grouped_changelog_entries to model.py
from trac.util.text import to_unicode
from trac.web.href import Href
from trac.versioncontrol.api import NoSuchChangeset

try:
    from optparse import OptionParser
except ImportError:
    try:
        from optik import OptionParser
    except ImportError:
        raise ImportError, 'Requires Python 2.3 or the Optik option parsing library.'

parser = OptionParser()
depr = '(not used anymore)'
parser.add_option('-e', '--require-envelope', dest='env', default='',
                  help="""
Require commands to be enclosed in an envelope.
If -e[], then commands must be in the form of [closes #4].
Must be two characters.""")
parser.add_option('-p', '--project', dest='project',
                  help='Path to the Trac project.')
parser.add_option('-r', '--revision', dest='rev',
                  help='Repository revision number.')
parser.add_option('-u', '--user', dest='user',
                  help='The user who is responsible for this action '+depr)
parser.add_option('-m', '--msg', dest='msg',
                  help='The log message to search '+depr)
parser.add_option('-c', '--encoding', dest='encoding',
                  help='The encoding used by the log message '+depr)
parser.add_option('-s', '--siteurl', dest='url',
                  help='The base URL to the project\'s trac website (to which '
                       '/ticket/## is appended).  If this is not specified, '
                       'the project URL from trac.ini will be used.')

(options, args) = parser.parse_args(sys.argv[1:])

if options.env:
    leftEnv = '\\' + options.env[0]
    rghtEnv = '\\' + options.env[1]
else:
    leftEnv = ''
    rghtEnv = ''

commandPattern = re.compile(leftEnv + r'(?P<action>[A-Za-z]*).?(?P<ticket>#[0-9]+(?:(?:[, &]*|[ ]?and[ ]?)#[0-9]+)*)' + rghtEnv)
ticketPattern = re.compile(r'#([0-9]*)')

class CommitHook:
    _supported_cmds = {'close':      '_cmdClose',
                       'closed':     '_cmdClose',
                       'closes':     '_cmdClose',
                       'fix':        '_cmdClose',
                       'fixed':      '_cmdClose',
                       'fixes':      '_cmdClose',
                       'addresses':  '_cmdRefs',
                       're':         '_cmdRefs',
                       'references': '_cmdRefs',
                       'refs':       '_cmdRefs',
                       'ticket':     '_cmdRefs',                      
                       'Ticket':     '_cmdRefs',       
                       'see':        '_cmdRefs'}

    def __init__(self, project=options.project, author=options.user,
                 rev=options.rev, url=options.url):
        self.env = open_environment(project)
        repos = self.env.get_repository()
        repos.sync()
       
        # Instead of bothering with the encoding, we'll use unicode data
        # as provided by the Trac versioncontrol API (#1310).
        try:
            chgset = repos.get_changeset(rev)
        except NoSuchChangeset:
            return # out of scope changesets are not cached
        self.author = chgset.author
        self.rev = rev
        self.msg = "(In [%s]) %s" % (rev, chgset.message)
        self.now = int(time.time())
        if url is None:
            url = self.env.config.get('trac', 'base_url')
        self.env.href = Href(url)
        self.env.abs_href = Href(url)

        cmdGroups = commandPattern.findall(self.msg)
       
        tickets = {}
        for cmd, tkts in cmdGroups:
            funcname = CommitHook._supported_cmds.get(cmd.lower(), '')
            if funcname:
                for tkt_id in ticketPattern.findall(tkts):
                    func = getattr(self, funcname)
                    tickets.setdefault(tkt_id, []).append(func)

        for tkt_id, cmds in tickets.iteritems():
            try:
                db = self.env.get_db_cnx()
               
                ticket = Ticket(self.env, int(tkt_id), db)
                for cmd in cmds:
                    cmd(ticket)

                # determine sequence number...
                cnum = 0
                tm = TicketModule(self.env)
                for change in tm.grouped_changelog_entries(ticket, db):
                    if change['permanent']:
                        cnum += 1
               
                ticket.save_changes(self.author, self.msg, self.now, db, cnum+1)
                db.commit()
               
                tn = TicketNotifyEmail(self.env)
                tn.notify(ticket, newticket=0, modtime=self.now)
            except Exception, e:
                # import traceback
                # traceback.print_exc(file=sys.stderr)
                print>>sys.stderr, 'Unexpected error while processing ticket ' \
                                   'ID %s: %s' % (tkt_id, e)
           

    def _cmdClose(self, ticket):
        ticket['status'] = 'closed'
        ticket['resolution'] = 'fixed'

    def _cmdRefs(self, ticket):
        pass


if __name__ == "__main__":
    if len(sys.argv) < 5:
        print "For usage: %s --help" % (sys.argv[0])
        print
        print "Note that the deprecated options will be removed in Trac 0.12."
    else:
        CommitHook()




5단계: 테스트


이제 커밋을 해 보자. 제대로 설정이 되었다면 svn 의 commit comment 에는 Ticket #XXX 형식의 링크가 표시되어야 하고, ticket에는 변경이력 부분에 해당 revision의 commit comment 및 revision 조회 링크가 표시되어야 한다. 제대로 되지 않았다면 3단계의 post hooking script 의 log를 살펴보자. 직접 trac-post-commit-hook.py 를 실행해보는 것도 좋은 방법이다.

핑백

  • 우탱위키: 20090310~20090316 2009-03-14 09:30:22 #

    ... trac xmlrpc http://trac-hacks.org/wiki/XmlRpcPlugin java example을 다운로드 SVN hook 사용 예제 http://kingori.egloos.com/3922223 RCP or Plug-in? http://www.ibm.com/developerworks/kr/library/os-eclipse-snippet/ h ... more

  • 그런지 Ltd. : trac 2012-06-04 20:31:14 #

    ... http://jinself.tistory.com/tag/trac http://kingori.egloos.com/3922223 ... more

덧글

  • 제우스 2008/09/30 11:16 #

    Mylyn과 eclipse, redmine을 연결하였는데..
    아직 정식 지원이 아니라 단순 웹연결이라서 메리트가 좀 떨어지네요.
    그냥 옆에 브라우저 띄워놓는거랑 별반 차이가 엄써서요 ㅠㅜ

    그래도 svn post commit를 이용해서 디스트 파일 목록을 자동생성하는 방법의
    아이디어랑 비슷하게 revision으로 파일내용을 가져오는 방법을 찾아서 ^^
    2개중에 한개는 건졌어요 ^^
  • 오리대마왕 2008/09/30 12:52 #

    mylyn의 좋은 기능 중 하나가 해당 이슈에 관련된 자원만을 볼 수 있도록 하여집중도를 높이는 것인데(context 관리), xmlrpc 등이 아닌 단순 웹 연결에서는 이게 안되지요. 그런데 xmlrpc 연결이 되도 썩 그리 편하다는 느낌은 없어요. 파일 첨부는 drag&drop 이 되니 훨씬 편하지만.

    post commit 는 상당히 유용하지요. 위에 스크립트 2개 중 앞의 녀석은 굳이 trac이 아니더라도 다른 환경에서도 살짝 바꿔서 유용하게 사용할 수 있을 겁니다~


  • 아이리스 2008/10/16 17:32 #

    =_= 우리말로 해주련?
  • elliott 2010/02/05 11:19 # 삭제

    정말 좋은 내용 감사합니다. 퍼가겠습니다.
※ 로그인 사용자만 덧글을 남길 수 있습니다.