git-trace

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"}
```