package gitx import ( "strings" "testing" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" "github.com/go-git/go-git/v5/plumbing/format/diff" ) // --- fakes implementing the go-git diff interfaces for pure-function tests --- type fakeFile struct{ path string } func (f fakeFile) Hash() plumbing.Hash { return plumbing.ZeroHash } func (f fakeFile) Mode() filemode.FileMode { return filemode.Regular } func (f fakeFile) Path() string { return f.path } type fakeChunk struct { content string op diff.Operation } func (c fakeChunk) Content() string { return c.content } func (c fakeChunk) Type() diff.Operation { return c.op } type fakeFilePatch struct { from, to diff.File binary bool chunks []diff.Chunk } func (f fakeFilePatch) IsBinary() bool { return f.binary } func (f fakeFilePatch) Files() (diff.File, diff.File) { return f.from, f.to } func (f fakeFilePatch) Chunks() []diff.Chunk { return f.chunks } func file(p string) diff.File { return fakeFile{path: p} } func TestMapFilePatches(t *testing.T) { add := func(s string) diff.Chunk { return fakeChunk{content: s, op: diff.Add} } del := func(s string) diff.Chunk { return fakeChunk{content: s, op: diff.Delete} } eq := func(s string) diff.Chunk { return fakeChunk{content: s, op: diff.Equal} } tests := []struct { name string fp fakeFilePatch want FileChange }{ { "added", fakeFilePatch{to: file("new.txt"), chunks: []diff.Chunk{add("a\nb\n")}}, FileChange{Status: "A", Path: "new.txt", Additions: 2}, }, { "deleted", fakeFilePatch{from: file("gone.txt"), chunks: []diff.Chunk{del("x\n")}}, FileChange{Status: "D", Path: "gone.txt", Deletions: 1}, }, { "modified", fakeFilePatch{ from: file("a.txt"), to: file("a.txt"), chunks: []diff.Chunk{eq("ctx\n"), add("n\n"), del("o1\no2\n")}, }, FileChange{Status: "M", Path: "a.txt", Additions: 1, Deletions: 2}, }, { "renamed with edit", fakeFilePatch{ from: file("old.txt"), to: file("new.txt"), chunks: []diff.Chunk{add("x\n"), del("y\n")}, }, FileChange{Status: "R", OldPath: "old.txt", Path: "new.txt", Additions: 1, Deletions: 1}, }, { "binary skips counts", fakeFilePatch{ from: file("img.bin"), to: file("img.bin"), binary: true, chunks: []diff.Chunk{add("should be ignored\n")}, }, FileChange{Status: "M", Path: "img.bin", Binary: true}, }, { "unterminated final line counts as one", fakeFilePatch{to: file("f"), chunks: []diff.Chunk{add("noeol")}}, FileChange{Status: "A", Path: "f", Additions: 1}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := mapFilePatches([]diff.FilePatch{tt.fp}) if len(got) != 1 { t.Fatalf("len = %d, want 1", len(got)) } if got[0] != tt.want { t.Errorf("got %+v, want %+v", got[0], tt.want) } }) } t.Run("empty from and to is skipped", func(t *testing.T) { got := mapFilePatches([]diff.FilePatch{fakeFilePatch{}}) if len(got) != 0 { t.Errorf("got %+v, want none", got) } }) } func TestCutPatch(t *testing.T) { fileA := "diff --git a/a.txt b/a.txt\n@@ -1 +1 @@\n-x\n+y\n" fileB := "diff --git a/b.txt b/b.txt\n@@ -1 +1 @@\n-p\n+q\n" full := fileA + fileB t.Run("under limit unchanged", func(t *testing.T) { got, trunc := cutPatch(full, 1<<20) if trunc || got != full { t.Errorf("trunc=%v got=%q", trunc, got) } }) t.Run("zero limit unchanged", func(t *testing.T) { got, trunc := cutPatch(full, 0) if trunc || got != full { t.Errorf("trunc=%v", trunc) } }) t.Run("cut at file boundary drops torn file", func(t *testing.T) { // A limit that lands inside fileB must cut back to the end of fileA. limit := int64(len(fileA) + 10) got, trunc := cutPatch(full, limit) if !trunc { t.Fatalf("expected truncated") } if got != fileA { t.Errorf("got %q, want just fileA (no torn diff --git)", got) } if strings.Count(got, "diff --git") != 1 { t.Errorf("torn file leaked: %q", got) } }) t.Run("single huge leading file cut hard at limit", func(t *testing.T) { huge := "diff --git a/big b/big\n" + strings.Repeat("+line\n", 1000) got, trunc := cutPatch(huge, 40) if !trunc { t.Fatalf("expected truncated") } if int64(len(got)) != 40 { t.Errorf("len = %d, want 40", len(got)) } }) }