Skip to content

Commit 343457c

Browse files
Merge branch 'main' into feat/watch-mode
2 parents 4fdd35f + 523996b commit 343457c

File tree

1 file changed

+100
-6
lines changed

1 file changed

+100
-6
lines changed

codex-cli/src/utils/get-diff.ts

+100-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,26 @@
11
import { execSync } from "node:child_process";
22

3+
// The objects thrown by `child_process.execSync()` are `Error` instances that
4+
// include additional, undocumented properties such as `status` (exit code) and
5+
// `stdout` (captured standard output). Declare a minimal interface that captures
6+
// just the fields we need so that we can avoid the use of `any` while keeping
7+
// the checks type-safe.
8+
interface ExecSyncError extends Error {
9+
// Exit status code. When a diff is produced, git exits with code 1 which we
10+
// treat as a non-error signal.
11+
status?: number;
12+
// Captured stdout. We rely on this to obtain the diff output when git exits
13+
// with status 1.
14+
stdout?: string;
15+
}
16+
17+
// Type-guard that narrows an unknown value to `ExecSyncError`.
18+
function isExecSyncError(err: unknown): err is ExecSyncError {
19+
return (
20+
typeof err === "object" && err != null && "status" in err && "stdout" in err
21+
);
22+
}
23+
324
/**
425
* Returns the current Git diff for the working directory. If the current
526
* working directory is not inside a Git repository, `isGitRepo` will be
@@ -15,13 +36,86 @@ export function getGitDiff(): {
1536
execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" });
1637

1738
// If the above call didn’t throw, we are inside a git repo. Retrieve the
18-
// diff including color codes so that the overlay can render them.
19-
const output = execSync("git diff --color", {
20-
encoding: "utf8",
21-
maxBuffer: 10 * 1024 * 1024, // 10 MB ought to be enough for now
22-
});
39+
// diff for tracked files **and** include any untracked files so that the
40+
// `/diff` overlay shows a complete picture of the working tree state.
41+
42+
// 1. Diff for tracked files (unchanged behaviour)
43+
let trackedDiff = "";
44+
try {
45+
trackedDiff = execSync("git diff --color", {
46+
encoding: "utf8",
47+
maxBuffer: 10 * 1024 * 1024, // 10 MB ought to be enough for now
48+
});
49+
} catch (err) {
50+
// Exit status 1 simply means that differences were found. Capture the
51+
// diff from stdout in that case. Re-throw for any other status codes.
52+
if (
53+
isExecSyncError(err) &&
54+
err.status === 1 &&
55+
typeof err.stdout === "string"
56+
) {
57+
trackedDiff = err.stdout;
58+
} else {
59+
throw err;
60+
}
61+
}
62+
63+
// 2. Determine untracked files.
64+
// We use `git ls-files --others --exclude-standard` which outputs paths
65+
// relative to the repository root, one per line. These are files that
66+
// are not tracked *and* are not ignored by .gitignore.
67+
const untrackedOutput = execSync(
68+
"git ls-files --others --exclude-standard",
69+
{
70+
encoding: "utf8",
71+
maxBuffer: 10 * 1024 * 1024,
72+
},
73+
);
74+
75+
const untrackedFiles = untrackedOutput
76+
.split("\n")
77+
.map((p) => p.trim())
78+
.filter(Boolean);
79+
80+
let untrackedDiff = "";
81+
82+
const nullDevice = process.platform === "win32" ? "NUL" : "/dev/null";
83+
84+
for (const file of untrackedFiles) {
85+
try {
86+
// `git diff --no-index` produces a diff even outside the index by
87+
// comparing two paths. We compare the file against /dev/null so that
88+
// the file is treated as "new".
89+
//
90+
// `git diff --color --no-index /dev/null <file>` exits with status 1
91+
// when differences are found, so we capture stdout from the thrown
92+
// error object instead of letting it propagate.
93+
execSync(`git diff --color --no-index -- "${nullDevice}" "${file}"`, {
94+
encoding: "utf8",
95+
stdio: ["ignore", "pipe", "ignore"],
96+
maxBuffer: 10 * 1024 * 1024,
97+
});
98+
} catch (err) {
99+
if (
100+
isExecSyncError(err) &&
101+
// Exit status 1 simply means that the two inputs differ, which is
102+
// exactly what we expect here. Any other status code indicates a
103+
// real error (e.g. the file disappeared between the ls-files and
104+
// diff calls), so re-throw those.
105+
err.status === 1 &&
106+
typeof err.stdout === "string"
107+
) {
108+
untrackedDiff += err.stdout;
109+
} else {
110+
throw err;
111+
}
112+
}
113+
}
114+
115+
// Concatenate tracked and untracked diffs.
116+
const combinedDiff = `${trackedDiff}${untrackedDiff}`;
23117

24-
return { isGitRepo: true, diff: output };
118+
return { isGitRepo: true, diff: combinedDiff };
25119
} catch {
26120
// Either git is not installed or we’re not inside a repository.
27121
return { isGitRepo: false, diff: "" };

0 commit comments

Comments
 (0)