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>
137 lines
3.2 KiB
Go
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
|
|
}
|