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.

Phase 2: an assignee is a person you can click

67dd8854450865e9c5d2a8643b99c9ef7f71c284 · Keenan Nemetz · 17d ago · parent 307cbc2a

Phase 2: an assignee is a person you can click

`assignees:` was in the frontmatter, written by the tool and read by nothing:
not validated, not set by any command, not in the index, not on a page. It now
goes the whole way — the field is checked at the push boundary, the index holds
it in an `issue_assignees` table mirroring `issue_labels`, and the web renders it
as a filter link on both the list and the detail page.

The field is held to the *roster's* account-name rule rather than the label
rule. It names a person, so the label rule (which allows dots) would accept a
value no account could ever have; a capital or a `@` prefix is a typo that would
render as a link to nobody. It is deliberately NOT resolved against the roster at
push time: a clone may be offline, an assignment may name someone who has not
been created yet, and a name in an issue file is a claim on the same footing as
`author` — the hook's job is the format, not the org chart. Stored as written
rather than as an account id, for the same reason `author` is: the file stays
readable in a plain clone, and because names are never reissued an assignment
keeps meaning the same person after a rename.

The index needed a schema version to go with the new table, because
`EnsureIssues` only re-reads when the ref moves: an existing instance would have
served rows derived from the old shape until someone happened to push. A bump
now empties the derived issue rows and leaves every repo's `issues_sha` empty, so
the next read rebuilds from git. The index is disposable; a stale row is worse
than a rebuild.

`vole issue new -a`, `assign`, `unassign`, `list --assignee` and `%(assignees)`
are the CLI half. An assignment that changes nothing is not a commit — `Update`
reports `ErrNoChange` and the branch does not move — so a script that reruns
stays idempotent without growing a linear log.

Verified: TestAssigneeRuleMatchesAccountNames compares this package's regex with
identity.ValidateName over the edge cases (a name the roster accepts and the
format rejects would only ever show up as a rejected push);
TestAssigneesAreAccountNames drives valid, capitalised, dotted, `@`-prefixed,
too-short, too-long and space-bearing values through the reader;
TestAssigneeRoundTrip proves the writer emits a sorted one-per-line list that the
reader reads back; TestSchemaVersionDropsRowsFromAnOlderShape writes rows an
older build would have left, reopens, and checks they are gone, `issues_sha` is
empty and a second open does not re-wipe; and
TestIssueAssigneesRenderedFilteredAndValidated plus the CLI end-to-end test push
an assignment over SSH, click it, filter by it and get a push rejected for
`Wilbur` with the path and value named and the branch unmoved. scripts/ci.sh
green (gofmt, vet, build, markdown links, go test -race ./...).

internal/web/templates.go +8−1

@@ -271,6 +271,7 @@ select{background:var(--card);color:var(--fg);border:1px solid var(--line);borde
271 271 <option value="closed"{{if eq .Filter.State "closed"}} selected{{end}}>closed</option>
272 272 </select>
273 273 <input type="hidden" name="label" value="{{.Filter.Label}}">
274 <input type="hidden" name="assignee" value="{{.Filter.Assignee}}">
274 275 <button type="submit">Filter</button>
275 276 {{if .Filtered}}<a href="/r/{{.Name}}/issues">clear</a>{{end}}
276 277 </form>
@@ -280,13 +281,18 @@ select{background:var(--card);color:var(--fg);border:1px solid var(--line);borde
280 281 {{range .Labels}}<a class="label" href="/r/{{$.Name}}/issues?label={{.}}">{{.}}</a>{{end}}
281 282 </p>
282 283 {{end}}
284 {{if .Assignees}}
285 <p class="muted">Assignees:
286 {{range .Assignees}}<a class="label" href="/r/{{$.Name}}/issues?assignee={{.}}">@{{.}}</a>{{end}}
287 </p>
288 {{end}}
283 289 {{if .Issues}}
284 290 <table>
285 291 <tr><th>ID</th><th>Title</th><th>State</th><th>Author</th><th>Replies</th><th>Age</th></tr>
286 292 {{range .Issues}}
287 293 <tr>
288 294 <td class="sha">{{short .ID}}</td>
289 <td><a href="/r/{{$.Name}}/issues/{{.ID}}">{{.Title}}</a>{{range .Labels}}<span class="label">{{.}}</span>{{end}}</td>
295 <td><a href="/r/{{$.Name}}/issues/{{.ID}}">{{.Title}}</a>{{range .Labels}}<span class="label">{{.}}</span>{{end}}{{range .Assignees}} <a class="label" href="/r/{{$.Name}}/issues?assignee={{.}}">@{{.}}</a>{{end}}</td>
290 296 <td class="muted">{{.State}}</td>
291 297 <td class="muted">{{.Author}}</td>
292 298 <td class="muted">{{.Comments}}</td>
@@ -310,6 +316,7 @@ labels, then the body as markdown.</p>
310 316 <p class="crumbs">
311 317 <span class="sha">{{.Issue.ID}}</span> · {{.Issue.State}} · opened by {{.Issue.Author}} {{since .Issue.Created.Unix}}
312 318 {{if .Labels}}<span class="label">{{.Labels}}</span>{{end}}
319 {{range .Issue.Assignees}}<a class="label" href="/r/{{$.Name}}/issues?assignee={{.}}">@{{.}}</a>{{end}}
313 320 </p>
314 321 <article class="md">{{.Description}}</article>
315 322 <h2>{{len .Comments}} comment(s)</h2>