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/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) != "" {