Files
jarl/internal/engine/diffcontext.go
adamandClaude Opus 5.5 b097e10bef Keep options in a fixed order, and say which block a diff hunk is in
Options of a block (server options, a view, a zone) are listed and
written grouped by topic, alphabetical within a group, so the same
option is in the same place everywhere. Top-level statements keep the
order they were added in.

named.conf diffs add the enclosing blocks to each hunk header, like
git's function context: @@ -50,6 +49,8 @@ view "internal" {.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:06:22 +01:00

137 lines
3.2 KiB
Go

package engine
import (
"regexp"
"strconv"
"strings"
)
// hunkHeader matches a unified diff hunk header; the line counts are left
// out when they are 1.
var hunkHeader = regexp.MustCompile(`^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@$`)
// labelHunks adds the blocks a hunk's first change is in to its header,
// like git does with function names, so a change to named.conf says where
// it is:
//
// @@ -50,6 +49,8 @@ view "internal" {
//
// Removed lines are looked up in the old file, added ones in the new.
func labelHunks(diff, old, new string) string {
if !strings.Contains(diff, "\n@@ ") {
return diff
}
oldCtx, newCtx := blockContext(old), blockContext(new)
lines := strings.SplitAfter(diff, "\n")
for i, line := range lines {
m := hunkHeader.FindStringSubmatch(strings.TrimSuffix(line, "\n"))
if m == nil {
continue
}
o, _ := strconv.Atoi(m[1])
n, _ := strconv.Atoi(m[2])
label := ""
hunk:
for _, l := range lines[i+1:] {
switch {
case strings.HasPrefix(l, "-"):
label = at(oldCtx, o)
break hunk
case strings.HasPrefix(l, "+"):
label = at(newCtx, n)
break hunk
case strings.HasPrefix(l, " "):
o, n = o+1, n+1
case strings.HasPrefix(l, `\`):
default:
break hunk
}
}
if label != "" {
lines[i] = strings.TrimSuffix(line, "\n") + " " + label + "\n"
}
}
return strings.Join(lines, "")
}
// at returns the context of line n (1-based), or "".
func at(ctx []string, n int) string {
if n < 1 || n > len(ctx) {
return ""
}
return ctx[n-1]
}
// blockContext returns, for each line of a named.conf file, the blocks
// open at its start: `view "internal" { zone "example.com" {`, or "" at
// the top level. It knows named.conf's strings and comments.
func blockContext(text string) []string {
var (
out []string
stack []string
stmt strings.Builder // the current statement, up to a { or ;
inStr bool
inBlock bool // in a /* */ comment
)
label := func() string {
if len(stack) == 0 {
return ""
}
return strings.Join(stack, " { ") + " {"
}
for _, line := range strings.SplitAfter(text, "\n") {
if line == "" {
continue
}
out = append(out, label())
for i := 0; i < len(line); i++ {
c := line[i]
switch {
case inBlock:
if c == '*' && i+1 < len(line) && line[i+1] == '/' {
inBlock = false
i++
}
case inStr:
stmt.WriteByte(c)
if c == '\\' && i+1 < len(line) {
stmt.WriteByte(line[i+1])
i++
} else if c == '"' {
inStr = false
}
case c == '"':
inStr = true
stmt.WriteByte(c)
case c == '#' || c == '/' && i+1 < len(line) && line[i+1] == '/':
i = len(line) // a comment to the end of the line
case c == '/' && i+1 < len(line) && line[i+1] == '*':
inBlock = true
i++
case c == '{':
stack = append(stack, shortLabel(stmt.String()))
stmt.Reset()
case c == '}':
if len(stack) > 0 {
stack = stack[:len(stack)-1]
}
stmt.Reset()
case c == ';':
stmt.Reset()
default:
stmt.WriteByte(c)
}
}
}
return out
}
// shortLabel tidies the text before a { for a hunk header.
func shortLabel(s string) string {
s = strings.Join(strings.Fields(s), " ")
if len(s) > 60 {
s = s[:57] + "..."
}
return s
}