diff --git a/apps/server/src/vcs/GitVcsDriverCore.test.ts b/apps/server/src/vcs/GitVcsDriverCore.test.ts index 6e352f013fe5..0c850111dcd7 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.test.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.test.ts @@ -783,6 +783,25 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { }), ); + it.effect("includes untracked and staged files before the first commit", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + const driver = yield* GitVcsDriver.GitVcsDriver; + yield* driver.initRepo({ cwd }); + yield* writeTextFile(cwd, "untracked.txt", "untracked\n"); + yield* writeTextFile(cwd, "staged.txt", "staged\n"); + yield* git(cwd, ["add", "staged.txt"]); + + const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false }); + const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? ""; + + assert.include(diff, "diff --git a/untracked.txt b/untracked.txt"); + assert.include(diff, "+untracked"); + assert.include(diff, "diff --git a/staged.txt b/staged.txt"); + assert.include(diff, "+staged"); + }), + ); + it.effect("honors whitespace filtering for worktree and branch previews", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); @@ -818,6 +837,309 @@ it.layer(TestLayer)("GitVcsDriver core integration", (it) => { }), ); + it.effect("detects an unstaged rename with edits as one working-tree diff", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + yield* initRepoWithCommit(cwd); + const fileSystem = yield* FileSystem.FileSystem; + const pathService = yield* Path.Path; + const driver = yield* GitVcsDriver.GitVcsDriver; + yield* writeTextFile( + cwd, + "before.ts", + [ + "export const first = 1;", + "export const second = 2;", + "export const third = 3;", + "export const fourth = 4;", + "", + ].join("\n"), + ); + yield* git(cwd, ["add", "before.ts"]); + yield* git(cwd, ["commit", "-m", "add source file"]); + yield* fileSystem.rename( + pathService.join(cwd, "before.ts"), + pathService.join(cwd, "after.ts"), + ); + yield* writeTextFile( + cwd, + "after.ts", + [ + "export const first = 1;", + "export const second = 2;", + "export const third = 30;", + "export const fourth = 4;", + "", + ].join("\n"), + ); + + const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false }); + const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? ""; + + assert.include(diff, "rename from before.ts"); + assert.include(diff, "rename to after.ts"); + assert.include(diff, "-export const third = 3;"); + assert.include(diff, "+export const third = 30;"); + }), + ); + + it.effect("preserves sparse-checkout entries in the working-tree diff", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + yield* initRepoWithCommit(cwd); + const driver = yield* GitVcsDriver.GitVcsDriver; + yield* writeTextFile(cwd, "included/file.ts", "export const included = 1;\n"); + yield* writeTextFile(cwd, "excluded/file.ts", "export const excluded = 1;\n"); + yield* git(cwd, ["add", "."]); + yield* git(cwd, ["commit", "-m", "add sparse files"]); + yield* git(cwd, ["sparse-checkout", "set", "included"]); + yield* writeTextFile(cwd, "included/file.ts", "export const included = 2;\n"); + + const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false }); + const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? ""; + + assert.include(diff, "diff --git a/included/file.ts b/included/file.ts"); + assert.include(diff, "+export const included = 2;"); + assert.notInclude(diff, "excluded/file.ts"); + }), + ); + + it.effect("preserves staged index-only deletions for ignored and unignored files", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + yield* initRepoWithCommit(cwd); + const driver = yield* GitVcsDriver.GitVcsDriver; + yield* writeTextFile(cwd, ".gitignore", "ignored.env\n"); + yield* writeTextFile(cwd, "ignored.env", "IGNORED=value\n"); + yield* writeTextFile(cwd, "visible.txt", "visible\n"); + yield* git(cwd, ["add", "-f", ".gitignore", "ignored.env", "visible.txt"]); + yield* git(cwd, ["commit", "-m", "add tracked files"]); + yield* git(cwd, ["rm", "--cached", "ignored.env", "visible.txt"]); + + const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false }); + const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? ""; + + assert.include(diff, "diff --git a/ignored.env b/ignored.env"); + assert.include(diff, "-IGNORED=value"); + assert.include(diff, "diff --git a/visible.txt b/visible.txt"); + assert.include(diff, "-visible"); + }), + ); + + it.effect("preserves staged deletions when their bulk enumeration is truncated", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + yield* initRepoWithCommit(cwd); + yield* writeTextFile(cwd, "untracked-again.txt", "still here\n"); + yield* git(cwd, ["add", "untracked-again.txt"]); + yield* git(cwd, ["commit", "-m", "add file"]); + yield* git(cwd, ["rm", "--cached", "untracked-again.txt"]); + + const delegate = yield* ChildProcessSpawner.ChildProcessSpawner; + const truncatingSpawner = ChildProcessSpawner.make((command) => { + if ( + ChildProcess.isStandardCommand(command) && + command.args.includes("--cached") && + command.args.includes("--name-only") && + command.args.includes("--diff-filter=D") + ) { + return Effect.succeed(makeSuccessfulHandle("x".repeat(130_000))); + } + return delegate.spawn(command); + }); + const driver = yield* makeGitVcsDriverCore().pipe( + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, truncatingSpawner), + Effect.provide(ServerConfigLayer), + ); + + const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false }); + const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? ""; + + assert.include(diff, "diff --git a/untracked-again.txt b/untracked-again.txt"); + assert.include(diff, "deleted file mode"); + assert.include(diff, "-still here"); + }), + ); + + it.effect("keeps tracked diffs when an untracked nested repository cannot be added", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + yield* initRepoWithCommit(cwd); + const driver = yield* GitVcsDriver.GitVcsDriver; + const fileSystem = yield* FileSystem.FileSystem; + const pathService = yield* Path.Path; + yield* writeTextFile(cwd, "README.md", "# changed\n"); + const nested = pathService.join(cwd, "nested"); + yield* fileSystem.makeDirectory(nested); + yield* driver.initRepo({ cwd: nested }); + + const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false }); + const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? ""; + + assert.include(diff, "diff --git a/README.md b/README.md"); + assert.include(diff, "+# changed"); + }), + ); + + it.effect("does not create shared index files while reading a split index", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + yield* initRepoWithCommit(cwd); + const driver = yield* GitVcsDriver.GitVcsDriver; + yield* git(cwd, ["update-index", "--split-index"]); + const before = yield* git(cwd, ["rev-parse", "--shared-index-path"]); + yield* writeTextFile(cwd, "README.md", "# changed\n"); + + const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false }); + const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? ""; + const after = yield* git(cwd, ["rev-parse", "--shared-index-path"]); + + assert.include(diff, "+# changed"); + assert.strictEqual(after, before); + }), + ); + + it.effect("keeps tracked diffs when temporary split-index expansion fails", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + yield* initRepoWithCommit(cwd); + yield* git(cwd, ["update-index", "--split-index"]); + yield* writeTextFile(cwd, "README.md", "# changed\n"); + + const delegate = yield* ChildProcessSpawner.ChildProcessSpawner; + const failingSpawner = ChildProcessSpawner.make((command) => { + if ( + ChildProcess.isStandardCommand(command) && + command.args.includes("update-index") && + command.args.includes("--no-split-index") + ) { + return Effect.succeed(makeNonRepositoryHandle()); + } + return delegate.spawn(command); + }); + const driver = yield* makeGitVcsDriverCore().pipe( + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, failingSpawner), + Effect.provide(ServerConfigLayer), + ); + + const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false }); + const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? ""; + + assert.include(diff, "diff --git a/README.md b/README.md"); + assert.include(diff, "+# changed"); + }), + ); + + it.effect("keeps tracked diffs when inspecting the Git index fails", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + yield* initRepoWithCommit(cwd); + yield* writeTextFile(cwd, "README.md", "# changed\n"); + + let failNextExists = false; + const delegateSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const failingSpawner = ChildProcessSpawner.make((command) => { + if ( + ChildProcess.isStandardCommand(command) && + command.args.includes("rev-parse") && + command.args.includes("--git-path") && + command.args.includes("index") + ) { + failNextExists = true; + } + return delegateSpawner.spawn(command); + }); + const delegateFileSystem = yield* FileSystem.FileSystem; + const failingFileSystem = { + ...delegateFileSystem, + exists: (pathOrDescriptor: string) => { + if (!failNextExists) return delegateFileSystem.exists(pathOrDescriptor); + failNextExists = false; + return Effect.fail( + PlatformError.systemError({ + _tag: "Unknown", + module: "FileSystem", + method: "exists", + pathOrDescriptor, + }), + ); + }, + }; + const driver = yield* makeGitVcsDriverCore().pipe( + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, failingSpawner), + Effect.provideService(FileSystem.FileSystem, failingFileSystem), + Effect.provide(ServerConfigLayer), + ); + + const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false }); + const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? ""; + + assert.include(diff, "diff --git a/README.md b/README.md"); + assert.include(diff, "+# changed"); + }), + ); + + it.effect("keeps tracked diffs when the temporary-index diff fails", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + yield* initRepoWithCommit(cwd); + yield* writeTextFile(cwd, "README.md", "# changed\n"); + + let patchDiffCount = 0; + const delegate = yield* ChildProcessSpawner.ChildProcessSpawner; + const failingSpawner = ChildProcessSpawner.make((command) => { + if ( + ChildProcess.isStandardCommand(command) && + command.args.includes("diff") && + command.args.includes("--patch") && + ++patchDiffCount === 1 + ) { + return Effect.succeed(makeNonRepositoryHandle()); + } + return delegate.spawn(command); + }); + const driver = yield* makeGitVcsDriverCore().pipe( + Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, failingSpawner), + Effect.provide(ServerConfigLayer), + ); + + const preview = yield* driver.getReviewDiffPreview({ cwd, ignoreWhitespace: false }); + const diff = preview.sources.find((source) => source.kind === "working-tree")?.diff ?? ""; + + assert.strictEqual(patchDiffCount, 2); + assert.include(diff, "diff --git a/README.md b/README.md"); + assert.include(diff, "+# changed"); + }), + ); + + it.effect("detects a committed rename with edits in the branch diff", () => + Effect.gen(function* () { + const cwd = yield* makeTmpDir(); + const { initialBranch } = yield* initRepoWithCommit(cwd); + const driver = yield* GitVcsDriver.GitVcsDriver; + yield* writeTextFile(cwd, "before.ts", "one\ntwo\nthree\nfour\n"); + yield* git(cwd, ["add", "before.ts"]); + yield* git(cwd, ["commit", "-m", "add source file"]); + yield* git(cwd, ["checkout", "-b", "feature/rename"]); + yield* git(cwd, ["mv", "before.ts", "after.ts"]); + yield* writeTextFile(cwd, "after.ts", "one\ntwo\nTHREE\nfour\n"); + yield* git(cwd, ["add", "after.ts"]); + yield* git(cwd, ["commit", "-m", "rename source file"]); + + const preview = yield* driver.getReviewDiffPreview({ + cwd, + baseRef: initialBranch, + ignoreWhitespace: false, + }); + const diff = preview.sources.find((source) => source.kind === "branch-range")?.diff ?? ""; + + assert.include(diff, "rename from before.ts"); + assert.include(diff, "rename to after.ts"); + assert.include(diff, "-three"); + assert.include(diff, "+THREE"); + }), + ); + it.effect("loads full file contents for working-tree diff expansion", () => Effect.gen(function* () { const cwd = yield* makeTmpDir(); diff --git a/apps/server/src/vcs/GitVcsDriverCore.ts b/apps/server/src/vcs/GitVcsDriverCore.ts index 5b9359adaa84..a6e8d4590547 100644 --- a/apps/server/src/vcs/GitVcsDriverCore.ts +++ b/apps/server/src/vcs/GitVcsDriverCore.ts @@ -1,4 +1,3 @@ -import * as Arr from "effect/Array"; import * as Cache from "effect/Cache"; import * as Data from "effect/Data"; import * as Crypto from "effect/Crypto"; @@ -46,7 +45,6 @@ const RANGE_COMMIT_SUMMARY_MAX_OUTPUT_BYTES = 19_000; const RANGE_DIFF_SUMMARY_MAX_OUTPUT_BYTES = 19_000; const RANGE_DIFF_PATCH_MAX_OUTPUT_BYTES = 59_000; const REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES = 120_000; -const REVIEW_UNTRACKED_DIFF_MAX_OUTPUT_BYTES = 80_000; const REVIEW_DIFF_FILE_MAX_OUTPUT_BYTES = 1024 * 1024; const WORKSPACE_FILES_MAX_OUTPUT_BYTES = 120_000; const STATUS_UPSTREAM_REFRESH_INTERVAL = Duration.seconds(15); @@ -2091,54 +2089,246 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* }; }); - const readUntrackedReviewDiffs = Effect.fn("readUntrackedReviewDiffs")(function* (cwd: string) { - const untrackedResult = yield* executeGit( - "GitVcsDriver.readUntrackedReviewDiffs.list", + const readWorkingTreeReviewDiff = Effect.fn("readWorkingTreeReviewDiff")(function* ( + cwd: string, + ignoreWhitespace: boolean | undefined, + ) { + const tempDirectory = yield* fileSystem + .makeTempDirectory({ prefix: "t3code-review-index-" }) + .pipe( + Effect.mapError( + (cause) => + new GitCommandError({ + operation: "GitVcsDriver.readWorkingTreeReviewDiff.createTempIndex", + command: "git diff", + cwd, + detail: "Failed to create a temporary Git index for the review diff.", + cause, + }), + ), + ); + const indexPath = path.join(tempDirectory, "index"); + const env = { GIT_INDEX_FILE: indexPath } satisfies NodeJS.ProcessEnv; + + return yield* Effect.gen(function* () { + const headResult = yield* executeGit( + "GitVcsDriver.readWorkingTreeReviewDiff.resolveHead", + cwd, + ["rev-parse", "--verify", "--quiet", "HEAD"], + { allowNonZeroExit: true }, + ); + const hasHead = headResult.exitCode === 0; + const baseline = hasHead + ? "HEAD" + : (yield* executeGit( + "GitVcsDriver.readWorkingTreeReviewDiff.resolveEmptyTree", + cwd, + ["hash-object", "-t", "tree", "--stdin"], + { stdin: "" }, + )).stdout.trim(); + const gitIndexResult = yield* executeGit( + "GitVcsDriver.readWorkingTreeReviewDiff.resolveIndex", + cwd, + ["rev-parse", "--git-path", "index"], + ); + const gitIndexPath = path.isAbsolute(gitIndexResult.stdout.trim()) + ? gitIndexResult.stdout.trim() + : path.resolve(cwd, gitIndexResult.stdout.trim()); + + const gitIndexExists = yield* fileSystem.exists(gitIndexPath).pipe( + Effect.mapError( + (cause) => + new GitCommandError({ + operation: "GitVcsDriver.readWorkingTreeReviewDiff.checkIndex", + command: "git diff", + cwd, + detail: "Failed to inspect the Git index for the review diff.", + cause, + }), + ), + ); + + if (gitIndexExists) { + yield* fileSystem.copyFile(gitIndexPath, indexPath).pipe( + Effect.mapError( + (cause) => + new GitCommandError({ + operation: "GitVcsDriver.readWorkingTreeReviewDiff.copyIndex", + command: "git diff", + cwd, + detail: "Failed to copy the Git index for the review diff.", + cause, + }), + ), + ); + + const sharedIndexResult = yield* executeGit( + "GitVcsDriver.readWorkingTreeReviewDiff.resolveSharedIndex", + cwd, + ["rev-parse", "--shared-index-path"], + { allowNonZeroExit: true }, + ); + const sharedIndexValue = sharedIndexResult.stdout.trim(); + if (sharedIndexResult.exitCode === 0 && sharedIndexValue.length > 0) { + const sharedIndexPath = path.isAbsolute(sharedIndexValue) + ? sharedIndexValue + : path.resolve(cwd, sharedIndexValue); + yield* fileSystem + .copyFile(sharedIndexPath, path.join(tempDirectory, path.basename(sharedIndexPath))) + .pipe( + Effect.mapError( + (cause) => + new GitCommandError({ + operation: "GitVcsDriver.readWorkingTreeReviewDiff.copySharedIndex", + command: "git diff", + cwd, + detail: "Failed to copy the shared Git index for the review diff.", + cause, + }), + ), + ); + yield* executeGit( + "GitVcsDriver.readWorkingTreeReviewDiff.expandSplitIndex", + cwd, + ["update-index", "--no-split-index"], + { env }, + ); + } + } else { + yield* executeGit( + "GitVcsDriver.readWorkingTreeReviewDiff.readEmptyTree", + cwd, + ["read-tree", "--empty"], + { env }, + ); + } + + const stagedDeletionResult = hasHead + ? yield* executeGit( + "GitVcsDriver.readWorkingTreeReviewDiff.stagedDeletions", + cwd, + ["diff", "--cached", "--name-only", "--diff-filter=D", "-z", "HEAD", "--"], + { + maxOutputBytes: WORKSPACE_FILES_MAX_OUTPUT_BYTES, + appendTruncationMarker: true, + }, + ) + : null; + const stagedDeletions = new Set( + stagedDeletionResult ? splitNullSeparatedGitStdoutPaths(stagedDeletionResult) : [], + ); + const untrackedResult = yield* executeGit( + "GitVcsDriver.readWorkingTreeReviewDiff.untracked", + cwd, + ["ls-files", "--others", "--exclude-standard", "-z"], + { + maxOutputBytes: WORKSPACE_FILES_MAX_OUTPUT_BYTES, + appendTruncationMarker: true, + }, + ); + const untrackedPaths = splitNullSeparatedGitStdoutPaths(untrackedResult); + const intentToAddPaths = + stagedDeletionResult?.stdoutTruncated === true + ? yield* Effect.filter( + untrackedPaths, + (relativePath) => + executeGit( + "GitVcsDriver.readWorkingTreeReviewDiff.checkStagedDeletion", + cwd, + ["diff", "--cached", "--quiet", "--diff-filter=D", "HEAD", "--", relativePath], + { allowNonZeroExit: true }, + ).pipe(Effect.map((result) => result.exitCode === 0)), + { concurrency: 1 }, + ) + : untrackedPaths.filter((relativePath) => !stagedDeletions.has(relativePath)); + yield* Effect.forEach( + intentToAddPaths, + (relativePath) => + executeGit( + "GitVcsDriver.readWorkingTreeReviewDiff.addIntentToAdd", + cwd, + ["add", "--intent-to-add", "--", relativePath], + { env, allowNonZeroExit: true }, + ), + { concurrency: 1, discard: true }, + ); + + const result = yield* executeGit( + "GitVcsDriver.readWorkingTreeReviewDiff.diff", + cwd, + [ + "diff", + "--patch", + "--no-color", + "--no-ext-diff", + "--no-textconv", + "--minimal", + "--find-renames", + ...(ignoreWhitespace ? ["--ignore-all-space"] : []), + baseline, + "--", + ], + { + env, + maxOutputBytes: REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES, + appendTruncationMarker: true, + }, + ); + return { + ...result, + stdoutTruncated: + result.stdoutTruncated || + stagedDeletionResult?.stdoutTruncated === true || + untrackedResult.stdoutTruncated, + }; + }).pipe( + Effect.ensuring( + fileSystem.remove(tempDirectory, { recursive: true, force: true }).pipe(Effect.ignore), + ), + ); + }); + + const readTrackedWorkingTreeReviewDiff = Effect.fn("readTrackedWorkingTreeReviewDiff")(function* ( + cwd: string, + ignoreWhitespace: boolean | undefined, + ) { + const headResult = yield* executeGit( + "GitVcsDriver.readTrackedWorkingTreeReviewDiff.resolveHead", cwd, - ["ls-files", "--others", "--exclude-standard", "-z"], + ["rev-parse", "--verify", "--quiet", "HEAD"], + { allowNonZeroExit: true }, + ); + const baseline = + headResult.exitCode === 0 + ? "HEAD" + : (yield* executeGit( + "GitVcsDriver.readTrackedWorkingTreeReviewDiff.resolveEmptyTree", + cwd, + ["hash-object", "-t", "tree", "--stdin"], + { stdin: "" }, + )).stdout.trim(); + + return yield* executeGit( + "GitVcsDriver.readTrackedWorkingTreeReviewDiff.diff", + cwd, + [ + "diff", + "--patch", + "--no-color", + "--no-ext-diff", + "--no-textconv", + "--minimal", + "--find-renames", + ...(ignoreWhitespace ? ["--ignore-all-space"] : []), + baseline, + "--", + ], { - maxOutputBytes: WORKSPACE_FILES_MAX_OUTPUT_BYTES, + allowNonZeroExit: true, + maxOutputBytes: REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES, appendTruncationMarker: true, }, ); - const untrackedPaths = splitNullSeparatedGitStdoutPaths(untrackedResult); - if (untrackedPaths.length === 0) { - return { diff: "", truncated: untrackedResult.stdoutTruncated }; - } - - const diffs = yield* Effect.forEach( - untrackedPaths, - (relativePath) => - executeGit( - "GitVcsDriver.readUntrackedReviewDiffs.diff", - cwd, - [ - "diff", - "--no-index", - "--patch", - "--no-color", - "--no-ext-diff", - "--no-textconv", - "--minimal", - "--", - "/dev/null", - relativePath, - ], - { - allowNonZeroExit: true, - maxOutputBytes: REVIEW_UNTRACKED_DIFF_MAX_OUTPUT_BYTES, - appendTruncationMarker: true, - }, - ), - { concurrency: 4 }, - ); - - return { - diff: Arr.filterMap(diffs, (result) => - result.stdout.trim().length > 0 ? Result.succeed(result.stdout) : Result.failVoid, - ).join("\n"), - truncated: untrackedResult.stdoutTruncated || diffs.some((result) => result.stdoutTruncated), - }; }); const getReviewDiffPreview = Effect.fn("getReviewDiffPreview")(function* ( @@ -2162,25 +2352,10 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* ) : null); - const dirtyTrackedResult = yield* executeGit( - "GitVcsDriver.getReviewDiffPreview.dirtyTracked", - input.cwd, - [ - "diff", - "--patch", - "--no-color", - "--no-ext-diff", - "--no-textconv", - "--minimal", - ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []), - "HEAD", - "--", - ], - { - maxOutputBytes: REVIEW_DIFF_PATCH_MAX_OUTPUT_BYTES, - appendTruncationMarker: true, - }, - ).pipe( + const dirtyResult = yield* readWorkingTreeReviewDiff(input.cwd, input.ignoreWhitespace).pipe( + Effect.catchTags({ + GitCommandError: () => readTrackedWorkingTreeReviewDiff(input.cwd, input.ignoreWhitespace), + }), Effect.orElseSucceed(() => ({ exitCode: 0, stdout: "", @@ -2189,12 +2364,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* stderrTruncated: false, })), ); - const dirtyUntracked = yield* readUntrackedReviewDiffs(input.cwd).pipe( - Effect.orElseSucceed(() => ({ diff: "", truncated: false })), - ); - const dirtyDiff = [dirtyTrackedResult.stdout.trimEnd(), dirtyUntracked.diff.trimEnd()] - .filter((diff) => diff.length > 0) - .join("\n"); + const dirtyDiff = dirtyResult.stdout; const baseResult = baseRef && branch @@ -2208,6 +2378,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* "--no-ext-diff", "--no-textconv", "--minimal", + "--find-renames", ...(input.ignoreWhitespace ? ["--ignore-all-space"] : []), `${baseRef}...HEAD`, ], @@ -2254,7 +2425,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function* headRef: null, diff: dirtyDiff, diffHash: dirtyDiffHash, - truncated: dirtyTrackedResult.stdoutTruncated || dirtyUntracked.truncated, + truncated: dirtyResult.stdoutTruncated, }, { id: "branch-range",