Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}