mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-11 08:06:25 -04:00
The spec regained file-header (f) and hunk-header (h) records: f never carries a line number, h always does (the first line of the hunk it heads). Accept them in the OSC metadata backend, so a conforming pager's header rows resolve to the same DiffLineFileHeader/DiffLineHunkHeader identities the buffer parser already reports for raw diffs. With header rows located, next/previous file navigation and the jump-to-file menu land on a file's header row, and header rows become usable scroll-restore anchors. The consumers need a few adjustments: - File navigation used to reach a file's top by backing up over the untagged rows above its first located row. With tagged headers that overshoots onto the blank separator row above the file header, so drop the back-up (backUpOverHeader) and land on the first located row itself: the header for any conforming source, or the first content line under a pager that leaves its headers untagged — an accepted degradation for non-conforming pagers, now that the spec makes f/h mandatory. - SamePatchLine now requires headers to match headers of the same kind. A hunk header shares its line number with the hunk's first content line (and a file header shares "0" with a deleted file's hunk header), so without this a position restore aiming at one could land on the other. This also applied to raw diffs before, but headers used to be unlikely restore targets; now that navigation deliberately lands on them, the ambiguity would bite. - Editing a file-header row opens the file without jumping to a line, like pressing edit on a whole file in a side panel. (It used to open at line 1 for raw diffs, where headers resolved already.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
148 lines
6.4 KiB
Go
148 lines
6.4 KiB
Go
package helpers
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// A two-file commit diff as it appears (decolorized) in the main view. file1 has
|
|
// two consecutive deletions (grape, pear) that share a new-file line number;
|
|
// file2 has two consecutive additions.
|
|
const twoFileDiff = `diff --git a/file1.go b/file1.go
|
|
index 1111111..2222222 100644
|
|
--- a/file1.go
|
|
+++ b/file1.go
|
|
@@ -1,4 +1,2 @@
|
|
apple
|
|
-grape
|
|
-pear
|
|
lemon
|
|
diff --git a/dir/file2.go b/dir/file2.go
|
|
index 3333333..4444444 100644
|
|
--- a/dir/file2.go
|
|
+++ b/dir/file2.go
|
|
@@ -10,2 +9,4 @@ func foo() {
|
|
ctx
|
|
+added1
|
|
+added2
|
|
ctx2`
|
|
|
|
func TestParseDiffLineFromBuffer(t *testing.T) {
|
|
bufferLines := strings.Split(twoFileDiff, "\n")
|
|
|
|
scenarios := []struct {
|
|
name string
|
|
targetIdx int
|
|
expected parsedDiffLine
|
|
expectOk bool
|
|
}{
|
|
{"file header", 0, parsedDiffLine{RelPath: "file1.go", Type: types.DiffLineFileHeader, NewLine: 1}, true},
|
|
{"hunk header", 4, parsedDiffLine{RelPath: "file1.go", Type: types.DiffLineHunkHeader, NewLine: 1}, true},
|
|
{"context line", 5, parsedDiffLine{RelPath: "file1.go", Type: types.DiffLineContext, NewLine: 1}, true},
|
|
// The two deletions share new-file line 2 but have distinct old-file lines.
|
|
{"first deletion", 6, parsedDiffLine{RelPath: "file1.go", Type: types.DiffLineDeleted, NewLine: 2, OldLine: 2}, true},
|
|
{"second deletion", 7, parsedDiffLine{RelPath: "file1.go", Type: types.DiffLineDeleted, NewLine: 2, OldLine: 3}, true},
|
|
// The second file: its path comes from the second "diff --git" section,
|
|
// and its additions get distinct new-file line numbers.
|
|
{"first addition", 15, parsedDiffLine{RelPath: "dir/file2.go", Type: types.DiffLineAdded, NewLine: 10}, true},
|
|
{"second addition", 16, parsedDiffLine{RelPath: "dir/file2.go", Type: types.DiffLineAdded, NewLine: 11}, true},
|
|
{"out of range", 999, parsedDiffLine{}, false},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.name, func(t *testing.T) {
|
|
result, ok := parseDiffLineFromBuffer(bufferLines, s.targetIdx)
|
|
assert.Equal(t, s.expectOk, ok)
|
|
if s.expectOk {
|
|
assert.Equal(t, s.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseDiffLineFromBufferNotADiff(t *testing.T) {
|
|
// A rendering with no "diff --git" line (e.g. delta's default mode) can't be
|
|
// parsed, so the caller falls back to another backend.
|
|
bufferLines := []string{"some", "lines", "that", "are not a diff"}
|
|
_, ok := parseDiffLineFromBuffer(bufferLines, 2)
|
|
assert.False(t, ok)
|
|
}
|
|
|
|
func TestParseDiffLineFromBufferGutterMangled(t *testing.T) {
|
|
// delta with line-number gutters keeps the diff/hunk headers but pushes the
|
|
// +/- markers off the start of each body line, so every line reads as
|
|
// context. The body no longer matches the hunk header, so we refuse to parse
|
|
// (and the caller falls back) rather than return a confident mis-parse.
|
|
mangled := strings.Split(`diff --git a/file1.txt b/file1.txt
|
|
index 1111111..2222222 100644
|
|
--- a/file1.txt
|
|
+++ b/file1.txt
|
|
@@ -1,5 +1,3 @@
|
|
1 ⋮ 1 │ apple
|
|
2 ⋮ │-grape
|
|
3 ⋮ │-pear
|
|
4 ⋮ 2 │ lemon
|
|
5 ⋮ 3 │ mango`, "\n")
|
|
|
|
_, ok := parseDiffLineFromBuffer(mangled, 6)
|
|
assert.False(t, ok)
|
|
}
|
|
|
|
func TestParseDiffLineMetadata(t *testing.T) {
|
|
scenarios := []struct {
|
|
name string
|
|
payload string
|
|
expected parsedDiffLine
|
|
expectOk bool
|
|
}{
|
|
// These payloads are exactly what the patched delta emits (verified
|
|
// against the real binary; see diff-line-metadata-notes.md §9).
|
|
{"context", "1;c;1;;foo.txt", parsedDiffLine{RelPath: "foo.txt", Type: types.DiffLineContext, NewLine: 1}, true},
|
|
{"added", "1;a;3;;foo.txt", parsedDiffLine{RelPath: "foo.txt", Type: types.DiffLineAdded, NewLine: 3}, true},
|
|
// A deletion carries both numbers; two consecutive deletions share the
|
|
// new-file line and differ only in the old-file line.
|
|
{"first deletion", "1;d;2;2;foo.txt", parsedDiffLine{RelPath: "foo.txt", Type: types.DiffLineDeleted, NewLine: 2, OldLine: 2}, true},
|
|
{"second deletion", "1;d;2;3;foo.txt", parsedDiffLine{RelPath: "foo.txt", Type: types.DiffLineDeleted, NewLine: 2, OldLine: 3}, true},
|
|
// A whole-file deletion has new-file position 0 and the old path.
|
|
{"deleted file", "1;d;0;1;gone.txt", parsedDiffLine{RelPath: "gone.txt", Type: types.DiffLineDeleted, NewLine: 0, OldLine: 1}, true},
|
|
// The path is the last field, so a ';' within it is preserved.
|
|
{"path with semicolon", "1;c;5;;weird;name.txt", parsedDiffLine{RelPath: "weird;name.txt", Type: types.DiffLineContext, NewLine: 5}, true},
|
|
// A pager may emit an absolute path; the parser keeps it verbatim (the
|
|
// caller decides whether to join the worktree path).
|
|
{"absolute path", "1;a;7;;/abs/foo.txt", parsedDiffLine{RelPath: "/abs/foo.txt", Type: types.DiffLineAdded, NewLine: 7}, true},
|
|
// A file header carries no line numbers; a hunk header carries the
|
|
// new-file line of the hunk's first line (0 for a whole-file deletion,
|
|
// mirroring `@@ -1,N +0,0 @@`).
|
|
{"file header", "1;f;;;foo.txt", parsedDiffLine{RelPath: "foo.txt", Type: types.DiffLineFileHeader}, true},
|
|
{"hunk header", "1;h;10;;foo.txt", parsedDiffLine{RelPath: "foo.txt", Type: types.DiffLineHunkHeader, NewLine: 10}, true},
|
|
{"hunk header of a deleted file", "1;h;0;;gone.txt", parsedDiffLine{RelPath: "gone.txt", Type: types.DiffLineHunkHeader, NewLine: 0}, true},
|
|
// The spec leaves a file header's new-line always empty, but the parser
|
|
// doesn't police a number being there — it just carries it along.
|
|
{"file header with a line number (tolerated)", "1;f;10;;foo.txt", parsedDiffLine{RelPath: "foo.txt", Type: types.DiffLineFileHeader, NewLine: 10}, true},
|
|
|
|
{"unknown version", "2;c;1;;foo.txt", parsedDiffLine{}, false},
|
|
{"unknown type", "1;x;1;;foo.txt", parsedDiffLine{}, false},
|
|
{"too few fields", "1;c;1", parsedDiffLine{}, false},
|
|
{"non-numeric new-line", "1;c;x;;foo.txt", parsedDiffLine{}, false},
|
|
{"non-numeric old-line", "1;d;2;y;foo.txt", parsedDiffLine{}, false},
|
|
// Only a file header may omit the new-line; on any other type an empty
|
|
// new-line is a malformed record, and rejecting it makes the row fall
|
|
// back to the other backends rather than acting on wrong data.
|
|
{"empty new-line on a content line", "1;c;;;foo.txt", parsedDiffLine{}, false},
|
|
{"empty new-line on a hunk header", "1;h;;;foo.txt", parsedDiffLine{}, false},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.name, func(t *testing.T) {
|
|
result, ok := parseDiffLineMetadata(s.payload)
|
|
assert.Equal(t, s.expectOk, ok)
|
|
if s.expectOk {
|
|
assert.Equal(t, s.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|