vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software: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 · 14d 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/writes.go +10−5
| @@ -385,16 +385,21 @@ func (w *Web) MergeRequest(rw http.ResponseWriter, r *http.Request) { | |||
| 385 | 385 | http.Error(rw, "this change request is not open", http.StatusBadRequest) | |
| 386 | 386 | return | |
| 387 | 387 | } | |
| 388 | res, err := landing.Land(w.landGate(m, ctx.ACL, ctx.Name), id, ctx.Name, time.Now().UTC()) | ||
| 388 | _, err = landing.Land(w.landGate(m, ctx.ACL, ctx.Name), id, ctx.Name, time.Now().UTC()) | ||
| 389 | 389 | if err != nil { | |
| 390 | // The gate's refusal is the answer, so it is shown as written: it names the | ||
| 391 | // missing approval, the failing check, or the conflicting path. | ||
| 392 | http.Error(rw, err.Error(), http.StatusConflict) | ||
| 390 | // The refusal is the answer, so it is shown as written: it names the missing | ||
| 391 | // approval, the failing check, or the conflicting path. It is shown *on the | ||
| 392 | // page* rather than as bare text, so the reader keeps the conflict block and | ||
| 393 | // the diff that explain it. | ||
| 394 | if htmxRequest(r) { | ||
| 395 | w.render(rw, "request_sidebar", w.requestSidebar(r, m, req, "", err.Error()), 0) | ||
| 396 | return | ||
| 397 | } | ||
| 398 | w.renderRequestPage(rw, r, err.Error(), http.StatusConflict) | ||
| 393 | 399 | return | |
| 394 | 400 | } | |
| 395 | 401 | if problems := metadata.ValidateRequests(m.Git(), metadata.Ref); len(problems) > 0 { | |
| 396 | 402 | w.Logger("web: merge left the request tree invalid: %v", problems) | |
| 397 | 403 | } | |
| 398 | 404 | http.Redirect(rw, r, "/r/"+m.Name+"/requests/"+id, http.StatusSeeOther) | |
| 399 | _ = res | ||
| 400 | 405 | } | |