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]
すらついてませんでしたよーだ - どうせ Trac も mod_wsgi に移行しようと思ってたから,勉強をかねて書いてみようと思った
はまったところとかメモとか
- hgweb は WSGI 向けにコードを書いてあるので最初はわりとすんなりいけるかなと思いました。
- でも
mercurial.hgweb.staticfile
で Content-Length ヘッダの値を数値で指定しているので Internal Server Error になってしまいました*1。ので,ラッピングしてます。 - ロケールまわりの設定をしてないユーザ?(apache とか)だと,charset が ANSI_X3.4-1968 とかいう謎のものになってしまうので,
mercurial.util._encoding
という内部変数に直接 UTF-8 を指定してあります。問題があれば変更するなり削除するなりしてください。os.environ
のHGENCODING
に設定するようにしました。 hgwebdir
はPATH_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 ってインデント強制のおかげで,人様のコードはとても読みやすいですね。個人的には書く際にインデント強制されてムキーってなる。慣れとは思うんですがデバッグ中とかあえて変なインデントにする癖をもってるんで。))。アドバイス歓迎します。