Skip to content

overshard/heartwood

Repository files navigation

Heartwood

A minimal web frontend for the bare git repos on a single-operator server. Built to replace GitHub as the place my code is publicly visible.

Single-binary axum service. No database, no auth, no pull requests, no issues. Repo metadata is read live from the bare repos via gix (gitoxide). The commit-diff view shells out to git show --patch, and git clone over HTTPS works through git http-backend as a CGI subprocess. Everything else is in-process.

Features

  • Repo list auto-discovered from a directory of *.git bare repos
  • Per-repo landing page: README, recent commits, clone URL, default branch
  • Tree / blob browser with syntect syntax highlighting
  • Commit log and per-commit unified-diff view (rename detection on)
  • Atom feed of recent commits per repo
  • Read-only git clone over HTTPS (/info/refs + /git-upload-pack)
  • Pushes are intentionally 405; the only way code arrives is git push server master

Stack

Concern Crate / Tool
Web framework axum + tokio
Git read gix (gitoxide) for browse, log, tree, blob, atom
Git serve git http-backend subprocess for clone
Diff git show --patch subprocess + in-house unified parser
Templates minijinja
Markdown pulldown-cmark (tables, footnotes, strikethrough, tasklists)
Highlighting syntect (default-fancy, base16-eighties.dark theme)
Static assets Vite + Bun, SCSS, JetBrains Mono (self-hosted via @fontsource)

Requirements

Production runs through Docker (see Dockerfile). The only runtime dependencies are git and ca-certificates on top of alpine:3.23.

For local development:

  • rust (cargo) for the backend
  • bun for the frontend bundler (Vite)
  • git on PATH (heartwood shells out to it for http-backend and show)

Running locally

cp samplefiles/env.sample .env  # optional; defaults are fine for dev
make seed                        # one-time: synthesize 8 fake bare repos in fixtures/git/
make run                         # vite watch + cargo run on port 8000

make run does not seed automatically; a fresh checkout shows an empty repo list until you run make seed once. The seed step is opt-in so you can also point HEARTWOOD_REPO_ROOT at a real directory and skip it entirely.

make seed runs the seed bin (see src/bin/seed.rs), which synthesizes fake-but-realistic bare git repos under fixtures/git/. It picks a mix of archetypes (Rust crate, TS lib, Python package, markdown blog, dotfiles) with realistic file shapes, commit messages drawn from per-archetype corpora, and ~30 days of history across five rotating authors. Deterministic: the same --seed value always produces the same set of repos.

Override the defaults inline:

make seed COUNT=12 DAYS=45         # more repos, longer history
make seed SEED=42                  # different deterministic mix

make seed is idempotent: existing repo directories are left alone, so re-running is cheap. make seed-reset wipes fixtures/git/ first.

Configuration

All config comes from environment variables (loaded from .env via dotenvy):

Variable Required Purpose
PORT no (default 8000) HTTP listen port
HEARTWOOD_ROOT no (default .) Project root (where templates/ and dist/ live)
HEARTWOOD_REPO_ROOT no (default /srv/git) Directory of <name>.git/ bare repos
HEARTWOOD_CLONE_BASE no Public origin used in clone URLs and atom self-links
HEARTWOOD_TITLE no (default heartwood) Topbar title
HEARTWOOD_TAGLINE no (default every commit a ring) Topbar tagline
BASE_URL no <base href> if served on a subpath

In dev, make run sets HEARTWOOD_REPO_ROOT to ./fixtures/git so you don't need a real /srv/git/.

Make targets

Target What it does
make run (default) Vite watch + cargo run on port 8000
make build Vite assets + release binary (target/release/heartwood)
make start Run the release binary (after make build)
make seed Synthesize fake bare repos under fixtures/git/ (idempotent, opt-in)
make seed-reset Wipe and re-synthesize fixtures/git/
make push git push to every configured remote
make clean Remove target/, dist/, and frontend/node_modules/ (leaves fixtures)

seed / seed-reset accept COUNT=, DAYS=, and SEED= overrides (see Running locally).

There are no tests or linters configured.

Key Routes

  • /: repo list (auto-discovered from HEARTWOOD_REPO_ROOT, sorted by most-recent HEAD)
  • /<name>: repo landing (README, recent commits, clone URL)
  • /<name>/log: commit log (default branch unless ?rev= given, ?limit= up to 500)
  • /<name>/commit/<sha>: single commit + unified diff
  • /<name>/tree/<rev>[/<path>]: file browser
  • /<name>/blob/<rev>/<path>: file view with syntax highlighting
  • /<name>/raw/<rev>/<path>: raw blob bytes
  • /<name>/atom.xml: atom feed of recent commits
  • /<name>.git/info/refs and /<name>.git/git-upload-pack: smart HTTP clone
  • /static/*: Vite assets (1y cache header)

Production deploy

Same git push server master flow used by the rest of my projects:

Server:

apk update && apk upgrade && apk add docker docker-compose caddy git iptables ip6tables ufw
ufw allow 22/tcp && ufw allow 80/tcp && ufw allow 443/tcp && ufw --force enable
rc-update add docker boot && service docker start
mkdir -p /srv/git/heartwood.git && cd /srv/git/heartwood.git && git init --bare

Local:

git remote add server root@heartwood.example.com:/srv/git/heartwood.git
git push --set-upstream server master

Server:

mkdir -p /srv/docker && cd /srv/docker && git clone /srv/git/heartwood.git heartwood && cd /srv/docker/heartwood
cp samplefiles/Caddyfile.sample /etc/caddy/Caddyfile
cp samplefiles/env.sample .env  # edit HEARTWOOD_CLONE_BASE and HEARTWOOD_TITLE/TAGLINE
cp samplefiles/post-receive.sample /srv/git/heartwood.git/hooks/post-receive && chmod +x /srv/git/heartwood.git/hooks/post-receive
docker-compose up --build --detach
rc-update add caddy boot && service caddy start

The host's /srv/git/ is bind-mounted into the container read-only so the web process can't damage a bare repo, even accidentally. Pushes still arrive the usual way: SSH to the bare repo, post-receive hook runs the deploy.

Backups

All your code already lives in /srv/git/*.git/; back that up and you have a complete backup. There is no application database to preserve.

Support

I won't be providing user support for this project. I'm happy to accept good pull requests and fix bugs but I don't have time to help people run or use this project.

About

A minimal self-hosted git browser. Renders bare repos as a website with commits, diffs, syntax-highlighted blobs, and atom feeds, plus clone over HTTPS.

Topics

Resources

License

Stars

Watchers

Forks

Contributors