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 ./...).
README.md +18−1
| @@ -146,7 +146,10 @@ Or use the CLI, which is the same thing with the paperwork done: | |||
| 146 | 146 | ||
| 147 | 147 | ```sh | |
| 148 | 148 | vole issue new --title "Nothing renders on the docs page" -m "Steps..." -l bug -a wilbur | |
| 149 | vole issue comment -m "Also affects search" 01M5JR2JK29KKMAA1T9MDNTPBP | ||
| 149 | vole issue comment -m "Also affects search" 01M5JR2JK29KKMAA1T9MDNTPBP # prints the comment id | ||
| 150 | vole issue edit-comment 01M5JR2JK29KKMAA1T9MDNTPBP 01M5N1PRC4GJPZNEEG8PR8J4D6 -m "Corrected." | ||
| 151 | vole issue rm-comment 01M5JR2JK29KKMAA1T9MDNTPBP 01M5N1PRC4GJPZNEEG8PR8J4D6 | ||
| 152 | vole issue edit 01M5JR2JK29KKMAA1T9MDNTPBP --title "A better title" -l bug -l docs | ||
| 150 | 153 | vole issue assign 01M5JR2JK29KKMAA1T9MDNTPBP wilbur | |
| 151 | 154 | vole issue unassign 01M5JR2JK29KKMAA1T9MDNTPBP wilbur | |
| 152 | 155 | vole issue close 01M5JR2JK29KKMAA1T9MDNTPBP | |
| @@ -177,6 +180,20 @@ frontmatter. Closing an issue is editing `state` to `closed`. The push is | |||
| 177 | 180 | rejected with the offending path named if the tree does not match the format, and | |
| 178 | 181 | every issue is a normal commit you can `git log`, `git blame` and merge. | |
| 179 | 182 | ||
| 183 | Editing is a commit, not a special operation. Rewriting a comment's body keeps | ||
| 184 | its file, so `git log -p -- issues/<id>/comments/<ulid>.md` is what it used to | ||
| 185 | say; deleting one removes the file, and the text stays in history. Neither adds | ||
| 186 | an `edited` field — the commit that changed the file carries the time. | ||
| 187 | |||
| 188 | A comment belongs to whoever wrote it: `pre-receive` compares the author recorded | ||
| 189 | on the **old** blob (the one that was validated when it landed) against the | ||
| 190 | account the SSH key resolved to, so only that person can change or delete it, and | ||
| 191 | editing the author line in the same push does not help. A repo admin may override | ||
| 192 | — a thread written by someone who has left is otherwise unrepairable — and that | ||
| 193 | override is written to `data/audit.log`. The issue's own file is not covered: | ||
| 194 | closing, relabelling and assigning somebody else's issue is triage, not | ||
| 195 | authorship. Deleting is not redaction; redaction is still an admin force-push. | ||
| 196 | |||
| 180 | 197 | ## Reviewing a change | |
| 181 | 198 | ||
| 182 | 199 | Review is a git artifact, not a web session. An approval records the exact commit | |