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.
Compact the change request target controls
41d1fee900568b1bff5c7cff3b4c52942f47fabd · Keenan Nemetz · 13d ago · parent e050e14a
Compact the change request target controls Replace the separate base and head forms and the duplicated labels with one row: base picker, head picker, update. The pickers preselect the current targets, and a pinned head appears as the current commit. Collapse the help text to one line. Post both targets to one /target handler and write them in one metadata commit. Either change invalidates approvals; keeping the current value is allowed even if its branch has gone missing so the other side can still be repaired.
server/web/writes.go +51−69
| @@ -369,15 +369,15 @@ func (w *Web) ChangeRequestState(rw http.ResponseWriter, r *http.Request) { | |||
| 369 | 369 | http.Redirect(rw, r, "/r/"+m.Name+"/requests/"+id, http.StatusSeeOther) | |
| 370 | 370 | } | |
| 371 | 371 | ||
| 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. | ||
| 372 | // UpdateRequestTarget moves an open request's base and the branch it follows. The | ||
| 373 | // request page posts both together because the two targets are one review surface; | ||
| 374 | // changing either one makes approvals that named the old base and commit stale. | ||
| 376 | 375 | // | |
| 377 | // The head must be a branch tip, and a contributor may only name a branch in their | ||
| 378 | // own namespace — the same rule the browser applies when opening a request, so the | ||
| 379 | // control offers nothing the gate would refuse. | ||
| 380 | func (w *Web) UpdateRequestHead(rw http.ResponseWriter, r *http.Request) { | ||
| 376 | // A value that is already the request's target is kept even if its branch has since | ||
| 377 | // been deleted: that lets a reader change only the other side without first having | ||
| 378 | // to repair a missing branch. A value that is changing has to name a branch that | ||
| 379 | // exists, and a contributor may only name a head branch in their own namespace. | ||
| 380 | func (w *Web) UpdateRequestTarget(rw http.ResponseWriter, r *http.Request) { | ||
| 381 | 381 | ctx, ok := w.beginWrite(rw, r, maxSmallForm) | |
| 382 | 382 | if !ok { | |
| 383 | 383 | return | |
| @@ -390,84 +390,66 @@ func (w *Web) UpdateRequestHead(rw http.ResponseWriter, r *http.Request) { | |||
| 390 | 390 | return | |
| 391 | 391 | } | |
| 392 | 392 | if !ctx.canTriage(req.Author) { | |
| 393 | w.refuse(rw, r, "you do not have permission to move this change request's head") | ||
| 393 | w.refuse(rw, r, "you do not have permission to change this change request's target") | ||
| 394 | 394 | return | |
| 395 | 395 | } | |
| 396 | 396 | if !req.Open() { | |
| 397 | w.refuse(rw, r, "only an open change request can move its head") | ||
| 397 | w.refuse(rw, r, "only an open change request can change its target") | ||
| 398 | 398 | return | |
| 399 | 399 | } | |
| 400 | branch := strings.TrimPrefix(strings.TrimSpace(r.PostFormValue("head")), "refs/heads/") | ||
| 400 | |||
| 401 | base := strings.TrimPrefix(strings.TrimSpace(r.PostFormValue("base")), "refs/heads/") | ||
| 402 | head := strings.TrimPrefix(strings.TrimSpace(r.PostFormValue("head")), "refs/heads/") | ||
| 401 | 403 | metadataBranch := strings.TrimPrefix(metadata.Ref, "refs/heads/") | |
| 402 | if branch == "" || branch == metadataBranch || !m.Git().ValidBranch(branch) { | ||
| 403 | http.Error(rw, "choose the branch whose tip should be reviewed", http.StatusBadRequest) | ||
| 404 | return | ||
| 405 | } | ||
| 406 | headRef := "refs/heads/" + branch | ||
| 407 | if contributorOnly(ctx.ACL, ctx.Name) && !access.IsContributorRef(ctx.Name, headRef) { | ||
| 408 | w.refuse(rw, r, "contributors may only propose a branch in their own namespace") | ||
| 404 | if base == "" || base == metadataBranch || !m.Git().ValidBranch(base) { | ||
| 405 | http.Error(rw, "choose the branch this change should land on", http.StatusBadRequest) | ||
| 409 | 406 | return | |
| 410 | 407 | } | |
| 411 | tip, err := m.Git().RevParse(headRef) | ||
| 412 | if err != nil || tip == "" { | ||
| 413 | http.Error(rw, "that branch does not exist; push it first", http.StatusBadRequest) | ||
| 414 | return | ||
| 408 | if base != req.Base { | ||
| 409 | if tip, err := m.Git().RevParse("refs/heads/" + base); err != nil || tip == "" { | ||
| 410 | http.Error(rw, "that base branch does not exist", http.StatusBadRequest) | ||
| 411 | return | ||
| 412 | } | ||
| 415 | 413 | } | |
| 416 | if tip != req.Head || branch != req.HeadRef { | ||
| 417 | if _, err := metadata.SetRequestHead(m.Git(), id, tip, branch, ctx.Name); err != nil { | ||
| 418 | w.serverError(rw, r, "cannot move the change request's head: "+err.Error()) | ||
| 414 | |||
| 415 | headTip, headRef := req.Head, req.HeadRef | ||
| 416 | switch { | ||
| 417 | case head == "": | ||
| 418 | // The picker showed a pinned commit and the reader did not choose a branch. | ||
| 419 | case head == req.HeadRef: | ||
| 420 | // The current branch: follow it if it still exists, otherwise keep the last | ||
| 421 | // tip. A reader who is only changing the base must not have to repair the | ||
| 422 | // head first. | ||
| 423 | if tip, err := m.Git().RevParse("refs/heads/" + head); err == nil && tip != "" { | ||
| 424 | headTip = tip | ||
| 425 | } | ||
| 426 | default: | ||
| 427 | if head == metadataBranch || !m.Git().ValidBranch(head) { | ||
| 428 | http.Error(rw, "choose the branch whose tip should be reviewed", http.StatusBadRequest) | ||
| 419 | 429 | return | |
| 420 | 430 | } | |
| 421 | w.audit("web.request.head", ctx.Name, m.Name+" "+id+" "+req.Head+"->"+tip+" following "+branch) | ||
| 422 | if problems := metadata.ValidateRequests(m.Git(), metadata.Ref); len(problems) > 0 { | ||
| 423 | w.Logger("web: request head %s left the request tree invalid: %v", id, problems) | ||
| 431 | ref := "refs/heads/" + head | ||
| 432 | if contributorOnly(ctx.ACL, ctx.Name) && !access.IsContributorRef(ctx.Name, ref) { | ||
| 433 | w.refuse(rw, r, "contributors may only propose a branch in their own namespace") | ||
| 434 | return | ||
| 424 | 435 | } | |
| 436 | tip, err := m.Git().RevParse(ref) | ||
| 437 | if err != nil || tip == "" { | ||
| 438 | http.Error(rw, "that head branch does not exist; push it first", http.StatusBadRequest) | ||
| 439 | return | ||
| 440 | } | ||
| 441 | headTip, headRef = tip, head | ||
| 425 | 442 | } | |
| 426 | http.Redirect(rw, r, "/r/"+m.Name+"/requests/"+id, http.StatusSeeOther) | ||
| 427 | } | ||
| 428 | 443 | ||
| 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()) | ||
| 444 | if base != req.Base || headTip != req.Head || headRef != req.HeadRef { | ||
| 445 | if _, err := metadata.SetRequestTarget(m.Git(), id, base, headTip, headRef, ctx.Name); err != nil { | ||
| 446 | w.serverError(rw, r, "cannot update the change request's target: "+err.Error()) | ||
| 466 | 447 | return | |
| 467 | 448 | } | |
| 468 | w.audit("web.request.base", ctx.Name, m.Name+" "+id+" "+req.Base+"->"+base) | ||
| 449 | w.audit("web.request.target", ctx.Name, | ||
| 450 | m.Name+" "+id+" "+req.Base+"<-"+req.HeadRef+" -> "+base+"<-"+headRef) | ||
| 469 | 451 | 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) | ||
| 452 | w.Logger("web: request target %s left the request tree invalid: %v", id, problems) | ||
| 471 | 453 | } | |
| 472 | 454 | } | |
| 473 | 455 | http.Redirect(rw, r, "/r/"+m.Name+"/requests/"+id, http.StatusSeeOther) | |