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/issues/write.go +5−3
| @@ -128,7 +128,7 @@ func AddComment(g *gitx.Repo, id string, c *Comment, actor string) error { | |||
| 128 | 128 | if c.Created.IsZero() { | |
| 129 | 129 | c.Created = time.Now().UTC() | |
| 130 | 130 | } | |
| 131 | raw, err := renderCommentFile(c) | ||
| 131 | raw, err := RenderComment(c) | ||
| 132 | 132 | if err != nil { | |
| 133 | 133 | return err | |
| 134 | 134 | } | |
| @@ -162,7 +162,7 @@ func EditComment(g *gitx.Repo, issueID, commentID, body, actor string) (*Comment | |||
| 162 | 162 | return nil, fmt.Errorf("issues: a comment needs a body") | |
| 163 | 163 | } | |
| 164 | 164 | c.Body = body | |
| 165 | raw, err := renderCommentFile(c) | ||
| 165 | raw, err := RenderComment(c) | ||
| 166 | 166 | if err != nil { | |
| 167 | 167 | return nil, err | |
| 168 | 168 | } | |
| @@ -222,7 +222,9 @@ func findComment(g *gitx.Repo, issueID, commentID string) (*Comment, string, err | |||
| 222 | 222 | return nil, "", fmt.Errorf("issues: no issue %s", issueID) | |
| 223 | 223 | } | |
| 224 | 224 | ||
| 225 | func renderCommentFile(c *Comment) ([]byte, error) { | ||
| 225 | // RenderComment writes a comment as frontmatter plus body. Exported because a | ||
| 226 | // comment on a request is the same file shape, read and written by the same code. | ||
| 227 | func RenderComment(c *Comment) ([]byte, error) { | ||
| 226 | 228 | d := frontmatter.New() | |
| 227 | 229 | d.Word("author", c.Author) | |
| 228 | 230 | d.Word("created", c.Created.UTC().Format(time.RFC3339)) | |