diff --git a/internal/diff/git.go b/internal/diff/git.go index 24423efe..3028e376 100644 --- a/internal/diff/git.go +++ b/internal/diff/git.go @@ -261,13 +261,18 @@ func (p *Provider) computeMergeBase(ctx context.Context, from, to string) string } func (p *Provider) workspaceTrackedDiff(ctx context.Context) (string, error) { - out, err := p.runGit(ctx, "-c", "core.quotepath=false", "diff", "--no-ext-diff", "--no-textconv", "--find-renames", "--src-prefix=a/", "--dst-prefix=b/", "--no-color", "-U"+fmt.Sprint(DiffContextLines), "HEAD", "--") + out, err := p.runGit(ctx, "-c", "core.quotepath=false", "diff", "--no-ext-diff", "--no-textconv", "--find-renames", "--src-prefix=a/", "--dst-prefix=b/", "--no-color", "-U"+fmt.Sprint(DiffContextLines), "--end-of-options", "HEAD", "--") if err == nil && out != "" { return out, nil } if ctx.Err() != nil { return "", ctx.Err() } + // Fall back to the staged diff when `git diff HEAD` errored or was empty. This is + // not redundant with the call above: in a repository with no commits yet there is no + // HEAD, so `git diff HEAD` fails with "bad revision 'HEAD'", but `git diff --staged` + // still surfaces staged changes by diffing the index against the empty tree — the only + // way to review a workspace before its first commit. return p.runGit(ctx, "-c", "core.quotepath=false", "diff", "--no-ext-diff", "--no-textconv", "--find-renames", "--src-prefix=a/", "--dst-prefix=b/", "--no-color", "-U"+fmt.Sprint(DiffContextLines), "--staged", "--") } diff --git a/internal/diff/git_test.go b/internal/diff/git_test.go index 4c15a3c7..f420e759 100644 --- a/internal/diff/git_test.go +++ b/internal/diff/git_test.go @@ -205,6 +205,36 @@ func TestWorkspaceDiffSurvivesExternalDiffTool(t *testing.T) { } } +// TestWorkspaceDiffNoCommitsUsesStagedFallback pins the second runGit call in +// workspaceTrackedDiff: in a repository with no commits there is no HEAD, so +// `git diff HEAD` fails, and the staged diff is the only way to review the +// workspace. Removing the `--staged` fallback (as an over-eager "simplification" +// might) makes this return zero diffs. +func TestWorkspaceDiffNoCommitsUsesStagedFallback(t *testing.T) { + repo := t.TempDir() + runGitTest(t, repo, "init", "-q") + + // Staged file, no commit yet -> no HEAD. No commit also means no need for + // the user.email/user.name/commit.gpgsign config the committing tests set. + file := filepath.Join(repo, "staged.txt") + if err := os.WriteFile(file, []byte("alpha\nbeta\n"), 0o644); err != nil { + t.Fatalf("write staged.txt: %v", err) + } + runGitTest(t, repo, "add", "staged.txt") + + runner := gitcmd.New(0) + provider := NewWorkspaceProvider(repo, runner) + + diffs, err := provider.GetDiff(context.Background()) + if err != nil { + t.Fatalf("GetDiff returned error: %v", err) + } + if len(diffs) == 0 { + t.Fatalf("expected staged changes to be surfaced in a repo with no commits " + + "(the --staged fallback in workspaceTrackedDiff), got 0 diffs") + } +} + // TestCommitDiffSurvivesExternalDiffTool covers the ModeCommit call site // (git show ), which likewise must pass --no-ext-diff so that a // user's external diff tool does not break single-commit analysis.