PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/moul/forge/v1

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v1
Namespace
moul / forge
Files
18 (README)(gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/moul/forge/v1
gno
0.9

Files (18)

  • README.mdmarkdown
  • gnomod.tomltoml
  • change.gnogno
  • errors.gnogno
  • forge.gnogno
  • issue.gnogno
  • pkg.gnogno
  • ref.gnogno
  • release.gnogno
  • validate.gnogno
  • change_test.gnogno
  • forge_test.gnogno
  • helpers_test.gnogno
  • issue_test.gnogno
  • pkg_test.gnogno
  • ref_test.gnogno
  • release_test.gnogno
  • validate_test.gnogno
  • forge.gnogno
    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 (pull3// requests), with no chain imports of its own.4//5// What it does NOT do, on purpose: store blobs, trees or packfiles. Git objects6// stay wherever git already puts them (a mirror, an IPFS CID, a peer) and this7

    Functions

    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.
    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}
    385