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

11.06 Kb · 367 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/nym-moultest123/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}
 94
 95// New returns an empty forge.
 96func New() *Forge {
 97	return &Forge{repos: avl.NewTree()}
 98}
 99
100// Repo is one repository. Nothing here is the code: Mirrors says where the
101// objects can be fetched, Refs says what the objects are supposed to be.
102type Repo struct {
103	ID          string // "<namespace>/<name>", immutable
104	Description string
105	DefaultRef  string // fully-qualified, e.g. "refs/heads/main"
106	Mirrors     []string
107	ParentID    string // fork lineage, "" for a root repo
108	CreatedAt   int64  // block height
109	Archived    bool
110
111	// Merge policy.
112	RequiredApprovals int  // approvals needed to merge a change
113	AllowSelfApproval bool // may the change author's own approval count
114
115	members *avl.Tree // address string -> Role
116	refs    *avl.Tree // ref name -> *Ref
117	log     *avl.Tree // padded seq -> *LogEntry (append-only)
118	issues  *avl.Tree // padded id -> *Issue
119	changes *avl.Tree // padded id -> *Change
120
121	head       string // digest of the last log entry ("" when the log is empty)
122	nextSeq    int64
123	nextIssue  int64
124	nextChange int64
125}
126
127// CreateRepo registers a repo owned by actor.
128func (f *Forge) CreateRepo(actor address, height int64, id, description, defaultRef string) (*Repo, error) {
129	if !ValidRepoID(id) {
130		return nil, ErrInvalidRepoID
131	}
132	if !ValidText(description, MaxDescLen) {
133		return nil, ErrInvalidText
134	}
135	if defaultRef == "" {
136		defaultRef = "refs/heads/main"
137	}
138	if !ValidRefName(defaultRef) {
139		return nil, ErrInvalidRefName
140	}
141	if f.repos.Has(id) {
142		return nil, ErrRepoExists
143	}
144	r := &Repo{
145		ID:                id,
146		Description:       description,
147		DefaultRef:        defaultRef,
148		CreatedAt:         height,
149		RequiredApprovals: 1,
150		members:           avl.NewTree(),
151		refs:              avl.NewTree(),
152		log:               avl.NewTree(),
153		issues:            avl.NewTree(),
154		changes:           avl.NewTree(),
155	}
156	r.members.Set(actor.String(), RoleOwner)
157	f.repos.Set(id, r)
158	return r, nil
159}
160
161// Fork registers newID as a fork of srcID and copies the parent's current refs
162// into the child's log, so the fork records exactly what it forked from. The
163// objects are not copied: they never were on chain: so the child inherits the
164// parent's mirrors as its initial fetch locators.
165func (f *Forge) Fork(actor address, height int64, srcID, newID string) (*Repo, error) {
166	src := f.Repo(srcID)
167	if src == nil {
168		return nil, ErrRepoNotFound
169	}
170	child, err := f.CreateRepo(actor, height, newID, src.Description, src.DefaultRef)
171	if err != nil {
172		return nil, err
173	}
174	child.ParentID = srcID
175	child.Mirrors = append([]string{}, src.Mirrors...)
176	note := "fork of " + srcID + " at seq " + strconv.FormatInt(src.nextSeq, 10)
177	if len(note) > MaxNoteLen {
178		note = "fork of " + srcID
179	}
180	n := 0
181	src.refs.Iterate("", "", func(key string, value any) bool {
182		ref := value.(*Ref)
183		child.appendLog(actor, height, ref.Name, "", ref.OID, KindCreate, 0, note)
184		child.refs.Set(ref.Name, &Ref{Name: ref.Name, OID: ref.OID, UpdatedAt: height, UpdatedBy: actor})
185		n++
186		return n >= 32 // a fork records a snapshot, not an unbounded copy
187	})
188	return child, nil
189}
190
191// Repo returns the repo, or nil.
192func (f *Forge) Repo(id string) *Repo {
193	v := f.repos.Get(id)
194	if v == nil {
195		return nil
196	}
197	return v.(*Repo)
198}
199
200// HasRepo reports whether the id is taken.
201func (f *Forge) HasRepo(id string) bool { return f.repos.Has(id) }
202
203// Size is the number of repos.
204func (f *Forge) Size() int { return f.repos.Size() }
205
206// IterateRepos walks repos in id order, newest-last, and stops when cb returns
207// true. offset/count page the walk; count <= 0 means "to the end".
208func (f *Forge) IterateRepos(offset, count int, cb func(*Repo) bool) {
209	if count <= 0 {
210		count = f.repos.Size()
211	}
212	f.repos.IterateByOffset(offset, count, func(_ string, value any) bool {
213		return cb(value.(*Repo))
214	})
215}
216
217// IterateNamespace walks the repos of one namespace in id order.
218func (f *Forge) IterateNamespace(ns string, cb func(*Repo) bool) {
219	f.repos.Iterate(ns+"/", ns+"0", func(_ string, value any) bool { // '0' is '/'+1
220		return cb(value.(*Repo))
221	})
222}
223
224// RoleOf returns the member's role, RoleNone when not a member.
225func (r *Repo) RoleOf(a address) Role {
226	v := r.members.Get(a.String())
227	if v == nil {
228		return RoleNone
229	}
230	return v.(Role)
231}
232
233// Can reports whether a holds at least the given role.
234func (r *Repo) Can(a address, min Role) bool { return r.RoleOf(a) >= min }
235
236// MemberCount is the number of members with an explicit role.
237func (r *Repo) MemberCount() int { return r.members.Size() }
238
239// IterateMembers walks members in address order.
240func (r *Repo) IterateMembers(cb func(addr string, role Role) bool) {
241	r.members.Iterate("", "", func(key string, value any) bool {
242		return cb(key, value.(Role))
243	})
244}
245
246// SetMember grants or revokes a role. Admins manage members; only an owner may
247// mint another owner, and the last owner cannot be demoted: a repo with no
248// owner is a repo nobody can ever unarchive.
249func (r *Repo) SetMember(actor address, target address, role Role) error {
250	if role < RoleNone || role > RoleOwner {
251		return ErrInvalidRole
252	}
253	if !r.Can(actor, RoleAdmin) {
254		return ErrUnauthorized
255	}
256	if role == RoleOwner && !r.Can(actor, RoleOwner) {
257		return ErrUnauthorized
258	}
259	cur := r.RoleOf(target)
260	if cur == RoleOwner && role != RoleOwner {
261		if !r.Can(actor, RoleOwner) {
262			return ErrUnauthorized // an admin cannot demote an owner
263		}
264		if r.ownerCount() == 1 {
265			return ErrLastOwner
266		}
267	}
268	if role == RoleNone {
269		r.members.Remove(target.String())
270		return nil
271	}
272	r.members.Set(target.String(), role)
273	return nil
274}
275
276func (r *Repo) ownerCount() int {
277	n := 0
278	r.members.Iterate("", "", func(_ string, value any) bool {
279		if value.(Role) == RoleOwner {
280			n++
281		}
282		return false
283	})
284	return n
285}
286
287// SetDescription updates the one-line description.
288func (r *Repo) SetDescription(actor address, description string) error {
289	if !r.Can(actor, RoleAdmin) {
290		return ErrUnauthorized
291	}
292	if !ValidText(description, MaxDescLen) {
293		return ErrInvalidText
294	}
295	r.Description = description
296	return nil
297}
298
299// SetMirrors replaces the fetch locators. The first one is the canonical
300// remote; the rest are fallbacks. The chain records them, it never fetches.
301func (r *Repo) SetMirrors(actor address, mirrors []string) error {
302	if !r.Can(actor, RoleMaintainer) {
303		return ErrUnauthorized
304	}
305	if len(mirrors) > MaxMirrors {
306		return ErrTooMany
307	}
308	for _, m := range mirrors {
309		if !ValidMirror(m) {
310			return ErrInvalidMirror
311		}
312	}
313	r.Mirrors = append([]string{}, mirrors...)
314	return nil
315}
316
317// SetDefaultRef points the repo at another default branch.
318func (r *Repo) SetDefaultRef(actor address, name string) error {
319	if !r.Can(actor, RoleMaintainer) {
320		return ErrUnauthorized
321	}
322	if !ValidRefName(name) {
323		return ErrInvalidRefName
324	}
325	r.DefaultRef = name
326	return nil
327}
328
329// SetPolicy sets the merge policy: how many approvals a change needs, and
330// whether the author's own approval counts.
331func (r *Repo) SetPolicy(actor address, requiredApprovals int, allowSelfApproval bool) error {
332	if !r.Can(actor, RoleAdmin) {
333		return ErrUnauthorized
334	}
335	if requiredApprovals < 0 || requiredApprovals > 16 {
336		return ErrTooMany
337	}
338	r.RequiredApprovals = requiredApprovals
339	r.AllowSelfApproval = allowSelfApproval
340	return nil
341}
342
343// SetArchived freezes (or unfreezes) the repo. An archived repo takes no
344// mutation except unarchiving: the log stays readable forever.
345func (r *Repo) SetArchived(actor address, archived bool) error {
346	if !r.Can(actor, RoleAdmin) {
347		return ErrUnauthorized
348	}
349	r.Archived = archived
350	return nil
351}
352
353// Counts for rendering.
354func (r *Repo) RefCount() int    { return r.refs.Size() }
355func (r *Repo) IssueCount() int  { return r.issues.Size() }
356func (r *Repo) ChangeCount() int { return r.changes.Size() }
357
358// seqKey zero-pads an id to a fixed width so avl keys sort numerically.
359// ufmt has no width flags in gno, so the padding is done by hand: "%016d"
360// silently returns the bare number and would sort 10 before 2.
361func seqKey(n int64) string {
362	s := strconv.FormatInt(n, 10)
363	if len(s) >= 16 {
364		return s
365	}
366	return strings.Repeat("0", 16-len(s)) + s
367}