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/names_test.go +5−4
| @@ -8,19 +8,20 @@ import ( | |||
| 8 | 8 | ) | |
| 9 | 9 | ||
| 10 | 10 | func TestOpenNamesResolves(t *testing.T) { | |
| 11 | dir := filepath.Join(t.TempDir(), "system.git") | ||
| 12 | if err := identity.Init(dir); err != nil { | ||
| 11 | path := filepath.Join(t.TempDir(), "users.db") | ||
| 12 | if err := identity.Init(path); err != nil { | ||
| 13 | 13 | t.Fatal(err) | |
| 14 | 14 | } | |
| 15 | s, err := identity.Open(dir) | ||
| 15 | s, err := identity.Open(path) | ||
| 16 | 16 | if err != nil { | |
| 17 | 17 | t.Fatal(err) | |
| 18 | 18 | } | |
| 19 | defer s.Close() | ||
| 19 | 20 | a, err := s.Create("keenan", "test") | |
| 20 | 21 | if err != nil { | |
| 21 | 22 | t.Fatal(err) | |
| 22 | 23 | } | |
| 23 | got := openNames(dir)(a.ID) | ||
| 24 | got := openNames(path)(a.ID) | ||
| 24 | 25 | if got != "keenan" { | |
| 25 | 26 | t.Fatalf("openNames = %q, want keenan (id %s)", got, a.ID) | |
| 26 | 27 | } | |