From 48ad06910ac5865c7a4ab806b1fdde6a84ee6404 Mon Sep 17 00:00:00 2001 From: gonzaloriestra <14979109+gonzaloriestra@users.noreply.github.com> Date: Sat, 27 Jun 2026 00:18:04 +0000 Subject: [PATCH] [Performance] Memoize hasGit check Checking for Git presence via `git --version` is an expensive operation that involves spawning a new process. This check is performed multiple times during the execution of various CLI commands. Since Git's presence is unlikely to change during the execution of a single command, we can memoize the result to improve performance. This PR memoizes the `hasGit` function in `packages/cli-kit/src/public/node/context/local.ts` using a module-level promise. - Introduced `hasGitPromise` to cache the result. - Updated `hasGit` to return the cached promise if available. - Added and exported `_resetHasGit` for test isolation. - Updated unit tests in `packages/cli-kit/src/public/node/context/local.test.ts` to verify memoization. --- .../src/public/node/context/local.test.ts | 18 ++++++++++ .../cli-kit/src/public/node/context/local.ts | 35 +++++++++++++++---- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/packages/cli-kit/src/public/node/context/local.test.ts b/packages/cli-kit/src/public/node/context/local.test.ts index abe71bb52b7..8ccc100c0ce 100644 --- a/packages/cli-kit/src/public/node/context/local.test.ts +++ b/packages/cli-kit/src/public/node/context/local.test.ts @@ -1,6 +1,7 @@ import { ciPlatform, hasGit, + _resetHasGit, isDevelopment, isShopify, isTerminalInteractive, @@ -125,6 +126,7 @@ describe('isShopify', () => { describe('hasGit', () => { test('returns false if git --version errors', async () => { // Given + _resetHasGit() vi.mocked(exec).mockRejectedValue(new Error('git not found')) // When @@ -136,6 +138,7 @@ describe('hasGit', () => { test('returns true if git --version succeeds', async () => { // Given + _resetHasGit() vi.mocked(exec).mockResolvedValue(undefined) // When @@ -144,6 +147,21 @@ describe('hasGit', () => { // Then expect(got).toBeTruthy() }) + + test('memoizes the result', async () => { + // Given + _resetHasGit() + vi.mocked(exec).mockResolvedValue(undefined) + + // When + await hasGit() + await hasGit() + const got = await hasGit() + + // Then + expect(got).toBeTruthy() + expect(vi.mocked(exec)).toHaveBeenCalledTimes(1) + }) }) describe('analitycsDisabled', () => { diff --git a/packages/cli-kit/src/public/node/context/local.ts b/packages/cli-kit/src/public/node/context/local.ts index ad816399db9..3dabc23626a 100644 --- a/packages/cli-kit/src/public/node/context/local.ts +++ b/packages/cli-kit/src/public/node/context/local.ts @@ -231,19 +231,40 @@ export function cloudEnvironment(env: NodeJS.ProcessEnv = process.env): { return {platform: 'localhost', editor: false} } +/** + * Memoized promise for the hasGit check. + */ +let hasGitPromise: Promise | undefined + /** * Returns whether the environment has Git available. * * @returns A promise that resolves with the value. */ -export async function hasGit(): Promise { - try { - await lazyExec('git', ['--version']) - return true - // eslint-disable-next-line no-catch-all/no-catch-all - } catch { - return false +export function hasGit(): Promise { + if (hasGitPromise) { + return hasGitPromise } + + hasGitPromise = (async () => { + try { + await lazyExec('git', ['--version']) + return true + // eslint-disable-next-line no-catch-all/no-catch-all + } catch { + return false + } + })() + + return hasGitPromise +} + +/** + * Resets the memoized value for the hasGit check. + * This is only intended for use in tests. + */ +export function _resetHasGit(): void { + hasGitPromise = undefined } /**