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.

release.gno

4.69 Kb · 162 lines
  1package forge
  2
  3import "strings"
  4
  5// Size caps for a release.
  6const (
  7	MaxTagLen     = 64
  8	MaxRelNameLen = 200
  9	MaxNotesLen   = 8192
 10)
 11
 12// Release is a named point in a repo's history, with notes.
 13//
 14// A tag ref already says "this name points at this object". A release says why
 15// anyone should care: what changed, whether it is ready, and which deployed
 16// package paths came out of it. That last part is the one an explorer wants,
 17// and it is also the only honest bridge between a git object id and an on-chain
 18// deploy, because the chain cannot verify either against the other.
 19type Release struct {
 20	Tag        string // "v1.2.0", the key; immutable once cut
 21	Name       string // human title, may be empty
 22	Notes      string // markdown, may be empty
 23	OID        string // the object this release names
 24	Ref        string // the tag ref, "" if the release does not have one
 25	Prerelease bool
 26	CreatedAt  int64 // block height
 27	CreatedBy  address
 28	Yanked     bool // withdrawn; kept, because a version that existed cannot un-exist
 29	YankReason string
 30}
 31
 32// ValidTag reports whether s is usable as a release tag.
 33//
 34// Not semver-enforcing: gno.land's own packages version as v0, v1 with no minor
 35// or patch, and a forge that refused those would refuse its own ecosystem.
 36// Ordering is therefore lexical over a padded form, not semantic, and
 37// LatestRelease says so.
 38func ValidTag(s string) bool {
 39	if s == "" || len(s) > MaxTagLen {
 40		return false
 41	}
 42	if strings.Contains(s, "..") || strings.Contains(s, "/") {
 43		return false
 44	}
 45	for i := 0; i < len(s); i++ {
 46		c := s[i]
 47		ok := (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
 48			(c >= '0' && c <= '9') || c == '.' || c == '-' || c == '_' || c == '+'
 49		if !ok {
 50			return false
 51		}
 52	}
 53	return true
 54}
 55
 56// CutRelease records a release. The tag is the key and cannot be re-cut: a
 57// consumer that pinned v1.2.0 and got different bytes later has been lied to,
 58// and the whole point of a tag is that it does not move. Withdrawing one is
 59// YankRelease, which leaves it visible.
 60func (r *Repo) CutRelease(actor address, height int64, tag, name, notes, oid, ref string, prerelease bool) (*Release, error) {
 61	if r.Archived {
 62		return nil, ErrRepoArchived
 63	}
 64	if !r.Can(actor, RoleWriter) {
 65		return nil, ErrUnauthorized
 66	}
 67	if !ValidTag(tag) {
 68		return nil, ErrInvalidTag
 69	}
 70	if !ValidText(name, MaxRelNameLen) || !ValidText(notes, MaxNotesLen) {
 71		return nil, ErrInvalidText
 72	}
 73	if oid != "" && !ValidOID(oid) {
 74		return nil, ErrInvalidOID
 75	}
 76	if ref != "" && !ValidRefName(ref) {
 77		return nil, ErrInvalidRefName
 78	}
 79	if r.releases.Has(tag) {
 80		return nil, ErrReleaseExists
 81	}
 82
 83	rel := &Release{
 84		Tag:        tag,
 85		Name:       name,
 86		Notes:      notes,
 87		OID:        oid,
 88		Ref:        ref,
 89		Prerelease: prerelease,
 90		CreatedAt:  height,
 91		CreatedBy:  actor,
 92	}
 93	r.releases.Set(tag, rel)
 94	r.releaseOrder.Set(seqKey(int64(r.releases.Size())), tag)
 95	return rel, nil
 96}
 97
 98// YankRelease marks a release withdrawn without deleting it.
 99//
100// Deleting would make a pinned version silently vanish, which reads to a
101// consumer as "this was never published" rather than "do not use this".
102func (r *Repo) YankRelease(actor address, tag, reason string) error {
103	if !r.Can(actor, RoleWriter) {
104		return ErrUnauthorized
105	}
106	if !ValidText(reason, MaxNoteLen) {
107		return ErrInvalidText
108	}
109	v := r.releases.Get(tag)
110	if v == nil {
111		return ErrReleaseNotFound
112	}
113	rel := v.(*Release)
114	rel.Yanked = true
115	rel.YankReason = reason
116	return nil
117}
118
119// Release returns one release by tag, or nil.
120func (r *Repo) Release(tag string) *Release {
121	v := r.releases.Get(tag)
122	if v == nil {
123		return nil
124	}
125	return v.(*Release)
126}
127
128// ReleaseCount is how many releases this repo has cut, yanked ones included.
129func (r *Repo) ReleaseCount() int { return r.releases.Size() }
130
131// IterateReleases walks releases newest-cut first.
132//
133// Cut order, not tag order: tags are not required to be semver here (gno.land's
134// own packages are v0 and v1), so there is no total order to sort by that would
135// not be wrong for somebody. When they were cut is a fact the forge does know.
136func (r *Repo) IterateReleases(offset, count int, cb func(*Release) bool) {
137	if count <= 0 {
138		count = r.releaseOrder.Size()
139	}
140	r.releaseOrder.ReverseIterateByOffset(offset, count, func(_ string, v any) bool {
141		rel := r.releases.Get(v.(string))
142		if rel == nil {
143			return false
144		}
145		return cb(rel.(*Release))
146	})
147}
148
149// LatestRelease is the most recently cut release that is neither a prerelease
150// nor yanked, or nil. "Most recently cut", never "highest version": see
151// IterateReleases.
152func (r *Repo) LatestRelease() *Release {
153	var out *Release
154	r.IterateReleases(0, 0, func(rel *Release) bool {
155		if rel.Prerelease || rel.Yanked {
156			return false
157		}
158		out = rel
159		return true
160	})
161	return out
162}