README.md
Repo Renderer
Repo Renderer takes a git repo and renders it into a purely static website so you can host your code on the web without relying on someone else's git forge (GitHub, Codeberg, Bitbucket, etc.) and without having to host your own code forge (Gitea, Forgejo), which are significantly more resource-intensive than a static site. If you configure Repo Renderer to re-render your repo as a git hook, you can also host your own git backend that keeps the static web view up to date by incrementally re-rendering files in your repo only when they change.
| Syntax-highlighted files | Search, in the browser | The repository index |
|---|---|---|
![]() |
![]() |
![]() |
Manifesto
The main reason I made this project is to allow me to keep hosting my own code on my own website in an era when GitHub is increasingly enshittified by Microsoft and badly behaved AI company web crawlers are increasingly hammering sites like mine into the ground. In my personal experience, traffic from Microsoft and Amazon has been especially badly behaved on this front.
I do not subscribe to the view that websites are only for humans or that humans should need to prove their humanity through CAPTCHAs in order to use the web. In the words of Michael Bolton:
No way! Why should I [do CAPTCHAs]? He's the one who sucks.
I stand in favor of respectful web users, whether they have Javascript disabled, whether they are using Tor, whether they are CURLing a site to view offline, or whether they are writing a properly rate-limited Python script to crawl a site. With that in mind, this project is an effort to make sure that even a minimum-spec VPS (1 vCPU / 1GB RAM) is capable of hosting all your code without forcing users to jump through hoops and without breaking a sweat as badly-behaved crawlers hammer the site. And hopefully it offers a user experience that is comparable or superior to sites like GitHub or Codeberg for the casual code viewer.
⚠️ SLOP ALERT ⚠️
This project is heavily LLM-authored. I've spent a fair amount of time and effort refining the design and looking over the code, but if LLMs are a dealbreaker for you, this project probably isn't something you'll want to use. I think it's appropriate to use an LLM to mitigate the harmful impact of LLM scrapers on my personal site and to reduce my dependency on GitHub and Microsoft's attempts to enclose the software commons. But that's just my opinion and I can respect people who don't feel the same way.
Overview
This is intended to be used for ultra-lightweight servers that may end up
getting absolutely hammered by abusive web crawlers. This project is built with
OpenBSD's httpd(8) in mind, which is a high-performance, lightweight,
high-security static HTTP server. Each page is rendered at most once per git
commit, and served statically to every visitor, with no server rendering taking
place on page views.
The renderer only shows the latest commit of each repository's default branch,
plus any other branches you put in your config. In order to save on resources,
the renderer does not show the full git history of the repository. Users who
care about the full git history can clone the repository and use git or their
favorite IDE to explore the history.
Features
- Repository index: A listing of all your repositories, grouped into categories according to your config.
- Repository overview: the repository overview shows the file listing, README, and stats.
- Tags: Label repositories with tags like
luaorlanguagein the config. They show on the index and on each repository's page, for information only. - Lines of code counts: Each repo shows a breakdown of lines of code, words of documentation, and bytes of assets.
- Source code: each file is viewable as statically rendered, syntax-highlighted pages. No javascript is required.
- Custom language syntax highlighting: A very wide number of languages are supported out of the box, but it's also possible to extend the renderer using syntax highlighting for custom programming language syntax.
- Markdown rendering: Markdown files are nicely rendered to make it easy to read your docs, with a separate tab to view the syntax-highlighted Markdown source code. Fenced code blocks get full syntax highlighting in both rendered Markdown and Markdown source code. Images and links also work properly.
- File tree: Each page shows a collapsible file drawer for quick and easy navigation of the files in the repo.
- Source archive downloads: Download the whole repo as a .tar.gz file, one archive for each tag and one for the latest code.
- Super fast client-side search: Seriously, you can just load the code into memory and do a string search to find stuff. It's orders of magnitude faster than you get with GitHub and you can use regexes.
- Branch browsing. You can list extra branches to render beside the default one (see Browsing other branches).
- A sitemap: Search engines get every page of each repository's default branch, dated by the last commit that changed it. Other branches, and repositories hidden from the index, are left out.
- Private repositories: A repository in no section of the index is
private by default. With
private_htdocsset, it's rendered to a private site at/private/, behind a password; without it, it isn't rendered at all (see Private repositories). Pushes to it are still mirrored either way. - A 404 page: A missing address gets a page in the site's own style that links back to the index, not httpd's stock error.
- Light and dark themes: The site follows your system setting, and a button lets you switch and remembers your choice. You can replace the built-in favicon in the config.
- Incremental rendering: A push re-renders only the pages that changed. It re-renders everything when the templates or config change, when someone rewrites history, when the set of rendered branches changes, when the repository's description changes, or when the last render failed or was cut short. That last case sweeps away whatever pages a failed render had already written, so reverting a bad commit, or force-pushing back to before it, leaves nothing of it on the site. The file drawer and the Download menu's list are documents of their own that every page shows in a frame, so adding a file rewrites the drawer, not every page, and the list, which names the latest commit, is the only document every push rewrites. Pushing a tag rewrites only that list.
- A small security surface: httpd serves the pages with a strict Content
Security Policy (
default-src 'none'), so the only scripts that run are the two the site serves itself, and no other site can show the pages in a frame (frame-ancestors 'self'). Raw files keep their real names and are served with asandboxpolicy, which stops them from running as part of the site. The renderer validates every path and tag name before it writes anything, and it restricts its own filesystem access withunveilon OpenBSD.
Every page loads one small script for progressive enhancement: ui.js, which adds the theme toggle, a "Copy" button for the clone command, Escape and click-away handling for the Download menu, and fits the menu's frame to its contents. Without JavaScript, the site is plain HTML: those controls disappear and the download list scrolls in a fixed-height frame.
Code search also uses javascript, but because the site is purely static, there is no fallback for browsers that don't support javascript (no server-side search).
Building
make # builds ./repo-render for this machine
make openbsd # builds dist/repo-render-openbsd-amd64, the one file the server needs
make check # runs go vet, shellcheck, the Go tests, and the node tests
make fuzz # fuzzes the checks on pushed names and paths, 30s each (FUZZTIME)
make screenshots # retakes the README's screenshots of the preview site
You need Go 1.26 and git. You only need node and shellcheck for make check,
and node and firefox for make screenshots.
How the server is laid out
The files follow OpenBSD's hier(7). Each location is a config setting, named in parentheses below with its default path:
/etc/repo-render/ configuration, owned by root
repo-render.yaml the config file (see examples/repo-render.yaml)
lexers/<name>.xml your own syntax highlighters (lexer_dirs)
mirror_key optional ssh key for pushing to mirrors (ssh_key)
/var/git/ git's home (git_dir)
<repo>.git bare repositories, owner and group git
.ssh/authorized_keys the keys that may push
/var/www/htdocs/<host>/ the website, inside httpd's chroot (htdocs)
index.html repository index
err/404.html the page for a missing address (httpd's errdocs)
robots.txt keeps crawlers to the pages, off tarballs, raw files
and the clone export, asks them to wait 10 seconds
between fetches, and names the sitemap
sitemap.xml sitemap index: one sitemap per repository on the index
sitemap/<repo>.xml every page of the default branch, dated by its last change
ads.txt the spec's placeholder record: no one may sell ads
on the site
code/<repo>.html a repository's root page
code/<repo>/<path>.html its files and directories
code/<repo>@<branch>/… extra branches (more below)
raw/<repo>/<path> raw files, byte for byte
svg/<repo>/<path>.svg SVGs prepared for embedding, with the theme
colour applied
snapshot/<repo>/<repo>@<commit>.tar.gz
tarball of the latest commit (the Download menu's
"latest"), named by the first 12 digits of its hash
archive/<repo>/<repo>-<tag>.tar.gz tarball of each tag
iframes/<repo>/filetree.html the file drawer, one document framed by every page
iframes/<repo>/downloads.html
the download list: latest commit and tags, framed by
the Download menu
types/<repo>/<row>/<type>.html
the files of one type, linked from the root
page (row: code, docs or binary)
search/<repo>/index.html search page, at /search/<repo>/
search/<repo>/corpus.json the text the search page searches; the page adds
?v=<hash of it>, so httpd can cache it for good
static/ style.css, chroma.css, ui.js, search.js, favicon;
pages (and style.css's icons) add ?v=<hash> to
these, so an upgrade never pairs a page with a
cached old copy, and httpd can cache them for good
git/<repo>.git/ read-only clone export, served at /git/
/var/www/private/<host>/ optional private site, git:www 0750 (private_htdocs)
private/… the same layout for private repositories, served
at /private/ behind a password; no sitemap, robots.txt,
error pages or static/ of its own
/var/www/conf/repo-render.htpasswd
the private site's logins, root:www 0640 (htpasswd)
/var/spool/repo-render/queue job queue: the hook adds to it, the renderer drains it (spool_dir)
/var/db/repo-render/ render state and lock (state_dir)
/var/log/daemon the renderer's log, through syslog (log)
/usr/local/share/repo-render/ the post-receive hook, bundled lexers and examples
(repo-render share-files, or make install)
A directory has a page of its own, just like a file does: /code/foo/src is
the file code/foo/src.html. That way, a file named index can live
anywhere in a repository without conflicting with anything.
Configuration
The config file is /etc/repo-render/repo-render.yaml. To use another file,
pass --config FILE or set REPO_RENDER_CONFIG. Only host is required.
This example shows the settings you are most likely to change:
host: code.example.com
site:
title: Bruce's code
tagline: Things I made
footer: >- # optional HTML at the bottom of every page
Built with <a href="/code/repo-renderer">Repo Renderer</a>.
clone_url: "https://code.example.com/git/{repo}.git"
unlisted: show # repositories in no section go in a trailing section (default: private)
unlisted_title: Other projects
sections:
- title: Command line programs
repos: [bp, bb, btui]
- title: "[Tomo](https://tomo.example.com)" # title and description are markdown
description: The [Tomo](https://tomo.example.com) programming language and related projects.
repos: [tomo, tomo-json]
lexers:
tomo: # uses tomo.xml from lexer_dirs, or a chroma builtin
globs: ["*.tm"]
aliases: [tomo, tm] # names you can use in ```fences
repos:
bp:
mirrors: [git@github.com:me/bp.git, git@codeberg.org:me/bp.git]
tomo:
branches: [dev] # also render the dev branch
tags: [language, tomo] # shown on the index and the repository's page
The config is YAML. Write prose and HTML as a folded block (>-, then
indented lines) rather than in quotes; the lines are joined with spaces and
nothing inside needs escaping. Quote short values that start with *, {
or [, like the globs and clone_url above.
A few more settings are worth knowing about:
styleandstyle_lightchoose the syntax highlighting colours for dark and light mode.site.faviconreplaces the built-in icon. Point it at an.svg,.png, or.icofile.git_dir,htdocs,spool_dir,state_dir,log,ssh_keyandlexer_dirsmove the files listed in the layout above. A relative path in the config is relative to the config file.site.unlisteddecides what happens to a repository that is in no section.private, the default, keeps it off the public site: it goes on the private site, or nowhere when there is none (see Private repositories), and whatever an earlier render left on the public site is removed on its next push orrender.showlists it in a trailing section titledsite.unlisted_title.hiderenders it but leaves it off the index, so anyone with the URL can still read it.repo-render checklists the private repositories.- Rendered markdown drops Pandoc title blocks (the leading
%lines). repo_max_bytes(default 1 GiB,0for no cap) caps the total size of a commit's files. A branch whose tip is over it is not rendered: the site keeps its last render and the log says why. A tag over it gets no tarball. The other size limits apply per file, so without this a commit of many files just under them could fill the disk the whole site is on. The output takes a few times the cap (raw copies, pages and a tarball), and the cap applies to each branch and each tag separately.repo_max_files(default 25000,0for no cap) does the same for the number of files, symlinks and submodules in a commit. Each file becomes several files of output, so many tiny ones could use up the disk's inodes while staying far underrepo_max_bytes.archive_retention(default1h) is how long a latest-commit tarball stays after a push replaces it. Its name never changes, so httpd can tell browsers to cache it for good. Keeping it a while means a page or menu loaded before the push still links to a file that exists. The renderer removes old ones when it next renders the repository. A tarball of a commit that a force push took off the branch goes at once. Tag tarballs follow their tags: each render remakes the tarball of a tag that has moved and removes that of a tag that is gone, even when the tag was changed in the bare repository, where no hook runs.
examples/repo-render.yaml documents every other
setting, including size limits and worker count. The renderer rejects
unknown keys. Run repo-render check after you edit the config.
The repository includes syntax highlighters for Tomo, BP, and Nomsu in
lexers/. make install copies them to
/usr/local/share/repo-render/lexers, which lexer_dirs searches by
default, so you only need to add their entries under lexers: to the
config. Put your own lexers in /etc/repo-render/lexers/. That directory is
searched first, so a file there also overrides a bundled lexer of the same
name.
Private repositories
A repository in no section is private (unless site.unlisted says
otherwise). Without private_htdocs, private repositories aren't rendered
at all. With it, they're rendered as a site of their own at /private/,
with its own index, and httpd asks for a password before it serves
anything there:
private_htdocs: /var/www/private/code.example.com
htpasswd: /var/www/conf/repo-render.htpasswd # the default
-
private_htdocsmust be inside httpd's chroot but outsidehtdocs, so only the password-protected locations of the generated httpd.conf ever serve it. An httpd.conf from before you set it doesn't serve it at all. The renderer writes into itsprivate/directory. It should begit:www, mode 0750, so no other local user can read it. -
The logins are in the
htpasswdfile, which you make with htpasswd(1). httpd reads it on every request, so a new password needs no reload:htpasswd /var/www/conf/repo-render.htpasswd bruce chown root:www /var/www/conf/repo-render.htpasswd chmod 640 /var/www/conf/repo-render.htpasswd -
After setting
private_htdocs, write the httpd.conf again (repo-render httpd-conf > /etc/repo-render/httpd.conf), reload httpd, and runrepo-render render --all.repo-render setup-checkchecks the directory and the htpasswd file, and that httpd.conf is up to date. -
Private pages link only within
/private/, carrynoindex, are left out of the sitemap, and are cached by the browser alone. Clone one with the login in the URL, or let git ask for it:git clone https://bruce@code.example.com/private/git/REPO.git. -
A repository that moves between the sites, because you added it to a section or took it out, is removed from the one it left on its next push or
render. -
Basic authentication sends the password with every request, so the site must only be reached over TLS, as relayd serves it.
Browsing other branches
By default, the site shows each repository's default branch. To show others, list them in the repository's config:
repos:
tomo:
branches: [dev, my-name/feature-with-kebabs]
Write exact branch names. Patterns like feature/* are not supported, so
only branches you name appear on the site. Branch names can contain letters,
digits, ., _, and -, with / between parts. The same goes for
branch, which sets the default branch in place of the bare repository's
HEAD.
The default branch keeps the same URLs it always had. Each listed branch
gets its own copy of the site under <repo>@<branch>, with every / in the
branch name written as ::
/code/tomo/src/main.c the default branch
/code/tomo@dev/src/main.c the dev branch
/code/tomo@my-name:feature-with-kebabs/src/main.c
Raw files, images, search, and tarballs follow the same pattern
(/raw/tomo@dev/…, /search/tomo@dev, and so on). Each branch renders
incrementally, like a separate repository. A branch picker next to the
Download button lets visitors switch between them, and it links to the root
of the other branch. Search engines are asked not to index the non-default
branches. The clone export includes the listed branches too, so
git clone -b dev works. Each branch's Download menu offers that branch's
latest commit. Tags belong to the repository, so every branch lists the
same tags.
If you list a branch that does not exist, the renderer skips it until it appears. If you remove a branch from the list, or delete it from the repository, the renderer deletes its pages on the next run.
Commands
repo-render [--config FILE] <command>
| Command | What it does |
|---|---|
kick |
Process the job queue while holding the render lock. The post-receive hook runs this. |
render [--full] [--no-mirror] REPO… |
Render the named repositories, and push the ones that changed to their mirrors. --full ignores saved state and re-renders everything; --no-mirror skips the mirrors. |
render --all [--full] [--no-mirror] |
Render every repository in git_dir, and delete the output of any that is no longer there. To take a repository off the site, remove it from git_dir and run this, or make it private. |
index |
Rebuild only the index page and the static files. |
enqueue REPO REF OLD NEW |
Add a job to the queue. The post-receive hook runs this. |
add-repo [--branch B] REPO [DESCRIPTION] |
Create a bare repository ready for pushes: branch and tag deletions refused, and the hook attached through core.hooksPath. Run it as git. The default branch is main. DESCRIPTION goes in the repository's description file. |
check |
Check the config, lexers, and templates. Exits with an error if anything is wrong. |
serve [--listen ADDR] |
Run a development server that answers as examples/httpd.conf does: the same rewrites, security headers and clone export. |
setup-check [--host HOST] |
Check the install on OpenBSD and print the commands that fix what is missing or wrong, changing nothing. Run it as root. Works before there is a config. |
httpd-conf |
Print the site's httpd server block, for /etc/repo-render/httpd.conf. |
share-files |
Write the hook, lexers and examples as a tar archive, for tar -xpf - -C /usr/local/share/repo-render. |
Trying it locally
The quickest way is make preview. It clones (or fetches) the repositories
listed in preview/repos.txt, renders them with preview/repo-render.yaml
into .preview/, and serves the site at http://127.0.0.1:8080/. Edit those
two files to change what gets rendered; make preview PREVIEW_LISTEN=:9000
picks another address, and make preview-clean starts over from scratch.
To try it on a project of your own by hand, make a scratch directory with a config whose paths are relative to it, clone a project into it, render it, and start the development server:
mkdir -p /tmp/repo-render/git
cat > /tmp/repo-render/repo-render.yaml <<'EOF'
host: localhost
git_dir: git
htdocs: htdocs
spool_dir: spool
state_dir: db
log: ""
EOF
export REPO_RENDER_CONFIG=/tmp/repo-render/repo-render.yaml
git clone --bare ~/some/project /tmp/repo-render/git/project.git
./repo-render render --all
./repo-render serve # http://127.0.0.1:8080/
To test the push path, install the hook in the scratch repository. The
hook passes REPO_RENDER_CONFIG on to repo-render:
cp hooks/post-receive /tmp/repo-render/git/project.git/hooks/
REPO_RENDER="$PWD/repo-render" git push /tmp/repo-render/git/project.git
Deploying on OpenBSD
Checking the install
make openbsd builds dist/repo-render-openbsd-amd64 (OPENBSD_ARCH=arm64
for an arm64 machine). It is the only file to copy to the server: it
carries the hook, lexers and examples. Then, on the server:
doas ./repo-render-openbsd-amd64 setup-check --host code.example.com
setup-check changes nothing. It checks each part of the install, in
the order the steps under By hand take, and prints ✓ for each
that is in place. Under each ✗ it prints the commands, to run as root,
that fix it on this system: the useradd for a missing git account,
the chown -R for a site directory someone else owns, the line
/etc/httpd.conf lacks. A · waits on a fix above it, and a ? is
something it cannot see without root. Run the commands you agree with,
then check again, until everything is ✓. --host names the site for the
config it tells you to write; once there is a config, it is not needed.
The last checks run httpd -n, run repo-render check as git, and ask
httpd for the site's front page by its name, on the config's listen
address, expecting the site's security headers. So when everything is
✓, the site is being served, and a push will render.
To upgrade, copy the new binary over and run its setup-check: it says
to install it, to rewrite the hook and lexers if they changed (with
repo-render share-files), and to rewrite the site's httpd
configuration if the rules changed (with repo-render httpd-conf).
httpd serves the site as plain HTTP on the config's listen address
(*:8080 unless you set it), and relayd terminates TLS and forwards to
it, as in:
relay "https" {
listen on $external_address port 443 tls
protocol "https"
forward to <httpd> port 8080 check tcp interval 30
}
relayd and certificates are yours to set up. The site's server block
logs relayd's forwarded client address and still answers acme-client's
challenges, for certificates fetched through it. If httpd.conf already
has a server for the host, take it out: put its other names in the
config's aliases.
setup-check confines itself with pledge(2) and unveil(2): it can read
the filesystem, run httpd, rcctl and repo-render by absolute path,
and connect to httpd, and nothing else. Those commands start unconfined,
as exec(2) clears both.
By hand
The git user owns the repositories, takes the pushes over SSH, and runs
the renderer: a push triggers a hook that adds a job to a queue and starts
repo-render kick in the background. The steps below assume the site is
code.example.com. Run them as root.
-
Install git and create the user:
pkg_add git echo /usr/local/bin/git-shell >> /etc/shells useradd -d /var/git -g =uid -s /usr/local/bin/git-shell git install -d -o git -g git -m 755 /var/git install -d -o git -g git -m 700 /var/git/.sshIts home is the repositories, so a push names a repository by its name alone. Put the SSH keys of the people who may push in
/var/git/.ssh/authorized_keys(ownergit, mode 600).git-shelllets them push but not log in. -
Install the program. On a machine with Go, run
make openbsdand copydist/repo-render-openbsd-amd64to the server. Then:install -o root -g bin -m 755 repo-render-openbsd-amd64 /usr/local/bin/repo-render install -d -m 755 /usr/local/share/repo-render repo-render share-files | tar -xpf - -C /usr/local/share/repo-rendershare-fileswrites the hook, lexers and examples it carries. If Go is installed on the server,make installdoes the same. -
Create the config and the directories:
install -d /etc/repo-render echo 'host: code.example.com' > /etc/repo-render/repo-render.yaml install -d -o git -g git -m 755 /var/www/htdocs/code.example.com \ /var/db/repo-render /var/spool/repo-render su -s /bin/sh git -c "repo-render check"checkprints every path it uses, runs git under the same sandbox as the renderer, and reports anything missing. To use the bundled syntax highlighters for Tomo, BP and Nomsu, add theirlexers:entries from examples/repo-render.yaml.The renderer logs through syslog, to
/var/log/daemon, which newsyslog already rotates. The config belongs to root, so a person who can push cannot change it. They cannot change the renderer's state or the site either, but only becausegit-shelllets them run nothing but git: the renderer runs asgittoo. -
Add each repository:
su -s /bin/sh git -c 'repo-render add-repo NAME "One-line description"'Then push to
git@code.example.com:NAME.git. The site shows themainbranch. Useadd-repo --branch masterif you pushmaster.The description is the repository's
descriptionfile, the one gitweb and cgit read, and the only place the site takes it from. To change it, rewrite the file and render the repository, which redoes every page, since each one carries the description:su -s /bin/sh git -c "echo 'New description' > /var/git/NAME.git/description && repo-render render NAME"Older versions also took a
descriptionunderrepos:in the config, which overrode the file. The renderer now rejects it and names the file to move it to.To wire up a repository you created some other way, give it the hook:
su -s /bin/sh git -c "git -C /var/git/NAME.git config core.hooksPath /usr/local/share/repo-render/hooks" -
Configure httpd.
repo-render httpd-confprints a complete server block, headers and all, for the config's host,htdocsandlisten, serving plain HTTP for relayd to forward to (examples/httpd.conf is one forcode.example.com). Put it in its own file and include that:repo-render httpd-conf > /etc/repo-render/httpd.conf echo 'include "/etc/repo-render/httpd.conf"' >> /etc/httpd.conf httpd -n && rcctl enable httpd && rcctl start httpdAfter an upgrade, write it again: the rules can change. Its
errdocsline makes httpd answer a missing address with the site's own 404 page. The site needs OpenBSD 8.0 or later, whosehttpdcan set response headers. Keep the headers: the pages carry no Content Security Policy of their own, and raw files keep their real names (.html,.svgand all), so thesandboxpolicy on/rawand/svgis what stops them from running as part of the site. httpd uses only the firstlocationthat matches a request, so a rule you add goes inside the existing block for its path, not in a new block after it. The rules themselves live ininternal/httpd/rules.go, which the development server serves by too: each is aGlobLocation(location "…", where only*is allowed) or aPatternLocation(location match "…", a Lua pattern), written exactly as httpd.conf has it. -
Optional: mirror to other hosts. List git URLs under
mirrorsin a repository's config, and every push is pushed on to each of them, in turn: GitHub, Codeberg, sourcehut, another server of yours, anythinggit pushcan reach. One that fails is logged and retried on the next push, and does not hold up the others. The renderer uses/etc/repo-render/mirror_keyas its ssh key (thessh_keysetting) and ssh's system-wide host keys, so nothing needs to go in a home directory:ssh-keygen -t ed25519 -N '' -f /etc/repo-render/mirror_key # add the .pub to each host as a key with write access chown git /etc/repo-render/mirror_key ssh-keyscan github.com codeberg.org >> /etc/ssh/ssh_known_hosts # each mirror's hostMirroring only ever sends. Your server is the repository; each mirror holds a copy, and the renderer never fetches from one, so a change made on a mirror (a merged pull request, a web edit, a compromised account) never reaches your site, and the next push overwrites the branch there. Take pull requests by fetching them yourself and pushing to your server, and turn off merging on the mirrors (or protect the branch) so no one merges there by mistake.
Older versions mirrored to one URL, set as
github, with the key at/etc/repo-render/github_key. The renderer now rejectsgithub: write it asmirrors: [URL], and rename the key or setssh_keyto it.
The renderer only ever reads the repositories and writes to the website,
/var/db/repo-render and /var/spool/repo-render. The hook is a
fixed script that people who push cannot change. Before the renderer writes
anything, it checks every path and tag name from the repositories.
Tests
go test ./... creates temporary bare repositories and pushes to them for
real, then checks the rendered output. It covers:
- the first render, and incremental renders for modified, added, and deleted files
- force pushes, config changes, and tags (including tag names with slashes), and that a tag push rewrites only the download list
- latest-commit tarballs: named by commit, linked only from the download
list, and removed once replaced for longer than
archive_retention, or at once when a force push drops their commit - recovery from a hostile commit: after a revert or a force push, nothing it put on the site remains, even when rendering it failed partway or died, and tag tarballs follow tags moved or deleted on the server
- README selection and listing truncation
- markdown link rewriting and HTML escaping
- raw copies and themed SVGs
- symlinks and submodules
- the file drawer, and which pushes rewrite it
- index sections and branch views
- the clone export, which the tests clone back with git
- the development server's rewrites and its 404 page, and that it answers
every URL of a rendered site as
examples/httpd.confwould, by a model of httpd serving it (first match wins, a rewrite is looked up again) that matches locations withinternal/httpd, and thatexamples/httpd.confis what the generator writes (go test ./internal/httpd -run TestHTTPDConfExample -updateregenerates it) - the guarantee that a file page and a markdown fence highlight the same code identically
- falling back to plain text for highlighters that loop, crash, or run too long
node --test test/*_test.js tests the search matching logic and the "2 days ago" wording.



