Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.

Make change requests follow branches and allow base retargeting

e050e14a64f6f19c1b1478c4c073d690ef3d3129 · Keenan Nemetz · 13d ago · parent 59464445

Make change requests follow branches and allow base retargeting

A request opened from a branch now follows it: post-receive moves the
request's head to the new tip after an accepted push, so approvals and
checks that named the old tip stop counting. Approvals stay bound to an
exact base and commit; the approval writers now require the base and head
the approver saw, so a push or retarget cannot silently approve a target
nobody reviewed. Merged requests freeze the reviewed head.

Add `vole change update --base` and a Change base control for stacked
requests. A new base changes the diff and therefore invalidates approvals,
which is what retargeting an upper request after its lower branch lands
needs. Base names may now contain slashes.

Also fix a second-boundary flake in the landing test.

server/web/writes.go +68−12

@@ -195,7 +195,17 @@ func (w *Web) ApproveRequest(rw http.ResponseWriter, r *http.Request) {
195 195 sidebar := func(msg string) (string, any) {
196 196 return "request_sidebar", w.requestSidebar(r, m, req, "", msg)
197 197 }
198 if _, err := metadata.ApproveRequest(m.Git(), id, ctx.Name, time.Now().UTC(), ctx.Name); err != nil {
198 // The form carries the base and head the page displayed. If a push moved the
199 // followed branch, or the request was retargeted, the writer refuses rather than
200 // approving a target the approver never saw.
201 expectedBase := strings.TrimSpace(r.PostFormValue("base"))
202 expectedHead := strings.TrimSpace(r.PostFormValue("head"))
203 if expectedBase == "" || expectedHead == "" {
204 w.fail(rw, r, http.StatusBadRequest,
205 "reload the request before approving: the form did not name the base and head you reviewed", sidebar)
206 return
207 }
208 if _, err := metadata.ApproveRequest(m.Git(), id, ctx.Name, expectedBase, expectedHead, time.Now().UTC(), ctx.Name); err != nil {
199 209 w.fail(rw, r, http.StatusBadRequest, "cannot approve: "+err.Error(), sidebar)
200 210 return
201 211 }
@@ -242,9 +252,9 @@ func (w *Web) CreateChangeRequest(rw http.ResponseWriter, r *http.Request) {
242 252 http.Error(rw, "choose a base and a head branch", http.StatusBadRequest)
243 253 return
244 254 }
245 // The head is a commit; the branch it came from is recorded as a display hint.
246 // It has to be a real branch name, or a hand-edited form could store something
247 // the metadata reader would refuse.
255 // The head starts at the branch tip and the request follows that branch afterwards.
256 // It has to be a real branch name, or a hand-edited form could store something the
257 // metadata reader would refuse.
248 258 if !m.Git().ValidBranch(head) {
249 259 http.Error(rw, "that head branch does not exist yet; push it first", http.StatusBadRequest)
250 260 return
@@ -359,11 +369,10 @@ func (w *Web) ChangeRequestState(rw http.ResponseWriter, r *http.Request) {
359 369 http.Redirect(rw, r, "/r/"+m.Name+"/requests/"+id, http.StatusSeeOther)
360 370 }
361 371
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.
372 // UpdateRequestHead repoints an open change request at another branch. The request
373 // already follows the branch it was opened from, so this is only for changing which
374 // branch it follows, or for attaching a request that was opened from a bare commit.
375 // Pushes move the head from then on.
367 376 //
368 377 // The head must be a branch tip, and a contributor may only name a branch in their
369 378 // own namespace — the same rule the browser applies when opening a request, so the
@@ -404,12 +413,12 @@ func (w *Web) UpdateRequestHead(rw http.ResponseWriter, r *http.Request) {
404 413 http.Error(rw, "that branch does not exist; push it first", http.StatusBadRequest)
405 414 return
406 415 }
407 if tip != req.Head {
408 if _, err := metadata.SetRequestHead(m.Git(), id, tip, ctx.Name); err != nil {
416 if tip != req.Head || branch != req.HeadRef {
417 if _, err := metadata.SetRequestHead(m.Git(), id, tip, branch, ctx.Name); err != nil {
409 418 w.serverError(rw, r, "cannot move the change request's head: "+err.Error())
410 419 return
411 420 }
412 w.audit("web.request.head", ctx.Name, m.Name+" "+id+" "+req.Head+"->"+tip)
421 w.audit("web.request.head", ctx.Name, m.Name+" "+id+" "+req.Head+"->"+tip+" following "+branch)
413 422 if problems := metadata.ValidateRequests(m.Git(), metadata.Ref); len(problems) > 0 {
414 423 w.Logger("web: request head %s left the request tree invalid: %v", id, problems)
415 424 }
@@ -417,6 +426,53 @@ func (w *Web) UpdateRequestHead(rw http.ResponseWriter, r *http.Request) {
417 426 http.Redirect(rw, r, "/r/"+m.Name+"/requests/"+id, http.StatusSeeOther)
418 427 }
419 428
429 // UpdateRequestBase retargets an open change request at another base branch. This is
430 // how a stacked request is repointed when the branch below it lands, or when it
431 // should land somewhere else. A new base changes the diff, so the approvals that
432 // named the old base stop counting.
433 func (w *Web) UpdateRequestBase(rw http.ResponseWriter, r *http.Request) {
434 ctx, ok := w.beginWrite(rw, r, maxSmallForm)
435 if !ok {
436 return
437 }
438 m := ctx.Repo
439 id := r.PathValue("id")
440 req, err := metadata.GetRequest(m.Git(), id)
441 if err != nil {
442 w.notFound(rw, r)
443 return
444 }
445 if !ctx.canTriage(req.Author) {
446 w.refuse(rw, r, "you do not have permission to change this change request's base")
447 return
448 }
449 if !req.Open() {
450 w.refuse(rw, r, "only an open change request can change its base")
451 return
452 }
453 base := strings.TrimPrefix(strings.TrimSpace(r.PostFormValue("base")), "refs/heads/")
454 metadataBranch := strings.TrimPrefix(metadata.Ref, "refs/heads/")
455 if base == "" || base == metadataBranch || !m.Git().ValidBranch(base) {
456 http.Error(rw, "choose the branch this change should land on", http.StatusBadRequest)
457 return
458 }
459 if tip, err := m.Git().RevParse("refs/heads/" + base); err != nil || tip == "" {
460 http.Error(rw, "that base branch does not exist", http.StatusBadRequest)
461 return
462 }
463 if base != req.Base {
464 if _, err := metadata.SetRequestBase(m.Git(), id, base, ctx.Name); err != nil {
465 w.serverError(rw, r, "cannot change the change request's base: "+err.Error())
466 return
467 }
468 w.audit("web.request.base", ctx.Name, m.Name+" "+id+" "+req.Base+"->"+base)
469 if problems := metadata.ValidateRequests(m.Git(), metadata.Ref); len(problems) > 0 {
470 w.Logger("web: request base %s left the request tree invalid: %v", id, problems)
471 }
472 }
473 http.Redirect(rw, r, "/r/"+m.Name+"/requests/"+id, http.StatusSeeOther)
474 }
475
420 476 // landGate is the landing gate wired to the stores this process already holds. The
421 477 // run queue is the web's own open handle, not a path: the server is long-lived and
422 478 // the gate must not close it.