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.
Store changes under changes/<ulid>/change.md
b492b557f5cc5633b3dcb21fdbb5e2ad98d0bc16 · Keenan Nemetz · 3d ago · parent 9bce3723
Store changes under changes/<ulid>/change.md The web, the CLI, and the docs call these items changes; only the metadata tree still said requests. Rename the directory and the record file so the format and the product use one word. The tree is a contract, so this is a format change rather than a display change: the top-level layout stays closed, and a request path no longer validates. An existing repository is migrated by rewriting its vole branch, which is an audited admin force-push; the index rebuilds from the new tip and the #N aliases keep their ULIDs because the rewrite preserves commit order. Refs #202 (01M6S3JC3Y2KA6EFJK6APX7A1T).
metadata/request_write.go +20−20
| @@ -116,9 +116,9 @@ func CreateRequest(g *git.Repo, r *Request, actor string) error { | |||
| 116 | 116 | if err != nil { | |
| 117 | 117 | return err | |
| 118 | 118 | } | |
| 119 | base := fmt.Sprintf("%s/%s/", RequestsDir, r.ID) | ||
| 119 | base := fmt.Sprintf("%s/%s/", ChangesDir, r.ID) | ||
| 120 | 120 | _, err = git.CommitFiles(g, Ref, func(files map[string][]byte) error { | |
| 121 | if _, exists := files[base+requestName]; exists { | ||
| 121 | if _, exists := files[base+changeName]; exists { | ||
| 122 | 122 | return fmt.Errorf("requests: %s already exists", r.ID) | |
| 123 | 123 | } | |
| 124 | 124 | // A link is a promise that a merge closes the issue, so it has to name one. | |
| @@ -128,7 +128,7 @@ func CreateRequest(g *git.Repo, r *Request, actor string) error { | |||
| 128 | 128 | if err := issuesExist(files, r.Closes); err != nil { | |
| 129 | 129 | return err | |
| 130 | 130 | } | |
| 131 | files[base+requestName] = raw | ||
| 131 | files[base+changeName] = raw | ||
| 132 | 132 | return nil | |
| 133 | 133 | }, fmt.Sprintf("request: %s (%s)", r.Title, r.ID), actor) | |
| 134 | 134 | return err | |
| @@ -159,7 +159,7 @@ func GetRequest(g *git.Repo, id string) (*Request, error) { | |||
| 159 | 159 | func SetRequestDetails(g *git.Repo, id, title, body, actor string) (*Request, error) { | |
| 160 | 160 | title = strings.TrimSpace(title) | |
| 161 | 161 | var out *Request | |
| 162 | base := fmt.Sprintf("%s/%s/", RequestsDir, id) | ||
| 162 | base := fmt.Sprintf("%s/%s/", ChangesDir, id) | ||
| 163 | 163 | _, err := git.CommitFiles(g, Ref, func(files map[string][]byte) error { | |
| 164 | 164 | r, err := loadFrom(files, id) | |
| 165 | 165 | if err != nil { | |
| @@ -178,7 +178,7 @@ func SetRequestDetails(g *git.Repo, id, title, body, actor string) (*Request, er | |||
| 178 | 178 | if err != nil { | |
| 179 | 179 | return err | |
| 180 | 180 | } | |
| 181 | files[base+requestName] = raw | ||
| 181 | files[base+changeName] = raw | ||
| 182 | 182 | out = r | |
| 183 | 183 | return nil | |
| 184 | 184 | }, fmt.Sprintf("request: update %s", id), actor) | |
| @@ -199,7 +199,7 @@ func SetRequestDetails(g *git.Repo, id, title, body, actor string) (*Request, er | |||
| 199 | 199 | // in each caller is what keeps the CLI and the browser on the same rule. | |
| 200 | 200 | func SetRequestHead(g *git.Repo, id, head, headRef, actor string) (*Request, error) { | |
| 201 | 201 | var out *Request | |
| 202 | base := fmt.Sprintf("%s/%s/", RequestsDir, id) | ||
| 202 | base := fmt.Sprintf("%s/%s/", ChangesDir, id) | ||
| 203 | 203 | message := fmt.Sprintf("request: %s head -> %s", id, format.SHA(head)) | |
| 204 | 204 | if headRef != "" { | |
| 205 | 205 | message = fmt.Sprintf("request: %s follows %s at %s", id, headRef, format.SHA(head)) | |
| @@ -221,7 +221,7 @@ func SetRequestHead(g *git.Repo, id, head, headRef, actor string) (*Request, err | |||
| 221 | 221 | if err != nil { | |
| 222 | 222 | return err | |
| 223 | 223 | } | |
| 224 | files[base+requestName] = raw | ||
| 224 | files[base+changeName] = raw | ||
| 225 | 225 | out = r | |
| 226 | 226 | return nil | |
| 227 | 227 | }, message, actor) | |
| @@ -237,7 +237,7 @@ func SetRequestHead(g *git.Repo, id, head, headRef, actor string) (*Request, err | |||
| 237 | 237 | // landed on, and a closed one is not going to land at all. | |
| 238 | 238 | func SetRequestBase(g *git.Repo, id, newBase, actor string) (*Request, error) { | |
| 239 | 239 | var out *Request | |
| 240 | dir := fmt.Sprintf("%s/%s/", RequestsDir, id) | ||
| 240 | dir := fmt.Sprintf("%s/%s/", ChangesDir, id) | ||
| 241 | 241 | _, err := git.CommitFiles(g, Ref, func(files map[string][]byte) error { | |
| 242 | 242 | r, err := loadFrom(files, id) | |
| 243 | 243 | if err != nil { | |
| @@ -258,7 +258,7 @@ func SetRequestBase(g *git.Repo, id, newBase, actor string) (*Request, error) { | |||
| 258 | 258 | if err != nil { | |
| 259 | 259 | return err | |
| 260 | 260 | } | |
| 261 | files[dir+requestName] = raw | ||
| 261 | files[dir+changeName] = raw | ||
| 262 | 262 | out = r | |
| 263 | 263 | return nil | |
| 264 | 264 | }, fmt.Sprintf("request: %s base -> %s", id, newBase), actor) | |
| @@ -275,7 +275,7 @@ func SetRequestBase(g *git.Repo, id, newBase, actor string) (*Request, error) { | |||
| 275 | 275 | // them stale. | |
| 276 | 276 | func SetRequestTarget(g *git.Repo, id, newBase, newHead, newHeadRef, actor string) (*Request, error) { | |
| 277 | 277 | var out *Request | |
| 278 | dir := fmt.Sprintf("%s/%s/", RequestsDir, id) | ||
| 278 | dir := fmt.Sprintf("%s/%s/", ChangesDir, id) | ||
| 279 | 279 | target := newHeadRef | |
| 280 | 280 | if target == "" { | |
| 281 | 281 | target = format.SHA(newHead) | |
| @@ -302,7 +302,7 @@ func SetRequestTarget(g *git.Repo, id, newBase, newHead, newHeadRef, actor strin | |||
| 302 | 302 | if err != nil { | |
| 303 | 303 | return err | |
| 304 | 304 | } | |
| 305 | files[dir+requestName] = raw | ||
| 305 | files[dir+changeName] = raw | ||
| 306 | 306 | out = r | |
| 307 | 307 | return nil | |
| 308 | 308 | }, fmt.Sprintf("request: %s target %s <- %s", id, newBase, target), actor) | |
| @@ -359,7 +359,7 @@ func followBranch(files map[string][]byte, branch, head string) (int, error) { | |||
| 359 | 359 | } | |
| 360 | 360 | contents := make(map[string]string) | |
| 361 | 361 | for path, raw := range files { | |
| 362 | if strings.HasPrefix(path, RequestsDir+"/") { | ||
| 362 | if strings.HasPrefix(path, ChangesDir+"/") { | ||
| 363 | 363 | contents[path] = string(raw) | |
| 364 | 364 | } | |
| 365 | 365 | } | |
| @@ -377,7 +377,7 @@ func followBranch(files map[string][]byte, branch, head string) (int, error) { | |||
| 377 | 377 | if err != nil { | |
| 378 | 378 | return 0, err | |
| 379 | 379 | } | |
| 380 | files[fmt.Sprintf("%s/%s/%s", RequestsDir, r.ID, requestName)] = raw | ||
| 380 | files[fmt.Sprintf("%s/%s/%s", ChangesDir, r.ID, changeName)] = raw | ||
| 381 | 381 | updated++ | |
| 382 | 382 | } | |
| 383 | 383 | return updated, nil | |
| @@ -394,7 +394,7 @@ func ApproveRequest(g *git.Repo, id, principal, expectedBase, expectedHead strin | |||
| 394 | 394 | return nil, fmt.Errorf("requests: %q is not an account name", principal) | |
| 395 | 395 | } | |
| 396 | 396 | var out *Request | |
| 397 | base := fmt.Sprintf("%s/%s/", RequestsDir, id) | ||
| 397 | base := fmt.Sprintf("%s/%s/", ChangesDir, id) | ||
| 398 | 398 | _, err := git.CommitFiles(g, Ref, func(files map[string][]byte) error { | |
| 399 | 399 | r, err := loadFrom(files, id) | |
| 400 | 400 | if err != nil { | |
| @@ -447,7 +447,7 @@ func SetRequestState(g *git.Repo, id, state, actor string) (*Request, error) { | |||
| 447 | 447 | return nil, fmt.Errorf("requests: state must be open or closed") | |
| 448 | 448 | } | |
| 449 | 449 | var out *Request | |
| 450 | base := fmt.Sprintf("%s/%s/", RequestsDir, id) | ||
| 450 | base := fmt.Sprintf("%s/%s/", ChangesDir, id) | ||
| 451 | 451 | _, err := git.CommitFiles(g, Ref, func(files map[string][]byte) error { | |
| 452 | 452 | r, err := loadFrom(files, id) | |
| 453 | 453 | if err != nil { | |
| @@ -458,7 +458,7 @@ func SetRequestState(g *git.Repo, id, state, actor string) (*Request, error) { | |||
| 458 | 458 | if err != nil { | |
| 459 | 459 | return err | |
| 460 | 460 | } | |
| 461 | files[base+requestName] = raw | ||
| 461 | files[base+changeName] = raw | ||
| 462 | 462 | out = r | |
| 463 | 463 | return nil | |
| 464 | 464 | }, fmt.Sprintf("request: %s %s", id, state), actor) | |
| @@ -496,7 +496,7 @@ func MarkMerged(g *git.Repo, id, landedBase, landedHead, reviewedHead, actor str | |||
| 496 | 496 | out *Request | |
| 497 | 497 | closed []string | |
| 498 | 498 | ) | |
| 499 | base := fmt.Sprintf("%s/%s/", RequestsDir, id) | ||
| 499 | base := fmt.Sprintf("%s/%s/", ChangesDir, id) | ||
| 500 | 500 | _, err := git.CommitFiles(g, Ref, func(files map[string][]byte) error { | |
| 501 | 501 | r, err := loadFrom(files, id) | |
| 502 | 502 | if err != nil { | |
| @@ -524,7 +524,7 @@ func MarkMerged(g *git.Repo, id, landedBase, landedHead, reviewedHead, actor str | |||
| 524 | 524 | if err != nil { | |
| 525 | 525 | return err | |
| 526 | 526 | } | |
| 527 | files[base+requestName] = raw | ||
| 527 | files[base+changeName] = raw | ||
| 528 | 528 | // The issues this request promised to close are closed in the same commit as | |
| 529 | 529 | // the mark, so the merge and the closure cannot be separated by a crash. An | |
| 530 | 530 | // issue that is already closed, or that is no longer there, is left alone: the | |
| @@ -761,7 +761,7 @@ func SetRequestCloses(g *git.Repo, id string, closes []string, actor string) (*R | |||
| 761 | 761 | return nil, fmt.Errorf("%s", strings.Join(problems, "; ")) | |
| 762 | 762 | } | |
| 763 | 763 | var out *Request | |
| 764 | base := fmt.Sprintf("%s/%s/", RequestsDir, id) | ||
| 764 | base := fmt.Sprintf("%s/%s/", ChangesDir, id) | ||
| 765 | 765 | _, err := git.CommitFiles(g, Ref, func(files map[string][]byte) error { | |
| 766 | 766 | r, err := loadFrom(files, id) | |
| 767 | 767 | if err != nil { | |
| @@ -785,7 +785,7 @@ func SetRequestCloses(g *git.Repo, id string, closes []string, actor string) (*R | |||
| 785 | 785 | if err != nil { | |
| 786 | 786 | return err | |
| 787 | 787 | } | |
| 788 | files[base+requestName] = raw | ||
| 788 | files[base+changeName] = raw | ||
| 789 | 789 | out = r | |
| 790 | 790 | return nil | |
| 791 | 791 | }, fmt.Sprintf("request: %s closes %d issue(s)", id, len(closes)), actor) | |