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.
Land a change request on the server instead of by pushing
38b3025145d8cee8479733fd4d3b0df443c72607 · Keenan Nemetz · 13d ago · parent 536f08b9
Land a change request on the server instead of by pushing A change request could only be landed by merging locally and pushing the base branch: the web page was a review surface with no way to finish, and nothing recorded that the request had landed. `state: merged` was already accepted by the request reader and rendered by the activity feed, but nothing ever wrote it, so a merged request and an abandoned one looked identical. The gate that decides a landing lives in the pre-receive hook, and the hook only sees pushes. A merge performed inside the server moves the ref without the hook, so a server-side merge either re-runs that rule or becomes the easiest way to land an unapproved change. It now re-runs it: the approval and check rule moved to forge/landing, and both the hook and the new landing operation call the same Check. Landing is fast-forward when the base is an ancestor of the reviewed head, so the commit that passed CI is the commit that lands; otherwise a two-parent merge commit, which is the only other shape the gate accepts. Conflicts are refused with the conflicting paths, never resolved. The ref moves by compare-and-swap, so a concurrent push loses cleanly and the landing retries; the request is marked merged after the ref moves, which makes a retry after a crash finish the marking instead of failing. The request now records merged_head, merged_by and merged_at, and the reader refuses a merged request that names no commit, and refuses those fields on a request that is not merged. "merged" is no longer settable through the generic state writer: it has to travel with the commit. The browser gets a Merge button that preflights the same gate, so it only appears when the landing would succeed, and the page shows what landed, who landed it, and when. `vole change merge` follows in its own change: the CLI has no access to the ACL or the run queue, so it needs a verb on the SSH channel, and that transport is worth reviewing on its own. Closes the landing half of #01M5XF4XWHVMMCQEYENA8X32GK.
server/web/request_view.go +30−8
| @@ -44,14 +44,20 @@ type requestCI struct { | |||
| 44 | 44 | // refresh the whole sidebar without rebuilding the rest of the review. | |
| 45 | 45 | type requestSidebarView struct { | |
| 46 | 46 | Page | |
| 47 | Name string | ||
| 48 | Request *metadata.Request | ||
| 49 | Approvals []requestApproval | ||
| 50 | Checks []requestCheck | ||
| 51 | Required int | ||
| 52 | Current int | ||
| 53 | CanApprove bool | ||
| 54 | CanClose bool | ||
| 47 | Name string | ||
| 48 | Request *metadata.Request | ||
| 49 | Approvals []requestApproval | ||
| 50 | Checks []requestCheck | ||
| 51 | Required int | ||
| 52 | Current int | ||
| 53 | CanApprove bool | ||
| 54 | CanClose bool | ||
| 55 | // CanMerge is the merge control: true only when the gate would accept the | ||
| 56 | // landing now, so the button never offers something the handler would refuse. | ||
| 57 | CanMerge bool | ||
| 58 | // MergeBlocked is the gate's reason when the caller may merge in principle but | ||
| 59 | // the request is not ready (missing approval, failing check, conflict). | ||
| 60 | MergeBlocked string | ||
| 55 | 61 | CanDiscuss bool | |
| 56 | 62 | ContributorCI *requestCI | |
| 57 | 63 | Error string | |
| @@ -107,6 +113,21 @@ func (w *Web) requestSidebar(r *http.Request, m *repo.Meta, req *metadata.Reques | |||
| 107 | 113 | who := w.accountName(r) | |
| 108 | 114 | approvable := req.Open() && canApproveRequest(acl, who, req.Author) | |
| 109 | 115 | canClose := canTriage(acl, who, req.Author) | |
| 116 | // Preflight the landing rather than guessing from the approval count: the same | ||
| 117 | // gate the handler calls is asked here, so the button and the write agree about | ||
| 118 | // approvals, checks and conflicts. | ||
| 119 | var mergeable bool | ||
| 120 | var mergeBlocked string | ||
| 121 | if req.Open() && canMerge(acl, who) { | ||
| 122 | gate := w.landGate(m, acl, who) | ||
| 123 | if tip, _ := m.Git().RevParse("refs/heads/" + req.Base); tip == "" { | ||
| 124 | mergeBlocked = "The base branch " + req.Base + " does not exist." | ||
| 125 | } else if _, err := gate.Check(req.Base, req.Head, tip); err != nil { | ||
| 126 | mergeBlocked = err.Error() | ||
| 127 | } else { | ||
| 128 | mergeable = true | ||
| 129 | } | ||
| 130 | } | ||
| 110 | 131 | var contributorCI *requestCI | |
| 111 | 132 | if req.Open() { | |
| 112 | 133 | if ref := w.pendingContributorCI(m, req.Head); ref != "" { | |
| @@ -120,6 +141,7 @@ func (w *Web) requestSidebar(r *http.Request, m *repo.Meta, req *metadata.Reques | |||
| 120 | 141 | Page: w.page(r, title), Name: m.Name, Request: req, | |
| 121 | 142 | Approvals: approvals, Checks: checks, Required: required, Current: current, | |
| 122 | 143 | CanApprove: approvable, CanClose: canClose, | |
| 144 | CanMerge: mergeable, MergeBlocked: mergeBlocked, | ||
| 123 | 145 | CanDiscuss: w.canDiscuss(r, acl), | |
| 124 | 146 | ContributorCI: contributorCI, | |
| 125 | 147 | Error: errMsg, | |