Mercurial のウェブインタフェースを mod_wsgi にのせてみた

ちまたでは git が流行のきざしのようですが,わたしは今年 Mercurial を勉強する予定なのです。

で,表題の件,最終的に Trac with TracMercurial に移行するつもりなので必要なくなりそうですが,技術的興味からやってみました。あ,でも hg から http 経由で clone するときとかに使えるか。ま,つまり,マスターレポジトリ用途です。個人で使う分には hg serve で充分ですもんね。

使い方

末尾のコードを /var/www/app/hgweb.wsgi とかで保存して,httpd.conf に下記のように設定します。

LoadModule wsgi_module modules/mod_wsgi.so

WSGIScriptAlias /repos/ /var/www/app/hgweb.wsgi

<Directory /var/www/app>
    WSGIApplicationGroup %{GLOBAL}

    SetEnv hgweb.reposdir "/var/www/repos"

    Order deny,allow
    Allow from all
</Directory>

hgweb.reposdir という環境変数は,Mercurial のレポジトリ「群」を格納するフォルダを指定します(レポジトリ自身のフォルダを指定してもうまくいくように組んであります)。

動機など

  • 下位フォルダのレポジトリ一覧機能(Subversion でいうところの SVNListParentPath みたいなの)がほしかった
  • [http://www.selenic.com/repo/hg-stable/raw-file/tip/hgwebdir.cgi:title=hgwebdir.cgi] が使えるんだけど(⇒ HgWebDirStepByStep - Mercurial),レポジトリ一覧([][collections][])を自分で設定しないといけないのはめんどい
  • RHEL の epel レポジトリにはどうせそもそも [http://www.selenic.com/repo/hg-stable/raw-file/tip/hgweb.cgi:title=hgweb.cgi] すらついてませんでしたよーだ
  • どうせ Tracmod_wsgi に移行しようと思ってたから,勉強をかねて書いてみようと思った

はまったところとかメモとか

  • hgweb は WSGI 向けにコードを書いてあるので最初はわりとすんなりいけるかなと思いました。
  • でも mercurial.hgweb.staticfile で Content-Length ヘッダの値を数値で指定しているので Internal Server Error になってしまいました*1。ので,ラッピングしてます。
  • ロケールまわりの設定をしてないユーザ?(apache とか)だと,charset が ANSI_X3.4-1968 とかいう謎のものになってしまうので,mercurial.util._encoding という内部変数に直接 UTF-8 を指定してあります。問題があれば変更するなり削除するなりしてください。os.environHGENCODING に設定するようにしました。
  • hgwebdirPATH_INFO 等みて,レポジトリが指定されている場合,自動的に hgweb に処理を投げてくれます。なので自力で PATH_INFO をパースしなくて済むので楽でした。
  • レポジトリ生成機能もそのうち作りたい。でも,どうせごそっとコピーしたり hg init したりするだけで済むのでいらないといえばいらないか。

コード

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'))

いちお,CGI としても動くように書いてあるんですが(末尾のほう),チェックをまったくしていないです。あと,CGI の場合 /path/to/repos の部分を書き換える必要があります。

Pythonista じゃないのでコードが汚いのはご勘弁を((Python ってインデント強制のおかげで,人様のコードはとても読みやすいですね。個人的には書く際にインデント強制されてムキーってなる。慣れとは思うんですがデバッグ中とかあえて変なインデントにする癖をもってるんで。))。アドバイス歓迎します。

*1:Mercurial と mod_wsgi,どっちの責任なんだろう