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.
List a change's linked issues in a Details section
cad93cfe2b6d4ed4c3ed5a1431af4ecb194484ce · Keenan Nemetz · 10d ago · parent 5c22953e
List a change's linked issues in a Details section The conversation sidebar showed linked issues as one comma-separated line, so there was no way to see what each link was or change it without the CLI. The Details section lists them with the same rows the overview uses for recent issues, and the change's author or a maintainer can link another issue or remove one while it is open. Both controls go through SetRequestCloses, so the browser and 'vole change update --closes' write the same metadata.
server/web/request_view.go +70−12
| @@ -14,6 +14,7 @@ import ( | |||
| 14 | 14 | "github.com/rodent-software/vole/ci/runs" | |
| 15 | 15 | "github.com/rodent-software/vole/core/git" | |
| 16 | 16 | "github.com/rodent-software/vole/forge/access" | |
| 17 | "github.com/rodent-software/vole/forge/index" | ||
| 17 | 18 | "github.com/rodent-software/vole/forge/landing" | |
| 18 | 19 | "github.com/rodent-software/vole/forge/repo" | |
| 19 | 20 | "github.com/rodent-software/vole/metadata" | |
| @@ -95,6 +96,16 @@ type requestLandingView struct { | |||
| 95 | 96 | MergeChecked bool | |
| 96 | 97 | } | |
| 97 | 98 | ||
| 99 | // requestIssue is one issue linked to a change request. It carries the same facts | ||
| 100 | // the overview's recent-issue rows show, plus the id the remove control posts. | ||
| 101 | type requestIssue struct { | ||
| 102 | ID string | ||
| 103 | Number int | ||
| 104 | Title string | ||
| 105 | Author string | ||
| 106 | Created time.Time | ||
| 107 | } | ||
| 108 | |||
| 98 | 109 | // requestSidebarView is the data the change sidebar fragments need. One type for | |
| 99 | 110 | // the full page and the HTMX replacements, so an approval or state change can | |
| 100 | 111 | // refresh the whole sidebar without rebuilding the rest of the review. | |
| @@ -105,15 +116,21 @@ type requestSidebarView struct { | |||
| 105 | 116 | Request *metadata.Request | |
| 106 | 117 | // Number is the `#42` alias, derived from creation order in history. | |
| 107 | 118 | Number int | |
| 108 | // Numbers is every alias in the repo, so the `closes` links and the thread can | ||
| 109 | // name issues they were not given directly. | ||
| 110 | Numbers map[string]int | ||
| 111 | Approvals []requestApproval | ||
| 112 | Checks []*runs.Run | ||
| 113 | Required int | ||
| 114 | Current int | ||
| 115 | CanApprove bool | ||
| 116 | CanClose bool | ||
| 119 | // LinkedIssues is the Details section: the issues landing this change closes. | ||
| 120 | LinkedIssues []requestIssue | ||
| 121 | Approvals []requestApproval | ||
| 122 | Checks []*runs.Run | ||
| 123 | Required int | ||
| 124 | Current int | ||
| 125 | CanApprove bool | ||
| 126 | CanClose bool | ||
| 127 | // CanEditIssues is the add and remove controls. Only an open request may be | ||
| 128 | // relinked, and only by its author or a maintainer, matching `change update`. | ||
| 129 | CanEditIssues bool | ||
| 130 | // DetailsError is the reason a link change did not happen, shown beside the | ||
| 131 | // controls. It is separate from Error because a link write replaces only the | ||
| 132 | // Details section. | ||
| 133 | DetailsError string | ||
| 117 | 134 | // HeadRefNote says when the branch the request follows no longer points at the | |
| 118 | 135 | // commit under review ("branch moved", "branch deleted"). A push normally moves | |
| 119 | 136 | // the head with the branch, so "branch moved" means that sync did not happen and | |
| @@ -338,10 +355,12 @@ func (w *Web) requestSidebar(r *http.Request, m *repo.Meta, req *metadata.Reques | |||
| 338 | 355 | nums := w.entityNumbers(m) | |
| 339 | 356 | return requestSidebarView{ | |
| 340 | 357 | Page: w.page(r, title), Name: m.Name, Request: req, | |
| 341 | Number: nums[req.ID], Numbers: nums, | ||
| 342 | Approvals: approvals, Checks: checks, Required: required, Current: current, | ||
| 358 | Number: nums[req.ID], | ||
| 359 | LinkedIssues: w.requestLinkedIssues(m, req, nums), | ||
| 360 | Approvals: approvals, Checks: checks, Required: required, Current: current, | ||
| 343 | 361 | CanApprove: approvable, CanClose: canClose, HeadRefNote: headRefNote, | |
| 344 | CanRetarget: canRetarget, BaseOptions: baseOptions, HeadOptions: headOptions, | ||
| 362 | CanEditIssues: req.Open() && canClose, | ||
| 363 | CanRetarget: canRetarget, BaseOptions: baseOptions, HeadOptions: headOptions, | ||
| 345 | 364 | CanMerge: mergeable, MergePending: mergePending, MergeArmed: mergeArmed, | |
| 346 | 365 | MergeBlocked: mergeBlocked, | |
| 347 | 366 | CombinedNeeded: combinedNeeded, CombinedSHA: combinedSHA, | |
| @@ -352,6 +371,45 @@ func (w *Web) requestSidebar(r *http.Request, m *repo.Meta, req *metadata.Reques | |||
| 352 | 371 | } | |
| 353 | 372 | } | |
| 354 | 373 | ||
| 374 | // requestLinkedIssues reads the issues a request promises to close. The index | ||
| 375 | // supplies the rows the overview uses, and a row the index cannot name is read | ||
| 376 | // from the issue itself rather than dropped: the request still holds the link. | ||
| 377 | func (w *Web) requestLinkedIssues(m *repo.Meta, req *metadata.Request, nums map[string]int) []requestIssue { | ||
| 378 | if len(req.Closes) == 0 { | ||
| 379 | return nil | ||
| 380 | } | ||
| 381 | rows := make(map[string]index.IssueRow, len(req.Closes)) | ||
| 382 | if w.Index != nil { | ||
| 383 | if err := w.Index.Ensure(m); err == nil { | ||
| 384 | if list, err := w.Index.Issues(m.ID, index.Filter{State: "all"}); err == nil { | ||
| 385 | for _, row := range list { | ||
| 386 | rows[row.ID] = row | ||
| 387 | } | ||
| 388 | } | ||
| 389 | } | ||
| 390 | } | ||
| 391 | out := make([]requestIssue, 0, len(req.Closes)) | ||
| 392 | for _, id := range req.Closes { | ||
| 393 | if row, ok := rows[id]; ok { | ||
| 394 | out = append(out, requestIssue{ | ||
| 395 | ID: row.ID, Number: row.Number, Title: row.Title, | ||
| 396 | Author: row.Author, Created: row.Created, | ||
| 397 | }) | ||
| 398 | continue | ||
| 399 | } | ||
| 400 | iss, err := index.LoadIssue(m, id) | ||
| 401 | if err != nil { | ||
| 402 | out = append(out, requestIssue{ID: id, Number: nums[id], Title: "Linked issue unavailable"}) | ||
| 403 | continue | ||
| 404 | } | ||
| 405 | out = append(out, requestIssue{ | ||
| 406 | ID: id, Number: nums[id], Title: iss.Title, | ||
| 407 | Author: iss.Author, Created: iss.Created, | ||
| 408 | }) | ||
| 409 | } | ||
| 410 | return out | ||
| 411 | } | ||
| 412 | |||
| 355 | 413 | // pendingContributorCI finds the contributor branch whose tip is head and whose | |
| 356 | 414 | // CI still waits for a maintainer. It is keyed by the commit: a branch that has | |
| 357 | 415 | // moved past this request is not what is under review here. | |