vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
The roster is a database, not a repository
427c47611f7db09e52618a0f2d05291a8641b2d0 · Keenan Nemetz · 17d ago · parent 0f442b1c
The roster is a database, not a repository data/system.git is gone. Accounts, every name they have held and their SSH keys live in data/users.db, next to the other authoritative store (runs.db) and the disposable index. users id, name, status, created user_names every name ever held -- primary key, so a name is issued once user_keys fingerprint, type, line -- primary key, so a key has one owner Two invariants that used to be parse-time checks are now table keys, and a roster change is one transaction instead of a commit, a CAS retry and a tree walk. The snapshot, the five-second reload and the audit line per change are unchanged, so `vole user add-key` still applies without a restart and revocation is still immediate. Clients no longer clone the roster, because there is nothing to clone. The one view a client needs -- active id and name -- is served by a `vole-roster` SSH verb over the key it already pushes with, so name resolution gained no endpoint and no second credential. `vole request new --author keenan` now resolves through that verb (TestRequestCLIResolvesNamesOverSSH: the request landed with author_id = the account id, and an unknown name fails before pushing). A WAL database cannot be backed up with cp, so `vole user backup <path>` takes a consistent copy with VACUUM INTO (TestRosterIsOneDatabaseFile opens the copy and resolves a renamed account and its key from it). This replaces the "identity survives without a side database" criterion, which is withdrawn rather than left looking unmet: the roster is now exactly one file, and the backup is what proves it is not lost with the index. Verified through the binary: init -> create -> add-key -> rename -> list -> backup, the restored copy listing the same account, former name and key; then scripts/ci.sh green (gofmt, vet, build, markdown links, go test -race ./...).
internal/hook/hook.go +7−7
| @@ -29,9 +29,9 @@ type Config struct { | |||
| 29 | 29 | // ActorID is the account id resolved at the SSH handshake. ACL entries and | |
| 30 | 30 | // approvals are keyed by it. | |
| 31 | 31 | ActorID string | |
| 32 | // SystemRepo, when set, is the roster repository, used only to render account | ||
| 33 | // ids as names in messages. | ||
| 34 | SystemRepo string | ||
| 32 | // UsersDB, when set, is the roster database, used only to render account ids | ||
| 33 | // as names in messages. | ||
| 34 | UsersDB string | ||
| 35 | 35 | // SpoolDir receives one file per accepted ref update, for the CI queue. | |
| 36 | 36 | SpoolDir string | |
| 37 | 37 | // RunsDB is the run queue, read here so required checks are enforced at the | |
| @@ -82,7 +82,7 @@ func PreReceive(cfg Config) int { | |||
| 82 | 82 | return 1 | |
| 83 | 83 | } | |
| 84 | 84 | // Names are a courtesy for the person reading the rejection; ids decide. | |
| 85 | names := openNames(cfg.SystemRepo) | ||
| 85 | names := openNames(cfg.UsersDB) | ||
| 86 | 86 | who := func(accountID string) string { return names(accountID) } | |
| 87 | 87 | ||
| 88 | 88 | role := a.RoleFor(actorID) | |
| @@ -427,11 +427,11 @@ func repoNameFromDir(dir string) string { | |||
| 427 | 427 | // openNames returns a function rendering account ids as names, falling back to | |
| 428 | 428 | // the id when the roster is unavailable. Display only: nothing is authorized by | |
| 429 | 429 | // the result. | |
| 430 | func openNames(systemRepo string) func(string) string { | ||
| 431 | if systemRepo == "" { | ||
| 430 | func openNames(usersDB string) func(string) string { | ||
| 431 | if usersDB == "" { | ||
| 432 | 432 | return func(accountID string) string { return accountID } | |
| 433 | 433 | } | |
| 434 | store, err := identity.Open(systemRepo) | ||
| 434 | store, err := identity.Open(usersDB) | ||
| 435 | 435 | if err != nil { | |
| 436 | 436 | return func(accountID string) string { return accountID } | |
| 437 | 437 | } | |