Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Phase 1: a working git remote with access control

653e0cfc227c992e5b26ea10057aa0b424b6ce4f · Keenan Nemetz · 18d ago · parent c5626f1b

Phase 1: a working git remote with access control

Vole is now a functioning forge foundation: one CGO-free binary that serves
real git over smart HTTP and enforces per-repo policy in pre-receive.

What is in:

- internal/transport drives `git http-backend` as a CGI subprocess. Chunked
  uploads are spooled to learn CONTENT_LENGTH (git's default client config
  produces them), spool files are cleaned on error and abort, and a janitor
  sweeps anything a crash left behind. Pushes are capped by size and
  serialised behind a semaphore, because Phase 0 measured 1.13 GB of git RSS
  for a single 1 GiB push.
- internal/acl stores roles on refs/vole/acl inside each repository, so
  access rules travel with a clone. One evaluation function is shared by the
  hook and the web layer.
- internal/hook enforces policy in pre-receive: authentication, write role,
  protected refs, per-blob size limits, and a refusal to accept pushes to
  refs/vole/* - without that last check a writer could push their own
  acl.json and grant themselves admin.
- internal/auth holds users, personal access tokens and web sessions in
  SQLite. Passwords use PBKDF2-SHA256; tokens are 256-bit random values and
  are stored as SHA-256 hashes, since there is no low-entropy secret to grind.
- internal/index is the derived read model, rebuildable from disk and refs.
- internal/repo owns the bare-repo store and the metadata ref CAS helper.
- internal/web is the browse surface: repo list, tree, blob, raw, history,
  login, and per-repo settings for access rules, rename and delete.
- internal/signing creates the instance ed25519 key at first boot for the
  Phase 7 work.

Every Phase 1 exit criterion has a test that would fail if it regressed:
clone/push against a real listener, read-only and anonymous rejection with the
ref proven unmoved, rebuild-from-refs compared against the actual acl ref, a
spool file proven absent after an aborted chunked upload, and two pushes
completing through a single-slot semaphore. scripts/ci.sh runs gofmt, vet,
build, a markdown link check and the race-enabled suite.

Verified by hand as well: a clean `vole init` -> user -> token -> push ->
clone cycle, web login and browse, and a 256 MiB chunked push killed
mid-flight leaving no ref, no spool file and no orphaned git processes.

scripts/ci.sh +47

@@ -0,0 +1,47 @@
1 #!/bin/sh
2 # Everything that must pass before a push. Keep this the single entry point so CI
3 # and humans cannot drift apart.
4 set -eu
5 cd "$(dirname "$0")/.."
6
7 fail() { echo "FAIL: $*" >&2; exit 1; }
8
9 echo "== gofmt =="
10 unformatted=$(gofmt -l . 2>/dev/null | grep -v '^$' || true)
11 [ -z "$unformatted" ] || fail "unformatted files:
12 $unformatted"
13
14 echo "== go vet =="
15 go vet ./... || fail "vet"
16
17 echo "== build =="
18 go build ./... || fail "build"
19
20 echo "== markdown links =="
21 python3 - <<'EOF' || fail "markdown links"
22 import os, re, sys
23 bad = []
24 for root, dirs, files in os.walk('.'):
25 dirs[:] = [d for d in dirs if d not in ('.git', 'data')]
26 for f in files:
27 if not f.endswith('.md'):
28 continue
29 p = os.path.join(root, f)
30 text = open(p, encoding='utf-8').read()
31 for m in re.finditer(r'\]\(([^)#][^)]*)\)', text):
32 target = m.group(1)
33 if target.startswith(('http://', 'https://', 'mailto:')):
34 continue
35 path = os.path.normpath(os.path.join(root, target.split('#')[0]))
36 if not os.path.exists(path):
37 bad.append(f"{p}: {target}")
38 if bad:
39 print("\n".join(bad))
40 sys.exit(1)
41 EOF
42
43 echo "== tests (race) =="
44 go test -race -count=1 ./... || fail "tests"
45
46 echo
47 echo "all checks passed"