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 3: a review comment points at a line, and stays glued to it

9cc2f08757a36a49c7baa3ce502f5934341e7a5b · Keenan Nemetz · 17d ago · parent 7c6e52e7

Phase 3: a review comment points at a line, and stays glued to it

A review thread can now say where it applies. A comment may carry an anchor — path,
side, line and blob — on the same file it already lives in, written by the same
reader and writer as before (four extra frontmatter lines, refused as a half-written
set if any is missing).

The interesting part is what it anchors on. A line number is not an identity: the
file moves and the number points at different code. A commit id is too coarse: any
unrelated change to the branch would invalidate every anchor. The **blob** of the
content reviewed is content-addressed, so a comment is glued to exactly what was
read: it stays current for as long as that file is unchanged — the same file at a
later head has the same blob id — and becomes stale the moment it changes, rather
than sliding to a line that now says something else. That is what "survive a
rewrite" has to mean, and the page enforces it by resolving the blob at the head (or
at the merge base, for `side: old`) and comparing.

Which is also why nothing is verified at the push boundary. An anchor is a display
hint, not an authorization: an anchor that lies can only make itself stale, and a
rule in pre-receive would have to re-check every old anchor against a head that has
since moved — rejecting pushes for comments nobody touched.

`vole request comment <id> -m … --path f --line n [--side old]` computes the blob
from the clone, refuses a line past the end of the file, and says `git fetch` rather
than writing an anchor it cannot compute. On the page, a current anchor puts a marker
under the line that links to the comment, and the card says `path:line (side)`; a
stale one keeps its place in the thread with "the file has changed since this
comment" and loses the marker. `side: old` covers reviewing a deletion, since that
line exists only at the merge base.

Verified: TestAnchorRoundTrips and TestAnchorMustBeWhole (blob is full-length, side
is new or old, line is 1 or more, path stays inside the repository, and half an
anchor is a refusal naming the file); and end to end through the binary,
TestReviewCommentAnchorsALine — a new-side comment whose blob matches
`rev-parse <head>:code.txt` on the server, the marker and the labelled card on the
page, an old-side comment whose blob matches the merge base, a line past the end
refused with the branch unmoved, and then a new head that changes the file, after
which the new-side comment is labelled stale and loses its marker while the old-side
one does not. scripts/ci.sh green (gofmt, vet, build, markdown links, go test -race
./...).

internal/issues/write.go +10

@@ -4,6 +4,7 @@ import (
4 4 "errors"
5 5 "fmt"
6 6 "sort"
7 "strconv"
7 8 "strings"
8 9 "time"
9 10
@@ -228,6 +229,15 @@ func RenderComment(c *Comment) ([]byte, error) {
228 229 d := frontmatter.New()
229 230 d.Word("author", c.Author)
230 231 d.Word("created", c.Created.UTC().Format(time.RFC3339))
232 if c.Anchor != nil {
233 // The anchor goes after the header fields so a reader that only wants the
234 // thread still sees author and time first, and one added anchor is one
235 // added block rather than a rewritten line.
236 d.Word("path", c.Anchor.Path)
237 d.Word("side", c.Anchor.Side)
238 d.Word("line", strconv.Itoa(c.Anchor.Line))
239 d.Word("blob", c.Anchor.Blob)
240 }
231 241 return d.Bytes(strings.TrimRight(c.Body, "\n"))
232 242 }
233 243