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.

Code

395 commit(s) on main

update readme Keenan Nemetz · 38m ago 32290114
source preview history blame raw 17.3 KiB
1# Change requests
2
3Review should not depend on a web session or a vendor. In Vole, a change request
4is a normal branch and a normal push. A request opened from a branch follows that
5branch: every push moves the request's head, so an approval always names the exact
6commit it reviewed and a moved branch starts the review over.
7
8## Files behind a request
9
10```text
11changes/<ulid>/change.md # title, state, author, base, head, closes
12changes/<ulid>/comments/<ulid>.md # discussion
13changes/<ulid>/approvals/<account-name>.md # one approval per account
14```
15
16## Opening and landing a request
17
18A request starts from a branch you may push. Which branches those are depends on
19your role:
20
21- A `maintain` or `admin` account may push shared branches, so it can propose any
22 ordinary branch in the repository.
23- A `contribute` account may propose only a branch in its own namespace,
24 `refs/heads/contrib/<name>/`.
25
26The **Open change request** form on the repository's change-request page lists
27exactly the branches your account may propose; pre-receive checks the branch the
28request names the same way. A `contribute` account cannot name an arbitrary
29commit or another account's branch. The namespace is the account name, which is
30fixed at creation. CI for a branch under `refs/heads/contrib/` is held until a
31maintainer allows the exact tip from the repository page; a shared branch runs CI
32when it is pushed.
33
34The change's author or a maintainer can correct its title with the pencil beside
35the heading, or its description from the card's menu. Each opens its own field in
36place, so saving does not leave the page or replace the review. The prose is not
37what an approval names — the base and head are — so the review and its approvals
38stay as they are, and a closed or merged record can be corrected too.
39
40A maintainer can open a request from a shared branch with the CLI:
41
42```sh
43# The author pushes a branch and opens a request for the issue it fixes.
44git push origin feature:refs/heads/feature
45vole change new --title "Retry on timeout" --base main --head feature \
46 --closes 01M5JR2JK29KKMAA1T9MDNTPBP -m "Rationale."
47
48# An approver reviews that commit and approves it.
49vole change approve <id> --as <your-name>
50
51# The author lands it. Vole checks that the approval still matches the new tip.
52git push origin feature:refs/heads/main
53```
54
55The request records the branch it follows in `head_ref:` and the commit currently
56under review in `head:`. A branch is resolved on the remote, not in the local
57clone: if the clone is behind, the request records the remote tip and says so on
58stderr, because that is the commit the forge will review and land. A branch that
59is only local is recorded too, with a note that it is not on the remote yet. A
60request whose head is already in its base — the same commit, or an ancestor — is
61refused: it would review nothing. The request page, the request list and
62`vole change show` show both. After every accepted push, Vole moves `head:` to the new tip of the
63followed branch, so a fixup does not need a second command. Approvals and checks
64still name a commit: the approvals that named the old tip stop counting, and the new
65tip needs its own CI result. An approval is never carried silently to a commit nobody
66reviewed — the approver's form names the base and head it displayed, and the writer
67refuses if a push or a retarget moved either one in between.
68
69A request opened from a bare commit id records no branch and stays pinned to that
70commit. `vole change update <id> --head <branch>` makes an open request follow a
71branch (or a different one); `--head <commit>` pins it to one commit. The request
72page's base/head picker does the same. Neither is needed after a normal push: the
73request already follows the branch it was opened from. A branch that is deleted
74leaves the last tip it saw in `head:` and the page says the branch is gone.
75
76Other commands:
77
78```sh
79vole change list [--state open|closed]
80vole change show <id>
81vole change update <id> [--base <branch>] [--head <rev>] [--closes <issue>]... [--no-closes]
82vole change close <id>
83vole change reopen <id>
84```
85
86`vole change show <id>` also tells you whether the branch still merges, when the
87needed commits are available locally. If they are not, it says so rather than
88guessing.
89
90## Stacked requests
91
92A request may target another feature branch instead of `main`. Push the lower
93branch first, then open the upper request with `--base <lower-branch> --head
94<upper-branch>`. Its diff is only the upper change, not everything the lower branch
95adds as well.
96
97When the lower request lands, retarget the upper one:
98
99```sh
100vole change update <id> --base main
101```
102
103The request page's target picker changes the base the same way. A new base changes
104the diff, and every approval named the old base, so the upper request needs a fresh
105review against the new base. Changing the base does not change the branch the
106request follows; pick a different head branch too only if the upper branch itself
107moved.
108
109A landing moves the base branch without a push, and Vole still moves the open
110requests that follow that branch onto the landed commit — the same rule a push to
111the branch applies. So a lower request follows its branch when an upper request
112lands into it, with no separate command.
113
114## Linking the issues it closes
115
116A change request names the issues it fixes in its own metadata, not in its
117description: `change.md` carries a `closes` list, the request page shows those
118issues, and landing the request closes them in the same commit that marks it
119merged. A `#42`, `#<ulid>` or `Fixes #42` in the description is only prose — it
120does not link anything and closes nothing.
121
122Set the list when the request is opened, or later while it is still open:
123
124```sh
125vole change new --title "Retry on timeout" --base main --head feature \
126 --closes 01M5JR2JK29KKMAA1T9MDNTPBP,01M5N1PRC4GJPZNEEG8PR8J4D6
127vole change new --title "Retry on timeout" --base main --head feature \
128 --closes 42,43 # the same list by number
129vole change update <id> --closes 42 # replace the list
130vole change update <id> --no-closes # clear it
131```
132
133The flag takes a full ULID or the `#42` number, and `42` is resolved to the ULID
134before the link is written, so the file always holds ids. The issue has to exist
135when the link is written, so a typo fails while the author is still looking at the
136command, and a number that names a change request is refused: a request cannot
137close a request. The browser's **Open change request** form takes the same list in
138its **Closes** field.
139
140A number is shared with issues, so `#42` may name either kind and the server
141resolves it when the link is rendered. `closes:` is not where that resolution
142lives: it holds ULIDs, because that is the reference that survives a rewritten
143history.
144
145A repository can require a linked issue for every new change request. The
146checkbox is in **Settings → Access**, and the same rule is available to an
147instance admin from the terminal:
148
149```sh
150vole admin acl require-issues enable demo
151```
152
153The request page marks **Closes** as required, the browser refuses an unlinked
154request, and the push gate refuses one added through the CLI or a hand-written
155metadata commit. Existing requests are unaffected: turning the setting on does
156not invalidate history, and turning it off allows new requests with no link
157again.
158
159Only an open request may be relinked: a merged request's list records what the
160merge did, and a closed one is not going to land. The request page's **Issues**
161section lists the linked issues in the same rows as the overview's recent issues.
162Its search box finds issues by title, `#number`, or ULID; picking a result links
163it, and the unlink icon removes one. Both controls use the same `closes` list as
164the CLI, and only the request's author or a maintainer may change it. Landing is
165what closes an issue; linking one does not, and a request that is closed without
166landing leaves its issues open.
167
168## How approvals work
169
170A protected branch only accepts an update that carries the reviewed head. When
171nothing else has landed, the update is the head itself. When the branch has moved,
172Vole builds the merge, tests that, and lands the commit it tested.
173
174The approval names the base branch and the head, not the commit the base currently
175points at. A base move therefore retests the combined result but does not invalidate
176the approval: nobody has to rebase or re-approve just because something else landed
177first.
178
179Adding a commit to the branch after approval still does not carry the old approval.
180The request follows its branch, so the push moves its head and the approval names a
181commit that is no longer the head.
182
183"Merge, then push" is accepted only when the pushed merge commit has the tree a
184clean merge of the branch tip and the reviewed head would produce, and the combined
185result has passed. A merge commit with extra content, or with conflicts resolved by
186hand, is refused: it is content nobody reviewed under that approval. The **Merge**
187button and `vole change merge` are the supported path.
188
189## The combined result
190
191For a landing that needs a merge commit, Vole builds it and runs the repository's
192workflows against that tree. A combined result is tested when a head is pushed, and
193again when the base moves if the request is under review or already waiting to
194merge. The server looks at open requests on a short interval, so a burst of
195work-in-progress pushes settles on the head that is actually there before a run is
196queued.
197
198The combined result uses the workflow files at the merge, so a workflow change on
199the base applies to the landing. It is a normal run for the base branch, but it does
200not deploy the repository's site.
201
202A landing lands that same commit, so the push that moves the branch does not run
203those jobs a second time: the tree, the workflow and the job already ran for this
204branch, and the run list shows the run that tested it once. A job that deploys the
205site is the exception — a combined result never deploys — so that one still runs on
206the branch, because publishing the site is work the combined result did not do. A combined result derived from a contributor
207branch is still untrusted code: it runs only after that exact head was allowed, and
208it uses the contributor's cache namespace.
209
210The request page shows one status:
211
212- **Passed with the latest main** — Merge lands immediately.
213- **Testing the merge with the latest main** — Merge becomes **Merge when ready**
214 and lands as soon as the run passes.
215- **The combined result failed** — the runs name the job. Pushing a fix starts a new
216 review, because the head changes.
217- **Conflicts with the latest main** — resolve them on the branch; the server does
218 not invent a resolution.
219
220**Merge when ready** survives the base moving. It stops when the head changes, the
221request closes, the actor loses merge permission, or the request conflicts. Every
222landing re-checks the approvals, the actor's permission, and the combined result at
223the moment the branch moves. `vole change merge` does the same thing from the
224terminal, and `vole change show` prints the combined result and whether it is ready.
225
226An approval can also be recorded from the request page in the browser when the
227signed-in account has maintain access. The approver is the session's account name, not
228a name from the page. The page's Approve form carries the base and head it
229displayed; if a push moved the branch, or the request was retargeted, before the form
230was submitted, the approval is refused and the approver sees the new target instead
231of approving it unseen.
232
233An approval is stored at `approvals/<account-name>.md`. Only the account named in
234that path can create, change, or remove it. `change.md` records the author's
235account name in `author`; that is the only author field and it is what blocks
236self-approval. An admin can repair an
237approval, and the override is recorded in `data/audit.log`.
238
239An approval counts only while its author still has the standing to give it: the
240account must be active and must currently hold `maintain` or `admin` on the
241repository. Retiring an account or reducing its role therefore withdraws its
242approvals without editing any file — the file stays on the branch, but the gate
243stops counting it, and the page shows it as **no longer counts** rather than as a
244review the landing will accept. The page and the gate use the same rule, so a
245request page cannot promise a merge that the push then refuses.
246
247The request page has three tabs. **Conversation** holds the description,
248discussion, approvals and checks. **Files** is the review surface: a tree of the
249changed files beside the selected file's diff, with line comments and their
250composer in place. **Runs** lists the workflow results recorded for the head
251commit, with each run's duration and attempts, and links to the run page, step
252logs and full log. `vole change show` prints the same runs as one line each, with the
25326-character id, so a failed check can be opened with `vole run show <id>` and read
254with `vole run logs <id>`. It also lists the combined result's runs when the landing
255needs one, because that commit can fail separately from the head.
256
257Together they show:
258
259- the changes compared with the branch it would merge into,
260- CI checks for the head commit,
261- the combined result that would land, and whether it has passed,
262- how many approvals are required and who approved which commit,
263- whether the branch still merges cleanly.
264
265Vole checks the last item with Git itself, without touching a working copy. It is
266shown for information, not enforced. Vole leaves it out when it cannot answer — for
267example, if the base branch no longer exists.
268
269A merged request keeps showing the change it landed. The mark records
270`merged_base`, the tip the base pointed at before it moved, because afterwards the
271head is in the base and a diff of the two is empty. That is what the Files tab
272diffs when the request is already merged.
273
274## The change list
275
276The change list shows one status per row, so the queue can be read without
277opening every change:
278
279- **Waiting on CI approval** — a contributor branch at this commit has not been
280 allowed by a maintainer yet.
281- **Waiting on CI jobs** — checks for the commit are queued or running.
282- **CI failed** — a check failed; pushing a fix starts a new review.
283- **CI could not run** — a runner could not start a job for this commit, so nothing
284 was tested. The commit is not what failed: rerun the job once the machine can
285 build, and the check takes the new result.
286- **Waiting for review** — fewer current approvals than the base branch requires.
287- **Merge conflict** — the branch no longer merges into its base cleanly.
288- **Ready to merge** — the required approvals are in (or none are required) and
289 the commit's checks passed.
290- **Merged**, **Closed** — the change landed, or was closed without landing.
291
292The list also shows when each change was last updated: the newest commit that
293touched its directory, which covers an edit, a comment, and an approval. It sorts
294by that, so the change that moved most recently is first.
295
296## Review comments
297
298```sh
299vole change comment <id> -m "Does this hold for the empty repository case?"
300vole change comment <id> -m "Good catch, amended." # prints the comment id
301vole change comment <id> --reply-to <comment-id> -m "A follow-up."
302vole change edit-comment <id> <comment-id> -m "Rephrased."
303vole change rm-comment <id> <comment-id>
304```
305
306A comment can reply to any comment with `--reply-to`. The web shows one level of
307nesting by collecting a root's descendants under it. Each comment has a context menu with
308Reply, Copy, Edit and Delete. Reply targets the one composer at the foot of the
309discussion; **Cancel reply** clears the target without clearing the draft. Replies
310can be folded under their root, while the root comment stays visible. Editing and
311deleting are limited to the author or an admin. Deleting a comment leaves a
312tombstone so its replies keep the thread they answer.
313
314Only the author can edit or delete a comment. An admin can override that, and the
315override is recorded in `data/audit.log`. Comments appear in the Conversation tab
316with an anchor for each one (`#c-<ulid>`); a line comment also appears beneath its
317line in the Files tab (`#inline-c-<ulid>`). A line comment in the conversation
318carries a collapsed excerpt of the lines around it, read from the exact blob the
319comment named, so a stale comment still shows the code that was reviewed.
320
321You can attach a comment to a line on either side of the diff:
322
323```sh
324vole change comment <id> -m "this drops the check" \
325 --path internal/auth.go --line 88
326vole change comment <id> -m "and this was the old line" \
327 --path internal/auth.go --line 88 --side old
328```
329
330The comment remembers the version of the file it was written about, not just the
331line number. If the file changes, the web page marks the comment as out of date.
332The CLI needs the commits in your clone; if they are missing, it tells you to run
333`git fetch` instead of guessing.
334
335## Branch rules
336
337```sh
338vole admin acl protect add demo refs/heads/main # protect a branch
339vole admin acl set-approvals demo 2 # require two approvals
340vole admin acl set-approvals demo 0 # back to admin-only
341vole admin acl checks enable demo # also require passing CI checks
342```
343
344Admins can push a protected branch directly. Every merge through review is
345recorded in `data/audit.log` with the request id and approvers. For the runner
346allowlist behind required checks, see
347[Runners](runner.md#who-can-run-jobs-and-report-results).
348