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.
Land a change request on the server instead of by pushing
38b3025145d8cee8479733fd4d3b0df443c72607 · Keenan Nemetz · 13d ago · parent 536f08b9
Land a change request on the server instead of by pushing A change request could only be landed by merging locally and pushing the base branch: the web page was a review surface with no way to finish, and nothing recorded that the request had landed. `state: merged` was already accepted by the request reader and rendered by the activity feed, but nothing ever wrote it, so a merged request and an abandoned one looked identical. The gate that decides a landing lives in the pre-receive hook, and the hook only sees pushes. A merge performed inside the server moves the ref without the hook, so a server-side merge either re-runs that rule or becomes the easiest way to land an unapproved change. It now re-runs it: the approval and check rule moved to forge/landing, and both the hook and the new landing operation call the same Check. Landing is fast-forward when the base is an ancestor of the reviewed head, so the commit that passed CI is the commit that lands; otherwise a two-parent merge commit, which is the only other shape the gate accepts. Conflicts are refused with the conflicting paths, never resolved. The ref moves by compare-and-swap, so a concurrent push loses cleanly and the landing retries; the request is marked merged after the ref moves, which makes a retry after a crash finish the marking instead of failing. The request now records merged_head, merged_by and merged_at, and the reader refuses a merged request that names no commit, and refuses those fields on a request that is not merged. "merged" is no longer settable through the generic state writer: it has to travel with the commit. The browser gets a Merge button that preflights the same gate, so it only appears when the landing would succeed, and the page shows what landed, who landed it, and when. `vole change merge` follows in its own change: the CLI has no access to the ACL or the run queue, so it needs a verb on the SSH channel, and that transport is worth reviewing on its own. Closes the landing half of #01M5XF4XWHVMMCQEYENA8X32GK.
core/git/git.go +24−2
| @@ -266,9 +266,31 @@ func (r *Repo) CommitTreeAs(tree, parent, msg, author string) (string, error) { | |||
| 266 | 266 | } | |
| 267 | 267 | ||
| 268 | 268 | func (r *Repo) commitTree(tree, parent, msg, author string) (string, error) { | |
| 269 | args := []string{"commit-tree", tree} | ||
| 269 | parents := []string{} | ||
| 270 | 270 | if parent != "" { | |
| 271 | args = append(args, "-p", parent) | ||
| 271 | parents = append(parents, parent) | ||
| 272 | } | ||
| 273 | return r.commitTreeParents(tree, parents, msg, author) | ||
| 274 | } | ||
| 275 | |||
| 276 | // CommitMergeTree writes a merge commit over an already-built tree with the given | ||
| 277 | // parents, in order: the base tip first, then the approved head. The order matters | ||
| 278 | // to the gate, which accepts a merge whose parents are the old tip and the approved | ||
| 279 | // head in either order but nothing else. | ||
| 280 | func (r *Repo) CommitMergeTree(tree string, parents []string, msg, author string) (string, error) { | ||
| 281 | if len(parents) != 2 { | ||
| 282 | return "", fmt.Errorf("commit-tree: a merge commit needs two parents, got %d", len(parents)) | ||
| 283 | } | ||
| 284 | return r.commitTreeParents(tree, parents, msg, author) | ||
| 285 | } | ||
| 286 | |||
| 287 | func (r *Repo) commitTreeParents(tree string, parents []string, msg, author string) (string, error) { | ||
| 288 | args := []string{"commit-tree", tree} | ||
| 289 | for _, p := range parents { | ||
| 290 | if p == "" { | ||
| 291 | continue | ||
| 292 | } | ||
| 293 | args = append(args, "-p", p) | ||
| 272 | 294 | } | |
| 273 | 295 | env := []string{} | |
| 274 | 296 | if strings.TrimSpace(author) != "" { | |