diff --git a/src/core/git/gitCommand.ts b/src/core/git/gitCommand.ts index 1fe0db160..64db463d6 100644 --- a/src/core/git/gitCommand.ts +++ b/src/core/git/gitCommand.ts @@ -7,6 +7,10 @@ import { logger } from '../../shared/logger.js'; const execFileAsync = promisify(execFile); +const GIT_REMOTE_TIMEOUT = 30000; +const gitRemoteEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' }; +const gitRemoteOpts = { timeout: GIT_REMOTE_TIMEOUT, env: gitRemoteEnv }; + export const execGitLogFilenames = async ( directory: string, maxCommits = 100, @@ -93,7 +97,7 @@ export const execLsRemote = async ( validateGitUrl(url); try { - const result = await deps.execFileAsync('git', ['ls-remote', '--heads', '--tags', '--', url]); + const result = await deps.execFileAsync('git', ['ls-remote', '--heads', '--tags', '--', url], gitRemoteOpts); return result.stdout || ''; } catch (error) { logger.trace('Failed to execute git ls-remote:', (error as Error).message); @@ -115,7 +119,11 @@ export const execGitShallowClone = async ( await deps.execFileAsync('git', ['-C', directory, 'init']); await deps.execFileAsync('git', ['-C', directory, 'remote', 'add', '--', 'origin', url]); try { - await deps.execFileAsync('git', ['-C', directory, 'fetch', '--depth', '1', 'origin', remoteBranch]); + await deps.execFileAsync( + 'git', + ['-C', directory, 'fetch', '--depth', '1', 'origin', remoteBranch], + gitRemoteOpts, + ); await deps.execFileAsync('git', ['-C', directory, 'checkout', 'FETCH_HEAD']); } catch (err: unknown) { // git fetch --depth 1 origin always throws "couldn't find remote ref" error @@ -139,11 +147,11 @@ export const execGitShallowClone = async ( // Maybe the error is due to a short SHA, let's try again // Can't use --depth 1 here as we need to fetch the specific commit - await deps.execFileAsync('git', ['-C', directory, 'fetch', 'origin']); + await deps.execFileAsync('git', ['-C', directory, 'fetch', 'origin'], gitRemoteOpts); await deps.execFileAsync('git', ['-C', directory, 'checkout', remoteBranch]); } } else { - await deps.execFileAsync('git', ['clone', '--depth', '1', '--', url, directory]); + await deps.execFileAsync('git', ['clone', '--depth', '1', '--', url, directory], gitRemoteOpts); } // Clean up .git directory diff --git a/tests/core/git/gitCommand.test.ts b/tests/core/git/gitCommand.test.ts index 6c26b53e8..8083cbbae 100644 --- a/tests/core/git/gitCommand.test.ts +++ b/tests/core/git/gitCommand.test.ts @@ -12,6 +12,11 @@ import { logger } from '../../../src/shared/logger.js'; vi.mock('../../../src/shared/logger'); +const expectGitRemoteOpts = expect.objectContaining({ + timeout: 30000, + env: expect.objectContaining({ GIT_TERMINAL_PROMPT: '0' }), +}); + describe('gitCommand', () => { beforeEach(() => { vi.resetAllMocks(); @@ -122,7 +127,11 @@ file2.ts await execGitShallowClone(url, directory, remoteBranch, { execFileAsync: mockFileExecAsync }); - expect(mockFileExecAsync).toHaveBeenCalledWith('git', ['clone', '--depth', '1', '--', url, directory]); + expect(mockFileExecAsync).toHaveBeenCalledWith( + 'git', + ['clone', '--depth', '1', '--', url, directory], + expectGitRemoteOpts, + ); }); test('should throw error when git clone fails', async () => { @@ -135,7 +144,11 @@ file2.ts execGitShallowClone(url, directory, remoteBranch, { execFileAsync: mockFileExecAsync }), ).rejects.toThrow('Authentication failed'); - expect(mockFileExecAsync).toHaveBeenCalledWith('git', ['clone', '--depth', '1', '--', url, directory]); + expect(mockFileExecAsync).toHaveBeenCalledWith( + 'git', + ['clone', '--depth', '1', '--', url, directory], + expectGitRemoteOpts, + ); }); test('should execute commands correctly when branch is specified', async () => { @@ -158,15 +171,12 @@ file2.ts 'origin', url, ]); - expect(mockFileExecAsync).toHaveBeenNthCalledWith(3, 'git', [ - '-C', - directory, - 'fetch', - '--depth', - '1', - 'origin', - remoteBranch, - ]); + expect(mockFileExecAsync).toHaveBeenNthCalledWith( + 3, + 'git', + ['-C', directory, 'fetch', '--depth', '1', 'origin', remoteBranch], + expectGitRemoteOpts, + ); expect(mockFileExecAsync).toHaveBeenNthCalledWith(4, 'git', ['-C', directory, 'checkout', 'FETCH_HEAD']); }); @@ -195,15 +205,11 @@ file2.ts 'origin', url, ]); - expect(mockFileExecAsync).toHaveBeenLastCalledWith('git', [ - '-C', - directory, - 'fetch', - '--depth', - '1', - 'origin', - remoteBranch, - ]); + expect(mockFileExecAsync).toHaveBeenLastCalledWith( + 'git', + ['-C', directory, 'fetch', '--depth', '1', 'origin', remoteBranch], + expectGitRemoteOpts, + ); }); test('should handle short SHA correctly', async () => { @@ -233,16 +239,18 @@ file2.ts 'origin', url, ]); - expect(mockFileExecAsync).toHaveBeenNthCalledWith(3, 'git', [ - '-C', - directory, - 'fetch', - '--depth', - '1', - 'origin', - shortSha, - ]); - expect(mockFileExecAsync).toHaveBeenNthCalledWith(4, 'git', ['-C', directory, 'fetch', 'origin']); + expect(mockFileExecAsync).toHaveBeenNthCalledWith( + 3, + 'git', + ['-C', directory, 'fetch', '--depth', '1', 'origin', shortSha], + expectGitRemoteOpts, + ); + expect(mockFileExecAsync).toHaveBeenNthCalledWith( + 4, + 'git', + ['-C', directory, 'fetch', 'origin'], + expectGitRemoteOpts, + ); expect(mockFileExecAsync).toHaveBeenLastCalledWith('git', ['-C', directory, 'checkout', shortSha]); }); @@ -272,15 +280,11 @@ file2.ts 'origin', url, ]); - expect(mockFileExecAsync).toHaveBeenLastCalledWith('git', [ - '-C', - directory, - 'fetch', - '--depth', - '1', - 'origin', - remoteBranch, - ]); + expect(mockFileExecAsync).toHaveBeenLastCalledWith( + 'git', + ['-C', directory, 'fetch', '--depth', '1', 'origin', remoteBranch], + expectGitRemoteOpts, + ); }); }); @@ -387,13 +391,11 @@ c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8\trefs/tags/v1.0.0 const result = await execLsRemote('https://github.com/user/repo.git', { execFileAsync: mockFileExecAsync }); expect(result).toBe(mockOutput); - expect(mockFileExecAsync).toHaveBeenCalledWith('git', [ - 'ls-remote', - '--heads', - '--tags', - '--', - 'https://github.com/user/repo.git', - ]); + expect(mockFileExecAsync).toHaveBeenCalledWith( + 'git', + ['ls-remote', '--heads', '--tags', '--', 'https://github.com/user/repo.git'], + expectGitRemoteOpts, + ); }); test('should throw error when git ls-remote fails', async () => {