A minimal self-hosted git browser on Rust axum: bare repos rendered as a website with commits, diffs, syntax-highlighted blobs, atom feeds, and clone over HTTPS.
axumdockergitgit-browsergitoxidegixrustself-hosted
1> **Archived.** This project is no longer in use and no longer maintained. Last updated July 2026.
2
3# Repos
4
5A minimal, self-hosted web frontend for browsing the bare git repos on a
6single-operator server. I built it to replace GitHub as the place my code is
7publicly visible: a small, public, read-only window into a directory of bare
8repos, with nothing else attached.
9
10It is a single-binary axum service. No database, no auth, no pull requests, no
11issues. Repo metadata is read live from the bare repos via `gix` (gitoxide).
12The commit-diff view shells out to `git show --patch`, and `git clone` over
13HTTPS works through `git http-backend` as a CGI subprocess. Everything else is
14in-process.
15
16
17## Features
18
19- Repo list auto-discovered from a directory of `*.git` bare repos
20- Per-repo landing page: README, recent commits, clone URL, default branch
21- Tree / blob browser with syntect syntax highlighting
22- Commit log and per-commit unified-diff view (rename detection on)
23- Atom feed of recent commits per repo
24- Read-only `git clone` over HTTPS (`/info/refs` + `/git-upload-pack`)
25- Pushes are intentionally 405; the only way code arrives is `git push server master`
26
27
28## Stack
29
30| Concern | Crate / Tool |
31|----------------|-------------------------------------------------------|
32| Web framework | axum + tokio |
33| Git read | gix (gitoxide) for browse, log, tree, blob, atom |
34| Git serve | `git http-backend` subprocess for clone |
35| Diff | `git show --patch` subprocess + in-house unified parser |
36| Templates | minijinja |
37| Markdown | pulldown-cmark (tables, footnotes, strikethrough, tasklists) |
38| Highlighting | syntect (`default-fancy`, base16-eighties.dark theme) |
39| Static assets | Vite + Bun, SCSS, JetBrains Mono (self-hosted via `@fontsource`) |
40
41
42## Requirements
43
44Production runs through Docker (see `Dockerfile`). The only runtime
45dependencies are `git` and `ca-certificates` on top of `alpine:3.23`.
46
47For local development:
48
49- rust (cargo) for the backend
50- bun for the frontend bundler (Vite)
51- `git` on `PATH` (repos shells out to it for `http-backend` and `show`)
52
53
54## Running locally
55
56 cp samplefiles/env.sample .env # optional; defaults are fine for dev
57 make seed # one-time: synthesize 8 fake bare repos in fixtures/git/
58 make run # vite watch + cargo run on port 8000
59
60`make run` does not seed automatically; a fresh checkout shows an empty repo
61list until you run `make seed` once. The seed step is opt-in so you can also
62point `REPOS_REPO_ROOT` at a real directory and skip it entirely.
63
64`make seed` runs the `seed` bin (see `src/bin/seed.rs`), which synthesizes
65fake-but-realistic bare git repos under `fixtures/git/`. It picks a mix of
66archetypes (Rust crate, TS lib, Python package, markdown blog, dotfiles) with
67realistic file shapes, commit messages drawn from per-archetype corpora, and
68~30 days of history across five rotating authors. Deterministic: the same
69`--seed` value always produces the same set of repos.
70
71Override the defaults inline:
72
73 make seed COUNT=12 DAYS=45 # more repos, longer history
74 make seed SEED=42 # different deterministic mix
75
76`make seed` is idempotent: existing repo directories are left alone, so
77re-running is cheap. `make seed-reset` wipes `fixtures/git/` first.
78
79
80## Configuration
81
82All config comes from environment variables (loaded from `.env` via `dotenvy`):
83
84| Variable | Required | Purpose |
85|---|---|---|
86| `PORT` | no (default `8000`) | HTTP listen port |
87| `REPOS_ROOT` | no (default `.`) | Project root (where `templates/` and `dist/` live) |
88| `REPOS_REPO_ROOT` | no (default `/srv/git`) | Directory of `<name>.git/` bare repos |
89| `REPOS_CLONE_BASE` | no (default `https://repos.bythewood.me`) | Public origin used in clone URLs and atom self-links |
90| `REPOS_TITLE` | no (default `repos`) | Topbar title |
91| `REPOS_TAGLINE` | no (default empty) | Optional topbar tagline; hidden when empty |
92| `BASE_URL` | no | `<base href>` if served on a subpath |
93
94In dev, `make run` sets `REPOS_REPO_ROOT` to `./fixtures/git` so you don't
95need a real `/srv/git/`.
96
97
98## Make targets
99
100| Target | What it does |
101|---|---|
102| `make run` (default) | Vite watch + `cargo run` on port 8000 |
103| `make build` | Vite assets + release binary (`target/release/repos`) |
104| `make start` | Run the release binary (after `make build`) |
105| `make seed` | Synthesize fake bare repos under `fixtures/git/` (idempotent, opt-in) |
106| `make seed-reset` | Wipe and re-synthesize `fixtures/git/` |
107| `make push` | `git push` to every configured remote |
108| `make clean` | Remove `target/`, `dist/`, and `frontend/node_modules/` (leaves fixtures) |
109
110`seed` / `seed-reset` accept `COUNT=`, `DAYS=`, and `SEED=` overrides (see
111Running locally).
112
113There are no tests or linters configured.
114
115
116## Key Routes
117
118- `/`: repo list (auto-discovered from `REPOS_REPO_ROOT`, sorted by most-recent HEAD)
119- `/<name>`: repo landing (README, recent commits, clone URL)
120- `/<name>/log`: commit log (default branch unless `?rev=` given, `?limit=` up to 500)
121- `/<name>/commit/<sha>`: single commit + unified diff
122- `/<name>/tree/<rev>[/<path>]`: file browser
123- `/<name>/blob/<rev>/<path>`: file view with syntax highlighting
124- `/<name>/raw/<rev>/<path>`: raw blob bytes
125- `/<name>/atom.xml`: atom feed of recent commits
126- `/<name>.git/info/refs` and `/<name>.git/git-upload-pack`: smart HTTP clone
127- `/static/*`: Vite assets (1y cache header)
128
129
130## Production deploy
131
132Same `git push server master` flow used by the rest of my projects:
133
134Server:
135
136 apk update && apk upgrade && apk add docker docker-compose caddy git iptables ip6tables ufw
137 ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp && ufw --force enable
138 rc-update add docker boot && service docker start
139 mkdir -p /srv/git/repos.git && cd /srv/git/repos.git && git init --bare
140
141Local:
142
143 git remote add server [email protected]:/srv/git/repos.git
144 git push --set-upstream server master
145
146Server:
147
148 mkdir -p /srv/docker && cd /srv/docker && git clone /srv/git/repos.git repos && cd /srv/docker/repos
149 cp samplefiles/Caddyfile.sample /etc/caddy/Caddyfile
150 cp samplefiles/env.sample .env # edit REPOS_CLONE_BASE and REPOS_TITLE if you want
151 cp samplefiles/post-receive.sample /srv/git/repos.git/hooks/post-receive && chmod +x /srv/git/repos.git/hooks/post-receive
152 docker-compose up --build --detach
153 rc-update add caddy boot && service caddy start
154
155The host's `/srv/git/` is bind-mounted into the container read-only so the
156web process can't damage a bare repo, even accidentally. Pushes still arrive
157the usual way: SSH to the bare repo, post-receive hook runs the deploy.
158
159
160## Backups
161
162All your code already lives in `/srv/git/*.git/`; back that up and you have a
163complete backup. There is no application database to preserve.
164
165
166## Support
167
168I won't be providing user support for this project. I'm happy to accept good
169pull requests and fix bugs but I don't have time to help people run or use
170this project.