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