Kind Pure package
Name v0
Namespace moul / forge
Exported functions n/a — not supported for pure packages by the node (vm/qfuncs)
Module gno.land/p/moul/forge/v0
gno 0.9 issue.gno gno
⧉
1 package forge 2 3 import "gno.land/p/nt/avl/v0" 4 5 // Issue is a discussion thread bound to a repo. Anyone with an address may open 6 // one: the spam gate is not a moderator, it is that the author pays gas and 7 // locks the storage deposit for every byte they write. not supported for pure packages by the node (vm/qfuncs)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
8
type Issue struct {
9 ID int64
10 Title string
11 Body string
12 Author address
13 Open bool
14 Labels [] string
15 CreatedAt int64
16 UpdatedAt int64
17
18 comments *avl.Tree // padded id -> *Comment
19 nextComment int64
20 }
21
22 // Comment is one reply, on an issue or on a change request.
23 type Comment struct {
24 ID int64
25 Author address
26 Body string
27 CreatedAt int64
28 }
29
30 // OpenIssue files an issue. Permissionless by design.
31 func (r *Repo) OpenIssue(actor address, height int64 , title, body string , labels [] string ) (*Issue, error ) {
32 if r.Archived {
33 return nil , ErrRepoArchived
34 }
35 if title == "" || !ValidLine(title, MaxTitleLen) {
36 return nil , ErrInvalidText
37 }
38 if !ValidText(body, MaxBodyLen) {
39 return nil , ErrInvalidText
40 }
41 if err := checkLabels(labels); err != nil {
42 return nil , err
43 }
44 i := &Issue{
45 ID: r.nextIssue,
46 Title: title,
47 Body: body,
48 Author: actor,
49 Open: true ,
50 Labels: append([] string {}, labels...),
51 CreatedAt: height,
52 UpdatedAt: height,
53 comments: avl.NewTree(),
54 }
55 r.issues.Set(seqKey(i.ID), i)
56 r.nextIssue++
57 return i, nil
58 }
59
60 // CommentIssue appends a reply. Closed issues still take comments (closing is a
61 // triage state, not a gag); an archived repo takes none.
62 func (r *Repo) CommentIssue(actor address, height, id int64 , body string ) (*Comment, error ) {
63 if r.Archived {
64 return nil , ErrRepoArchived
65 }
66 i := r.Issue(id)
67 if i == nil {
68 return nil , ErrIssueNotFound
69 }
70 if body == "" || !ValidText(body, MaxCommentLen) {
71 return nil , ErrInvalidText
72 }
73 c := &Comment{ID: i.nextComment, Author: actor, Body: body, CreatedAt: height}
74 i.comments.Set(seqKey(c.ID), c)
75 i.nextComment++
76 i.UpdatedAt = height
77 return c, nil
78 }
79
80 // SetIssueOpen closes or reopens an issue. The author can always close their
81 // own; maintainers can close anyone's.
82 func (r *Repo) SetIssueOpen(actor address, height, id int64 , open bool ) error {
83 if r.Archived {
84 return ErrRepoArchived
85 }
86 i := r.Issue(id)
87 if i == nil {
88 return ErrIssueNotFound
89 }
90 if i.Author != actor && !r.Can(actor, RoleMaintainer) {
91 return ErrUnauthorized
92 }
93 i.Open = open
94 i.UpdatedAt = height
95 return nil
96 }
97
98 // SetIssueLabels replaces an issue's labels. Triage is a maintainer action.
99 func (r *Repo) SetIssueLabels(actor address, height, id int64 , labels [] string ) error {
100 if r.Archived {
101 return ErrRepoArchived
102 }
103 i := r.Issue(id)
104 if i == nil {
105 return ErrIssueNotFound
106 }
107 if !r.Can(actor, RoleMaintainer) {
108 return ErrUnauthorized
109 }
110 if err := checkLabels(labels); err != nil {
111 return err
112 }
113 i.Labels = append([] string {}, labels...)
114 i.UpdatedAt = height
115 return nil
116 }
117
118 // Issue returns an issue by id, or nil.
119 func (r *Repo) Issue(id int64 ) *Issue {
120 v := r.issues.Get(seqKey(id))
121 if v == nil {
122 return nil
123 }
124 return v.(*Issue)
125 }
126
127 // IterateIssues walks issues newest-first.
128 func (r *Repo) IterateIssues(offset, count int , cb func (*Issue) bool ) {
129 if count <= 0 {
130 count = r.issues.Size()
131 }
132 r.issues.ReverseIterateByOffset(offset, count, func (_ string , value any ) bool {
133 return cb(value.(*Issue))
134 })
135 }
136
137 // OpenIssueCount counts issues still open.
138 func (r *Repo) OpenIssueCount() int {
139 n := 0
140 r.issues.Iterate( "" , "" , func (_ string , value any ) bool {
141 if value.(*Issue).Open {
142 n++
143 }
144 return false
145 })
146 return n
147 }
148
149 // CommentCount is the number of replies on the issue.
150 func (i *Issue) CommentCount() int { return i.comments.Size() }
151
152 // IterateComments walks replies oldest-first.
153 func (i *Issue) IterateComments(offset, count int , cb func (*Comment) bool ) {
154 if count <= 0 {
155 count = i.comments.Size()
156 }
157 i.comments.IterateByOffset(offset, count, func (_ string , value any ) bool {
158 return cb(value.(*Comment))
159 })
160 }
161
162 func checkLabels(labels [] string ) error {
163 if len(labels) > MaxLabels {
164 return ErrTooMany
165 }
166 for _, l := range labels {
167 if !ValidLabel(l) {
168 return ErrInvalidText
169 }
170 }
171 return nil
172 }
173