# 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.
