# yum install httpd mod_python mod_ssl mercurial mercurial-hgk
The following is a documentation HOWTO on setting up Mercurial repository on Fedora with HTTPS + LDAP.
Please read the full documentation before you try it!
# yum install httpd mod_python mod_ssl mercurial mercurial-hgk
# mkdir /var/hg/repos
# cd /var/hg/repos # mkdir hgtest; cd hgtest # hg init # echo 'Hello world' > README # hg add README # hg commit -m 'This is the first change to my test repository' # chown -R apache:apache .
Create .hgrc at /var/hg/repos/hgtest/.hg/:
[web] contact = Qvantel description = testing Mercurial web style = gitweb allow_push = * push_ssl = false
Create modpython_gateway.py at /var/hg/repos/:
""" WSGI wrapper for mod_python. Requires Python 2.2 or greater. Example httpd.conf section for a CherryPy app called "mcontrol": <Location /mcontrol> SetHandler python-program PythonFixupHandler mcontrol.cherry::startup PythonHandler modpython_gateway::handler PythonOption wsgi.application cherrypy._cpwsgi::wsgiApp </Location> Some WSGI implementations assume that the SCRIPT_NAME environ variable will always be equal to "the root URL of the app"; Apache probably won't act as you expect in that case. You can add another PythonOption directive to tell modpython_gateway to force that behavior: PythonOption SCRIPT_NAME /mcontrol Some WSGI applications need to be cleaned up when Apache exits. You can register a cleanup handler with yet another PythonOption directive: PythonOption wsgi.cleanup module::function The module.function will be called with no arguments on server shutdown, once for each child process or thread. """ import traceback from mod_python import apache class InputWrapper(object): def __init__(self, req): self.req = req def close(self): pass def read(self, size=-1): return self.req.read(size) def readline(self, size=-1): return self.req.readline(size) def readlines(self, hint=-1): return self.req.readlines(hint) def __iter__(self): line = self.readline() while line: yield line # Notice this won't prefetch the next line; it only # gets called if the generator is resumed. line = self.readline() class ErrorWrapper(object): def __init__(self, req): self.req = req def flush(self): pass def write(self, msg): self.req.log_error(msg) def writelines(self, seq): self.write(''.join(seq)) bad_value = ("You must provide a PythonOption '%s', either 'on' or 'off', " "when running a version of mod_python < 3.1") class Handler: def __init__(self, req): self.started = False options = req.get_options() # Threading and forking try: q = apache.mpm_query threaded = q(apache.AP_MPMQ_IS_THREADED) forked = q(apache.AP_MPMQ_IS_FORKED) except AttributeError: threaded = options.get('multithread', '').lower() if threaded == 'on': threaded = True elif threaded == 'off': threaded = False else: raise ValueError(bad_value % "multithread") forked = options.get('multiprocess', '').lower() if forked == 'on': forked = True elif forked == 'off': forked = False else: raise ValueError(bad_value % "multiprocess") env = self.environ = dict(apache.build_cgi_env(req)) if 'SCRIPT_NAME' in options: # Override SCRIPT_NAME and PATH_INFO if requested. env['SCRIPT_NAME'] = options['SCRIPT_NAME'] env['PATH_INFO'] = req.uri[len(options['SCRIPT_NAME']):] env['wsgi.input'] = InputWrapper(req) env['wsgi.errors'] = ErrorWrapper(req) env['wsgi.version'] = (1,0) env['wsgi.run_once'] = False if env.get("HTTPS") in ('yes', 'on', '1'): env['wsgi.url_scheme'] = 'https' else: env['wsgi.url_scheme'] = 'http' env['wsgi.multithread'] = threaded env['wsgi.multiprocess'] = forked self.request = req def run(self, application): try: result = application(self.environ, self.start_response) for data in result: self.write(data) if not self.started: self.request.set_content_length(0) if hasattr(result, 'close'): result.close() except: traceback.print_exc(None, self.environ['wsgi.errors']) if not self.started: self.request.status = 500 self.request.content_type = 'text/plain' data = "A server error occurred. Please contact the administrator." self.request.set_content_length(len(data)) self.request.write(data) def start_response(self, status, headers, exc_info=None): if exc_info: try: if self.started: raise exc_info[0], exc_info[1], exc_info[2] finally: exc_info = None self.request.status = int(status[:3]) for key, val in headers: if key.lower() == 'content-length': self.request.set_content_length(int(val)) elif key.lower() == 'content-type': self.request.content_type = val else: self.request.headers_out.add(key, val) return self.write def write(self, data): if not self.started: self.started = True self.request.write(data) startup = None cleanup = None def handler(req): # Run a startup function if requested. global startup if not startup: func = req.get_options().get('wsgi.startup') if func: module_name, object_str = func.split('::', 1) module = __import__(module_name, globals(), locals(), ['']) startup = apache.resolve_object(module, object_str) startup(req) # Register a cleanup function if requested. global cleanup if not cleanup: func = req.get_options().get('wsgi.cleanup') if func: module_name, object_str = func.split('::', 1) module = __import__(module_name, globals(), locals(), ['']) cleanup = apache.resolve_object(module, object_str) def cleaner(data): cleanup() try: # apache.register_cleanup wasn't available until 3.1.4. apache.register_cleanup(cleaner) except AttributeError: req.server.register_cleanup(req, cleaner) # Import the wsgi 'application' callable and pass it to Handler.run modname, objname = req.get_options()['wsgi.application'].split('::', 1) module = __import__(modname, globals(), locals(), ['']) app = getattr(module, objname) Handler(req).run(app) # status was set in Handler; always return apache.OK return apache.OK
Create hgwebdir.py at /var/hg/repos/:
#!/usr/bin/env python # # $Id: hgwebdir.py 1478 2008-11-21 11:09:22Z matthew $ # # An example CGI script to export multiple hgweb repos, edit as necessary # adjust python path if not a system-wide install: #import sys #sys.path.insert(0, "/path/to/python/lib") # Uncomment to send python tracebacks to the browser if an error occurs: import cgitb cgitb.enable() # enable importing on demand to reduce startup time # from mercurial import demandimport; demandimport.enable() # from mercurial import demandload; demandload.enable() # If you'd like to serve pages with UTF-8 instead of your default # locale charset, you can do so by uncommenting the following lines. # Note that this will cause your .hgrc files to be interpreted in # UTF-8 and all your repo files to be displayed using UTF-8. # #import os #os.environ["HGENCODING"] = "UTF-8" from mercurial.hgweb.hgweb_mod import hgweb from mercurial.hgweb.hgwebdir_mod import hgwebdir from mercurial.hgweb.request import wsgiapplication # The config file looks like this. You can have paths to individual # repos, collections of repos in a directory tree, or both. # # [paths] # virtual/path = /real/path # virtual/path = /real/path # # [collections] # /prefix/to/strip/off = /root/of/tree/full/of/repos # # collections example: say directory tree /foo contains repos /foo/bar, # /foo/quux/baz. Give this config section: # [collections] # /foo = /foo # Then repos will list as bar and quux/baz. # # Alternatively you can pass a list of ('virtual/path', '/real/path') tuples # or use a dictionary with entries like 'virtual/path': '/real/path' # application = hgwebdir('hgweb.config') # wsgicgi.launch(application) def make_web_app(): return hgwebdir("/var/hg/hgweb.config") def gateway(environ, start_response): app = wsgiapplication(make_web_app) return app(environ, start_response) # ### EOF: $HeadURL: http://aventinesolutions.mine.nu/repos-nossl/config/httpd/hgwebdir.py $ #
Create /var/hg/hgweb.config as follows:
[collections] /var/hg/repos = /var/hg/repos
Make sure /var/hg is owned by apache:
# chown -R apache:apache /var/hg
Create /etc/httpd/conf.d/hg.conf:
<Location /hg> PythonPath "sys.path + [ '/var/hg/repos' ]" PythonDebug On SetHandler mod_python PythonHandler modpython_gateway::handler PythonOption SCRIPT_NAME /hg PythonOption wsgi.application hgwebdir::gateway AuthType Basic AuthBasicProvider ldap AuthzLDAPAuthoritative Off AuthName "Mercurial Repository" AuthLDAPBIndDN "cn=ldapusr, dc=company, dc=com" AuthLDAPBindPassword abcdefgh AuthLDAPGroupAttributeIsDN off AuthLDAPGroupAttribute memberUid AuthLDAPURL ldap://10.0.0.1:389/ou=People,dc=company,dc=com?uid require ldap-group cn=internal, ou=group, dc=company, dc=com </Location>
10.0.0.1 is assumed to have the LDAP server in the above. If you want a specific LDAP group, say, services, to only have access to the hgtest repository, apart from the above you can create a project specific entry using:
<Location /hg/hgtest> PythonPath "sys.path + [ '/var/hg/repos' ]" PythonDebug On SetHandler mod_python PythonHandler modpython_gateway::handler PythonOption SCRIPT_NAME /hg PythonOption wsgi.application hgwebdir::gateway AuthType Basic AuthBasicProvider ldap AuthzLDAPAuthoritative Off AuthName "Mercurial Repository" AuthLDAPBIndDN "cn=ldapusr, dc=company, dc=com" AuthLDAPBindPassword abcdefgh AuthLDAPGroupAttributeIsDN off AuthLDAPGroupAttribute memberUid AuthLDAPURL ldap://10.0.0.1:389/ou=People,dc=company,dc=com?uid require ldap-group cn=services, ou=group, dc=company, dc=com </Location>
Append the following to /etc/httpd/conf/httpd.conf:
<VirtualHost *:80> ServerName ferrari DocumentRoot /var/hg/repos SSLEngine off Redirect permanent / https://ferrari/ <Directory /var/hg/repos> ErrorDocument 403 "Use SSL, please" Order allow,deny Allow from none Deny from all </Directory> </VirtualHost>
ferrari is the hostname of the system. Give executable permission for the script files.
# cd /var/hg/repos # chmod 755 hgwebdir.py modpython_gateway.py
Make sure:
selinux is disabled.
Set SELINUX=disabled in /etc/selinux/config. Reboot. You can test if it is disabled from the output of:
$ sestatus
iptables is disabled, and switched off.
# iptables -F # chkconfig iptables off
Start the server!
# service httpd start
You can now open the Mercurial web on the browser through https://ferrari/hg/.
You can checkout code using:
$ hg clone https://ferrari/hg/hgtest
Sendmail should be running by default on Fedora. You can enable e-mail notification by enabling NotifyExtension.
In each of the project repository, at /var/hg/repos/project-name/.hg/hgrc file, append the following:
[extensions] hgext.notify = [hooks] # Enable either changegroup or incoming. # changegroup will send one email for each push, # whereas incoming sends one email per changeset. changegroup.notify = python:hgext.notify.hook #incoming.notify = python:hgext.notify.hook [email] from = your@email.address [smtp] host = localhost # Optional options: # username = joeuser # password = secret # port = 25 # tls = true # local_hostname = me.example.com # presently it is necessary to specify the baseurl for the notify # extension to work. It can be a dummy value if your repo isn't # available via http [web] baseurl = https://ferrari/hg/hgtest [notify] # multiple sources can be specified as a whitespace separated list sources = serve push pull bundle # set this to False when you're ready for mail to start sending test = False # you can override the changeset template here, if you want. # If it doesn't start with \n it may confuse the email parser. # here's an example that makes the changeset template look more like hg log: template = \ndetails: {baseurl}{webroot}/rev/{node|short}\nchangeset: {rev}:{node|short}\nuser: {author}\ndate: {date|date}\ndescription:\n{desc}\n # max lines of diffs to include (0=none, -1=all) maxdiff = -1 [usersubs] # key is subscriber email, value is comma-separated list of glob patterns user@email.address = * [reposubs]
Whenever you do a hg push, an e-mail will be sent to the ones mentioned in the usersubs.