Exclude submodule worktree-only changes from Stage All action (#7298)
- Prevent the "Stage All" button from being enabled when only nested submodule worktree changes are present. - Remove the disabled stage button on submodule worktree-only rows, replacing it with an explanatory tooltip. - Fix WSL terminal environment assertion in daemon PTY tests to allow inherited agent-hook environment variables.
This commit is contained in:
parent
7c77ccab7d
commit
0982882436
|
|
@ -2028,15 +2028,14 @@ describe('createPtySubprocess', () => {
|
|||
}
|
||||
}
|
||||
|
||||
expect(spawnMock).toHaveBeenCalledWith(
|
||||
'wsl.exe',
|
||||
expect.any(Array),
|
||||
expect.objectContaining({
|
||||
env: expect.objectContaining({
|
||||
ORCA_TERMINAL_HANDLE: 'term_wsl',
|
||||
WSLENV: 'FOO/u:ORCA_TERMINAL_HANDLE/u:POWERLEVEL9K_DISABLE_CONFIGURATION_WIZARD'
|
||||
})
|
||||
})
|
||||
const spawnCall = spawnMock.mock.calls.at(-1)!
|
||||
expect(spawnCall[0]).toBe('wsl.exe')
|
||||
expect(spawnCall[1]).toEqual(expect.any(Array))
|
||||
expect(spawnCall[2].env.ORCA_TERMINAL_HANDLE).toBe('term_wsl')
|
||||
// Why: the daemon inherits optional agent-hook env in development. This
|
||||
// test owns only the terminal handle and Powerlevel10k WSLENV contract.
|
||||
expect(spawnCall[2].env.WSLENV?.split(':')).toEqual(
|
||||
expect.arrayContaining(['FOO/u', 'ORCA_TERMINAL_HANDLE/u', POWERLEVEL10K_WIZARD_DISABLE_ENV])
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ const mocks = vi.hoisted(() => {
|
|||
openBranchDiff: vi.fn(),
|
||||
createEmptySplitGroup: vi.fn(),
|
||||
discardRuntimeGitPath: vi.fn(),
|
||||
bulkStageRuntimeGitPaths: vi.fn(),
|
||||
refreshGitStatusForWorktree: vi.fn(),
|
||||
requestEditorSaveQuiesce: vi.fn(),
|
||||
notifyEditorExternalFileChange: vi.fn()
|
||||
|
|
@ -85,7 +86,8 @@ vi.mock('@/runtime/runtime-git-client', async (importOriginal) => {
|
|||
const actual = await importOriginal<Record<string, unknown>>()
|
||||
return {
|
||||
...actual,
|
||||
discardRuntimeGitPath: mocks.calls.discardRuntimeGitPath
|
||||
discardRuntimeGitPath: mocks.calls.discardRuntimeGitPath,
|
||||
bulkStageRuntimeGitPaths: mocks.calls.bulkStageRuntimeGitPaths
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -140,6 +142,7 @@ function resetState(overrides: Partial<Record<string, unknown>> = {}): void {
|
|||
vi.clearAllMocks()
|
||||
mocks.calls.createEmptySplitGroup.mockReturnValue('group-2')
|
||||
mocks.calls.discardRuntimeGitPath.mockResolvedValue(undefined)
|
||||
mocks.calls.bulkStageRuntimeGitPaths.mockResolvedValue(undefined)
|
||||
mocks.calls.refreshGitStatusForWorktree.mockResolvedValue(undefined)
|
||||
mocks.calls.requestEditorSaveQuiesce.mockResolvedValue(undefined)
|
||||
mocks.state = {
|
||||
|
|
@ -437,13 +440,76 @@ describe('SourceControl preview row opens', () => {
|
|||
const row = container.querySelector<HTMLDivElement>(
|
||||
'[data-source-control-path="packages/nested"]'
|
||||
)
|
||||
expect(row?.textContent).toContain('Submodule changes - stage inside submodule')
|
||||
expect(row?.textContent).toContain('Stage inside submodule')
|
||||
expect(
|
||||
row?.querySelector('[title*="cannot stage file changes inside a submodule"]')
|
||||
).not.toBeNull()
|
||||
|
||||
const stageButton = row?.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="Stage these changes inside the submodule"]'
|
||||
expect(row?.querySelector<HTMLButtonElement>('button[aria-label="Stage"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('does not render a commit-area Stage All primary for submodule worktree-only rows', () => {
|
||||
resetState({
|
||||
gitStatusByWorktree: {
|
||||
[mocks.activeWorktree.id]: [
|
||||
gitEntry({
|
||||
path: 'june-11th-launch',
|
||||
submodule: { commitChanged: false, trackedChanges: true, untrackedChanges: false }
|
||||
})
|
||||
]
|
||||
}
|
||||
})
|
||||
renderSourceControl()
|
||||
|
||||
const row = container.querySelector<HTMLDivElement>(
|
||||
'[data-source-control-path="june-11th-launch"]'
|
||||
)
|
||||
expect(row?.textContent).toContain('Stage inside submodule')
|
||||
|
||||
const stageAllButton = [...container.querySelectorAll<HTMLButtonElement>('button')].find(
|
||||
(button) => button.textContent?.trim() === 'Stage All'
|
||||
)
|
||||
expect(stageAllButton).toBeUndefined()
|
||||
|
||||
const commitButton = [...container.querySelectorAll<HTMLButtonElement>('button')].find(
|
||||
(button) => button.textContent?.trim() === 'Commit'
|
||||
)
|
||||
expect(commitButton).toBeDefined()
|
||||
expect(commitButton?.disabled).toBe(true)
|
||||
|
||||
act(() => {
|
||||
commitButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
})
|
||||
expect(mocks.calls.bulkStageRuntimeGitPaths).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps the commit-area Stage All primary for changed submodule gitlinks', async () => {
|
||||
resetState({
|
||||
gitStatusByWorktree: {
|
||||
[mocks.activeWorktree.id]: [
|
||||
gitEntry({
|
||||
path: 'packages/nested',
|
||||
submodule: { commitChanged: true, trackedChanges: false, untrackedChanges: false }
|
||||
})
|
||||
]
|
||||
}
|
||||
})
|
||||
renderSourceControl()
|
||||
|
||||
const stageAllButton = [...container.querySelectorAll<HTMLButtonElement>('button')].find(
|
||||
(button) => button.textContent?.trim() === 'Stage All'
|
||||
)
|
||||
expect(stageAllButton).toBeDefined()
|
||||
expect(stageAllButton?.disabled).toBe(false)
|
||||
|
||||
await act(async () => {
|
||||
stageAllButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
||||
await Promise.resolve()
|
||||
})
|
||||
expect(mocks.calls.bulkStageRuntimeGitPaths).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ worktreeId: mocks.activeWorktree.id, worktreePath: '/repo/wt' }),
|
||||
['packages/nested']
|
||||
)
|
||||
expect(stageButton).not.toBeNull()
|
||||
expect(stageButton?.getAttribute('aria-disabled')).toBe('true')
|
||||
})
|
||||
|
||||
it('passes preview=true when a plain branch row click opens a branch diff tab', () => {
|
||||
|
|
|
|||
|
|
@ -536,8 +536,9 @@ const SOURCE_CONTROL_TREE_DIRECTORY_PADDING_PX = 8
|
|||
const SOURCE_CONTROL_TREE_FILE_PADDING_PX = 20
|
||||
const EMPTY_GIT_HISTORY_STATE: GitHistoryPanelState = { status: 'idle' }
|
||||
const DEFAULT_COLLAPSED_SECTIONS = ['history'] as const
|
||||
const SUBMODULE_WORKTREE_ONLY_LABEL = 'Submodule changes - stage inside submodule'
|
||||
const SUBMODULE_WORKTREE_ONLY_STAGE_TOOLTIP = 'Stage these changes inside the submodule'
|
||||
const SUBMODULE_WORKTREE_ONLY_LABEL = 'Stage inside submodule'
|
||||
const SUBMODULE_WORKTREE_ONLY_TOOLTIP =
|
||||
'The parent repo (including Stage All) cannot stage file changes inside a submodule'
|
||||
const SUBMODULE_LOADING_LABEL = 'Loading submodule changes…'
|
||||
const SUBMODULE_EMPTY_LABEL = 'No changes in submodule'
|
||||
const SUBMODULE_ERROR_LABEL = 'Failed to load submodule changes'
|
||||
|
|
@ -3995,7 +3996,12 @@ function SourceControlInner(): React.JSX.Element {
|
|||
])
|
||||
|
||||
const hasUnstagedChanges = grouped.unstaged.length > 0 || grouped.untracked.length > 0
|
||||
const hasStageableChanges = hasUnstagedChanges
|
||||
const hasStageableChanges = useMemo(
|
||||
() =>
|
||||
grouped.unstaged.some(isStageableStatusEntry) ||
|
||||
grouped.untracked.some(isStageableStatusEntry),
|
||||
[grouped.unstaged, grouped.untracked]
|
||||
)
|
||||
const hasPartiallyStagedChanges = useMemo(() => {
|
||||
if (grouped.staged.length === 0 || grouped.unstaged.length === 0) {
|
||||
return false
|
||||
|
|
@ -8115,9 +8121,17 @@ const UncommittedEntryRow = React.memo(function UncommittedEntryRow({
|
|||
<span className="ml-1.5 text-[11px] text-muted-foreground">{dirPath}</span>
|
||||
)}
|
||||
</span>
|
||||
{(conflictLabel || isSubmoduleWorktreeOnly) && (
|
||||
<div className="truncate text-[11px] text-muted-foreground">
|
||||
{conflictLabel ?? SUBMODULE_WORKTREE_ONLY_LABEL}
|
||||
{conflictLabel && (
|
||||
<div className="truncate text-[11px] text-muted-foreground">{conflictLabel}</div>
|
||||
)}
|
||||
{isSubmoduleWorktreeOnly && (
|
||||
// Why: parent git can stage a changed gitlink, but not nested
|
||||
// worktree dirtiness. Keep that boundary visible in the row.
|
||||
<div
|
||||
className="truncate text-[11px] text-muted-foreground"
|
||||
title={SUBMODULE_WORKTREE_ONLY_TOOLTIP}
|
||||
>
|
||||
{SUBMODULE_WORKTREE_ONLY_LABEL}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -8176,19 +8190,14 @@ const UncommittedEntryRow = React.memo(function UncommittedEntryRow({
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{(canStage || isSubmoduleWorktreeOnly) && (
|
||||
{canStage && (
|
||||
<ActionButton
|
||||
icon={Plus}
|
||||
title={
|
||||
isSubmoduleWorktreeOnly
|
||||
? SUBMODULE_WORKTREE_ONLY_STAGE_TOOLTIP
|
||||
: translate('auto.components.right.sidebar.SourceControl.8cde1a2fb0', 'Stage')
|
||||
}
|
||||
title={translate('auto.components.right.sidebar.SourceControl.8cde1a2fb0', 'Stage')}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
void onStage(entry.path)
|
||||
}}
|
||||
disabled={isSubmoduleWorktreeOnly}
|
||||
/>
|
||||
)}
|
||||
{canUnstage && (
|
||||
|
|
|
|||
Loading…
Reference in New Issue