From fd6bc7e96f3725f20c443c46c4e5941a2dd06540 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 9 Oct 2026 02:03:04 -0700 Subject: [PATCH] Keep managed status warnings out of desktop gitignore writes (#26671) Co-authored-by: m4air --- .../use-status-refresh.local-ignore.test.tsx | 86 +++++++++++++++++++ .../source-control/sync/use-status-refresh.ts | 10 ++- 2 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.local-ignore.test.tsx diff --git a/src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.local-ignore.test.tsx b/src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.local-ignore.test.tsx new file mode 100644 index 00000000000..c936795f86c --- /dev/null +++ b/src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.local-ignore.test.tsx @@ -0,0 +1,86 @@ +/** @vitest-environment happy-dom */ +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, expect, it, vi } from 'vitest' +import { clearHugeRepoWarningDismissalsForTests } from '@/lib/source-control-huge-repo-warning-dismissals' +import { useSourceControlStatusRefresh } from './use-status-refresh' + +globalThis.IS_REACT_ACT_ENVIRONMENT = true + +const actions = vi.hoisted(() => ({ + setGitStatus: vi.fn(), + updateWorktreeGitIdentity: vi.fn(), + setUpstreamStatus: vi.fn(), + fetchUpstreamStatus: vi.fn() +})) +const warning = vi.hoisted(() => vi.fn()) +vi.mock('@/store', () => ({ + useAppStore: (select: (state: typeof actions) => unknown) => select(actions) +})) +vi.mock('sonner', () => ({ toast: { warning } })) +vi.mock('@/lib/connection-context', () => ({ getConnectionId: () => null })) +vi.mock('../../git-status-refresh', () => ({ refreshGitStatusForWorktree: vi.fn() })) +vi.mock('@/i18n/i18n', () => ({ translate: (_key: string, fallback: string) => fallback })) + +const findHugeFoldersToIgnore = vi.fn() +const appendGitignore = vi.fn() +let root: Root | null = null +let container: HTMLDivElement | null = null + +beforeEach(() => { + findHugeFoldersToIgnore.mockReset().mockResolvedValue(['dist']) + appendGitignore.mockReset().mockResolvedValue(true) + warning.mockReset() + vi.stubGlobal('window', { + ...window, + api: { git: { findHugeFoldersToIgnore, appendGitignore } } + }) + container = document.createElement('div') + root = createRoot(container) +}) + +afterEach(() => { + act(() => root?.unmount()) + root = null + container = null + vi.unstubAllGlobals() + clearHugeRepoWarningDismissalsForTests() +}) + +async function renderOwner( + runtimeEnvironmentId: string | null, + connectionId: string | null = null +) { + function Probe() { + useSourceControlStatusRefresh({ + activeRepoSettings: { activeRuntimeEnvironmentId: runtimeEnvironmentId }, + activeWorktreeId: 'repo::/same/path', + worktreePath: '/same/path', + isFolder: false, + repositoryHuge: { limit: 1000 }, + activeConnectionId: connectionId, + worktreeMap: new Map() + }) + return null + } + await act(async () => root?.render()) +} + +it('does not probe or offer a desktop ignore action for a managed owner without a legacy SSH ID', async () => { + await renderOwner('managed-owner') + expect(findHugeFoldersToIgnore).not.toHaveBeenCalled() + expect(warning).not.toHaveBeenCalled() + expect(appendGitignore).not.toHaveBeenCalled() +}) + +it('keeps the existing local suggestion', async () => { + await renderOwner(null) + expect(findHugeFoldersToIgnore).toHaveBeenCalledExactlyOnceWith({ worktreePath: '/same/path' }) + expect(warning).toHaveBeenCalledTimes(1) +}) + +it('keeps direct SSH targets out of the desktop helper', async () => { + await renderOwner(null, 'ssh-owner') + expect(findHugeFoldersToIgnore).not.toHaveBeenCalled() + expect(warning).not.toHaveBeenCalled() +}) diff --git a/src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.ts b/src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.ts index 46db77e99ab..d41b45b5019 100644 --- a/src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.ts +++ b/src/renderer/src/components/right-sidebar/source-control/sync/use-status-refresh.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect } from 'react' import { toast } from 'sonner' import { getConnectionId } from '@/lib/connection-context' +import { isLocalPathOpenBlocked } from '@/lib/local-path-open-guard' import { beginHugeRepoWarningProbe, hasDismissedHugeRepoWarning, @@ -46,6 +47,9 @@ export function useSourceControlStatusRefresh({ const updateWorktreeGitIdentity = useAppStore((s) => s.updateWorktreeGitIdentity) const setUpstreamStatus = useAppStore((s) => s.setUpstreamStatus) const fetchUpstreamStatus = useAppStore((s) => s.fetchUpstreamStatus) + const localIgnoreBlocked = isLocalPathOpenBlocked(activeRepoSettings, { + connectionId: activeConnectionId + }) const refreshActiveGitStatus = useCallback( async (signal?: AbortSignal): Promise => { if (!activeWorktreeId || !worktreePath || isFolder) { @@ -88,9 +92,9 @@ export function useSourceControlStatusRefresh({ } }, [refreshActiveGitStatus]) - // Why: when status is truncated, offer once per worktree to .gitignore the flooding folder; local-only since the SSH huge-folder write path isn't wired. + // Why: when status is truncated, offer once per worktree to .gitignore the flooding folder; desktop-only since the probe and write run on this machine, and managed repos have no connectionId. useEffect(() => { - if (!repositoryHuge || !activeWorktreeId || !worktreePath || activeConnectionId) { + if (!repositoryHuge || !activeWorktreeId || !worktreePath || localIgnoreBlocked) { return } const warningProbe = beginHugeRepoWarningProbe({ @@ -146,7 +150,7 @@ export function useSourceControlStatusRefresh({ activeWorktreeId, activeWorktreeInstanceId, worktreePath, - activeConnectionId, + localIgnoreBlocked, refreshActiveGitStatus ])