Kind Pure package
Name v1
Namespace moul / forge
Exported functions n/a — not supported for pure packages by the node (vm/qfuncs)
Module gno.land/p/moul/forge/v1
gno 0.9 forge.gno gno
⧉
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 not supported for pure packages by the node (vm/qfuncs)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
// 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.
31 package forge
32
33 import (
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.
42 type Role int
43
44 const (
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.
54 func (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.
72 func 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.
91 type 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.
101 func 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.
107 type 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.
140 func (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.
182 func (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.
209 func (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.
218 func (f *Forge) HasRepo(id string ) bool { return f.repos.Has(id) }
219
220 // Size is the number of repos.
221 func (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".
225 func (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.
235 func (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.
242 func (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.
251 func (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.
254 func (r *Repo) MemberCount() int { return r.members.Size() }
255
256 // IterateMembers walks members in address order.
257 func (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.
266 func (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
293 func (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.
305 func (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.
318 func (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.
335 func (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.
348 func (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.
362 func (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.
371 func (r *Repo) RefCount() int { return r.refs.Size() }
372 func (r *Repo) IssueCount() int { return r.issues.Size() }
373 func (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.
378 func 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 }
385