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.

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