Files
lazygit/pkg/commands/patch/parse.go
T
Stefan HallerandClaude Opus 5 b61bd44a24 Add a well-formedness check for a parsed patch
Parse is lenient: it takes any text and reads a diff out of it, which is
what we want when we hand it a diff, but it has no way to say "this
isn't one". We're about to parse the *rendered* contents of a diff view,
which a diff renderer is free to restructure — putting the line numbers
in a gutter, say, shifts the +/- marker off the start of each body line,
so every line reads as context and the parse silently lies about which
lines are changes.

Comparing each hunk's body against the lengths its header declares
catches exactly that, without teaching us anything about any particular
renderer's layout: a faithful unified diff agrees with its headers, a
restructured one doesn't.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-01 16:56:57 +02:00

101 lines
2.3 KiB
Go

package patch
import (
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Captures, in order: the old start, the old length (omitted by git when it is
// 1), the new start, the new length (likewise), and the trailing context.
var hunkHeaderRegexp = regexp.MustCompile(`(?m)^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$`)
func Parse(patchStr string) *Patch {
// ignore trailing newline.
lines := strings.Split(strings.TrimSuffix(patchStr, "\n"), "\n")
hunks := []*Hunk{}
patchHeader := []string{}
var currentHunk *Hunk
for _, line := range lines {
if strings.HasPrefix(line, "@@") {
oldStart, oldLength, newStart, newLength, headerContext := headerInfo(line)
currentHunk = &Hunk{
oldStart: oldStart,
newStart: newStart,
declaredOldLength: oldLength,
declaredNewLength: newLength,
headerContext: headerContext,
bodyLines: []*PatchLine{},
}
hunks = append(hunks, currentHunk)
} else if currentHunk != nil {
currentHunk.bodyLines = append(currentHunk.bodyLines, newHunkLine(line))
} else {
patchHeader = append(patchHeader, line)
}
}
return &Patch{
hunks: hunks,
header: patchHeader,
}
}
func headerInfo(header string) (oldStart int, oldLength int, newStart int, newLength int, headerContext string) {
match := hunkHeaderRegexp.FindStringSubmatch(header)
oldStart = utils.MustConvertToInt(match[1])
oldLength = declaredLength(match[2])
newStart = utils.MustConvertToInt(match[3])
newLength = declaredLength(match[4])
headerContext = match[5]
return oldStart, oldLength, newStart, newLength, headerContext
}
// declaredLength parses a length capture of a hunk header, which git omits when
// it is 1 (e.g. "@@ -0,0 +1 @@").
func declaredLength(match string) int {
if match == "" {
return 1
}
return utils.MustConvertToInt(match)
}
func newHunkLine(line string) *PatchLine {
if line == "" {
return &PatchLine{
Kind: CONTEXT,
Content: "",
}
}
firstChar := line[:1]
kind := parseFirstChar(firstChar)
return &PatchLine{
Kind: kind,
Content: line,
}
}
func parseFirstChar(firstChar string) PatchLineKind {
switch firstChar {
case " ":
return CONTEXT
case "+":
return ADDITION
case "-":
return DELETION
case "\\":
return NEWLINE_MESSAGE
}
return CONTEXT
}