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.
Let an author move their change request's head from the browser
53ff967be619894b62ca0ca69cd9a9f5f52bce01 · Keenan Nemetz · 13d ago · parent 8e1005eb
Let an author move their change request's head from the browser A request names the exact commit under review, so a fixup pushed after it was opened is invisible until the head moves. The only mover was `vole change update --head`, which writes the metadata branch — a write a contributor cannot make. The person who owns the branch was the one person who could not keep the request current. The request page now offers the author (or a maintainer) a branch picker that moves an open request's head to that branch's tip. It applies the same rule the new-request form does: the head must be a branch tip, and a contributor may only name a branch under their own namespace, which is what the pre-receive hook would have let them push. Approvals stay in the tree as history and stop matching, which is what a new head already means. SetRequestHead now refuses a closed or merged request, so the CLI and the browser share that rule instead of the web handler owning it alone.
server/web/writes.go +58
| @@ -359,6 +359,64 @@ func (w *Web) ChangeRequestState(rw http.ResponseWriter, r *http.Request) { | |||
| 359 | 359 | http.Redirect(rw, r, "/r/"+m.Name+"/requests/"+id, http.StatusSeeOther) | |
| 360 | 360 | } | |
| 361 | 361 | ||
| 362 | // UpdateRequestHead points an open change request at the tip of a branch. A request | ||
| 363 | // names a commit, not a branch, so a fixup pushed after it was opened is invisible | ||
| 364 | // until the head moves. The CLI can do that, but only by writing the metadata | ||
| 365 | // branch, which a contributor cannot push; this is the same move on the surface a | ||
| 366 | // contributor can already write to. | ||
| 367 | // | ||
| 368 | // The head must be a branch tip, and a contributor may only name a branch in their | ||
| 369 | // own namespace — the same rule the browser applies when opening a request, so the | ||
| 370 | // control offers nothing the gate would refuse. | ||
| 371 | func (w *Web) UpdateRequestHead(rw http.ResponseWriter, r *http.Request) { | ||
| 372 | ctx, ok := w.beginWrite(rw, r, maxSmallForm) | ||
| 373 | if !ok { | ||
| 374 | return | ||
| 375 | } | ||
| 376 | m := ctx.Repo | ||
| 377 | id := r.PathValue("id") | ||
| 378 | req, err := metadata.GetRequest(m.Git(), id) | ||
| 379 | if err != nil { | ||
| 380 | w.notFound(rw, r) | ||
| 381 | return | ||
| 382 | } | ||
| 383 | if !ctx.canTriage(req.Author) { | ||
| 384 | w.refuse(rw, r, "you do not have permission to move this change request's head") | ||
| 385 | return | ||
| 386 | } | ||
| 387 | if !req.Open() { | ||
| 388 | w.refuse(rw, r, "only an open change request can move its head") | ||
| 389 | return | ||
| 390 | } | ||
| 391 | branch := strings.TrimPrefix(strings.TrimSpace(r.PostFormValue("head")), "refs/heads/") | ||
| 392 | metadataBranch := strings.TrimPrefix(metadata.Ref, "refs/heads/") | ||
| 393 | if branch == "" || branch == metadataBranch || !m.Git().ValidBranch(branch) { | ||
| 394 | http.Error(rw, "choose the branch whose tip should be reviewed", http.StatusBadRequest) | ||
| 395 | return | ||
| 396 | } | ||
| 397 | headRef := "refs/heads/" + branch | ||
| 398 | if contributorOnly(ctx.ACL, ctx.Name) && !access.IsContributorRef(ctx.Name, headRef) { | ||
| 399 | w.refuse(rw, r, "contributors may only propose a branch in their own namespace") | ||
| 400 | return | ||
| 401 | } | ||
| 402 | tip, err := m.Git().RevParse(headRef) | ||
| 403 | if err != nil || tip == "" { | ||
| 404 | http.Error(rw, "that branch does not exist; push it first", http.StatusBadRequest) | ||
| 405 | return | ||
| 406 | } | ||
| 407 | if tip != req.Head { | ||
| 408 | if _, err := metadata.SetRequestHead(m.Git(), id, tip, ctx.Name); err != nil { | ||
| 409 | w.serverError(rw, r, "cannot move the change request's head: "+err.Error()) | ||
| 410 | return | ||
| 411 | } | ||
| 412 | w.audit("web.request.head", ctx.Name, m.Name+" "+id+" "+req.Head+"->"+tip) | ||
| 413 | if problems := metadata.ValidateRequests(m.Git(), metadata.Ref); len(problems) > 0 { | ||
| 414 | w.Logger("web: request head %s left the request tree invalid: %v", id, problems) | ||
| 415 | } | ||
| 416 | } | ||
| 417 | http.Redirect(rw, r, "/r/"+m.Name+"/requests/"+id, http.StatusSeeOther) | ||
| 418 | } | ||
| 419 | |||
| 362 | 420 | // landGate is the landing gate wired to the stores this process already holds. The | |
| 363 | 421 | // run queue is the web's own open handle, not a path: the server is long-lived and | |
| 364 | 422 | // the gate must not close it. | |