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.
Draw the merge control from the landing plan
097b21105b70f57fbf1c119c87ae7a75b3a44ca6 · Keenan Nemetz · 13d ago · parent b14a7819
Draw the merge control from the landing plan The merge button was drawn from the approval and check gate, which never looks at the tree, so a request that conflicted with its base still offered "Merge" while the "What would land" block two sections above said it did not merge cleanly. Clicking it answered with a bare 409 and one sentence: the page, the diff and the conflict paths were all replaced by text. The decision now lives in one place. landing.Prepare works out what a landing would do — fast-forward, merge commit, already landed, or the paths that conflict — and refuses the same things Land refuses. Land prepares and carries out; the page prepares to decide whether to draw the control; and the refusal path re-renders the request page with the message beside the conflict block instead of answering with plain text. A conflict is reported in the plan rather than as an error, because it is an outcome the page can style: the paths are already listed by "What would land", so the sidebar adds nothing and the button stays away. Prepare writes one unreferenced commit object for a non-fast-forward landing, because the approval matches a commit whose parents are the base tip and the approved head, and that commit has to exist to be matched. Nothing visible changes and git collects it.
server/web/request_view.go +21−7
| @@ -9,8 +9,10 @@ import ( | |||
| 9 | 9 | "net/http" | |
| 10 | 10 | "sort" | |
| 11 | 11 | "strings" | |
| 12 | "time" | ||
| 12 | 13 | ||
| 13 | 14 | "github.com/rodent-software/vole/forge/access" | |
| 15 | "github.com/rodent-software/vole/forge/landing" | ||
| 14 | 16 | "github.com/rodent-software/vole/forge/repo" | |
| 15 | 17 | "github.com/rodent-software/vole/metadata" | |
| 16 | 18 | ) | |
| @@ -119,12 +121,16 @@ func (w *Web) requestSidebar(r *http.Request, m *repo.Meta, req *metadata.Reques | |||
| 119 | 121 | var mergeable bool | |
| 120 | 122 | var mergeBlocked string | |
| 121 | 123 | 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 { | ||
| 124 | // Prepare is what Land runs, so the control and the write agree about | ||
| 125 | // approvals, checks and conflicts. A conflict is already called out with its | ||
| 126 | // paths by the "What would land" block, so it does not repeat a sentence here. | ||
| 127 | plan, err := landing.Prepare(w.landGate(m, acl, who), req.ID, who, time.Now().UTC()) | ||
| 128 | switch { | ||
| 129 | case err != nil: | ||
| 126 | 130 | mergeBlocked = err.Error() | |
| 127 | } else { | ||
| 131 | case len(plan.Conflicts) > 0: | ||
| 132 | mergeBlocked = "" // the "What would land" block already names the paths | ||
| 133 | default: | ||
| 128 | 134 | mergeable = true | |
| 129 | 135 | } | |
| 130 | 136 | } | |
| @@ -207,6 +213,14 @@ func (w *Web) requestThreadFrom(r *http.Request, m *repo.Meta, req *metadata.Req | |||
| 207 | 213 | } | |
| 208 | 214 | ||
| 209 | 215 | func (w *Web) RequestView(rw http.ResponseWriter, r *http.Request) { | |
| 216 | w.renderRequestPage(rw, r, "", http.StatusOK) | ||
| 217 | } | ||
| 218 | |||
| 219 | // renderRequestPage is the request page, optionally with a message at the top and a | ||
| 220 | // status of its own. A write that is refused for a reason the page can explain | ||
| 221 | // re-renders here rather than answering with bare text: the reader keeps the review, | ||
| 222 | // the conflict block and the controls, and the message sits beside them. | ||
| 223 | func (w *Web) renderRequestPage(rw http.ResponseWriter, r *http.Request, errMsg string, status int) { | ||
| 210 | 224 | m, ok := w.open(rw, r) | |
| 211 | 225 | if !ok { | |
| 212 | 226 | return | |
| @@ -238,7 +252,7 @@ func (w *Web) RequestView(rw http.ResponseWriter, r *http.Request) { | |||
| 238 | 252 | } | |
| 239 | 253 | } | |
| 240 | 254 | ||
| 241 | sidebar := w.requestSidebar(r, m, req, m.Name+" · "+req.Title, "") | ||
| 255 | sidebar := w.requestSidebar(r, m, req, m.Name+" · "+req.Title, errMsg) | ||
| 242 | 256 | w.render(rw, "request", requestPageView{ | |
| 243 | 257 | requestSidebarView: sidebar, | |
| 244 | 258 | Tab: "conversation", | |
| @@ -252,7 +266,7 @@ func (w *Web) RequestView(rw http.ResponseWriter, r *http.Request) { | |||
| 252 | 266 | Thread: w.requestThreadFrom(r, m, req, commentTargetFromQuery(r), "", comments), | |
| 253 | 267 | Conflicts: conflicts, | |
| 254 | 268 | MergeChecked: mergeChecked, | |
| 255 | }, 0) | ||
| 269 | }, status) | ||
| 256 | 270 | } | |
| 257 | 271 | ||
| 258 | 272 | // requestDiff computes the three-dot patch for a change and answers whether it | |