hgstatic

Lightweight static HTML alternative to hgweb UI

hg clone https://code.matthewwild.co.uk/hgstatic

tip: 09179ec0040f (2026-09-25)

hgweb-static

Serve a directory of Mercurial repositories with hg clone support but a fully static web rendering.

This became necessary for me due to the volume of requests my hgweb UI was receiving from AI scrapers. The static HTML is much faster to serve, though doesn't have all the features of hgweb. Currently it generates a repo index with dates and descriptions, a rendered README for each project, and source viewer for the latest (tip) revision, with line numbering and syntax highlighting.

How it works

While hg technically supports pulling a plain static repository served over HTTP, this is less efficient than the Mercurial wire protocol, which e.g. allows a hg client to discover and fetch only changesets that it doesn't already have locally.

The core "trick" used in this project is that a normal hg clone/hg pull over HTTP always request to repo root with a ?cmd= query parameter (i.e. the "smart" protocol). The web server can be configured to route those requests to a minimal hgweb instance, but serves everything else as static files regenerated by a push hook.

Components

Setup

Assumes repos in /srv/hg (nested directories are fine), static output in /var/www/hg, site at https://code.example.org.

  1. Install the generator and its optional deps:

    install -m 755 hgstatic.py /usr/local/bin/hgstatic
    apt install python3-pygments python3-markdown-it python3-docutils
    
  2. Initial full generation (rerun any time to rebuild everything):

    hgstatic --root /srv/hg --out /var/www/hg \
        --clone-base https://code.example.org --title "my repositories"
    
  3. Add a hook to regenerate on push. Hooks run with the repo as cwd, so one global snippet covers all repos, e.g. in /etc/mercurial/hgrc.d/hgstatic.rc (or per-repo .hg/hgrc):

    [hooks]
    changegroup.hgstatic = hgstatic --only "$PWD"
    

    --only regenerates just the pushed repo plus the top-level index. Given only --only, hgstatic locates an hgstatic.ini by walking up from the repo (also checking config/ subdirectories), and exits quietly if there is none. The ini holds the settings from step 2:

    [hgstatic]
    root = /srv/hg
    out = /var/www/hg
    clone-base = https://code.example.org
    title = my repositories
    

    With the ini in place, step 2's full run is just hgstatic -c /path/to/hgstatic.ini. Command-line options can be used to override ini values if necessary.

  4. hgweb for the wire protocol: copy examples/hgweb.config to /etc/hgweb.config and examples/hgweb_wsgi.py to /opt/hgweb/, then run it as the user owning the repos:

    # /etc/systemd/system/hgweb.service
    [Unit]
    Description=hgweb (Mercurial wire protocol)
    After=network.target
    
    [Service]
    User=hg
    RuntimeDirectory=hgweb
    ExecStart=/usr/bin/gunicorn --workers 2 \
        --bind unix:/run/hgweb/hgweb.sock --chdir /opt/hgweb \
        hgweb_wsgi:application
    
    [Install]
    WantedBy=multi-user.target
    

    The app reads its config path from $HGWEB_CONFIG (default /etc/hgweb.config). For several independent instances on one machine, use the examples/hgweb@.service template instead.

  5. Web server, either:

    • nginx: adapt examples/nginx.conf (server name, TLS, paths).
    • Apache: adapt examples/apache.conf; needs a2enmod rewrite proxy proxy_http, and gunicorn bound to 127.0.0.1:8420 instead of the unix socket.

Sympl

For a complete from-scratch walk-through on a Sympl host, including multiple domains on one machine, see docs/sympl.md.

Pushes happen over ssh. HTTP push is disabled in hgweb.config.

Notes

Files

.hgignore32 B
README.md4.7 KiB
docs/sympl.md5.5 KiB
examples/apache.conf797 B
examples/hgweb.config388 B
examples/hgweb@.service638 B
examples/hgweb_wsgi.py636 B
examples/nginx.conf842 B
hgstatic.py15.0 KiB