1
1
import { execSync } from "node:child_process" ;
2
2
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
+
3
24
/**
4
25
* Returns the current Git diff for the working directory. If the current
5
26
* working directory is not inside a Git repository, `isGitRepo` will be
@@ -15,13 +36,86 @@ export function getGitDiff(): {
15
36
execSync ( "git rev-parse --is-inside-work-tree" , { stdio : "ignore" } ) ;
16
37
17
38
// 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 } ` ;
23
117
24
- return { isGitRepo : true , diff : output } ;
118
+ return { isGitRepo : true , diff : combinedDiff } ;
25
119
} catch {
26
120
// Either git is not installed or we’re not inside a repository.
27
121
return { isGitRepo : false , diff : "" } ;
0 commit comments