repos

rename heartwood to repos

e526b602 by Isaac Bythewood · 2 hours ago

modified CLAUDE.md
@@ -61,7 +61,7 @@ QUERY_STRING, REQUEST_METHOD, CONTENT_TYPE, GIT_PROJECT_ROOT,GIT_HTTP_EXPORT_ALL, plus a few HTTP_* passthrough headers). Request bodyis piped into stdin; CGI headers are parsed off stdout, the rest isstreamed back as the response body. `/git-receive-pack` is wired butreturns 405 (heartwood is read-only).returns 405 (repos is read-only).**Templates (`templates/`):** Jinja2-compatible. `base.html` is the shell;`index.html` is the repo list; `repo.html` is README + recent commits +
@@ -96,21 +96,21 @@ URL gets the themed shell instead of a plain-text 404.All via env vars (defaults shown):- `PORT=8000` — HTTP listen port.- `HEARTWOOD_ROOT=.` — project root (where `templates/` and `dist/` live).- `HEARTWOOD_REPO_ROOT=/srv/git` — directory containing `<name>.git/` bare repos.- `HEARTWOOD_CLONE_BASE=https://heartwood.bythewood.me` — public origin used  in clone URLs and atom self-links.- `HEARTWOOD_TITLE=heartwood` and `HEARTWOOD_TAGLINE=every commit a ring`  for the topbar.- `REPOS_ROOT=.` — project root (where `templates/` and `dist/` live).- `REPOS_REPO_ROOT=/srv/git` — directory containing `<name>.git/` bare repos.- `REPOS_CLONE_BASE=https://repos.bythewood.me` — public origin used in clone  URLs and atom self-links.- `REPOS_TITLE=repos` — topbar title.- `REPOS_TAGLINE=` — optional tagline; hidden in the topbar and footer when empty.- `BASE_URL=` — optional `<base href>` if served on a subpath.In dev, `make run` sets `HEARTWOOD_REPO_ROOT=./fixtures/git` so you don'tIn dev, `make run` sets `REPOS_REPO_ROOT=./fixtures/git` so you don'tneed a real `/srv/git/`.## Layout```heartwood/repos/├── Cargo.toml, Cargo.lock├── Makefile, README.md, LICENSE.md, CLAUDE.md├── src/
@@ -146,7 +146,7 @@ heartwood/## Key Routes- `/` — repo list (auto-discovered from `HEARTWOOD_REPO_ROOT`)- `/` — repo list (auto-discovered from `REPOS_REPO_ROOT`)- `/: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
modified Cargo.lock
@@ -1239,33 +1239,6 @@ version = "0.17.1"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"[[package]]name = "heartwood"version = "0.1.0"dependencies = [ "ammonia", "anyhow", "async-stream", "axum", "bytes", "chrono", "dotenvy", "futures-util", "gix", "mime_guess", "minijinja", "pulldown-cmark", "serde", "serde_json", "syntect", "tokio", "tower", "tower-http", "tracing", "tracing-subscriber", "urlencoding",][[package]]name = "heck"version = "0.5.0"
@@ -2054,6 +2027,33 @@ version = "0.8.10"source = "registry+https://github.com/rust-lang/crates.io-index"checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"[[package]]name = "repos"version = "0.1.0"dependencies = [ "ammonia", "anyhow", "async-stream", "axum", "bytes", "chrono", "dotenvy", "futures-util", "gix", "mime_guess", "minijinja", "pulldown-cmark", "serde", "serde_json", "syntect", "tokio", "tower", "tower-http", "tracing", "tracing-subscriber", "urlencoding",][[package]]name = "rustix"version = "0.38.44"
modified Cargo.toml
@@ -1,10 +1,10 @@[package]name = "heartwood"name = "repos"version = "0.1.0"edition = "2021"# With a second binary in src/bin/ (seed), `cargo run` becomes ambiguous.# Pin the default to the server so `cargo run` from the Makefile still works.default-run = "heartwood"default-run = "repos"[dependencies]axum = { version = "0.8", features = ["macros"] }
@@ -30,7 +30,7 @@ async-stream = "0.3"mime_guess = "2"[profile.release]# Match darkfurrow: small + fast. Heartwood has no chunky dep tree# Match darkfurrow: small + fast. Repos has no chunky dep tree# (no typst, no chromium) so LTO is tractable.lto = truecodegen-units = 1
modified Dockerfile
@@ -16,7 +16,7 @@ RUN cd frontend && bun install --frozen-lockfile && bun run buildRUN --mount=type=cache,target=/usr/local/cargo/registry \    --mount=type=cache,target=/app/target \    cargo build --release && \    cp target/release/heartwood /app/heartwood    cp target/release/repos /app/repos# ----- runtime -----FROM alpine:3.23
@@ -36,7 +36,7 @@ RUN git config --system --add safe.directory '*'WORKDIR /appCOPY --from=builder /app/heartwood ./heartwoodCOPY --from=builder /app/repos ./reposCOPY --from=builder /app/dist ./distCOPY templates ./templates
@@ -46,7 +46,7 @@ RUN addgroup -S -g 1000 app && \USER appENV PORT=8000ENV HEARTWOOD_REPO_ROOT=/srv/gitENV REPOS_REPO_ROOT=/srv/gitEXPOSE 8000CMD ["./heartwood"]CMD ["./repos"]
modified Makefile
@@ -4,7 +4,7 @@ PORT  ?= 8000# In dev the bare repos at /srv/git live on the production server, not here.# Use ./fixtures/git as the local repo root; `make seed` populates it so# `make run` has something to show on the landing page.HEARTWOOD_REPO_ROOT ?= $(CURDIR)/fixtures/gitREPOS_REPO_ROOT ?= $(CURDIR)/fixtures/git.DEFAULT_GOAL := run.PHONY: run build start clean push seed seed-reset
@@ -15,7 +15,7 @@ HEARTWOOD_REPO_ROOT ?= $(CURDIR)/fixtures/gitrun: frontend/node_modules dist/.vite/manifest.json	@trap 'kill 0' EXIT INT TERM; \	(cd frontend && bun run dev) & \	PORT=$(PORT) HEARTWOOD_REPO_ROOT=$(HEARTWOOD_REPO_ROOT) $(CARGO) run	PORT=$(PORT) REPOS_REPO_ROOT=$(REPOS_REPO_ROOT) $(CARGO) run# Production build (Vite assets + release binary)build: frontend/node_modules
@@ -24,7 +24,7 @@ build: frontend/node_modules# Run the release binary (after `make build`)start:	PORT=$(PORT) ./target/release/heartwood	PORT=$(PORT) ./target/release/repos# Wipe regenerable output. Leaves fixtures/ alone so `make run` after `make# clean` doesn't blow away your seeded repos.
modified README.md
@@ -1,4 +1,4 @@# Heartwood# ReposA 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.
@@ -44,7 +44,7 @@ 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`)- `git` on `PATH` (repos shells out to it for `http-backend` and `show`)## Running locally
@@ -55,7 +55,7 @@ For local development:`make run` does not seed automatically; a fresh checkout shows an empty repolist until you run `make seed` once. The seed step is opt-in so you can alsopoint `HEARTWOOD_REPO_ROOT` at a real directory and skip it entirely.point `REPOS_REPO_ROOT` at a real directory and skip it entirely.`make seed` runs the `seed` bin (see `src/bin/seed.rs`), which synthesizesfake-but-realistic bare git repos under `fixtures/git/`. It picks a mix of
@@ -80,14 +80,14 @@ 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 || `REPOS_ROOT` | no (default `.`) | Project root (where `templates/` and `dist/` live) || `REPOS_REPO_ROOT` | no (default `/srv/git`) | Directory of `<name>.git/` bare repos || `REPOS_CLONE_BASE` | no (default `https://repos.bythewood.me`) | Public origin used in clone URLs and atom self-links || `REPOS_TITLE` | no (default `repos`) | Topbar title || `REPOS_TAGLINE` | no (default empty) | Optional topbar tagline; hidden when empty || `BASE_URL` | no | `<base href>` if served on a subpath |In dev, `make run` sets `HEARTWOOD_REPO_ROOT` to `./fixtures/git` so you don'tIn dev, `make run` sets `REPOS_REPO_ROOT` to `./fixtures/git` so you don'tneed a real `/srv/git/`.
@@ -96,7 +96,7 @@ need a real `/srv/git/`.| Target | What it does ||---|---|| `make run` (default) | Vite watch + `cargo run` on port 8000 || `make build` | Vite assets + release binary (`target/release/heartwood`) || `make build` | Vite assets + release binary (`target/release/repos`) || `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/` |
@@ -111,7 +111,7 @@ There are no tests or linters configured.## Key Routes- `/`: repo list (auto-discovered from `HEARTWOOD_REPO_ROOT`, sorted by most-recent HEAD)- `/`: repo list (auto-discovered from `REPOS_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
@@ -132,19 +132,19 @@ 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    mkdir -p /srv/git/repos.git && cd /srv/git/repos.git && git init --bareLocal:    git remote add server root@heartwood.example.com:/srv/git/heartwood.git    git remote add server root@repos.example.com:/srv/git/repos.git    git push --set-upstream server masterServer:    mkdir -p /srv/docker && cd /srv/docker && git clone /srv/git/heartwood.git heartwood && cd /srv/docker/heartwood    mkdir -p /srv/docker && cd /srv/docker && git clone /srv/git/repos.git repos && cd /srv/docker/repos    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    cp samplefiles/env.sample .env  # edit REPOS_CLONE_BASE and REPOS_TITLE if you want    cp samplefiles/post-receive.sample /srv/git/repos.git/hooks/post-receive && chmod +x /srv/git/repos.git/hooks/post-receive    docker-compose up --build --detach    rc-update add caddy boot && service caddy start
modified docker-compose.yml
@@ -1,11 +1,11 @@services:  web:    container_name: heartwood    container_name: repos    build: .    init: true    env_file: .env    environment:      HEARTWOOD_REPO_ROOT: /srv/git      REPOS_REPO_ROOT: /srv/git    volumes:      # /srv/git lives on the host (the bare repos the deploy hooks push to).      # Mount read-only so the web process can't corrupt a bare repo, even
modified frontend/static_src/styles/base.scss
@@ -1,4 +1,4 @@// heartwood — palette evokes the dense red-brown center of a tree.// repos — palette evokes the dense red-brown center of a tree (heartwood).:root {  --bg:        #1a1410;
@@ -7,7 +7,7 @@  --border:    #3d2f24;  --text:      #e8dccc;  --text-mute: #9c8a72;  --accent:    #b8543a; // heartwood red  --accent:    #b8543a; // heartwood-red core  --accent-2:  #c4a574; // sap gold  --green:     #6b9e78;  --code-bg:   #15100c;
@@ -68,7 +68,7 @@ code, pre, .sha, .clone-box input {  padding: 1.15rem 0;  // The outer accent stripe is a horizontal cross-section of the brand  // mark: heartwood-red core, sap-gold middle ring, green outer ring.  // mark: red core, sap-gold middle ring, green outer ring.  // Reads as a tree-ring slice across the top of every page.  &::before {    content: "";
modified frontend/vite.config.js
@@ -2,7 +2,7 @@ import { resolve } from "path";import { defineConfig } from "vite";// Vite output goes to ../dist; Rust serves it at /static/.// Single entry point — heartwood has no per-page scripts to split.// Single entry point, no per-page scripts to split.export default defineConfig({  base: "/static/",  publicDir: resolve(__dirname, "static_src/public"),
modified samplefiles/Caddyfile.sample
@@ -1,5 +1,5 @@heartwood.bythewood.me {  reverse_proxy heartwood:8000repos.bythewood.me {  reverse_proxy repos:8000  import common}
modified samplefiles/env.sample
@@ -1,4 +1,4 @@# Production .env for heartwood (drop in next to docker-compose.yml).# Production .env for repos (drop in next to docker-compose.yml).# All values shown are defaults; the only one you'll normally want to# override is BASE_URL when running behind a reverse proxy on a custom path.
@@ -8,11 +8,11 @@ PORT=8000# Where the bare repos live on the host. Mounted into the container in# docker-compose.yml as a read-only bind.HEARTWOOD_REPO_ROOT=/srv/gitREPOS_REPO_ROOT=/srv/git# Public origin used for clone URLs and atom feed self-links.HEARTWOOD_CLONE_BASE=https://heartwood.bythewood.meREPOS_CLONE_BASE=https://repos.bythewood.me# Optional cosmetics.HEARTWOOD_TITLE=heartwoodHEARTWOOD_TAGLINE=every commit a ringREPOS_TITLE=reposREPOS_TAGLINE=
modified samplefiles/post-receive.sample
@@ -1,7 +1,7 @@#!/bin/sh## post-receive hook generated for heartwood by taproot's quickstart.sh.# Lives on the alpine server at /srv/git/heartwood.git/hooks/post-receive.# post-receive hook generated for repos by taproot's quickstart.sh.# Lives on the alpine server at /srv/git/repos.git/hooks/post-receive.## Identical to every other project's hook: pull, rebuild, reattach to the# shared bythewood-edge network so Caddy can find us.
@@ -10,11 +10,11 @@ while read oldrev newrev ref; do  if [ "$ref" = "refs/heads/master" ]; then    unset GIT_DIR    START_TIME=$(date +%s)    cd /srv/docker/heartwood    cd /srv/docker/repos    git pull    docker compose up --build --detach    # Reattach every container in the project to the shared edge network.    # Resolved via `compose ps` (not `heartwood`) so this works regardless    # Resolved via `compose ps` (not `repos`) so this works regardless    # of the compose service name or container_name override.    for cid in $(docker compose ps -q); do      docker network connect bythewood-edge "$cid" 2>/dev/null || true
modified src/app.rs
@@ -27,19 +27,17 @@ pub struct Config {impl AppState {    pub fn from_env() -> Self {        let root: PathBuf = std::env::var("HEARTWOOD_ROOT")        let root: PathBuf = std::env::var("REPOS_ROOT")            .map(PathBuf::from)            .unwrap_or_else(|_| PathBuf::from("."));        let repo_root: PathBuf = std::env::var("HEARTWOOD_REPO_ROOT")        let repo_root: PathBuf = std::env::var("REPOS_REPO_ROOT")            .map(PathBuf::from)            .unwrap_or_else(|_| PathBuf::from("/srv/git"));        let base_url = std::env::var("BASE_URL").unwrap_or_default();        let clone_base = std::env::var("HEARTWOOD_CLONE_BASE")            .unwrap_or_else(|_| "https://heartwood.bythewood.me".to_string());        let site_title =            std::env::var("HEARTWOOD_TITLE").unwrap_or_else(|_| "heartwood".to_string());        let site_tagline = std::env::var("HEARTWOOD_TAGLINE")            .unwrap_or_else(|_| "every commit a ring".to_string());        let clone_base = std::env::var("REPOS_CLONE_BASE")            .unwrap_or_else(|_| "https://repos.bythewood.me".to_string());        let site_title = std::env::var("REPOS_TITLE").unwrap_or_else(|_| "repos".to_string());        let site_tagline = std::env::var("REPOS_TAGLINE").unwrap_or_default();        let templates_dir = root.join("templates");        let manifest_path = root.join("dist/.vite/manifest.json");
modified src/bin/seed.rs
@@ -1,5 +1,5 @@//! Generate fake-but-realistic bare git repos under `fixtures/git/` so the//! heartwood landing page has something to render in dev. Each repo gets a//! repos landing page has something to render in dev. Each repo gets a//! month of commit history with multiple authors, a per-archetype file shape//! (Rust crate, TS lib, Python package, markdown blog, dotfiles), and commit//! messages drawn from a per-archetype corpus.
@@ -1280,7 +1280,7 @@ fn seed_one(    // fixtures dir. Doing the bare clone at the end lets us use the regular    // working-tree commit flow (which is much simpler than driving    // commit-tree directly).    let work = std::env::temp_dir().join(format!("heartwood-seed-{name}"));    let work = std::env::temp_dir().join(format!("repos-seed-{name}"));    if work.exists() {        fs::remove_dir_all(&work).map_err(|e| e.to_string())?;    }
@@ -1349,7 +1349,7 @@ fn seed_one(        return Err(format!("clone exited {:?}", status.code()));    }    // Per-repo description (heartwood reads `description` for the landing    // Per-repo description (repos reads `description` for the landing    // page). git's stock placeholder is filtered out in src/git.rs.    fs::write(target.join("description"), format!("{}\n", arch.description))        .map_err(|e| e.to_string())?;
modified src/highlight.rs
@@ -13,7 +13,7 @@ fn theme() -> &'static Theme {    static CELL: OnceLock<Theme> = OnceLock::new();    CELL.get_or_init(|| {        let mut ts = ThemeSet::load_defaults();        // base16-eighties.dark reads well on the heartwood palette and ships        // base16-eighties.dark reads well on the dark palette and ships        // with syntect's default themes, so no theme files to vendor.        ts.themes            .remove("base16-eighties.dark")
modified src/main.rs
@@ -32,7 +32,7 @@ async fn main() -> anyhow::Result<()> {    let addr = SocketAddr::from(([0, 0, 0, 0], port));    let listener = tokio::net::TcpListener::bind(addr).await?;    tracing::info!("heartwood listening on http://{addr}");    tracing::info!("repos listening on http://{addr}");    // ConnectInfo so clone routes can pass REMOTE_ADDR to git http-backend.    axum::serve(        listener,
modified src/routes/clone.rs
@@ -76,7 +76,7 @@ async fn upload_pack(async fn receive_pack_forbidden() -> Response {    (        StatusCode::METHOD_NOT_ALLOWED,        "heartwood is read-only; push to the server's git remote directly",        "repos is read-only; push to the server's git remote directly",    )        .into_response()}
modified templates/base.html
@@ -4,7 +4,7 @@  <meta charset="utf-8">  <meta name="viewport" content="width=device-width, initial-scale=1">  <title>{% block title %}{% endblock %}{% if self.title() %} · {% endif %}{{ site.title }}</title>  <meta name="description" content="{% block description %}{{ site.tagline }}{% endblock %}">  <meta name="description" content="{% block description %}minimal git repo browser{% endblock %}">  {% if base_url %}<base href="{{ base_url }}">{% endif %}  <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">  {% block extra_head %}{% endblock %}
@@ -23,8 +23,10 @@        </svg>        <span class="brand-text">{{ site.title }}</span>      </a>      {% if site.tagline %}      <span class="topbar__divider" aria-hidden="true"></span>      <span class="tagline">{{ site.tagline }}</span>      {% endif %}      <nav class="topnav" aria-label="primary">        <a href="/" class="topnav__link {% if request.path == '/' %}is-active{% endif %}">          <span class="topnav__dot" aria-hidden="true"></span>repos
@@ -60,13 +62,13 @@          and blob browsing with syntax highlighting, unified diffs per          commit, and an Atom feed. Clone over HTTPS. Read-only,          single-operator, no Github required.</p>        <p class="footer__tagline">{{ site.tagline }}.</p>        {% if site.tagline %}<p class="footer__tagline">{{ site.tagline }}.</p>{% endif %}      </div>      <div class="footer__col">        <div class="footer__label">// Pages</div>        <ul>          <li><a href="/">Repos</a></li>          <li><a href="https://github.com/overshard/heartwood" target="_blank" rel="noopener">Source</a></li>          <li><a href="https://github.com/overshard/repos" target="_blank" rel="noopener">Source</a></li>        </ul>      </div>      <div class="footer__col">
@@ -84,7 +86,7 @@  <div class="footer-bar">    <div class="container footer-bar__row">      <small>&copy; {{ now.year }} Isaac Bythewood · Some rights reserved</small>      <a href="https://github.com/overshard/heartwood" target="_blank" rel="noopener" class="footer-bar__link" aria-label="GitHub">      <a href="https://github.com/overshard/repos" target="_blank" rel="noopener" class="footer-bar__link" aria-label="GitHub">        <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">          <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>        </svg>