()
| 47 | * made by the user. |
| 48 | */ |
| 49 | export async function fetchGitDiff(): Promise<GitDiffResult | null> { |
| 50 | const isGit = await getIsGit() |
| 51 | if (!isGit) return null |
| 52 | |
| 53 | // Skip diff calculation during transient git states since the |
| 54 | // working tree contains incoming changes, not user-intentional edits |
| 55 | if (await isInTransientGitState()) { |
| 56 | return null |
| 57 | } |
| 58 | |
| 59 | // Quick probe: use --shortstat to get totals without loading all content. |
| 60 | // This is O(1) memory and lets us detect massive diffs (e.g., jj workspaces) |
| 61 | // before committing to expensive operations. |
| 62 | const { stdout: shortstatOut, code: shortstatCode } = await execFileNoThrow( |
| 63 | gitExe(), |
| 64 | ['--no-optional-locks', 'diff', 'HEAD', '--shortstat'], |
| 65 | { timeout: GIT_TIMEOUT_MS, preserveOutputOnError: false }, |
| 66 | ) |
| 67 | |
| 68 | if (shortstatCode === 0) { |
| 69 | const quickStats = parseShortstat(shortstatOut) |
| 70 | if (quickStats && quickStats.filesCount > MAX_FILES_FOR_DETAILS) { |
| 71 | // Too many files - return accurate totals but skip per-file details |
| 72 | // to avoid loading hundreds of MB into memory |
| 73 | return { |
| 74 | stats: quickStats, |
| 75 | perFileStats: new Map(), |
| 76 | hunks: new Map(), |
| 77 | } |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | // Get stats via --numstat (all uncommitted changes vs HEAD) |
| 82 | const { stdout: numstatOut, code: numstatCode } = await execFileNoThrow( |
| 83 | gitExe(), |
| 84 | ['--no-optional-locks', 'diff', 'HEAD', '--numstat'], |
| 85 | { timeout: GIT_TIMEOUT_MS, preserveOutputOnError: false }, |
| 86 | ) |
| 87 | |
| 88 | if (numstatCode !== 0) return null |
| 89 | |
| 90 | const { stats, perFileStats } = parseGitNumstat(numstatOut) |
| 91 | |
| 92 | // Include untracked files (new files not yet staged) |
| 93 | // Just filenames - no content reading for performance |
| 94 | const remainingSlots = MAX_FILES - perFileStats.size |
| 95 | if (remainingSlots > 0) { |
| 96 | const untrackedStats = await fetchUntrackedFiles(remainingSlots) |
| 97 | if (untrackedStats) { |
| 98 | stats.filesCount += untrackedStats.size |
| 99 | for (const [path, fileStats] of untrackedStats) { |
| 100 | perFileStats.set(path, fileStats) |
| 101 | } |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | // Return stats only - hunks are fetched on-demand via fetchGitDiffHunks() |
| 106 | // to avoid expensive git diff HEAD call on every poll |
no test coverage detected