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/requests/write.go → metadata/request_write.go +46−63

@@ -1,20 +1,19 @@
1 package requests
1 package metadata
2 2
3 3 import (
4 4 "fmt"
5 5 "strings"
6 6 "time"
7 7
8 "github.com/rodent-software/vole/internal/frontmatter"
9 "github.com/rodent-software/vole/internal/gitx"
10 voleid "github.com/rodent-software/vole/internal/id"
11 "github.com/rodent-software/vole/internal/issues"
8 "github.com/rodent-software/vole/core/git"
9 voleid "github.com/rodent-software/vole/core/id"
10 "github.com/rodent-software/vole/internal/format"
12 11 )
13 12
14 13 // New builds a request with a fresh ULID. Nothing is assigned server-side, so
15 14 // concurrent authors never contend.
16 func New(title, body, author, authorID, base, head string, now time.Time) (*Request, error) {
17 id, err := issues.NewULID(now)
15 func NewRequest(title, body, author, authorID, base, head string, now time.Time) (*Request, error) {
16 id, err := voleid.New(now)
18 17 if err != nil {
19 18 return nil, err
20 19 }
@@ -29,18 +28,18 @@ func New(title, body, author, authorID, base, head string, now time.Time) (*Requ
29 28 Created: now.UTC(),
30 29 Description: body,
31 30 }
32 if err := validateForWrite(r); err != nil {
31 if err := validateRequestForWrite(r); err != nil {
33 32 return nil, err
34 33 }
35 34 return r, nil
36 35 }
37 36
38 func validateForWrite(r *Request) error {
37 func validateRequestForWrite(r *Request) error {
39 38 if r.Title == "" {
40 39 return fmt.Errorf("requests: title is required")
41 40 }
42 if len(r.Title) > maxTitleLen {
43 return fmt.Errorf("requests: title is %d bytes, limit %d", len(r.Title), maxTitleLen)
41 if len(r.Title) > maxRequestTitleLen {
42 return fmt.Errorf("requests: title is %d bytes, limit %d", len(r.Title), maxRequestTitleLen)
44 43 }
45 44 if r.Author == "" {
46 45 return fmt.Errorf("requests: author is required")
@@ -60,7 +59,7 @@ func validateForWrite(r *Request) error {
60 59 // renderRequest writes a request as one file: the head names the branch and the
61 60 // commit under review, the body says why.
62 61 func renderRequest(r *Request) ([]byte, error) {
63 d := frontmatter.New()
62 d := NewDoc()
64 63 d.Text("title", r.Title)
65 64 d.Word("state", r.State)
66 65 d.Word("author", r.Author)
@@ -73,7 +72,7 @@ func renderRequest(r *Request) ([]byte, error) {
73 72
74 73 // renderApproval writes an approval: three constrained fields and no body.
75 74 func renderApproval(a Approval) ([]byte, error) {
76 d := frontmatter.New()
75 d := NewDoc()
77 76 d.Word("base", a.Base)
78 77 d.Word("head", a.Head)
79 78 d.Word("created", a.Created.UTC().Format(time.RFC3339))
@@ -81,16 +80,16 @@ func renderApproval(a Approval) ([]byte, error) {
81 80 }
82 81
83 82 // Create writes a new request onto the metadata branch.
84 func Create(g *gitx.Repo, r *Request, actor string) error {
85 if err := validateForWrite(r); err != nil {
83 func CreateRequest(g *git.Repo, r *Request, actor string) error {
84 if err := validateRequestForWrite(r); err != nil {
86 85 return err
87 86 }
88 87 raw, err := renderRequest(r)
89 88 if err != nil {
90 89 return err
91 90 }
92 base := fmt.Sprintf("%s/%s/", Dir, r.ID)
93 _, err = gitx.CommitFiles(g, Ref, func(files map[string][]byte) error {
91 base := fmt.Sprintf("%s/%s/", RequestsDir, r.ID)
92 _, err = git.CommitFiles(g, Ref, func(files map[string][]byte) error {
94 93 if _, exists := files[base+requestName]; exists {
95 94 return fmt.Errorf("requests: %s already exists", r.ID)
96 95 }
@@ -101,8 +100,8 @@ func Create(g *gitx.Repo, r *Request, actor string) error {
101 100 }
102 101
103 102 // Get returns one request by id.
104 func Get(g *gitx.Repo, id string) (*Request, error) {
105 list, err := Load(g, Ref)
103 func GetRequest(g *git.Repo, id string) (*Request, error) {
104 list, err := LoadRequests(g, Ref)
106 105 if err != nil {
107 106 return nil, err
108 107 }
@@ -116,16 +115,16 @@ func Get(g *gitx.Repo, id string) (*Request, error) {
116 115
117 116 // SetHead repoints a request at a new commit. The approvals stay in the tree but
118 117 // stop matching, because they name the commit they reviewed.
119 func SetHead(g *gitx.Repo, id, head, actor string) (*Request, error) {
118 func SetRequestHead(g *git.Repo, id, head, actor string) (*Request, error) {
120 119 var out *Request
121 base := fmt.Sprintf("%s/%s/", Dir, id)
122 _, err := gitx.CommitFiles(g, Ref, func(files map[string][]byte) error {
120 base := fmt.Sprintf("%s/%s/", RequestsDir, id)
121 _, err := git.CommitFiles(g, Ref, func(files map[string][]byte) error {
123 122 r, err := loadFrom(files, id)
124 123 if err != nil {
125 124 return err
126 125 }
127 126 r.Head = head
128 if err := validateForWrite(r); err != nil {
127 if err := validateRequestForWrite(r); err != nil {
129 128 return err
130 129 }
131 130 raw, err := renderRequest(r)
@@ -135,18 +134,18 @@ func SetHead(g *gitx.Repo, id, head, actor string) (*Request, error) {
135 134 files[base+requestName] = raw
136 135 out = r
137 136 return nil
138 }, fmt.Sprintf("request: %s head -> %s", id, shortSHA(head)), actor)
137 }, fmt.Sprintf("request: %s head -> %s", id, format.SHA(head)), actor)
139 138 return out, err
140 139 }
141 140
142 141 // Approve records principal's approval of the request's current head and base.
143 func Approve(g *gitx.Repo, id, principal string, now time.Time, actor string) (*Request, error) {
142 func ApproveRequest(g *git.Repo, id, principal string, now time.Time, actor string) (*Request, error) {
144 143 if !voleid.Valid(principal) {
145 144 return nil, fmt.Errorf("requests: %q is not an account id", principal)
146 145 }
147 146 var out *Request
148 base := fmt.Sprintf("%s/%s/", Dir, id)
149 _, err := gitx.CommitFiles(g, Ref, func(files map[string][]byte) error {
147 base := fmt.Sprintf("%s/%s/", RequestsDir, id)
148 _, err := git.CommitFiles(g, Ref, func(files map[string][]byte) error {
150 149 r, err := loadFrom(files, id)
151 150 if err != nil {
152 151 return err
@@ -174,15 +173,15 @@ func Approve(g *gitx.Repo, id, principal string, now time.Time, actor string) (*
174 173 }
175 174
176 175 // SetState moves a request between open and closed.
177 func SetState(g *gitx.Repo, id, state, actor string) (*Request, error) {
176 func SetRequestState(g *git.Repo, id, state, actor string) (*Request, error) {
178 177 switch state {
179 178 case "open", "merged", "closed":
180 179 default:
181 180 return nil, fmt.Errorf("requests: state must be open or closed")
182 181 }
183 182 var out *Request
184 base := fmt.Sprintf("%s/%s/", Dir, id)
185 _, err := gitx.CommitFiles(g, Ref, func(files map[string][]byte) error {
183 base := fmt.Sprintf("%s/%s/", RequestsDir, id)
184 _, err := git.CommitFiles(g, Ref, func(files map[string][]byte) error {
186 185 r, err := loadFrom(files, id)
187 186 if err != nil {
188 187 return err
@@ -200,8 +199,8 @@ func SetState(g *gitx.Repo, id, state, actor string) (*Request, error) {
200 199 }
201 200
202 201 // NewComment builds a comment on a request with a fresh id.
203 func NewComment(body, author string, now time.Time) (*Comment, error) {
204 commentID, err := issues.NewULID(now)
202 func NewRequestComment(body, author string, now time.Time) (*Comment, error) {
203 commentID, err := voleid.New(now)
205 204 if err != nil {
206 205 return nil, err
207 206 }
@@ -225,7 +224,7 @@ func validateComment(c *Comment) error {
225 224 // AddComment appends a comment to a request. It is the same file shape a comment
226 225 // on an issue has, written by the same code, so review discussion needs no format
227 226 // of its own.
228 func AddComment(g *gitx.Repo, requestID string, c *Comment, actor string) error {
227 func AddRequestComment(g *git.Repo, requestID string, c *Comment, actor string) error {
229 228 if strings.TrimSpace(c.Author) == "" {
230 229 c.Author = actor
231 230 }
@@ -235,12 +234,12 @@ func AddComment(g *gitx.Repo, requestID string, c *Comment, actor string) error
235 234 if err := validateComment(c); err != nil {
236 235 return err
237 236 }
238 raw, err := issues.RenderComment(c)
237 raw, err := RenderComment(c)
239 238 if err != nil {
240 239 return err
241 240 }
242 path := CommentPath(requestID, c.ID)
243 _, err = gitx.CommitFiles(g, Ref, func(files map[string][]byte) error {
241 path := RequestCommentPath(requestID, c.ID)
242 _, err = git.CommitFiles(g, Ref, func(files map[string][]byte) error {
244 243 if _, err := loadFrom(files, requestID); err != nil {
245 244 return err
246 245 }
@@ -256,8 +255,8 @@ func AddComment(g *gitx.Repo, requestID string, c *Comment, actor string) error
256 255 // EditComment rewrites a comment's body in place. The path stays, so the comment
257 256 // keeps its id and one `git log -p -- <path>` answers what it used to say; there
258 257 // is no `edited` field, for the reason nothing else in this format has one.
259 func EditComment(g *gitx.Repo, requestID, commentID, body, actor string) (*Comment, error) {
260 c, path, err := findComment(g, requestID, commentID)
258 func EditRequestComment(g *git.Repo, requestID, commentID, body, actor string) (*Comment, error) {
259 c, path, err := findRequestComment(g, requestID, commentID)
261 260 if err != nil {
262 261 return nil, err
263 262 }
@@ -265,11 +264,11 @@ func EditComment(g *gitx.Repo, requestID, commentID, body, actor string) (*Comme
265 264 if err := validateComment(c); err != nil {
266 265 return nil, err
267 266 }
268 raw, err := issues.RenderComment(c)
267 raw, err := RenderComment(c)
269 268 if err != nil {
270 269 return nil, err
271 270 }
272 _, err = gitx.CommitFiles(g, Ref, func(files map[string][]byte) error {
271 _, err = git.CommitFiles(g, Ref, func(files map[string][]byte) error {
273 272 if _, ok := files[path]; !ok {
274 273 return fmt.Errorf("requests: no comment %s", commentID)
275 274 }
@@ -284,12 +283,12 @@ func EditComment(g *gitx.Repo, requestID, commentID, body, actor string) (*Comme
284 283
285 284 // DeleteComment removes a comment's file. The text stays in history: deleting is
286 285 // not redaction, which is an admin force-push.
287 func DeleteComment(g *gitx.Repo, requestID, commentID, actor string) (*Comment, error) {
288 c, path, err := findComment(g, requestID, commentID)
286 func DeleteRequestComment(g *git.Repo, requestID, commentID, actor string) (*Comment, error) {
287 c, path, err := findRequestComment(g, requestID, commentID)
289 288 if err != nil {
290 289 return nil, err
291 290 }
292 _, err = gitx.CommitFiles(g, Ref, func(files map[string][]byte) error {
291 _, err = git.CommitFiles(g, Ref, func(files map[string][]byte) error {
293 292 if _, ok := files[path]; !ok {
294 293 return fmt.Errorf("requests: no comment %s", commentID)
295 294 }
@@ -303,29 +302,20 @@ func DeleteComment(g *gitx.Repo, requestID, commentID, actor string) (*Comment,
303 302 }
304 303
305 304 // findComment locates one comment and the path it lives at.
306 func findComment(g *gitx.Repo, requestID, commentID string) (*Comment, string, error) {
307 r, err := Get(g, requestID)
305 func findRequestComment(g *git.Repo, requestID, commentID string) (*Comment, string, error) {
306 r, err := GetRequest(g, requestID)
308 307 if err != nil {
309 308 return nil, "", err
310 309 }
311 310 for i := range r.Comments {
312 311 if r.Comments[i].ID == commentID {
313 312 c := r.Comments[i]
314 return &c, CommentPath(requestID, commentID), nil
313 return &c, RequestCommentPath(requestID, commentID), nil
315 314 }
316 315 }
317 316 return nil, "", fmt.Errorf("requests: no comment %s on %s", commentID, requestID)
318 317 }
319 318
320 // actorOr keeps a commit authored by the person whose words changed, falling back
321 // to whoever is pushing when the record has no name.
322 func actorOr(primary, fallback string) string {
323 if strings.TrimSpace(primary) != "" {
324 return strings.TrimSpace(primary)
325 }
326 return strings.TrimSpace(fallback)
327 }
328
329 319 // loadFrom reads a request out of the tree map a commit is being built from, so
330 320 // mutations always see the tip they are about to modify.
331 321 func loadFrom(files map[string][]byte, id string) (*Request, error) {
@@ -333,7 +323,7 @@ func loadFrom(files map[string][]byte, id string) (*Request, error) {
333 323 for k, v := range files {
334 324 contents[k] = string(v)
335 325 }
336 list, err := Parse(contents)
326 list, err := ParseRequests(contents)
337 327 if err != nil {
338 328 if ve, ok := err.(*ValidationError); ok {
339 329 return nil, fmt.Errorf("requests: the metadata tree is invalid: %s", ve.Problems[0])
@@ -347,10 +337,3 @@ func loadFrom(files map[string][]byte, id string) (*Request, error) {
347 337 }
348 338 return nil, fmt.Errorf("requests: no request %s", id)
349 339 }
350
351 func shortSHA(s string) string {
352 if len(s) > 8 {
353 return s[:8]
354 }
355 return s
356 }