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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}