forge.gno
11.05 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/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}
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}