diff --git a/src/main/git/status.test.ts b/src/main/git/status.test.ts index cc2ec016960..285bfd78ea2 100644 --- a/src/main/git/status.test.ts +++ b/src/main/git/status.test.ts @@ -8,6 +8,7 @@ const { lstatMock, realpathMock, readFileMock, + statMock, rmMock, existsSyncMock } = vi.hoisted(() => ({ @@ -16,6 +17,7 @@ const { lstatMock: vi.fn(), realpathMock: vi.fn(), readFileMock: vi.fn(), + statMock: vi.fn(), rmMock: vi.fn(), existsSyncMock: vi.fn() })) @@ -33,6 +35,7 @@ vi.mock('fs/promises', () => ({ lstat: lstatMock, realpath: realpathMock, readFile: readFileMock, + stat: statMock, rm: rmMock })) @@ -252,7 +255,12 @@ describe('getDiff', () => { gitExecFileAsyncBufferMock.mockReset() lstatMock.mockReset() readFileMock.mockReset() + statMock.mockReset() existsSyncMock.mockReset() + statMock.mockResolvedValue({ + isFile: () => true, + size: 12 + }) }) it('uses the index as the left side for unstaged diffs when present', async () => { @@ -318,6 +326,21 @@ describe('getDiff', () => { expect(result.modifiedIsBinary).toBe(false) }) + it('does not read oversized working-tree files into memory', async () => { + gitExecFileAsyncBufferMock.mockResolvedValueOnce({ stdout: Buffer.from('index-content\n') }) + statMock.mockResolvedValueOnce({ + isFile: () => true, + size: 10 * 1024 * 1024 + 1 + }) + + const result = await getDiff('/repo', 'dist/large.log', false) + + expect(readFileMock).not.toHaveBeenCalled() + expect(result.kind).toBe('binary') + expect(result.modifiedIsBinary).toBe(true) + expect(result.modifiedContent).toBe('') + }) + it('includes preview metadata for pdf diffs', async () => { const pdfBuffer = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x00]) gitExecFileAsyncBufferMock.mockResolvedValueOnce({ stdout: pdfBuffer }) diff --git a/src/main/git/status.ts b/src/main/git/status.ts index 3883c9407d3..891b231087d 100644 --- a/src/main/git/status.ts +++ b/src/main/git/status.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ import { existsSync } from 'fs' -import { readFile } from 'fs/promises' +import { readFile, stat } from 'fs/promises' import * as path from 'path' import type { GitBranchChangeEntry, @@ -950,6 +950,15 @@ async function readGitBlobAtOidPath( async function readWorkingTreeFile(filePath: string): Promise { try { + const fileStat = await stat(filePath) + if (!fileStat.isFile()) { + return { content: '', isBinary: false, exists: false } + } + if (fileStat.size > MAX_GIT_SHOW_BYTES) { + // Why: git blob reads are capped through maxBuffer; mirror that bound for + // unstaged working-tree content before readFile can pull in huge assets. + return { content: '', isBinary: true, exists: true } + } const buffer = await readFile(filePath) return bufferToBlob(buffer, filePath) } catch {