From e4e9c657db8576533c2a381f1bc83a0ea4c1516c Mon Sep 17 00:00:00 2001 From: paulreginaphoto Date: Tue, 28 Apr 2026 23:55:12 +0200 Subject: [PATCH] Handle Git for Windows outside inherited PATH --- src/install/installationManager.ts | 4 +-- src/main-process/comfyInstallation.ts | 4 +-- src/utils.ts | 31 +++++++++++++++-- .../unit/install/installationManager.test.ts | 3 +- tests/unit/utils.test.ts | 34 ++++++++++++++++++- 5 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/install/installationManager.ts b/src/install/installationManager.ts index e4eb992ea..72ba8b45a 100644 --- a/src/install/installationManager.ts +++ b/src/install/installationManager.ts @@ -16,7 +16,7 @@ import { CmCli } from '../services/cmCli'; import { captureSentryException } from '../services/sentry'; import { type HasTelemetry, ITelemetry, trackEvent } from '../services/telemetry'; import { type DesktopConfig, useDesktopConfig } from '../store/desktopConfig'; -import { canExecuteShellCommand, compareVersions, validateHardware } from '../utils'; +import { canExecuteGit, compareVersions, validateHardware } from '../utils'; import type { ProcessCallbacks, VirtualEnvironment } from '../virtualEnvironment'; import { createProcessCallbacks } from './createProcessCallbacks'; import { InstallWizard } from './installWizard'; @@ -176,7 +176,7 @@ export class InstallationManager implements HasTelemetry { // Check if git is installed log.verbose('Checking if git is installed.'); appState.setInstallStage(createInstallStageInfo(InstallStage.GIT_CHECK, { progress: 5 })); - const gitInstalled = await canExecuteShellCommand('git --version'); + const gitInstalled = await canExecuteGit(); if (!gitInstalled) { log.verbose('git not detected in path, loading download-git page.'); diff --git a/src/main-process/comfyInstallation.ts b/src/main-process/comfyInstallation.ts index 3859f59aa..10af57960 100644 --- a/src/main-process/comfyInstallation.ts +++ b/src/main-process/comfyInstallation.ts @@ -8,7 +8,7 @@ import type { DesktopInstallState } from '../main_types'; import type { InstallValidation } from '../preload'; import { type ITelemetry, getTelemetry } from '../services/telemetry'; import { useDesktopConfig } from '../store/desktopConfig'; -import { canExecute, canExecuteShellCommand, pathAccessible } from '../utils'; +import { canExecute, canExecuteGit, pathAccessible } from '../utils'; import { VirtualEnvironment } from '../virtualEnvironment'; /** @@ -188,7 +188,7 @@ export class ComfyInstallation { } // Git - validation.git = (await canExecuteShellCommand('git --help')) ? 'OK' : 'error'; + validation.git = (await canExecuteGit()) ? 'OK' : 'error'; if (validation.git !== 'OK') log.warn('git not found in path.'); this.onUpdate?.(validation); diff --git a/src/utils.ts b/src/utils.ts index a3b7716a7..db859c097 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -41,14 +41,41 @@ export async function canExecute(path: string): Promise { export async function canExecuteShellCommand(command: string, timeout = 5000): Promise { const proc = exec(command); return new Promise((resolve, reject) => { - setTimeout(() => { + const timeoutHandle = setTimeout(() => { proc.kill(); reject(new Error('Timed out attempting to execute git')); }, timeout); - proc.on('exit', (code) => resolve(code === 0)); + proc.on('exit', (code) => { + clearTimeout(timeoutHandle); + resolve(code === 0); + }); }); } +const quoteShellPath = (value: string) => `"${value.replaceAll('"', '')}"`; + +function getWindowsGitCommands(): string[] { + const programFiles = [process.env.ProgramFiles, process.env['ProgramFiles(x86)']].filter( + (value): value is string => !!value + ); + + return programFiles.flatMap((basePath) => [ + `${quoteShellPath(path.join(basePath, 'Git', 'cmd', 'git.exe'))} --help`, + `${quoteShellPath(path.join(basePath, 'Git', 'bin', 'git.exe'))} --help`, + ]); +} + +export async function canExecuteGit(): Promise { + if (await canExecuteShellCommand('git --help')) return true; + if (process.platform !== 'win32') return false; + + for (const command of getWindowsGitCommands()) { + if (await canExecuteShellCommand(command)) return true; + } + + return false; +} + export async function containsDirectory(path: string, contains: string): Promise { if (await pathAccessible(path)) { const contents = await fsPromises.readdir(path, { withFileTypes: true }); diff --git a/tests/unit/install/installationManager.test.ts b/tests/unit/install/installationManager.test.ts index af9f92969..984826a39 100644 --- a/tests/unit/install/installationManager.test.ts +++ b/tests/unit/install/installationManager.test.ts @@ -63,6 +63,7 @@ vi.mock('@/utils', async () => { }), canExecute: vi.fn(() => Promise.resolve(true)), canExecuteShellCommand: vi.fn(() => Promise.resolve(true)), + canExecuteGit: vi.fn(() => Promise.resolve(true)), }; }); @@ -212,7 +213,7 @@ describe('InstallationManager', () => { { scenario: 'detects missing git', mockSetup: () => { - vi.mocked(utils.canExecuteShellCommand).mockResolvedValue(false); + vi.mocked(utils.canExecuteGit).mockResolvedValue(false); }, expectedErrors: ['git'], }, diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts index 6a4fdee1a..76ec758f1 100644 --- a/tests/unit/utils.test.ts +++ b/tests/unit/utils.test.ts @@ -4,7 +4,7 @@ import type { Systeminformation } from 'systeminformation'; import si from 'systeminformation'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { validateHardware } from '@/utils'; +import { canExecuteGit, validateHardware } from '@/utils'; vi.mock('node:child_process', () => ({ exec: vi.fn(), @@ -21,6 +21,8 @@ const createChildProcess = (): ChildProcess => type ExecResponse = { error?: Error | null; stdout?: string; stderr?: string }; +type ExitResponse = { code: number }; + const withExecResponses = (responses: Array<[RegExp, ExecResponse]>, fallback: ExecResponse = {}) => { execMock.mockImplementation((( command: string, @@ -33,6 +35,20 @@ const withExecResponses = (responses: Array<[RegExp, ExecResponse]>, fallback: E }) as typeof exec); }; +const withExitResponses = (responses: Array<[RegExp, ExitResponse]>, fallback?: ExitResponse) => { + execMock.mockImplementation((command: string) => { + const match = responses.find(([pattern]) => pattern.test(command)); + const { code } = match?.[1] ?? fallback ?? { code: 1 }; + + return { + kill: vi.fn(), + on: vi.fn((event: string, callback: (code: number) => void) => { + if (event === 'exit') setImmediate(() => callback(code)); + }), + } as unknown as ChildProcess; + }); +}; + beforeEach(() => { execMock.mockReset(); }); @@ -99,3 +115,19 @@ describe('validateHardware', () => { }); }); }); + +describe('canExecuteGit', () => { + it('falls back to the standard Git for Windows install path when git is missing from PATH', async () => { + vi.stubGlobal('process', { + ...process, + platform: 'win32', + env: { ...process.env, ProgramFiles: String.raw`C:\Program Files` }, + }); + withExitResponses([ + [/^git --help$/, { code: 1 }], + [/^"C:\\Program Files\\Git\\cmd\\git\.exe" --help$/, { code: 0 }], + ]); + + await expect(canExecuteGit()).resolves.toBe(true); + }); +});