1+ import fs from "node:fs" ;
2+ import { join } from "node:path" ;
13import { HunkUserError } from "./errors" ;
24import type { GitCommandInput , ShowCommandInput , StashShowCommandInput } from "./types" ;
35
@@ -306,10 +308,45 @@ function parseUntrackedFilePaths(statusText: string) {
306308 . flatMap ( ( entry ) => ( entry . startsWith ( "?? " ) ? [ entry . slice ( 3 ) ] : [ ] ) ) ;
307309}
308310
311+ /** Return whether one untracked path can be synthesized into a file diff. */
312+ function isReviewableUntrackedPath ( repoRoot : string , filePath : string ) {
313+ const absolutePath = join ( repoRoot , filePath ) ;
314+
315+ let pathInfo : fs . Stats ;
316+ try {
317+ pathInfo = fs . lstatSync ( absolutePath ) ;
318+ } catch {
319+ // If the path disappeared after `git status`, let the downstream Git diff
320+ // surface the same error path users would have seen before this filter.
321+ return true ;
322+ }
323+
324+ if ( pathInfo . isDirectory ( ) ) {
325+ return false ;
326+ }
327+
328+ if ( ! pathInfo . isSymbolicLink ( ) ) {
329+ return true ;
330+ }
331+
332+ try {
333+ // Git reports directory symlinks as untracked paths, but `git diff --no-index`
334+ // cannot synthesize a parseable file patch for them.
335+ return ! fs . statSync ( absolutePath ) . isDirectory ( ) ;
336+ } catch {
337+ // Broken symlinks still diff as reviewable path entries, so keep them.
338+ return true ;
339+ }
340+ }
341+
309342/** Return the repo-root-relative untracked files for a working-tree review input. */
310343export function listGitUntrackedFiles (
311344 input : GitCommandInput ,
312- { cwd = process . cwd ( ) , gitExecutable = "git" } : Omit < RunGitTextOptions , "input" | "args" > = { } ,
345+ {
346+ cwd = process . cwd ( ) ,
347+ repoRoot,
348+ gitExecutable = "git" ,
349+ } : Omit < RunGitTextOptions , "input" | "args" > & { repoRoot ?: string } = { } ,
313350) {
314351 if ( ! shouldIncludeUntrackedFiles ( input ) ) {
315352 return [ ] ;
@@ -322,7 +359,15 @@ export function listGitUntrackedFiles(
322359 gitExecutable,
323360 } ) ;
324361
325- return parseUntrackedFilePaths ( statusText ) ;
362+ const untrackedFiles = parseUntrackedFilePaths ( statusText ) ;
363+ if ( untrackedFiles . length === 0 ) {
364+ return [ ] ;
365+ }
366+
367+ const normalizedRepoRoot = repoRoot ?? resolveGitRepoRoot ( input , { cwd, gitExecutable } ) ;
368+ return untrackedFiles . filter ( ( filePath ) =>
369+ isReviewableUntrackedPath ( normalizedRepoRoot , filePath ) ,
370+ ) ;
326371}
327372
328373/** Return the raw Git patch text for one untracked file using `git diff --no-index`. */
0 commit comments