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/server/server.go +8−3
| @@ -50,7 +50,7 @@ func New(cfg *config.Config, binPath string, log *slog.Logger) (*Server, error) | |||
| 50 | 50 | if err := os.MkdirAll(cfg.ReposDir(), 0o755); err != nil { | |
| 51 | 51 | return nil, err | |
| 52 | 52 | } | |
| 53 | ident, err := identity.Open(cfg.SystemRepo()) | ||
| 53 | ident, err := identity.Open(cfg.UsersDB()) | ||
| 54 | 54 | if err != nil { | |
| 55 | 55 | return nil, fmt.Errorf("%w\nrun `vole init` to create the instance", err) | |
| 56 | 56 | } | |
| @@ -248,7 +248,7 @@ func (s *Server) watchRoster(ctx context.Context) { | |||
| 248 | 248 | continue | |
| 249 | 249 | } | |
| 250 | 250 | if after := s.Identity.Snapshot().Count; after != before { | |
| 251 | s.Log.Info("roster reloaded", "keys", after) | ||
| 251 | s.Log.Info("roster reloaded", "accounts", after) | ||
| 252 | 252 | } | |
| 253 | 253 | } | |
| 254 | 254 | } | |
| @@ -265,7 +265,12 @@ func (s *Server) Close() error { | |||
| 265 | 265 | _ = s.Runs.Close() | |
| 266 | 266 | } | |
| 267 | 267 | if s.Index != nil { | |
| 268 | return s.Index.Close() | ||
| 268 | if err := s.Index.Close(); err != nil { | ||
| 269 | return err | ||
| 270 | } | ||
| 271 | } | ||
| 272 | if s.Identity != nil { | ||
| 273 | return s.Identity.Close() | ||
| 269 | 274 | } | |
| 270 | 275 | return nil | |
| 271 | 276 | } | |