# compare.sr.ht A git compare/diff viewer for a self-hosted [SourceHut] instance. It compares two refs (branches, tags, or commits) of any git.sr.ht repository — `base...head` or `base..head` — and renders the diff with [`@pierre/diffs`] (Shiki-based syntax-highlighted diffs) and a [`@pierre/trees`] file tree, plus a single-commit view. Runs at `https://compare.srht.bigb.es`. [SourceHut]: https://sourcehut.org [`@pierre/diffs`]: https://www.npmjs.com/package/@pierre/diffs [`@pierre/trees`]: https://www.npmjs.com/package/@pierre/trees ## Architecture compare.sr.ht is a **stateless Go daemon**: it owns no database and no object storage, so it needs neither Postgres nor Redis. It integrates into SourceHut purely through configuration — no upstream sources are modified (the [sourcehut-custom-service integration model]). Repository data is read directly from the bare repos on disk (`{[git.sr.ht] repos}/~{owner}/{name}`) using [go-git] in-process; no `git` binary is executed at runtime. Every request is authorized against the git.sr.ht **internal GraphQL API** using core-go's internal-auth client, acting as the cookie's logged-in user — or anonymously, which git.sr.ht's own loader scopes to public/unlisted repositories, so private repos are never leaked (an unauthorized or missing repo is always a 404, never a 403). Identity comes from decrypting the shared `sr.ht.unified-login.v1` cookie with the instance network key; there is no login flow of our own. Pages are server-rendered Go templates that reproduce the SourceHut chrome (nav, service-switcher, login block, environment banner). The diff/tree UI is one vendored [pierre-libs] esbuild bundle driven by a JSON blob embedded in the page, so the browser does all diff rendering from a single request with no second authorization round-trip. A short-TTL in-memory cache in front of the authorizer spares git.sr.ht a GraphQL call on every page load. [sourcehut-custom-service integration model]: https://man.sr.ht/ [go-git]: https://github.com/go-git/go-git [pierre-libs]: https://www.npmjs.com/org/pierre ## Layout - `core/` — pure domain: owner/repo/ref validation, the compare-spec grammar, sentinel errors. No external dependencies. - `gitx/` — bare-repo access over go-git: refs, ref-to-ref diffs, single-commit diffs, and commit logs, all bounded by context timeouts and output-size caps. - `authz/` — cookie→identity and the git.sr.ht GraphQL authorizer with a short TTL cache. - `web/` — chi router, handlers, chrome, Go templates, embedded static assets. - `frontend/` + `scss/` — build-time TypeScript diff bundle and the SCSS entry. - `cmd/comparesrht/` — the daemon entry point and startup validation. - `contrib/` — nginx server block, systemd unit, and a dev GraphQL stub. The core-go dependency is pinned to a private fork (`git.srht.bigb.es/~bigbes/core-go`) via a `replace` directive in `go.mod`; the fork carries one production S3 patch on top of upstream. ## Build requirements - **Go** 1.26+ (module `go.bigb.es/sourcehut-compare`). - **sassc** and **minify** — CSS pipeline (`make css`), building against the shared sourcehut scss partials installed at `/usr/share/sourcehut/scss` (from `core.sr.ht`'s `make install`). `dart-sass` also works for local builds; the production `css` target uses `sassc -I /usr/share/sourcehut/scss`. - **Node.js** (build time only) — the frontend diff bundle (`make bundle`, esbuild). The vendored `web/static/bundle.js` is committed, so Node is not needed at runtime. ```sh make build # compile ./comparesrht make test # go test ./... make css # build the hashed stylesheet (sassc + minify) make bundle # rebuild the vendored frontend bundle (esbuild) ``` ## Configuration See `config.example.ini`. On a real instance, append the `[compare.sr.ht]` section to the shared `/etc/sr.ht/config.ini`; the service reads the instance's existing shared keys (`[sr.ht]`, `[webhooks]`, `[meta.sr.ht]`, `[git.sr.ht]`) in place. core-go's `LoadConfig()` searches, in order: `./config.ini`, `../config.ini`, `/etc/sr.ht/config.ini`, `/etc/sr.ht/*.ini`. At startup the daemon validates that all required keys are present — the `[sr.ht] network-key`, `[webhooks] private-key`, `[git.sr.ht] repos`, a git.sr.ht API origin candidate (`api-internal-origin` / `internal-origin` / `api-origin` / `origin`), `[meta.sr.ht] origin`, and `[compare.sr.ht] origin` — and exits with a single clear message listing anything missing. ## Development Run the service locally against a directory of bare repositories and the dev-stub GraphQL API (no real instance required). 1. **Write a local `config.ini`** in the repo root (git-ignored). Minimal template — the two crypto keys can be any dev values (see below): ```ini [sr.ht] network-key= site-name=sourcehut environment=development [webhooks] private-key= [compare.sr.ht] origin=http://localhost:5090 [meta.sr.ht] origin=http://localhost:5100 [git.sr.ht] origin=http://localhost:5101 api-origin=http://localhost:5101 # dev-stub serves POST /query here repos=/path/to/local/bare/repos # holds ~owner/name bare repos ``` Generate the keys with a throwaway program (both use core-go's fernet dependency): ```go // go run ./gen-keys.go package main import ( "crypto/rand" "encoding/base64" "fmt" "github.com/fernet/fernet-go" ) func main() { var k fernet.Key _ = k.Generate() seed := make([]byte, 32) _, _ = rand.Read(seed) fmt.Println("network-key =", k.Encode()) fmt.Println("private-key =", base64.StdEncoding.EncodeToString(seed)) } ``` 2. **Prepare a bare repo** for the stub to resolve. The dev-stub reports every repository as PUBLIC, so gitx just needs the on-disk repo to exist: ```sh mkdir -p /path/to/local/bare/repos/~alice git clone --bare /some/existing/repo /path/to/local/bare/repos/~alice/demo ``` 3. **Start the dev-stub** (fake git.sr.ht GraphQL API): ```sh go run ./contrib/dev-stub -addr 127.0.0.1:5101 ``` 4. **Run the daemon:** ```sh make run-dev # builds, then ./comparesrht -b localhost:5090 ``` Visit `http://localhost:5090/~alice/demo`, then a compare such as `http://localhost:5090/~alice/demo/compare/main...some-branch`, or a commit page at `/~alice/demo/commit/`. Append `.patch` to any compare/commit URL for the raw unified diff. ### Forging a dev login cookie There is no dedicated helper. Identity is just the `sr.ht.unified-login.v1` cookie: a Fernet token (sealed with `[sr.ht] network-key`) whose JSON payload's only meaningful field is `name`. Mint one with a five-line program that reuses the same config and crypto core-go uses at runtime: ```go // go run ./mint-cookie.go (run from the dir holding config.ini) package main import ( "encoding/json" "fmt" "git.sr.ht/~sircmpwn/core-go/config" "git.sr.ht/~sircmpwn/core-go/crypto" ) func main() { conf := config.LoadConfig() crypto.InitCrypto(conf) payload, _ := json.Marshal(map[string]string{"name": "bigbes"}) fmt.Println(string(crypto.Encrypt(payload))) } ``` Set the printed value as the `sr.ht.unified-login.v1` cookie for `localhost:5090` (browser devtools → Application → Cookies), reload, and the nav shows "Logged in as bigbes" with your repository list (from the stub's `me.repositories`). ## Deployment compare.sr.ht deploys like any other SourceHut web service. On the instance: 1. **Install** the binary and static assets: ```sh make install PREFIX=/usr/local # → /usr/local/bin/comparesrht # → /usr/share/sourcehut/compare.sr.ht/static/{bundle.js,main.min..css,logo.svg} ``` 2. **Config.** Append the `[compare.sr.ht]` section (`origin=` and `static-dir=`) to the shared `/etc/sr.ht/config.ini`. To make compare.sr.ht appear in the shared nav/service-switcher of the *other* services, that same `[compare.sr.ht] origin=` line must be visible to their configs too (on a single shared `config.ini` this is automatic) — then **restart those services** (git.sr.ht, meta.sr.ht, …) so they pick up the new switcher entry. 3. **DNS.** Point `compare.srht.bigb.es` at the instance. It must be a subdomain of the shared cookie domain (`*.srht.bigb.es`) so the unified-login cookie is sent to us. 4. **internal-ipnet.** This host must fall inside the git.sr.ht API's `[sr.ht] internal-ipnet` CIDR, or the internal-auth GraphQL calls (including anonymous `AUTH_ANON_INTERNAL`) are rejected. If compare.sr.ht runs on the same box as git.sr.ht, the default loopback/private ranges already cover it. 5. **nginx.** Install `contrib/compare.sr.ht.conf` alongside the other `*.sr.ht.conf` files (adjust `server_name`), provide the TLS cert include it references (`compare-ssl.conf`), and reload nginx. It is a plain `proxy_pass http://127.0.0.1:5090`; static assets are served by the app from its embedded FS with immutable cache headers. 6. **systemd.** Install `contrib/compare-srht.service`, then `systemctl enable --now compare-srht`. The unit runs as **`User=git`** so it has read access to the bare repositories under `/var/lib/git` (`ReadOnlyPaths=/var/lib/git`, `ProtectSystem=strict` — the service only ever reads). It stops with `KillSignal=SIGINT` because core-go's `server.Run` performs its warm shutdown on SIGINT, not the systemd default SIGTERM. 7. **Verify.** `GET https://compare.srht.bigb.es/healthz` → `ok`. Logged in, the landing page lists your repos and a PUBLIC repo's `main...branch` compare renders with syntax highlighting, the tree sidebar, and the split/unified toggle; a PRIVATE repo is a 404 to anonymous viewers and 200 to its owner. ## Design notes - **go-git, not `git` shell-out.** git.sr.ht itself shells out to the `git` binary, and the original plan did too. On the user's decision we read repositories in-process with go-git instead: no subprocess management, no `PATH`/argument-injection surface, and cleaner output-size bounding. The diff output was fidelity-verified against real `git` (see `gitx/fidelity_test.go`) so the patches `@pierre/diffs` parses carry standard `diff --git` headers and match git's rename/binary handling. - **10 MB vendored bundle.** `web/static/bundle.js` is a ~10 MB committed esbuild output. It is large because `@pierre/diffs` bundles Shiki with its grammars/themes for client-side syntax highlighting. We accept the size: it is built once, served with a long immutable cache lifetime, gzips/brotlis down substantially on the wire, and keeps the runtime a single static Go binary with zero Node dependency. Trimming Shiki's language set is the obvious future lever if the transfer size becomes a problem. - **LICENSE pending.** No `LICENSE` file has been chosen yet. SourceHut's own services are typically AGPL/GPL; pick and add one before any public distribution.