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 | } | ||