Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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 }