package git import ( "context" "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/require" ) func TestBuildDiffArgs(t *testing.T) { t.Parallel() cases := []struct { name string opts DiffOptions want []string }{ { name: "working tree vs HEAD", opts: DiffOptions{}, want: []string{"diff", "--no-color"}, }, { name: "staged", opts: DiffOptions{Staged: true}, want: []string{"diff", "--no-color", "--cached"}, }, { name: "single ref", opts: DiffOptions{Ref: "HEAD~1"}, want: []string{"diff", "--no-color", "HEAD~1"}, }, { name: "ref range", opts: DiffOptions{Ref: "main", Against: "feature"}, want: []string{"diff", "--no-color", "main..feature"}, }, { name: "context lines + paths", opts: DiffOptions{ContextLines: 5, Paths: []string{"a.txt", "dir/b.go"}}, want: []string{"diff", "--no-color", "-U5", "--", "a.txt", "dir/b.go"}, }, { name: "staged with ref range and paths", opts: DiffOptions{Staged: true, Ref: "main", Against: "dev", Paths: []string{"x"}}, want: []string{"diff", "--no-color", "--cached", "main..dev", "--", "x"}, }, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() require.Equal(t, tc.want, buildDiffArgs(tc.opts)) }) } } func TestDiff_WorkingTreeAndStaged(t *testing.T) { t.Parallel() gitAvailable(t) bare := makeBareRepo(t) r := newTestRunner(t) dst := filepath.Join(t.TempDir(), "wc") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() require.NoError(t, r.Clone(ctx, dst, bare, "main", nil)) // clean tree => empty diff res, err := r.Diff(ctx, dst, DiffOptions{}) require.NoError(t, err) require.Empty(t, res.Diff) require.False(t, res.Truncated) // modify a tracked file => unified diff against working tree require.NoError(t, os.WriteFile(filepath.Join(dst, "README.md"), []byte("hello\nworld\n"), 0o644)) res, err = r.Diff(ctx, dst, DiffOptions{}) require.NoError(t, err) require.Contains(t, res.Diff, "diff --git") require.Contains(t, res.Diff, "+world") // stage it; working-tree diff now empty, --cached shows the change mustGit(t, dst, "add", "README.md") res, err = r.Diff(ctx, dst, DiffOptions{}) require.NoError(t, err) require.Empty(t, res.Diff) res, err = r.Diff(ctx, dst, DiffOptions{Staged: true}) require.NoError(t, err) require.Contains(t, res.Diff, "+world") } func TestDiff_PathsFilter(t *testing.T) { t.Parallel() gitAvailable(t) bare := makeBareRepo(t) r := newTestRunner(t) dst := filepath.Join(t.TempDir(), "wc") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() require.NoError(t, r.Clone(ctx, dst, bare, "main", nil)) require.NoError(t, os.WriteFile(filepath.Join(dst, "README.md"), []byte("changed\n"), 0o644)) require.NoError(t, os.WriteFile(filepath.Join(dst, "other.txt"), []byte("new\n"), 0o644)) mustGit(t, dst, "add", "other.txt") // restrict to README only => other.txt not in output res, err := r.Diff(ctx, dst, DiffOptions{Paths: []string{"README.md"}}) require.NoError(t, err) require.Contains(t, res.Diff, "README.md") require.False(t, strings.Contains(res.Diff, "other.txt")) } func TestDiff_ErrorPropagation(t *testing.T) { t.Parallel() gitAvailable(t) r := newTestRunner(t) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() // not a git repo => git diff fails, error propagates _, err := r.Diff(ctx, t.TempDir(), DiffOptions{}) require.Error(t, err) }