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/sshserver/sshserver_test.go +3−3
| @@ -60,11 +60,11 @@ type harness struct { | |||
| 60 | 60 | func newHarness(t *testing.T) *harness { | |
| 61 | 61 | t.Helper() | |
| 62 | 62 | dir := t.TempDir() | |
| 63 | keysDir := filepath.Join(dir, "trusted_keys") | ||
| 64 | if err := identity.Init(keysDir); err != nil { | ||
| 63 | usersDB := filepath.Join(dir, "users.db") | ||
| 64 | if err := identity.Init(usersDB); err != nil { | ||
| 65 | 65 | t.Fatal(err) | |
| 66 | 66 | } | |
| 67 | ident, err := identity.Open(keysDir) | ||
| 67 | ident, err := identity.Open(usersDB) | ||
| 68 | 68 | if err != nil { | |
| 69 | 69 | t.Fatal(err) | |
| 70 | 70 | } | |