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/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.

server/web/request_view.go +30−8

@@ -44,14 +44,20 @@ type requestCI struct {
44 44 // refresh the whole sidebar without rebuilding the rest of the review.
45 45 type requestSidebarView struct {
46 46 Page
47 Name string
48 Request *metadata.Request
49 Approvals []requestApproval
50 Checks []requestCheck
51 Required int
52 Current int
53 CanApprove bool
54 CanClose bool
47 Name string
48 Request *metadata.Request
49 Approvals []requestApproval
50 Checks []requestCheck
51 Required int
52 Current int
53 CanApprove bool
54 CanClose bool
55 // CanMerge is the merge control: true only when the gate would accept the
56 // landing now, so the button never offers something the handler would refuse.
57 CanMerge bool
58 // MergeBlocked is the gate's reason when the caller may merge in principle but
59 // the request is not ready (missing approval, failing check, conflict).
60 MergeBlocked string
55 61 CanDiscuss bool
56 62 ContributorCI *requestCI
57 63 Error string
@@ -107,6 +113,21 @@ func (w *Web) requestSidebar(r *http.Request, m *repo.Meta, req *metadata.Reques
107 113 who := w.accountName(r)
108 114 approvable := req.Open() && canApproveRequest(acl, who, req.Author)
109 115 canClose := canTriage(acl, who, req.Author)
116 // Preflight the landing rather than guessing from the approval count: the same
117 // gate the handler calls is asked here, so the button and the write agree about
118 // approvals, checks and conflicts.
119 var mergeable bool
120 var mergeBlocked string
121 if req.Open() && canMerge(acl, who) {
122 gate := w.landGate(m, acl, who)
123 if tip, _ := m.Git().RevParse("refs/heads/" + req.Base); tip == "" {
124 mergeBlocked = "The base branch " + req.Base + " does not exist."
125 } else if _, err := gate.Check(req.Base, req.Head, tip); err != nil {
126 mergeBlocked = err.Error()
127 } else {
128 mergeable = true
129 }
130 }
110 131 var contributorCI *requestCI
111 132 if req.Open() {
112 133 if ref := w.pendingContributorCI(m, req.Head); ref != "" {
@@ -120,6 +141,7 @@ func (w *Web) requestSidebar(r *http.Request, m *repo.Meta, req *metadata.Reques
120 141 Page: w.page(r, title), Name: m.Name, Request: req,
121 142 Approvals: approvals, Checks: checks, Required: required, Current: current,
122 143 CanApprove: approvable, CanClose: canClose,
144 CanMerge: mergeable, MergeBlocked: mergeBlocked,
123 145 CanDiscuss: w.canDiscuss(r, acl),
124 146 ContributorCI: contributorCI,
125 147 Error: errMsg,