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/requests.go +34−19
| @@ -33,36 +33,51 @@ type requestFormData struct { | |||
| 33 | 33 | } | |
| 34 | 34 | ||
| 35 | 35 | func (w *Web) requestForm(r *http.Request, m *repo.Meta) requestFormData { | |
| 36 | var branches []string | ||
| 36 | branches := branchNames(m) | ||
| 37 | accountID := w.accountName(r) | ||
| 38 | acl, _ := m.ACL() | ||
| 39 | canOpen := acl != nil && accountID != "" && acl.CanOpenRequest(accountID) | ||
| 40 | return requestFormData{ | ||
| 41 | Base: m.DefaultBranch, BaseBranches: branches, | ||
| 42 | HeadBranches: headBranches(branches, acl, accountID), CanOpen: canOpen, | ||
| 43 | } | ||
| 44 | } | ||
| 45 | |||
| 46 | // branchNames lists the repository's normal branches, without the metadata branch. | ||
| 47 | // The new-request form and the control that moves a request's head draw their | ||
| 48 | // choices from the same list, so a branch is offered in both or in neither. | ||
| 49 | func branchNames(m *repo.Meta) []string { | ||
| 37 | 50 | metadataBranch := strings.TrimPrefix(metadata.Ref, "refs/heads/") | |
| 51 | var out []string | ||
| 38 | 52 | if refs, err := m.Git().ListRefs("refs/heads/"); err == nil { | |
| 39 | 53 | for ref := range refs { | |
| 40 | 54 | name := strings.TrimPrefix(ref, "refs/heads/") | |
| 41 | 55 | if name == "" || name == metadataBranch { | |
| 42 | 56 | continue | |
| 43 | 57 | } | |
| 44 | branches = append(branches, name) | ||
| 58 | out = append(out, name) | ||
| 45 | 59 | } | |
| 46 | sort.Strings(branches) | ||
| 60 | sort.Strings(out) | ||
| 47 | 61 | } | |
| 48 | baseBranches := branches | ||
| 49 | headBranches := branches | ||
| 50 | accountID := w.accountName(r) | ||
| 51 | acl, _ := m.ACL() | ||
| 52 | canOpen := acl != nil && accountID != "" && acl.CanOpenRequest(accountID) | ||
| 53 | if acl != nil && acl.RoleFor(accountID) == access.Contribute { | ||
| 54 | name := accountID | ||
| 55 | headBranches = nil | ||
| 56 | for _, branch := range branches { | ||
| 57 | if access.IsContributorRef(name, "refs/heads/"+branch) { | ||
| 58 | headBranches = append(headBranches, branch) | ||
| 59 | } | ||
| 60 | } | ||
| 62 | return out | ||
| 63 | } | ||
| 64 | |||
| 65 | // headBranches narrows the choices to the branches an account may propose as a | ||
| 66 | // change request's head: any normal branch for a maintainer, only its own | ||
| 67 | // contrib/ namespace for a contributor. That is the same rule the pre-receive hook | ||
| 68 | // applies to the account's pushes, so a control cannot offer a branch the hook | ||
| 69 | // would refuse. | ||
| 70 | func headBranches(all []string, acl *access.ACL, account string) []string { | ||
| 71 | if acl == nil || account == "" || acl.RoleFor(account) != access.Contribute { | ||
| 72 | return all | ||
| 61 | 73 | } | |
| 62 | return requestFormData{ | ||
| 63 | Base: m.DefaultBranch, BaseBranches: baseBranches, | ||
| 64 | HeadBranches: headBranches, CanOpen: canOpen, | ||
| 74 | out := make([]string, 0, len(all)) | ||
| 75 | for _, branch := range all { | ||
| 76 | if access.IsContributorRef(account, "refs/heads/"+branch) { | ||
| 77 | out = append(out, branch) | ||
| 78 | } | ||
| 65 | 79 | } | |
| 80 | return out | ||
| 66 | 81 | } | |
| 67 | 82 | ||
| 68 | 83 | type requestRow struct { | |