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 3: a change request can be discussed
7c6e52e75f3a33be57a81663bc840edd6e14f7f9 · Keenan Nemetz · 17d ago · parent 3c5d11fa
Phase 3: a change request can be discussed Until now the only ways to say something about a request were editing its body or approving it. A review thread is now a pushed file like everything else: requests/<ulid>/comments/<ulid>.md author + created, then the body It is the *same file shape* as a comment on an issue and is read and written by the same code (`issues.ParseComment`, `issues.RenderComment`, with `requests.Comment` as an alias), so the thread inherited the validation, the markdown boundary and the ownership rule instead of reimplementing them. That last one is the part worth having: `pre-receive` compares the author on the old blob to the pushing account, so a review comment is its author's to edit or delete, an admin may repair a thread (audited), and rewriting the author line in the same push does not launder it. `vole request comment <id> -m` prints the comment's id and the page anchor (`#c-<ulid>`), `edit-comment` and `rm-comment` are the author's own words, and `request show` prints the thread so the ids are discoverable at all — an id nobody can see is an id nobody can use. Adding a comment is free, like adding one to an issue: a new file's author is a claim, and the boundary has never pretended otherwise. The request's own file stays outside the ownership rule, for the reason an issue's does: state, head and base are shared, and updating them is the author's normal edit. Verified: TestParseReadsTheDiscussion (a thread plus an approval in one tree, ordered by id), TestCommentNeedsAnAuthorAndATime and TestUnknownFileInARequestIsRefused (the subtree stays a closed format, with both expected shapes named), TestCommentPaths (a request comment is not an issue comment and not an approval); and end to end through the binary, TestRequestDiscussionIsPushedAndOwned — the empty state prints the command, the CLI adds a comment, the page renders it with its anchor and `request show` lists it, a writer is refused rewriting, adopting and deleting somebody else's comment, may join the thread and may edit his own — with the issue-comment empty state assertion added to TestIssuePushedThenRendered earlier. scripts/ci.sh green (gofmt, vet, build, markdown links, go test -race ./...).
internal/web/requests.go +12
| @@ -130,6 +130,16 @@ func (w *Web) RequestView(rw http.ResponseWriter, r *http.Request) { | |||
| 130 | 130 | alreadyMerged = true | |
| 131 | 131 | } | |
| 132 | 132 | } | |
| 133 | // The review thread. Comments arrive by push, so they cross the same markdown | ||
| 134 | // boundary every other pushed body does — sanitised, never rendered raw. | ||
| 135 | type renderedComment struct { | ||
| 136 | requests.Comment | ||
| 137 | Body template.HTML | ||
| 138 | } | ||
| 139 | comments := make([]renderedComment, 0, len(req.Comments)) | ||
| 140 | for _, cm := range req.Comments { | ||
| 141 | comments = append(comments, renderedComment{Comment: cm, Body: w.renderMarkdown(m.Name, cm.Body)}) | ||
| 142 | } | ||
| 133 | 143 | added, removed := diffSummary(files) | |
| 134 | 144 | ||
| 135 | 145 | // Would it land? A diff says what changed; it cannot say whether the branch | |
| @@ -172,6 +182,7 @@ func (w *Web) RequestView(rw http.ResponseWriter, r *http.Request) { | |||
| 172 | 182 | Removed int | |
| 173 | 183 | Truncated bool | |
| 174 | 184 | AlreadyMerged bool | |
| 185 | Comments []renderedComment | ||
| 175 | 186 | Conflicts []string | |
| 176 | 187 | MergeChecked bool | |
| 177 | 188 | Required int | |
| @@ -179,5 +190,6 @@ func (w *Web) RequestView(rw http.ResponseWriter, r *http.Request) { | |||
| 179 | 190 | }{Page: w.base(m.Name + " · " + req.Title), Name: m.Name, Request: req, | |
| 180 | 191 | Description: w.renderMarkdown(m.Name, req.Description), Approvals: approvals, Checks: checks, | |
| 181 | 192 | Files: files, Added: added, Removed: removed, Truncated: truncated, AlreadyMerged: alreadyMerged, | |
| 193 | Comments: comments, | ||
| 182 | 194 | Conflicts: conflicts, MergeChecked: mergeChecked, Required: required, Current: current}, 0) | |
| 183 | 195 | } | |