package index
import (
"fmt"
"strconv"
"strings"
)
// A LINE NOTE is a landing remark pinned to one line of the diff. It is
// an ordinary reply in the record's thread, so it is history like every
// other remark and a clone reads it without asking the forge; the body
// opens with a short header naming the line, then a blank line, then
// what the reviewer wrote:
//
// zit-line: 1
// file: internal/web/web.go
// side: new
// line: 2251
// at: 8a0ba3d4805e…
//
// This should be recorded before the tickets close.
//
// side is "new" for a line the branch has (an addition or context, by
// its number in the branch) and "old" for a line it removed (by its
// number in the target). at is the branch tip the reviewer was looking
// at, so a page can say when the line has moved on since.
const lineNoteMarker = "zit-line: 1"
// LineNote is where a line note points.
type LineNote struct {
File string `json:"file"`
Side string `json:"side"`
Line int `json:"line"`
At string `json:"at,omitempty"`
}
// Valid reports whether n names a line a diff can have.
func (n LineNote) Valid() error {
switch {
case n.File == "" || len(n.File) > 400 || strings.ContainsAny(n.File, "\n\r"):
return fmt.Errorf("a line note needs a file path")
case n.Side != "new" && n.Side != "old":
return fmt.Errorf("a line note's side is new or old, not %q", n.Side)
case n.Line < 1 || n.Line > 10_000_000:
return fmt.Errorf("a line note's line is a positive number")
case len(n.At) > 64 || strings.Trim(n.At, "0123456789abcdef") != "":
return fmt.Errorf("a line note's at is a check-in hash")
}
return nil
}
// FormatLineNote writes a note's body: the header, a blank line, prose.
func FormatLineNote(n LineNote, prose string) string {
var b strings.Builder
fmt.Fprintf(&b, "%s\nfile: %s\nside: %s\nline: %d\n", lineNoteMarker, n.File, n.Side, n.Line)
if n.At != "" {
fmt.Fprintf(&b, "at: %s\n", n.At)
}
b.WriteString("\n")
b.WriteString(prose)
return b.String()
}
// ParseLineNote reads a body written by FormatLineNote: where it points,
// and the prose under the header. ok is false for any other body.
func ParseLineNote(body string) (n LineNote, prose string, ok bool) {
head, rest, found := strings.Cut(strings.ReplaceAll(body, "\r\n", "\n"), "\n\n")
if !found {
head, rest = body, ""
}
lines := strings.Split(head, "\n")
if len(lines) == 0 || strings.TrimSpace(lines[0]) != lineNoteMarker {
return LineNote{}, "", false
}
for _, l := range lines[1:] {
k, v, ok := strings.Cut(l, ":")
if !ok {
return LineNote{}, "", false
}
v = strings.TrimSpace(v)
switch strings.TrimSpace(k) {
case "file":
n.File = v
case "side":
n.Side = v
case "line":
i, err := strconv.Atoi(v)
if err != nil {
return LineNote{}, "", false
}
n.Line = i
case "at":
n.At = v
}
}
if n.Valid() != nil {
return LineNote{}, "", false
}
return n, rest, true
}