Artifact fee81bcbe5c516e02311be412a97871c757d29009338a1f6c85b6e323fd300fe:
- File
hub/users.go
— part of check-in
[40c9b9aff3]
at
2026-05-19 16:25:25
on branch main
— feat(hub): Hub.EnsureUser — idempotent user-table registration (closes #183) (#184)
Add Hub.EnsureUser(login, caps string) error as an idempotent counterpart to Hub.AddUser. Consumers (sesh, others) need "register if absent" semantics; AddUser errors on duplicate, forcing each consumer to write check-then-act + race-recheck wrappers. The user-table layer is the right place to own that idempotency.
Implementation choice (a) — GetUser → CreateUser, serialized by a Repo- level mutex. Picked over (b) "catch CreateUser's already-exists error" because libfossil's CreateUser surfaces a wrapped SQLite UNIQUE-constraint error with no sentinel or typed shape — string-matching would be brittle. A defensive GetUser retry after CreateUser failure absorbs the rare cross- process race where the in-process mutex doesn't apply.
Contract: existing caps are PRESERVED on duplicate (documented). Callers wanting replace-on-conflict use SetUserCaps explicitly.
Tests cover first-time registration, duplicate no-op, caps-change-on- duplicate (preservation), 5-goroutine concurrent race (exactly one row lands), and empty-login rejection. Full hub package + leaf + bridge + CLI suites pass; go vet clean; gofmt clean.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> (user: [email protected], size: 1621) [annotate] [blame] [check-ins using]
package hub // User describes a fossil user on the hub repo. type User struct { Login string Caps string } // AddUser creates a fossil user. Returns an error if Login is empty or the // user already exists. func (h *Hub) AddUser(u User) error { return h.repo.AddUser(u) } // GetUser returns the user with the given login. Returns an error if no // such user exists. func (h *Hub) GetUser(login string) (User, error) { return h.repo.GetUser(login) } // HasUser reports whether a user with the given login exists. func (h *Hub) HasUser(login string) bool { return h.repo.HasUser(login) } // ListUsers returns all users on the hub. func (h *Hub) ListUsers() ([]User, error) { return h.repo.ListUsers() } // RemoveUser deletes the user with the given login. func (h *Hub) RemoveUser(login string) error { return h.repo.RemoveUser(login) } // SetUserCaps replaces the caps string on an existing user. func (h *Hub) SetUserCaps(login, caps string) error { return h.repo.SetUserCaps(login, caps) } // EnsureUser registers a user with the given capabilities if not already // registered. Idempotent: repeated calls with the same login return nil and // do not modify the existing row, even when caps differ — existing caps are // PRESERVED. Callers that need replace-on-conflict semantics should use // SetUserCaps (or RemoveUser + AddUser) explicitly. // // Returns an error only for genuine failures (login empty, underlying repo // unreachable, etc.). Safe for concurrent in-process callers racing on the // same login. func (h *Hub) EnsureUser(login, caps string) error { return h.repo.EnsureUser(login, caps) }