Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

forge.gno

12.00 Kb · 384 lines
  1// Package forge is the domain engine of an on-chain software forge: repos,
  2// roles, an append-only reference log, issues and change requests (pull
  3// requests), with no chain imports of its own.
  4//
  5// What it does NOT do, on purpose: store blobs, trees or packfiles. Git objects
  6// stay wherever git already puts them (a mirror, an IPFS CID, a peer) and this
  7// package records what a forge is actually trusted for and what git alone does
  8// not authenticate:
  9//
 10//  1. which object id a ref points at, in what order it got there, and who said
 11//     so: an append-only, hash-chained reference log (the same shape as
 12//     gittuf's reference state log, with consensus playing the notary);
 13//  2. who is allowed to move which ref, and under what review policy;
 14//  3. the social layer: issues, change requests, reviews: bound to addresses
 15//     rather than to platform accounts;
 16//  4. the merge decision itself, recorded as one more entry in the same log.
 17//
 18// The chain cannot see the object graph, so it cannot verify that a new tip
 19// descends from the old one. It does not pretend to: every ref move is a
 20// compare-and-swap against the tip the caller expected (git's
 21// --force-with-lease, moved somewhere the forge operator cannot rewrite), any
 22// move that abandons that discipline is recorded as a force, and ancestry is
 23// checked by a client that has the objects. Ordering, attribution and policy
 24// are on chain; proof is local.
 25//
 26// All state lives in avl trees so every listing is ordered and paginatable, and
 27// every mutation takes the actor and the block height from the caller: the
 28// package is pure, deterministic and unit-testable without a chain.
 29//
 30// Live demo: gno.land/r/moul/forge/v0.
 31package forge
 32
 33import (
 34	"strconv"
 35	"strings"
 36
 37	"gno.land/p/nt/avl/v0"
 38)
 39
 40// Role is a repo-scoped capability level. Roles are totally ordered: every
 41// check is "at least this role", so there is one comparison to audit.
 42type Role int
 43
 44const (
 45	RoleNone       Role = iota // not a member
 46	RoleReader                 // explicit read (all repos are public in v0)
 47	RoleWriter                 // move non-protected refs, update own changes
 48	RoleMaintainer             // force-move refs, merge changes, triage issues
 49	RoleAdmin                  // manage members and repo settings
 50	RoleOwner                  // admin + transfer; at least one always exists
 51)
 52
 53// String renders the role as the lowercase token used by the realm API.
 54func (r Role) String() string {
 55	switch r {
 56	case RoleReader:
 57		return "reader"
 58	case RoleWriter:
 59		return "writer"
 60	case RoleMaintainer:
 61		return "maintainer"
 62	case RoleAdmin:
 63		return "admin"
 64	case RoleOwner:
 65		return "owner"
 66	default:
 67		return "none"
 68	}
 69}
 70
 71// ParseRole is the inverse of Role.String.
 72func ParseRole(s string) (Role, error) {
 73	switch s {
 74	case "none":
 75		return RoleNone, nil
 76	case "reader":
 77		return RoleReader, nil
 78	case "writer":
 79		return RoleWriter, nil
 80	case "maintainer":
 81		return RoleMaintainer, nil
 82	case "admin":
 83		return RoleAdmin, nil
 84	case "owner":
 85		return RoleOwner, nil
 86	}
 87	return RoleNone, ErrInvalidRole
 88}
 89
 90// Forge is the top-level registry: repo id -> repo.
 91type Forge struct {
 92	repos *avl.Tree // "<namespace>/<name>" -> *Repo
 93	// packages is the forge-wide index behind PackageRepo: a deployed package
 94	// path resolves to the repo claiming it without scanning every repo. It
 95	// lives here rather than only on the repo because the question an explorer
 96	// asks starts from the path and does not know the repo yet.
 97	packages *avl.Tree // package path -> *Package
 98}
 99
100// New returns an empty forge.
101func New() *Forge {
102	return &Forge{repos: avl.NewTree(), packages: avl.NewTree()}
103}
104
105// Repo is one repository. Nothing here is the code: Mirrors says where the
106// objects can be fetched, Refs says what the objects are supposed to be.
107type Repo struct {
108	ID          string // "<namespace>/<name>", immutable
109	Description string
110	DefaultRef  string // fully-qualified, e.g. "refs/heads/main"
111	Mirrors     []string
112	ParentID    string // fork lineage, "" for a root repo
113	CreatedAt   int64  // block height
114	Archived    bool
115
116	// Merge policy.
117	RequiredApprovals int  // approvals needed to merge a change
118	AllowSelfApproval bool // may the change author's own approval count
119
120	members *avl.Tree // address string -> Role
121	refs    *avl.Tree // ref name -> *Ref
122	log     *avl.Tree // padded seq -> *LogEntry (append-only)
123	issues  *avl.Tree // padded id -> *Issue
124	changes *avl.Tree // padded id -> *Change
125
126	packages *avl.Tree // package path -> *Package (this repo's half of the index)
127	releases *avl.Tree // tag -> *Release
128	// releaseOrder is cut order, which is the only order the forge knows to be
129	// true. Tags are not required to be semver, so sorting the release tree by
130	// key would present v10 before v2 and call it newest.
131	releaseOrder *avl.Tree // padded seq -> tag
132
133	head       string // digest of the last log entry ("" when the log is empty)
134	nextSeq    int64
135	nextIssue  int64
136	nextChange int64
137}
138
139// CreateRepo registers a repo owned by actor.
140func (f *Forge) CreateRepo(actor address, height int64, id, description, defaultRef string) (*Repo, error) {
141	if !ValidRepoID(id) {
142		return nil, ErrInvalidRepoID
143	}
144	if !ValidText(description, MaxDescLen) {
145		return nil, ErrInvalidText
146	}
147	if defaultRef == "" {
148		defaultRef = "refs/heads/main"
149	}
150	if !ValidRefName(defaultRef) {
151		return nil, ErrInvalidRefName
152	}
153	if f.repos.Has(id) {
154		return nil, ErrRepoExists
155	}
156	r := &Repo{
157		ID:                id,
158		Description:       description,
159		DefaultRef:        defaultRef,
160		CreatedAt:         height,
161		RequiredApprovals: 1,
162		members:           avl.NewTree(),
163		refs:              avl.NewTree(),
164		log:               avl.NewTree(),
165		issues:            avl.NewTree(),
166		changes:           avl.NewTree(),
167		// A fork routes through here too, and inherits neither: the parent
168		// publishes those package paths and cut those releases, not the child.
169		packages:     avl.NewTree(),
170		releases:     avl.NewTree(),
171		releaseOrder: avl.NewTree(),
172	}
173	r.members.Set(actor.String(), RoleOwner)
174	f.repos.Set(id, r)
175	return r, nil
176}
177
178// Fork registers newID as a fork of srcID and copies the parent's current refs
179// into the child's log, so the fork records exactly what it forked from. The
180// objects are not copied: they never were on chain: so the child inherits the
181// parent's mirrors as its initial fetch locators.
182func (f *Forge) Fork(actor address, height int64, srcID, newID string) (*Repo, error) {
183	src := f.Repo(srcID)
184	if src == nil {
185		return nil, ErrRepoNotFound
186	}
187	child, err := f.CreateRepo(actor, height, newID, src.Description, src.DefaultRef)
188	if err != nil {
189		return nil, err
190	}
191	child.ParentID = srcID
192	child.Mirrors = append([]string{}, src.Mirrors...)
193	note := "fork of " + srcID + " at seq " + strconv.FormatInt(src.nextSeq, 10)
194	if len(note) > MaxNoteLen {
195		note = "fork of " + srcID
196	}
197	n := 0
198	src.refs.Iterate("", "", func(key string, value any) bool {
199		ref := value.(*Ref)
200		child.appendLog(actor, height, ref.Name, "", ref.OID, KindCreate, 0, note)
201		child.refs.Set(ref.Name, &Ref{Name: ref.Name, OID: ref.OID, UpdatedAt: height, UpdatedBy: actor})
202		n++
203		return n >= 32 // a fork records a snapshot, not an unbounded copy
204	})
205	return child, nil
206}
207
208// Repo returns the repo, or nil.
209func (f *Forge) Repo(id string) *Repo {
210	v := f.repos.Get(id)
211	if v == nil {
212		return nil
213	}
214	return v.(*Repo)
215}
216
217// HasRepo reports whether the id is taken.
218func (f *Forge) HasRepo(id string) bool { return f.repos.Has(id) }
219
220// Size is the number of repos.
221func (f *Forge) Size() int { return f.repos.Size() }
222
223// IterateRepos walks repos in id order, newest-last, and stops when cb returns
224// true. offset/count page the walk; count <= 0 means "to the end".
225func (f *Forge) IterateRepos(offset, count int, cb func(*Repo) bool) {
226	if count <= 0 {
227		count = f.repos.Size()
228	}
229	f.repos.IterateByOffset(offset, count, func(_ string, value any) bool {
230		return cb(value.(*Repo))
231	})
232}
233
234// IterateNamespace walks the repos of one namespace in id order.
235func (f *Forge) IterateNamespace(ns string, cb func(*Repo) bool) {
236	f.repos.Iterate(ns+"/", ns+"0", func(_ string, value any) bool { // '0' is '/'+1
237		return cb(value.(*Repo))
238	})
239}
240
241// RoleOf returns the member's role, RoleNone when not a member.
242func (r *Repo) RoleOf(a address) Role {
243	v := r.members.Get(a.String())
244	if v == nil {
245		return RoleNone
246	}
247	return v.(Role)
248}
249
250// Can reports whether a holds at least the given role.
251func (r *Repo) Can(a address, min Role) bool { return r.RoleOf(a) >= min }
252
253// MemberCount is the number of members with an explicit role.
254func (r *Repo) MemberCount() int { return r.members.Size() }
255
256// IterateMembers walks members in address order.
257func (r *Repo) IterateMembers(cb func(addr string, role Role) bool) {
258	r.members.Iterate("", "", func(key string, value any) bool {
259		return cb(key, value.(Role))
260	})
261}
262
263// SetMember grants or revokes a role. Admins manage members; only an owner may
264// mint another owner, and the last owner cannot be demoted: a repo with no
265// owner is a repo nobody can ever unarchive.
266func (r *Repo) SetMember(actor address, target address, role Role) error {
267	if role < RoleNone || role > RoleOwner {
268		return ErrInvalidRole
269	}
270	if !r.Can(actor, RoleAdmin) {
271		return ErrUnauthorized
272	}
273	if role == RoleOwner && !r.Can(actor, RoleOwner) {
274		return ErrUnauthorized
275	}
276	cur := r.RoleOf(target)
277	if cur == RoleOwner && role != RoleOwner {
278		if !r.Can(actor, RoleOwner) {
279			return ErrUnauthorized // an admin cannot demote an owner
280		}
281		if r.ownerCount() == 1 {
282			return ErrLastOwner
283		}
284	}
285	if role == RoleNone {
286		r.members.Remove(target.String())
287		return nil
288	}
289	r.members.Set(target.String(), role)
290	return nil
291}
292
293func (r *Repo) ownerCount() int {
294	n := 0
295	r.members.Iterate("", "", func(_ string, value any) bool {
296		if value.(Role) == RoleOwner {
297			n++
298		}
299		return false
300	})
301	return n
302}
303
304// SetDescription updates the one-line description.
305func (r *Repo) SetDescription(actor address, description string) error {
306	if !r.Can(actor, RoleAdmin) {
307		return ErrUnauthorized
308	}
309	if !ValidText(description, MaxDescLen) {
310		return ErrInvalidText
311	}
312	r.Description = description
313	return nil
314}
315
316// SetMirrors replaces the fetch locators. The first one is the canonical
317// remote; the rest are fallbacks. The chain records them, it never fetches.
318func (r *Repo) SetMirrors(actor address, mirrors []string) error {
319	if !r.Can(actor, RoleMaintainer) {
320		return ErrUnauthorized
321	}
322	if len(mirrors) > MaxMirrors {
323		return ErrTooMany
324	}
325	for _, m := range mirrors {
326		if !ValidMirror(m) {
327			return ErrInvalidMirror
328		}
329	}
330	r.Mirrors = append([]string{}, mirrors...)
331	return nil
332}
333
334// SetDefaultRef points the repo at another default branch.
335func (r *Repo) SetDefaultRef(actor address, name string) error {
336	if !r.Can(actor, RoleMaintainer) {
337		return ErrUnauthorized
338	}
339	if !ValidRefName(name) {
340		return ErrInvalidRefName
341	}
342	r.DefaultRef = name
343	return nil
344}
345
346// SetPolicy sets the merge policy: how many approvals a change needs, and
347// whether the author's own approval counts.
348func (r *Repo) SetPolicy(actor address, requiredApprovals int, allowSelfApproval bool) error {
349	if !r.Can(actor, RoleAdmin) {
350		return ErrUnauthorized
351	}
352	if requiredApprovals < 0 || requiredApprovals > 16 {
353		return ErrTooMany
354	}
355	r.RequiredApprovals = requiredApprovals
356	r.AllowSelfApproval = allowSelfApproval
357	return nil
358}
359
360// SetArchived freezes (or unfreezes) the repo. An archived repo takes no
361// mutation except unarchiving: the log stays readable forever.
362func (r *Repo) SetArchived(actor address, archived bool) error {
363	if !r.Can(actor, RoleAdmin) {
364		return ErrUnauthorized
365	}
366	r.Archived = archived
367	return nil
368}
369
370// Counts for rendering.
371func (r *Repo) RefCount() int    { return r.refs.Size() }
372func (r *Repo) IssueCount() int  { return r.issues.Size() }
373func (r *Repo) ChangeCount() int { return r.changes.Size() }
374
375// seqKey zero-pads an id to a fixed width so avl keys sort numerically.
376// ufmt has no width flags in gno, so the padding is done by hand: "%016d"
377// silently returns the bare number and would sort 10 before 2.
378func seqKey(n int64) string {
379	s := strconv.FormatInt(n, 10)
380	if len(s) >= 16 {
381		return s
382	}
383	return strings.Repeat("0", 16-len(s)) + s
384}