romele / git-trace public
62 commits · 1 branch · 1 tag · 1 contributor (top: Romele) · first commit 3d ago · last push 17m ago
doc/federation.md
# Mirroring & lightweight federation
Two directions, one table (`repo_mirrors`, `direction` column):
## Pull — follow an upstream
A repository can follow an upstream remote — a repo on another
git-trace instance, or any URL `git fetch` understands. Configure it
in `<owner>/<repo>/settings` → **mirrors** (admins only).
- `git-trace mirror-sync [owner/repo]` runs `timeout 120 git fetch
<url> <refspec>` inside the bare repo. Without arguments it syncs
every enabled mirror — usable from cron, though you should prefer
the event-driven path below.
- Refs land under **`refs/mirror/*`**, a namespace separate from
`refs/heads/`: an upstream — hostile, compromised or merely
rewritten — can never overwrite local branches. Mirrored refs are
listed on `/refs` marked *read-only*; the default refspec is
`+refs/heads/*:refs/mirror/*` (force-updates, mirrors track
upstream rewrites).
- **Event-driven**: `POST /<o>/<r>/mirror-sync` accepts a
`X-Git-Trace-Signature: sha256=<hmac>` header verified against the
mirror `secret` over the raw body — exactly the format outbound
webhooks already emit. So federation is: on the upstream instance,
add a *push* webhook pointing at the follower's `/mirror-sync` URL
with the shared secret; every push wakes the follower and it
fetches — no polling daemon.
- The endpoint is signature-authenticated, not session-authenticated,
and deliberately returns `404` for repos without mirrors so it
can't be used as an existence oracle.
## Push — publish downstream
`direction='push'` rows fan out every local `refs/heads/*` update to
a downstream forge. The `post-receive` hook spawns
`git-trace mirror-push <o>/<r>` detached only when heads actually
moved — a no-op push runs no remote hooks, so an A⇄B configuration
converges structurally in one extra hop rather than looping forever.
- Push refspecs are constrained: the **source is locked to
`refs/heads/`** — mirrored refs can never leak outward as remote
branches; the destination is likewise `refs/heads/`.
- Credentials may ride inside the URL (`https://user:token@host/r.git`);
the settings UI shows them masked (`user:***@`) and the stored
error log is scrubbed before persistence.
- `git-trace mirror-push [o/r]` runs every enabled push mirror —
without arguments, or one repo's.
## Safety (both directions)
URL schemes are restricted to `http`, `https`, `git`, `ssh` (incl.
`user@host:` scp syntax); `file://` is refused outright since it
would let an admin exfiltrate arbitrary local paths into a repo.
Credential-bearing upstream URLs are allowed and stored in the DB
like webhook secrets. The fetch/push still runs with daemon
privileges — mirror URLs are an admin-trust boundary, same class as
outbound webhooks (SSRF-aware deployments should firewall the
daemon's egress).
## Webhooks
Per-repo outbound notifications, configured in
`<owner>/<repo>/settings` (repo admins only):
- **events**: `push` (from the `post-receive` hook), `mr` (merge
request opened), `patch` (patch set submitted or revised via web or
`mailrecv`), `ci` (build finished)
- **delivery**: payloads are queued in `webhook_deliveries` and
drained by `git-trace webhook-run`, spawned detached (double-fork +
setsid) so a slow endpoint can never stall a push. Retries stop
after 5 attempts; the settings page shows the last 10 deliveries.
- **transport**: `http://` is POSTed by a minimal in-tree client
(3s connect deadline, 5s I/O timeouts); `https://` is delegated to
`curl` via `fork`/`exec` — same process-tool model as `git`/`sshd`,
and the payload travels on stdin so nothing sensitive lands on argv.
- **signature**: when a secret is set, requests carry
`X-Git-Trace-Signature: sha256=<hmac>` — HMAC-SHA256 of the raw body,
computed by the vendored crypto.
```json
{"event":"push","repo":"alice/demo","ref":"refs/heads/main",
"old":"...","new":"...","actor":"alice"}
```