From 2b5b9929042029125d10a92ae0070eb0c58c680f Mon Sep 17 00:00:00 2001 From: hugo Date: Sun, 22 Feb 2026 10:30:58 +0100 Subject: [PATCH] fix: git errors on startup / check --- src/main/engine/GitEngine.ts | 37 +++++++++++++++++++++++++--------- tests/engine/GitEngine.test.ts | 31 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/main/engine/GitEngine.ts b/src/main/engine/GitEngine.ts index abad317..7911f33 100644 --- a/src/main/engine/GitEngine.ts +++ b/src/main/engine/GitEngine.ts @@ -392,6 +392,11 @@ export class GitEngine { return normalized.includes('no upstream branch') || normalized.includes('has no upstream branch'); } + private isSpawnBadFileDescriptorError(message: string): boolean { + const normalized = message.toLowerCase(); + return normalized.includes('spawn ebadf'); + } + private async getCurrentBranchName(git: ReturnType): Promise { try { const status = await git.status(); @@ -717,24 +722,36 @@ export class GitEngine { } async getHistory(projectPath: string, limit = 20): Promise { - const git = simpleGit(projectPath); + const git = this.createNonInteractiveGit(projectPath); const status = await git.status(); const localHistory = await git.log({ maxCount: limit }); + const mapLocalHistory = (): GitHistoryEntry[] => localHistory.all.map((entry) => ({ + hash: entry.hash, + shortHash: entry.hash.slice(0, 7), + date: entry.date, + subject: entry.message, + author: entry.author_name, + syncStatus: 'local-only', + })); + if (!status.tracking) { - return localHistory.all.map((entry) => ({ - hash: entry.hash, - shortHash: entry.hash.slice(0, 7), - date: entry.date, - subject: entry.message, - author: entry.author_name, - syncStatus: 'local-only', - })); + return mapLocalHistory(); } const behindCount = typeof status.behind === 'number' ? status.behind : Number(status.behind ?? 0); const remoteHistoryLimit = Math.max(limit, limit + Math.max(behindCount, 0)); - const remoteHistory = await git.log([status.tracking, '--max-count', String(remoteHistoryLimit)]); + let remoteHistory; + + try { + remoteHistory = await git.log([status.tracking, '--max-count', String(remoteHistoryLimit)]); + } catch (error) { + const message = error instanceof Error ? error.message : String(error ?? ''); + if (this.isSpawnBadFileDescriptorError(message)) { + return mapLocalHistory(); + } + throw error; + } type CommitLike = { hash: string; diff --git a/tests/engine/GitEngine.test.ts b/tests/engine/GitEngine.test.ts index b3660ba..af5afd8 100644 --- a/tests/engine/GitEngine.test.ts +++ b/tests/engine/GitEngine.test.ts @@ -434,6 +434,37 @@ describe('GitEngine', () => { }, ]); }); + + it('should fall back to local history when remote history lookup fails with spawn EBADF', async () => { + mockStatus.mockResolvedValue({ current: 'main', tracking: 'origin/main' }); + mockLog + .mockResolvedValueOnce({ + all: [ + { + hash: 'abc123def456', + date: '2026-02-16T10:00:00.000Z', + message: 'feat: local commit', + author_name: 'Local Dev', + }, + ], + }) + .mockRejectedValueOnce(new Error('Error: spawn EBADF')); + + const result = await gitEngine.getHistory('/tmp/project', 20); + + expect(mockLog).toHaveBeenNthCalledWith(1, { maxCount: 20 }); + expect(mockLog).toHaveBeenNthCalledWith(2, ['origin/main', '--max-count', '20']); + expect(result).toEqual([ + { + hash: 'abc123def456', + shortHash: 'abc123d', + date: '2026-02-16T10:00:00.000Z', + subject: 'feat: local commit', + author: 'Local Dev', + syncStatus: 'local-only', + }, + ]); + }); }); describe('getFileHistory', () => {