Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

refactor

c5902451905acdaa478f8476844ccd4a510d9ca9 · Keenan Nemetz · 16d ago · parent ec2aba4b

internal/issues/write.go → metadata/issue_write.go +35−35

@@ -1,4 +1,4 @@
1 package issues
1 package metadata
2 2
3 3 import (
4 4 "errors"
@@ -8,8 +8,8 @@ import (
8 8 "strings"
9 9 "time"
10 10
11 "github.com/rodent-software/vole/internal/frontmatter"
12 "github.com/rodent-software/vole/internal/gitx"
11 "github.com/rodent-software/vole/core/git"
12 voleid "github.com/rodent-software/vole/core/id"
13 13 )
14 14
15 15 // ErrNoChange is returned by Update when a mutator decides the entity already
@@ -18,8 +18,8 @@ import (
18 18 var ErrNoChange = errors.New("no change")
19 19
20 20 // New fills in the fields a caller should not have to get right.
21 func New(title, body, author string, labels, assignees []string, now time.Time) (*Issue, error) {
22 id, err := NewULID(now)
21 func NewIssue(title, body, author string, labels, assignees []string, now time.Time) (*Issue, error) {
22 id, err := voleid.New(now)
23 23 if err != nil {
24 24 return nil, err
25 25 }
@@ -36,8 +36,8 @@ func New(title, body, author string, labels, assignees []string, now time.Time)
36 36 }
37 37
38 38 // Comment builds a comment with a fresh id.
39 func NewComment(body, author string, now time.Time) (*Comment, error) {
40 id, err := NewULID(now)
39 func NewIssueComment(body, author string, now time.Time) (*Comment, error) {
40 id, err := voleid.New(now)
41 41 if err != nil {
42 42 return nil, err
43 43 }
@@ -45,16 +45,16 @@ func NewComment(body, author string, now time.Time) (*Comment, error) {
45 45 }
46 46
47 47 // Create writes a new issue onto the issues ref, creating the branch if needed.
48 func Create(g *gitx.Repo, iss *Issue, author string) error {
49 if err := validateForWrite(iss); err != nil {
48 func CreateIssue(g *git.Repo, iss *Issue, author string) error {
49 if err := validateIssueForWrite(iss); err != nil {
50 50 return err
51 51 }
52 52 raw, err := renderIssue(iss)
53 53 if err != nil {
54 54 return err
55 55 }
56 base := fmt.Sprintf("%s/%s/", Dir, iss.ID)
57 _, err = gitx.CommitFiles(g, Ref, func(files map[string][]byte) error {
56 base := fmt.Sprintf("%s/%s/", IssuesDir, iss.ID)
57 _, err = git.CommitFiles(g, Ref, func(files map[string][]byte) error {
58 58 if _, exists := files[base+issueName]; exists {
59 59 return fmt.Errorf("issues: %s already exists", iss.ID)
60 60 }
@@ -65,8 +65,8 @@ func Create(g *gitx.Repo, iss *Issue, author string) error {
65 65 }
66 66
67 67 // Update applies mutate to an existing issue and commits the result.
68 func Update(g *gitx.Repo, id string, mutate func(*Issue) error, actor string) (*Issue, error) {
69 list, err := Load(g, Ref)
68 func UpdateIssue(g *git.Repo, id string, mutate func(*Issue) error, actor string) (*Issue, error) {
69 list, err := LoadIssues(g, Ref)
70 70 if err != nil {
71 71 return nil, err
72 72 }
@@ -86,15 +86,15 @@ func Update(g *gitx.Repo, id string, mutate func(*Issue) error, actor string) (*
86 86 }
87 87 return nil, err
88 88 }
89 if err := validateForWrite(target); err != nil {
89 if err := validateIssueForWrite(target); err != nil {
90 90 return nil, err
91 91 }
92 92 raw, err := renderIssue(target)
93 93 if err != nil {
94 94 return nil, err
95 95 }
96 base := fmt.Sprintf("%s/%s/", Dir, target.ID)
97 _, err = gitx.CommitFiles(g, Ref, func(files map[string][]byte) error {
96 base := fmt.Sprintf("%s/%s/", IssuesDir, target.ID)
97 _, err = git.CommitFiles(g, Ref, func(files map[string][]byte) error {
98 98 files[base+issueName] = raw
99 99 return nil
100 100 }, fmt.Sprintf("issue: update %s", target.ID), actorOr(actor, target.Author))
@@ -105,8 +105,8 @@ func Update(g *gitx.Repo, id string, mutate func(*Issue) error, actor string) (*
105 105 }
106 106
107 107 // AddComment appends a comment file to an existing issue.
108 func AddComment(g *gitx.Repo, id string, c *Comment, actor string) error {
109 list, err := Load(g, Ref)
108 func AddIssueComment(g *git.Repo, id string, c *Comment, actor string) error {
109 list, err := LoadIssues(g, Ref)
110 110 if err != nil {
111 111 return err
112 112 }
@@ -133,8 +133,8 @@ func AddComment(g *gitx.Repo, id string, c *Comment, actor string) error {
133 133 if err != nil {
134 134 return err
135 135 }
136 path := fmt.Sprintf("%s/%s/%s/%s.md", Dir, id, commentsDir, c.ID)
137 _, err = gitx.CommitFiles(g, Ref, func(files map[string][]byte) error {
136 path := fmt.Sprintf("%s/%s/%s/%s.md", IssuesDir, id, commentsDir, c.ID)
137 _, err = git.CommitFiles(g, Ref, func(files map[string][]byte) error {
138 138 if _, exists := files[path]; exists {
139 139 return fmt.Errorf("issues: comment %s already exists", c.ID)
140 140 }
@@ -153,8 +153,8 @@ func AddComment(g *gitx.Repo, id string, c *Comment, actor string) error {
153 153 // Ownership is not checked here: the person with the key can only be themselves.
154 154 // Only the named author (or an admin, audited) may change someone else's comment,
155 155 // and that rule belongs at the push boundary where the account is known.
156 func EditComment(g *gitx.Repo, issueID, commentID, body, actor string) (*Comment, error) {
157 c, path, err := findComment(g, issueID, commentID)
156 func EditIssueComment(g *git.Repo, issueID, commentID, body, actor string) (*Comment, error) {
157 c, path, err := findIssueComment(g, issueID, commentID)
158 158 if err != nil {
159 159 return nil, err
160 160 }
@@ -167,7 +167,7 @@ func EditComment(g *gitx.Repo, issueID, commentID, body, actor string) (*Comment
167 167 if err != nil {
168 168 return nil, err
169 169 }
170 _, err = gitx.CommitFiles(g, Ref, func(files map[string][]byte) error {
170 _, err = git.CommitFiles(g, Ref, func(files map[string][]byte) error {
171 171 if _, ok := files[path]; !ok {
172 172 return fmt.Errorf("issues: no comment %s", commentID)
173 173 }
@@ -183,12 +183,12 @@ func EditComment(g *gitx.Repo, issueID, commentID, body, actor string) (*Comment
183 183 // DeleteComment removes a comment's file. The text stays in history — deleting is
184 184 // not redaction, which is still an admin force-push — but the thread no longer
185 185 // shows it. A flat thread keyed by ULID has no replies to orphan.
186 func DeleteComment(g *gitx.Repo, issueID, commentID, actor string) (*Comment, error) {
187 c, path, err := findComment(g, issueID, commentID)
186 func DeleteIssueComment(g *git.Repo, issueID, commentID, actor string) (*Comment, error) {
187 c, path, err := findIssueComment(g, issueID, commentID)
188 188 if err != nil {
189 189 return nil, err
190 190 }
191 _, err = gitx.CommitFiles(g, Ref, func(files map[string][]byte) error {
191 _, err = git.CommitFiles(g, Ref, func(files map[string][]byte) error {
192 192 if _, ok := files[path]; !ok {
193 193 return fmt.Errorf("issues: no comment %s", commentID)
194 194 }
@@ -203,8 +203,8 @@ func DeleteComment(g *gitx.Repo, issueID, commentID, actor string) (*Comment, er
203 203
204 204 // findComment locates one comment on the branch and returns it with the path it
205 205 // lives at, which is the file the edit or delete rewrites.
206 func findComment(g *gitx.Repo, issueID, commentID string) (*Comment, string, error) {
207 list, err := Load(g, Ref)
206 func findIssueComment(g *git.Repo, issueID, commentID string) (*Comment, string, error) {
207 list, err := LoadIssues(g, Ref)
208 208 if err != nil {
209 209 return nil, "", err
210 210 }
@@ -215,7 +215,7 @@ func findComment(g *gitx.Repo, issueID, commentID string) (*Comment, string, err
215 215 for i := range iss.Comments {
216 216 if iss.Comments[i].ID == commentID {
217 217 c := iss.Comments[i]
218 return &c, CommentPath(issueID, commentID), nil
218 return &c, IssueCommentPath(issueID, commentID), nil
219 219 }
220 220 }
221 221 return nil, "", fmt.Errorf("issues: no comment %s on %s", commentID, issueID)
@@ -226,7 +226,7 @@ func findComment(g *gitx.Repo, issueID, commentID string) (*Comment, string, err
226 226 // RenderComment writes a comment as frontmatter plus body. Exported because a
227 227 // comment on a request is the same file shape, read and written by the same code.
228 228 func RenderComment(c *Comment) ([]byte, error) {
229 d := frontmatter.New()
229 d := NewDoc()
230 230 d.Word("author", c.Author)
231 231 d.Word("created", c.Created.UTC().Format(time.RFC3339))
232 232 if c.Anchor != nil {
@@ -245,7 +245,7 @@ func RenderComment(c *Comment) ([]byte, error) {
245 245 // description as the body. One path per issue means one file to diff, blame or
246 246 // log, and no way for the two halves to disagree.
247 247 func renderIssue(iss *Issue) ([]byte, error) {
248 d := frontmatter.New()
248 d := NewDoc()
249 249 d.Text("title", iss.Title)
250 250 d.Word("state", iss.State)
251 251 d.Word("author", iss.Author)
@@ -259,15 +259,15 @@ func renderIssue(iss *Issue) ([]byte, error) {
259 259 return d.Bytes(strings.TrimRight(iss.Description, "\n"))
260 260 }
261 261
262 // validateForWrite runs the same checks the push boundary applies, so the CLI
262 // validateIssueForWrite runs the same checks the push boundary applies, so the CLI
263 263 // fails locally instead of after a round trip.
264 func validateForWrite(iss *Issue) error {
264 func validateIssueForWrite(iss *Issue) error {
265 265 var problems []string
266 266 if strings.TrimSpace(iss.Title) == "" {
267 267 problems = append(problems, "title is required")
268 268 }
269 if len(iss.Title) > maxTitleLen {
270 problems = append(problems, fmt.Sprintf("title is %d bytes, limit %d", len(iss.Title), maxTitleLen))
269 if len(iss.Title) > maxIssueTitleLen {
270 problems = append(problems, fmt.Sprintf("title is %d bytes, limit %d", len(iss.Title), maxIssueTitleLen))
271 271 }
272 272 if iss.State != "open" && iss.State != "closed" {
273 273 problems = append(problems, fmt.Sprintf("state must be open or closed, got %q", iss.State))