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 }