TracLightningでMercurialを使ってみた

 サーバリプレースのついでに、自己流でセットアップしたTrac0.10からTracLightning2.3.2に移行してみた。ついでにTracをmod_wsgiで動かしてみたり、Mercurialと連携させてみたり、チケットの自動クローズとかできるようにしてみたのでメモしておく。

環境はこんな感じ。

TracLightningをmod_wsgiで動かす

参考URL

これは参考URLのまま。
1.2つ目のURLからmod_wsgi.soをダウンロードして、C:\TracLight\CollabNetSVN\httpd\modulesにコピー
2.C:\TracLight\CollabNetSVN\httpd\conf\httpd.confの書き換え

--- a/CollabNetSVN/httpd/conf/httpd.conf	Fri Nov 20 14:55:06 2009 +0900
+++ b/CollabNetSVN/httpd/conf/httpd.conf	Fri Nov 20 15:05:18 2009 +0900
@@ -496,7 +496,7 @@
 LoadFile "../../python/python25.dll"
 LoadModule authz_svn_module modules/mod_authz_svn.so
 LoadModule dav_svn_module modules/mod_dav_svn.so
-LoadModule python_module modules/mod_python.so
+#LoadModule python_module modules/mod_python.so
 #LoadModule fcgid_module modules/mod_fcgid.so
 
 
@@ -534,14 +534,19 @@
 # PythonDebug On 
 
 #DefaultInitEnv TRAC_ENV_PARENT_DIR "C:\TracLight\projects\trac"
-ScriptAlias /trac "C:\TracLight\CollabNetSVN\httpd\cgi-bin\trac.cgi"
+#ScriptAlias /trac "C:\TracLight\CollabNetSVN\httpd\cgi-bin\trac.cgi"
+LoadModule wsgi_module modules/mod_wsgi.so
+WSGIScriptAlias /trac "C:/TracLight/CollabNetSVN/httpd/cgi-bin/trac.wsgi"
 
 <Location "/trac">
-  SetHandler mod_python
-  PythonHandler trac.web.modpython_frontend
-  PythonOption TracEnvParentDir "C:\TracLight\projects\trac"
-  PythonOption TracUriRoot /trac
-  PythonOption PYTHON_EGG_CACHE "C:\TracLight\projects\.egg-cache"
+#  SetHandler mod_python
+#  PythonHandler trac.web.modpython_frontend
+#  PythonOption TracEnvParentDir "C:\TracLight\projects\trac"
+#  PythonOption TracUriRoot /trac
+#  PythonOption PYTHON_EGG_CACHE "C:\TracLight\projects\.egg-cache"
+  WSGIApplicationGroup %{GLOBAL}
+  Order deny,allow
+  Allow from all
 </Location>
 
 <Location "/svn/">

3.C:\TracLight\CollabNetSVN\httpd\cgi-bin\trac.wsgiを作成

#!C:\TracLight/python/python.exe
# -*- coding: utf-8 -*-
#

import sys
sys.stdout = sys.stderr

import os
os.environ['TRAC_ENV_PARENT_DIR'] = 'C:/TracLight/projects/trac'
os.environ['PYTHON_EGG_CACHE'] = 'C:/TracLight/projects/.egg-cache'

import trac.web.main

application = trac.web.main.dispatch_request

Mercurialとの連携

参考URL

Mercurialはバイナリインストーラは使わず、easy_installでインストールする。でないと、TracMercurialを入れても「Unsupported version control system "hg"」と言われてしまう。

なので、まずはeasy_installから。ez_setup.pyをダウンロードして、コマンドラインから以下のコマンドを実行する。

python ez_setup.py


次に、Mercurialをコンパイルできるようにするため、2つ目のURLを参考にMinGWをインストール。
環境変数PATHに以下の設定を追加して、

%PATH%;C:\MinGW\bin;

C:\TracLight\python\Lib\distutilsの中に「distutils.cfg」というファイルを作成し、そのファイルに下記を記述して保存。

[build]
compiler=mingw32

あとは以下のコマンドでMercurialがインストールできる。

easy_install mercurial


次はTracMercurial – The Trac Project

easy_install -UZ http://svn.edgewall.com/repos/trac/sandbox/mercurial-plugin-0.11

SVNでチェックアウトできない場合は、ソースをダウンロードして、以下のコマンドでインストールする。

python setup.py install


最後にtrac.iniを変更する。
Mercurialリポジトリを「C:\TracLight\projects\hg」に作ることにして、「C:\TracLight\projects\hg\SampleProject」にリポジトリを作成。

--- a/projects/trac/SampleProject/conf/trac.ini	Fri Nov 20 15:05:18 2009 +0900
+++ b/projects/trac/SampleProject/conf/trac.ini	Fri Nov 20 16:59:36 2009 +0900
@@ -29,7 +29,8 @@
 resolve.permissions = TICKET_MODIFY
 
 [trac]
-repository_dir = C:\TracLight\projects/svn/SampleProject
+repository_dir = C:\TracLight\projects/hg/SampleProject
+repository_type = hg
 
 authz_module_name = SampleProject 
 [mainnav]
@@ -53,6 +54,7 @@
 timingandestimationplugin.ticket_daemon.timetrackingticketobserver = disabled
 timingandestimationplugin.ticket_webui.ticketwebuiaddon = disabled
 timingandestimationplugin.webui.timingestimationandbillingpage = disabled
+tracext.hg.* = enabled
 
 [ticket-custom]
 due_assign = text
@@ -73,8 +75,11 @@
 private_wikis = SECRET
 
 #[searchhyperestraier]
 #index_path = C:\TracLight\casket
 #replace_left = C:\TracLight\rep
 #url_left = /
 
+[hg]
+show_rev = yes
+node_format = short

チェンジセット1がない、とエラーにはなるが、コミットすれば問題なく使えるようになる。

チケットの自動クローズ

参考URL

これが一番苦労した。なぜかバッチファイルはどんなものを指定しても実行してくれない。権限の関係?

悩んでても解決しないので、1個目のURLを参考に別の方法でアプローチする。


まずMercurialリポジトリをhttpで公開する。
2個目と3個目のURLを参考に、C:/TracLight/CollabNetSVN/httpd/cgi-bin/hgweb.wsgiを作成して、

import os
os.environ['HGENCODING'] = 'UTF-8'

import mercurial.hg as hg
from mercurial.ui import ui
from mercurial.hgweb.hgweb_mod import hgweb
from mercurial.hgweb.hgwebdir_mod import hgwebdir
from mercurial.hgweb.request import wsgiapplication

def listdir_dironly(base_dir):
    results = []    # prepare for failure
    for root, dirs, files in os.walk(base_dir):
        results = map(lambda d: os.path.join(root, d), dirs)
        dirs[:] = []
    results.sort()
    return results

def get_repo_for_path(path):
    return hg.repository(ui(interactive=False,
                            report_untrusted=False),
                         path=path)

def isrepo(path):
    if False:   # too redundant
        try:
            get_repo_for_path(path)
            return True
        except hg.RepoError:
            return False
    else:
        return os.path.isdir(os.path.join(path, '.hg'))

def make_hgweb_maker(path):
    return lambda: hgweb(path, os.path.split(path)[1])
    #return lambda: hgweb(get_repo_for_path(path))

def make_hgwebdir_maker(path):
    dirs = listdir_dironly(path)
    repos = [(os.path.split(dir)[1], dir) for dir in dirs]
    return lambda: hgwebdir(repos)

def hgweb_wsgiapp(path):
    if isrepo(path):
        return wsgiapplication(make_hgweb_maker(path))
    else:
        return wsgiapplication(make_hgwebdir_maker(path))

# for WSGI
def application(environ, start_response):
    def filter_headers(status, response_headers):
        # stringify header content
        headers = [(key, str(value)) for key, value in response_headers]
        return start_response(status, headers)
    def error_dialog(message):
        headers = [('Content-Type',   'text/plain'),
                   ('Content-Length', str(len(message)))]
        start_response('500 Internal Server Error', headers)
        return [message]

    if 'hgweb.reposdir' in environ:
        reposdir = environ['hgweb.reposdir']
    else:
        return error_dialog("You must specify 'hgweb.reposdir' environment")

    wsgiapp = hgweb_wsgiapp(reposdir)
    return wsgiapp(environ, filter_headers)

# for CGI
if os.environ.get('GATEWAY_INTERFACE', '').startswith('CGI/'):
    import cgitb
    cgitb.enable()

    import mercurial.hgweb.wsgicgi as wsgicgi
    wsgicgi.launch(hgweb_wsgiapp('/path/to/repos'))

さらにhttpd.confを修正する。この時点でHTTP経由でリポジトリが参照できるはず。

--- a/CollabNetSVN/httpd/conf/httpd.conf	Fri Nov 20 16:59:36 2009 +0900
+++ b/CollabNetSVN/httpd/conf/httpd.conf	Fri Nov 20 17:30:12 2009 +0900
@@ -588,3 +588,19 @@
   ProxyPass        http://127.0.0.1:8010/hudson
   ProxyPassReverse http://127.0.0.1:8010/hudson
 </Location>
+
+WSGIScriptAlias /hg "C:/TracLight/CollabNetSVN/httpd/cgi-bin/hgweb.wsgi"
+
+<Location "/hg">
+  WSGIApplicationGroup %{GLOBAL}
+  SetEnv hgweb.reposdir "C:/TracLight/projects/hg/"
+  Order deny,allow
+  Allow from all
+
+  AuthType Digest
+  AuthName trac
+  AuthUserFile "C:\TracLight\projects\trac.htdigest"
+  <LimitExcept GET PROPFIND OPTIONS REPORT>
+    Require valid-user
+  </LimitExcept>
+</Location>

次に4個目のURLを参考に、C:\TracLight\python\Lib\site-packages\hg_post_commit.pyを作成する。これは複数リポジトリがあってもいいように、少し修正してある。

# -*- encoding: utf-8 -*-

hook_script = 'C:/TracLight/python-lib/trac/contrib/trac-post-commit-hook'

# style:: 'short', 'long', or 'number'
changeset_id_style = 'short'

import os

def invoke_trac_hook(trac_env, rev):
    def _make_smart_rev(trac_env, rev):
        if changeset_id_style == 'long':
            return rev
        else:
            import trac.env
            env = trac.env.open_environment(trac_env)
            # instance of mercurial.hg.repository
            repo = env.get_repository().repo
            ctx = repo.changectx(rev)
            if changeset_id_style == 'short':
                import mercurial.node
                return mercurial.node.short(ctx.node())
            else:   # 'number'
                return str(ctx.rev())

    smart_rev = _make_smart_rev(trac_env, rev)

    f = open(hook_script, 'r')
    try:
        import imp
        ext = os.path.splitext(hook_script)[1]
        m = imp.load_module('trac_hook', f, f.name, (ext, f.mode, imp.PY_SOURCE))
        m.CommitHook(project=trac_env, rev=smart_rev)
    finally:
        f.close()

def hook(ui, repo, **kwargs):
    hg_dir, _ = os.path.split(repo.path)
    hg_root_dir, project_name = os.path.split(hg_dir)
    trac_dir = os.path.join(os.path.join(hg_root_dir, os.path.pardir, 'trac', project_name))

    if 'PYTHON_EGG_CACHE' not in os.environ:
        os.environ['PYTHON_EGG_CACHE'] = os.path.join(trac_dir, '.egg-cache')

    invoke_trac_hook(trac_dir, kwargs.get('node'))

最後に「C:\TracLight\projects\hg\SampleProject\.hg\hgrc」を作成して、上記のスクリプトを指定する。

[hooks]
incoming.trac = python:hg_post_commit.hook

[web]
push_ssl = false
allow_push = *
deny_push = unauthenticated_user

あとはSVNのときと同じようにコミットしてpushすれば、チケットが自動クローズされる。