~bigbes/sr-ht-compare

compare.sr.ht — stateless Go diff/compare viewer for the instance. Mirror of sourcecraft.dev/bigbes/sr-ht-compare (primary).
9720ccc2 — bigbes 2 days ago
go.mod: take the shared libraries' current heads
7dfec322 — bigbes 2 days ago
ci: report this build's coverage and benchmarks
2569ec40 — bigbes 3 days ago
ci: publish the apk into artifacts.sr.ht as well

refs

master
browse  log 

clone

read-only
https://git.srht.bigb.es/~bigbes/sr-ht-compare
read/write
git@git.srht.bigb.es:~bigbes/sr-ht-compare

You can also use your local clone with git send-email.

#diff.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://diff.srht.bigb.es.

The service is diff.sr.ht; the repository is still sr-ht-compare. The service was called compare.sr.ht until it was renamed, and what the rename moved is everything the instance sees: the config.ini section, the origin, the apk package, the container. What it deliberately did not move is anything with a cost and no benefit — the Go module path (sourcecraft.dev/bigbes/sr-ht-compare), the repository name, the binary (comparesrht) and the /~owner/repo/compare/base...head routes, which are the URLs people have in their history.

#Architecture

diff.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 wrapped in the SourceHut chrome (nav, service-switcher, login block, environment banner), which comes from the shared sr-ht-ecore chrome package rather than from a copy of its own. 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.

#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/ — the git.sr.ht GraphQL authorizer with a short TTL cache. Identity is not here any more: the unified-login cookie is decoded by sr-ht-ecore's login, the instance's one copy of that decode, which — unlike the local one it replaced — refuses a name that could not be a username.
  • web/ — chi router, handlers, Go templates, embedded static assets. Most of the machinery around them is not here but in sr-ht-ecore, wired up in web/server.go: chrome for the page frame, pages for template discovery and the shared error page, assets for the hashed artefacts and their cache policy, csrf and middleware for the router's guards, chimw for the GET+HEAD route pair, the rendered 404/405 and the request log record. What is left is this service's own: the routes, the handlers, and the templates they render.
  • 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 sourcecraft.dev/bigbes/sr-ht-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, content-hashed web/static/bundle.<hash>.js is committed (the hash busts the browser cache on deploy, like the stylesheet), so Node is not needed at runtime.
make build   # compile ./comparesrht
make test    # go test ./...
make cover   # the same suite plus ./cover.out (what CI uploads to cov.sr.ht)
make bench   # the diffing benchmarks as benchfmt (what CI uploads to bench.sr.ht)
make css     # build the hashed stylesheet (sassc + minify)
make bundle  # rebuild the vendored frontend bundle (esbuild)

Static assets are //go:embed-ed into the binary (web/templates.go embeds static). make css / make bundle only update the files under web/static/ — the running daemon serves the copy baked into ./comparesrht, so re-run make build after either or the change won't appear. Rebuilding the CSS or bundle without a following go build is a common way to chase a stale asset.

#Configuration

See config.example.ini. On a real instance, append the [diff.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 [diff.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):

    [sr.ht]
    network-key=<a fresh Fernet key>
    site-name=sourcehut
    environment=development
    
    [webhooks]
    private-key=<base64 of 32 random bytes>
    
    [diff.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 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:

    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):

    go run ./contrib/dev-stub -addr 127.0.0.1:5101
    
  4. Run the daemon:

    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/<sha>. Append .patch to any compare/commit URL for the raw unified diff.

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 run ./mint-cookie.go   (run from the dir holding config.ini)
package main

import (
    "encoding/json"
    "fmt"

    "sourcecraft.dev/bigbes/sr-ht-core/config"
    "sourcecraft.dev/bigbes/sr-ht-core/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

diff.sr.ht deploys like any other SourceHut web service. On the instance:

  1. Install the binary and static assets:

    make install PREFIX=/usr/local
    # → /usr/local/bin/comparesrht
    # → /usr/share/sourcehut/diff.sr.ht/static/{bundle.<hash>.js,main.min.<hash>.css}
    
  2. Config. Append the [diff.sr.ht] section (origin= and static-dir=) to the shared /etc/sr.ht/config.ini. To make diff.sr.ht appear in the shared nav/service-switcher of the other services, that same [diff.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 diff.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 diff.sr.ht runs on the same box as git.sr.ht, the default loopback/private ranges already cover it.

  5. nginx. Install contrib/diff.sr.ht.conf alongside the other *.sr.ht.conf files (adjust server_name), provide the TLS cert include it references (diff-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/diff-srht.service, then systemctl enable --now diff-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://diff.srht.bigb.es/healthzok. 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.<hash>.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.