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.
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/merge.go +14−3
| @@ -28,22 +28,33 @@ var treeIDRE = regexp.MustCompile(`^[0-9a-f]{40}([0-9a-f]{24})?$`) | |||
| 28 | 28 | // output: a real merge prints the resulting tree's id first, and a refusal prints | |
| 29 | 29 | // a message instead. | |
| 30 | 30 | func (r *Repo) MergeConflicts(base, head string) ([]string, error) { | |
| 31 | _, conflicts, err := r.MergeTree(base, head) | ||
| 32 | return conflicts, err | ||
| 33 | } | ||
| 34 | |||
| 35 | // MergeTree performs the same in-memory merge and also returns the id of the tree | ||
| 36 | // it built, which is what a real merge commit needs. Asking for it is how a | ||
| 37 | // landing operation merges without a worktree: the tree is in the object store, | ||
| 38 | // so a commit over it costs nothing even when the merge is thrown away because the | ||
| 39 | // ref moved in the meantime. | ||
| 40 | func (r *Repo) MergeTree(base, head string) (string, []string, error) { | ||
| 31 | 41 | out, errs, err := r.run(nil, "merge-tree", "--write-tree", "--name-only", "--no-messages", base, head) | |
| 32 | 42 | text := string(out) | |
| 33 | 43 | if err != nil { | |
| 34 | 44 | var ee *exec.ExitError | |
| 35 | 45 | if !errors.As(err, &ee) || ee.ExitCode() != 1 { | |
| 36 | return nil, fmt.Errorf("merge-tree %s %s: %w: %s", base, head, err, errs) | ||
| 46 | return "", nil, fmt.Errorf("merge-tree %s %s: %w: %s", base, head, err, errs) | ||
| 37 | 47 | } | |
| 38 | 48 | if !treeIDAhead(text) { | |
| 39 | 49 | reason := strings.TrimSpace(text) | |
| 40 | 50 | if reason == "" { | |
| 41 | 51 | reason = errs | |
| 42 | 52 | } | |
| 43 | return nil, fmt.Errorf("cannot merge %s into %s: %s", head, base, reason) | ||
| 53 | return "", nil, fmt.Errorf("cannot merge %s into %s: %s", head, base, reason) | ||
| 44 | 54 | } | |
| 45 | 55 | } | |
| 46 | return parseMergeTree(text), nil | ||
| 56 | tree, _, _ := strings.Cut(strings.TrimSpace(text), "\n") | ||
| 57 | return strings.TrimSpace(tree), parseMergeTree(text), nil | ||
| 47 | 58 | } | |
| 48 | 59 | ||
| 49 | 60 | // treeIDAhead reports whether the output starts with a tree id rather than a | |