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.
Phase 2: a comment is editable, by the person who wrote it
79c88d72d36d8974dca5859125f8f37f0b06c1f7 · Keenan Nemetz · 17d ago · parent 67dd8854
Phase 2: a comment is editable, by the person who wrote it Comment editing was the last open follow-up on Phase 2, and the workaround was "append another comment, or rewrite history and force-push" — a redaction tool used for a typo. An edit is now an ordinary commit that rewrites the comment's file in place: the path stays, so the id, the thread order and one `git log -p -- issues/<id>/comments/<cid>.md` that answers "what did it say" all survive. A delete removes the file and leaves the text in history. No `edited` field, for the same reason `updated` was dropped from issues: it would be a line rewritten on every edit, and the commit that changed the file already carries the time. Editing also does not re-surface anything — the feed question is a timestamp, not a field. Ownership is the interesting part. A comment's author is a name in a file, and a name is not authentication, so `pre-receive` compares the author on the OLD blob against the account the key resolved to. The old blob is the one that was validated when it landed; the new one is whatever the pusher typed, and reading it would let anyone adopt a comment by editing its author line in the same push. Names are never reissued, so a former name still resolves through the roster and an account keeps its own comments across a rename. An admin may override — a thread written by somebody who has left is otherwise unrepairable — and the override is written to data/audit.log, like a force-push or a release rewrite. Two things are deliberately outside the rule: - Adding a comment. A new file's author is a claim, exactly like an issue's `author`, and the boundary has never pretended otherwise. - The issue file itself. `state`, `labels` and `assignees` are shared fields, so closing or assigning somebody else's issue is triage; refusing it would make the fields useless, and it is the same write any writer could already do by hand. A description can therefore be corrected by any writer — unchanged behaviour, now stated. `vole issue edit` (title, body, labels, assignees) covers the issue file, and `issue comment` now prints the comment id it created while `issue show` and the web page print it too — otherwise `edit-comment` would take an id nobody could see. Two comments written in the same millisecond share their ULID timestamp prefix, so a short prefix is ambiguous there; the CLI names both matches instead of guessing. Also fixed on the way: `frontmatter.Value` reads one field without strict decoding. `AuthorOf` first used Decode, whose KnownFields is right for validation but wrong for "who wrote this" — the hook refused every comment edit with `field created not found`. Verified: TestCommentEditsBelongToTheirAuthor pushes as two accounts and drives all four refusals (rewrite, adopt-by-author-line, delete, and the untouched thread afterwards), plus an author editing his own comment, a writer closing someone else's issue, and an admin delete that lands only with its audit line. TestAuthorOfReadsAnyMetadataFile covers the strict-decoder bug directly; TestCommentPathMatchesTheReader pins the writer/validator path agreement; the CLI end-to-end test edits a comment, retitles an issue and deletes the comment through the binary against a real instance. scripts/ci.sh green (gofmt, vet, build, markdown links, go test -race ./...).
docs/roadmap.md +18−2
| @@ -192,8 +192,22 @@ Follow-ups: | |||
| 192 | 192 | a rename, since names are never reissued. `vole issue new -a`, `assign`, | |
| 193 | 193 | `unassign`, `list --assignee`, `%(assignees)`, the index's `issue_assignees` | |
| 194 | 194 | table and the web's filter links all read that one field. | |
| 195 | - Comment editing and deletion (today: append another comment, or rewrite history | ||
| 196 | and force-push). | ||
| 195 | - ✅ **Comments are editable and deletable, by their author.** Today the only | ||
| 196 | options were "append another comment" or rewrite history and force-push. An | ||
| 197 | edit is a normal commit that rewrites the comment's file in place — so the path, | ||
| 198 | the id and one `git log -p -- issues/<id>/comments/<cid>.md` all still work — | ||
| 199 | and a delete removes the file, leaving the text in history. Neither adds an | ||
| 200 | `edited` field, for the reason `updated` was dropped: the commit carries the | ||
| 201 | time. `pre-receive` compares the author on the **old** blob against the pushing | ||
| 202 | account, so only that person may change or delete a comment, and rewriting the | ||
| 203 | author line in the same push does not launder it; an admin may override, audited, | ||
| 204 | because a thread written by someone who has left is otherwise unrepairable. | ||
| 205 | `vole issue edit-comment`, `rm-comment` and `edit` (title, body, labels, | ||
| 206 | assignees) plus `issue comment` printing its id make the whole loop usable from | ||
| 207 | the CLI, and each comment shows its id on the web. The issue file itself is | ||
| 208 | deliberately **not** authorship-checked: state, labels and assignees are shared | ||
| 209 | fields, and refusing a maintainer's close or assignment would make them useless. | ||
| 210 | Deleting is not redaction — that remains an admin force-push. | ||
| 197 | 211 | ||
| 198 | 212 | Exit criteria: a user with an editor and a key files, comments on and closes an | |
| 199 | 213 | issue without installing Vole; an invalid tree is rejected with the offending path | |
| @@ -760,6 +774,8 @@ Filled in as phases land. Append; don't rewrite. | |||
| 760 | 774 | | 2026-10-23 | An assignee is a name held to the account-name rule, not roster-checked at push | It names a person, so the label rule (which allows dots) would accept a value no account can have — but resolving it against the roster at the gate would fail a push written offline, or for someone added after the assignment. A name in frontmatter is a claim on the same footing as `author`; the web shows it and the filter links by it. Stored as written, so a rename never rewrites history (names are never reissued) and the file stays readable in a clone | | |
| 761 | 775 | | 2026-10-23 | The index carries a schema version; a bump empties the derived rows | A new table and column would leave existing instances serving rows derived from the old shape until the next push touched each repo. The index is disposable, so the honest migration is "delete and let the next read rebuild from git" — a stale row is worse than a rebuild, and the version stops a half-applied schema from being read as if it were complete | | |
| 762 | 776 | | 2026-10-23 | An edit that changes nothing is not a commit | `vole issue assign` on an already-assigned person would otherwise add an empty commit to a linear branch, and a script that reruns has to be idempotent without growing the log. `Update` reports `ErrNoChange` and the branch does not move | | |
| 777 | | 2026-10-23 | A comment is edited in place, deleted by removing its file, and has no `edited` field | The file is the comment: keeping the path keeps the id, the thread order and one `git log -p -- <path>` that answers "what did it say". A deleted comment leaves its text in history, which is honest — redaction is still an admin force-push — and a flat ULID-keyed thread has no replies to orphan. `edited:` would repeat the argument that dropped `updated`: a line rewritten on every edit, when the commit already carries the time | | ||
| 778 | | 2026-10-23 | Comment authorship is checked against the old blob, and only for comments | The old blob was validated when it landed; the new one is whatever the pusher wrote, so comparing it would let anyone adopt a comment by editing its author line in the same push. Restricting the rule to comments is deliberate: `state`, `labels` and `assignees` are shared fields and a maintainer who cannot close or assign someone else's issue has no triage. Admins may still repair a thread, audited | | ||
| 763 | 779 | ||
| 764 | 780 | ## Open questions | |
| 765 | 781 | ||