Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,24 @@ func matchFragment(source []fileLine, start int, fragment []fileLine, ignoreWhit
}

func lineMatches(left, right fileLine, ignoreWhitespace bool) bool {
if left.hasNewline != right.hasNewline || left.eofMarker != right.eofMarker {
if left.hasNewline != right.hasNewline {
return false
}

if left.eofMarker != right.eofMarker {
// eofMarker is unreliable on blank lines: a mid-file blank and the
// synthetic source-EOF sentinel both serialize as " \n" in unified
// diff, so the parser can't tell them apart. We accept the position
// match here; applyHunk then writes the correct eofMarker by copying
// it from the matched source line instead of the patch line.
isBlankWithNewLine := func(line fileLine) bool {
return line.text == "" && line.hasNewline
}

if !isBlankWithNewLine(left) || !isBlankWithNewLine(right) {
return false
}
}
if left.text == right.text {
return true
}
Expand Down
8 changes: 7 additions & 1 deletion apply_result.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ type applyConflict struct {

// applyError reports the aggregate apply outcome.
type applyError struct {
DirectMisses int
// DirectMisses counts hunks whose preimage could not be located in the
// pristine file during a direct (non-merge) apply. The output content is
// left unchanged for those regions; no conflict markers are emitted.
DirectMisses int
// MergeConflicts counts hunks whose preimage could not be located during
// a merge-mode apply. The output content contains git-style conflict
// markers (<<<<<<<, =======, >>>>>>>) around each affected region.
MergeConflicts int
// ConflictingHunks keeps the legacy count available for callers that still
// reason about conflict hunks rather than the new miss/conflict split.
Expand Down
11 changes: 10 additions & 1 deletion apply_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,16 @@ func (s *applySession) applyHunk(hunk patchHunk, match matchedHunk) {
for _, hunkLine := range hunk.lines[match.hunkStart:match.hunkEnd] {
switch hunkLine.kind {
case ' ':
s.image = append(s.image, fileLine{text: hunkLine.text, hasNewline: hunkLine.hasNewline, eofMarker: hunkLine.newEOF})
// Source eofMarker from the matched source line. The parser's
// hunkLine.newEOF flag is unreliable for blank trailing context
// because markEOFMarkers cannot distinguish a real mid-file
// blank context line from the synthetic source EOF marker
// (see related comment in lineMatches).
eof := hunkLine.newEOF
if s.cursor < len(s.sourceLines) {
eof = s.sourceLines[s.cursor].eofMarker
}
s.image = append(s.image, fileLine{text: hunkLine.text, hasNewline: hunkLine.hasNewline, eofMarker: eof})
s.cursor++
case '-':
s.cursor++
Expand Down
38 changes: 38 additions & 0 deletions apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1019,3 +1019,41 @@ func TestApplyFile_PreservesExactBytes(t *testing.T) {
require.NoError(t, err)
assert.True(t, bytes.Equal(files.out, applied))
}

// blankTrailingContextFixture provides a hunk inserting lines between two
// structural blocks where the trailing context line is blank. markEOFMarkers
// previously flagged any blank context line at the hunk's count boundary as
// an EOF marker, causing it to mismatch a real mid-file blank line in source.
var blankTrailingContextFixture = struct {
original []byte
target []byte
}{
original: []byte("class Foo {\n void a() {}\n\n void b() {}\n\n void c() {}\n}\n"),
target: []byte("class Foo {\n void a() {}\n\n void b() {}\n\n void inserted() {}\n\n void c() {}\n}\n"),
}

// TestApplyFile_BlankTrailingContextMidFile is a regression test for the
// fixture above against the direct apply path.
func TestApplyFile_BlankTrailingContextMidFile(t *testing.T) {
t.Parallel()

f := blankTrailingContextFixture
patch := buildPatchWithContext(t, "Foo.java", f.original, f.target, 3)
applied, err := ApplyFile(f.original, patch)
require.NoError(t, err)
assert.Equal(t, f.target, applied)
}

// TestApplyFile_BlankTrailingContextWithConflicts mirrors the regression test
// above against the merge mode used by ApplyFileWithConflicts. The bug
// surfaced as a spurious conflict on a freshly generated file because the
// hunk's trailing blank context was misidentified as an EOF marker.
func TestApplyFile_BlankTrailingContextWithConflicts(t *testing.T) {
t.Parallel()

f := blankTrailingContextFixture
patch := buildPatchWithContext(t, "Foo.java", f.original, f.target, 3)
applied, err := ApplyFileWithConflicts(f.original, patch)
require.NoError(t, err)
assert.Equal(t, f.target, applied)
}
Loading