Skip to content

Commit 3f0e2da

Browse files
nullvariantclaude
andauthored
feat(commands): implement handleDeleteIdentity command (#239)
* feat(commands): implement handleDeleteIdentity command Add delete identity functionality with UI and command handler: - Add handleDeleteIdentity() in handlers.ts with vscodeLoader pattern - Add showDeleteIdentityQuickPick() in identityPicker.ts for selection UI - Add showErrorNotification() helper for consistent error display - Register deleteIdentity command in extension.ts - Migrate identity.ts to vscodeLoader pattern for E2E testability - Add comprehensive E2E tests (17 test cases) 🖥️ IDE: [Cursor](https://cursor.sh) 🔌 Extension: [Claude Code](https://claude.ai/download) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Model-Raw: claude-opus-4-5-20251101 * refactor(identity): extract notification logic to reduce cognitive complexity Extract showValidationErrorNotification() helper function from getIdentitiesWithValidation() to reduce cognitive complexity from 16 to 12. This satisfies SonarQube's maximum allowed complexity of 15. 🖥️ IDE: [Cursor](https://cursor.sh) 🔌 Extension: [Claude Code](https://claude.ai/download) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Model-Raw: claude-opus-4-5-20251101 --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent acc7037 commit 3f0e2da

6 files changed

Lines changed: 947 additions & 44 deletions

File tree

extensions/git-id-switcher/src/commands/handlers.ts

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
*/
1111

1212
import * as vscode from 'vscode';
13-
import { Identity, getIdentityLabel } from '../identity/identity';
14-
import { showIdentityQuickPick, showErrorNotification } from '../ui/identityPicker';
13+
import { getVSCode } from '../core/vscodeLoader';
14+
import { Identity, getIdentityLabel, deleteIdentityFromConfig } from '../identity/identity';
15+
import { showIdentityQuickPick, showDeleteIdentityQuickPick, showErrorNotification } from '../ui/identityPicker';
1516
import { requireWorkspaceTrust } from '../core/workspaceTrust';
1617
import { getUserSafeMessage, isFatalError } from '../core/errors';
1718
import { IdentityStatusBar } from '../ui/identityStatusBar';
@@ -105,3 +106,91 @@ export async function showWelcomeNotification(): Promise<void> {
105106
);
106107
}
107108
}
109+
110+
/**
111+
* Handle the delete identity command.
112+
*
113+
* Uses vscodeLoader for testability (allows mocking in E2E tests).
114+
*
115+
* Flow:
116+
* 1. Show quick pick for selection (empty check handled by picker)
117+
* 2. Check if selected identity is current
118+
* 3. Show confirmation dialog (different message for current identity)
119+
* 4. Delete identity from config
120+
* 5. Clear workspace state if current identity was deleted
121+
* 6. Update status bar
122+
* 7. Show success notification
123+
*
124+
* @param context - VS Code extension context
125+
* @param statusBar - Identity status bar instance for UI update
126+
*/
127+
export async function handleDeleteIdentity(
128+
context: vscode.ExtensionContext,
129+
statusBar: IdentityStatusBar
130+
): Promise<void> {
131+
const vs = getVSCode();
132+
if (!vs) {
133+
return;
134+
}
135+
136+
// SECURITY: Block command execution in untrusted workspaces
137+
if (!requireWorkspaceTrust()) {
138+
return;
139+
}
140+
141+
// Get current identity ID from workspace state
142+
const currentIdentityId = context.workspaceState.get<string>('currentIdentityId');
143+
144+
try {
145+
// Show quick pick for selection
146+
const selectedIdentity = await showDeleteIdentityQuickPick(currentIdentityId);
147+
148+
if (!selectedIdentity) {
149+
// User cancelled
150+
return;
151+
}
152+
153+
const isCurrentIdentity = selectedIdentity.id === currentIdentityId;
154+
const identityLabel = getIdentityLabel(selectedIdentity);
155+
156+
// Show confirmation dialog (different message for current identity)
157+
const confirmMessage = isCurrentIdentity
158+
? vs.l10n.t("'{0}' is your current identity. Delete anyway?", identityLabel)
159+
: vs.l10n.t("Are you sure you want to delete '{0}'?", identityLabel);
160+
161+
const deleteButton = vs.l10n.t('Delete');
162+
const confirmation = await vs.window.showWarningMessage(
163+
confirmMessage,
164+
{ modal: true },
165+
deleteButton
166+
);
167+
168+
if (confirmation !== deleteButton) {
169+
// User cancelled confirmation
170+
return;
171+
}
172+
173+
// Delete identity from config
174+
await deleteIdentityFromConfig(selectedIdentity.id);
175+
176+
// Clear workspace state and update status bar if current identity was deleted
177+
if (isCurrentIdentity) {
178+
await context.workspaceState.update('currentIdentityId', undefined);
179+
statusBar.setNoIdentity();
180+
}
181+
182+
// Show success notification
183+
vs.window.showInformationMessage(
184+
vs.l10n.t("Identity '{0}' has been deleted.", identityLabel)
185+
);
186+
} catch (error) {
187+
// SECURITY: Use getUserSafeMessage to prevent information leakage
188+
const safeMessage = getUserSafeMessage(error);
189+
showErrorNotification(vs.l10n.t('Failed to delete identity: {0}', safeMessage));
190+
191+
// Propagate fatal errors (security violations)
192+
if (isFatalError(error)) {
193+
throw error;
194+
}
195+
}
196+
}

extensions/git-id-switcher/src/core/extension.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { securityLogger } from '../security/securityLogger';
1212
import { getUserSafeMessage, isFatalError } from './errors';
1313
import { initializeWorkspaceTrust } from './workspaceTrust';
1414
import { tryRestoreSavedIdentity, tryDetectFromGit, tryDetectFromSsh, applyDetectedIdentity } from '../services/detection';
15-
import { selectIdentityCommand, showCurrentIdentityCommand, showWelcomeNotification } from '../commands/handlers';
15+
import { selectIdentityCommand, showCurrentIdentityCommand, showWelcomeNotification, handleDeleteIdentity } from '../commands/handlers';
1616

1717
// Global state
1818
let statusBar: IdentityStatusBar;
@@ -48,7 +48,11 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
4848
'git-id-switcher.showDocumentation',
4949
() => showDocumentation(context)
5050
);
51-
context.subscriptions.push(selectCommand, showCurrentCommand, showDocsCommand);
51+
const deleteCommand = vscode.commands.registerCommand(
52+
'git-id-switcher.deleteIdentity',
53+
() => handleDeleteIdentity(context, statusBar)
54+
);
55+
context.subscriptions.push(selectCommand, showCurrentCommand, showDocsCommand, deleteCommand);
5256

5357
// SECURITY: Check workspace trust before initializing sensitive operations
5458
const isTrusted = initializeWorkspaceTrust(context, async () => {

extensions/git-id-switcher/src/identity/identity.ts

Lines changed: 74 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
*
44
* Identities are loaded from VS Code settings, not hardcoded.
55
* Each identity represents a Git persona with optional SSH/GPG keys.
6+
*
7+
* Uses vscodeLoader for testability (allows mocking in E2E tests).
68
*/
79

8-
import * as vscode from 'vscode';
10+
import { getVSCode } from '../core/vscodeLoader';
911
import { validateIdentitySchema } from './configSchema';
1012
import { securityLogger } from '../security/securityLogger';
1113
import { MAX_IDENTITIES, MAX_ID_LENGTH } from '../core/constants';
@@ -17,6 +19,50 @@ import { isValidIdentityId } from '../validators/common';
1719
*/
1820
let hasShownValidationError = false;
1921

22+
/**
23+
* Show one-time validation error notification to user.
24+
*
25+
* Extracted from getIdentitiesWithValidation() to reduce cognitive complexity.
26+
* Sets flag immediately to prevent concurrent notifications from race conditions.
27+
*
28+
* @param invalidCount - Number of invalid identity configurations
29+
*/
30+
function showValidationErrorNotification(invalidCount: number): void {
31+
const vs = getVSCode();
32+
if (!vs || hasShownValidationError) {
33+
return;
34+
}
35+
36+
// Set flag immediately to prevent concurrent notifications
37+
hasShownValidationError = true;
38+
39+
const message = invalidCount === 1
40+
? vs.l10n.t(
41+
'Git ID Switcher: 1 identity configuration is invalid and was skipped. Check the settings.'
42+
)
43+
: vs.l10n.t(
44+
'Git ID Switcher: {0} identity configurations are invalid and were skipped. Check the settings.',
45+
invalidCount
46+
);
47+
48+
// Fire and forget: notification is non-blocking
49+
// Note: VS Code's Thenable doesn't have .catch(), so we use .then(onFulfilled, onRejected)
50+
vs.window.showWarningMessage(message, vs.l10n.t('Open Settings'))
51+
.then(
52+
action => {
53+
if (action) {
54+
vs.commands.executeCommand(
55+
'workbench.action.openSettings',
56+
'gitIdSwitcher.identities'
57+
);
58+
}
59+
},
60+
() => {
61+
// SECURITY: Don't let notification errors affect validation flow
62+
}
63+
);
64+
}
65+
2066
export interface Identity {
2167
/** Unique identifier (lowercase, no spaces) */
2268
id: string;
@@ -53,7 +99,11 @@ export interface Identity {
5399
* Use getIdentitiesWithValidation() for security-critical code paths.
54100
*/
55101
export function getIdentities(): Identity[] {
56-
const config = vscode.workspace.getConfiguration('gitIdSwitcher');
102+
const vs = getVSCode();
103+
if (!vs) {
104+
return [];
105+
}
106+
const config = vs.workspace.getConfiguration('gitIdSwitcher');
57107
const identities = config.get<Identity[]>('identities', []);
58108
return identities;
59109
}
@@ -67,7 +117,12 @@ export function getIdentities(): Identity[] {
67117
* @returns Array of valid identities (invalid ones are filtered out)
68118
*/
69119
export function getIdentitiesWithValidation(): Identity[] {
70-
const config = vscode.workspace.getConfiguration('gitIdSwitcher');
120+
const vs = getVSCode();
121+
if (!vs) {
122+
return [];
123+
}
124+
125+
const config = vs.workspace.getConfiguration('gitIdSwitcher');
71126
const rawIdentities = config.get<unknown[]>('identities', []);
72127

73128
// Type check: must be an array
@@ -141,39 +196,8 @@ export function getIdentitiesWithValidation(): Identity[] {
141196
}
142197

143198
// Show one-time notification if there are invalid identities
144-
// SECURITY: Set flag before async operation to prevent race conditions
145-
// Multiple concurrent validations won't trigger duplicate notifications
146-
if (invalidIndices.length > 0 && !hasShownValidationError) {
147-
// Set flag immediately to prevent concurrent notifications
148-
hasShownValidationError = true;
149-
150-
const message = invalidIndices.length === 1
151-
? vscode.l10n.t(
152-
'Git ID Switcher: 1 identity configuration is invalid and was skipped. Check the settings.'
153-
)
154-
: vscode.l10n.t(
155-
'Git ID Switcher: {0} identity configurations are invalid and were skipped. Check the settings.',
156-
invalidIndices.length
157-
);
158-
159-
// Fire and forget: notification is non-blocking
160-
// User can dismiss or open settings as needed
161-
// Note: VS Code's Thenable doesn't have .catch(), so we use .then(onFulfilled, onRejected)
162-
vscode.window.showWarningMessage(message, vscode.l10n.t('Open Settings'))
163-
.then(
164-
action => {
165-
if (action) {
166-
vscode.commands.executeCommand(
167-
'workbench.action.openSettings',
168-
'gitIdSwitcher.identities'
169-
);
170-
}
171-
},
172-
() => {
173-
// SECURITY: Don't let notification errors affect validation flow
174-
// Silently ignore notification errors
175-
}
176-
);
199+
if (invalidIndices.length > 0) {
200+
showValidationErrorNotification(invalidIndices.length);
177201
}
178202

179203
return validIdentities;
@@ -201,7 +225,12 @@ export function getIdentityById(id: string): Identity | undefined {
201225
* Uses validated identities to ensure only valid identities are returned.
202226
*/
203227
export function getDefaultIdentity(): Identity | undefined {
204-
const config = vscode.workspace.getConfiguration('gitIdSwitcher');
228+
const vs = getVSCode();
229+
if (!vs) {
230+
return undefined;
231+
}
232+
233+
const config = vs.workspace.getConfiguration('gitIdSwitcher');
205234
const defaultId = config.get<string>('defaultIdentity', '');
206235

207236
const identities = getIdentitiesWithValidation();
@@ -263,17 +292,23 @@ export function formatGitAuthor(identity: Identity): string {
263292
* @throws Error if identity ID format is invalid
264293
* @throws Error if identity with given ID is not found
265294
* @throws Error if configuration update fails
295+
* @throws Error if VS Code API is not available
266296
*
267297
* @example
268298
* await deleteIdentityFromConfig('work-github');
269299
*/
270300
export async function deleteIdentityFromConfig(id: string): Promise<void> {
301+
const vs = getVSCode();
302+
if (!vs) {
303+
throw new Error('VS Code API not available');
304+
}
305+
271306
// Validate ID format
272307
if (!isValidIdentityId(id, MAX_ID_LENGTH)) {
273308
throw new Error(`Invalid identity ID format: ${id}`);
274309
}
275310

276-
const config = vscode.workspace.getConfiguration('gitIdSwitcher');
311+
const config = vs.workspace.getConfiguration('gitIdSwitcher');
277312
const identities = config.get<Identity[]>('identities', []);
278313

279314
// Find the identity to delete
@@ -286,7 +321,7 @@ export async function deleteIdentityFromConfig(id: string): Promise<void> {
286321
const updatedIdentities = identities.filter(i => i.id !== id);
287322

288323
// Update configuration
289-
await config.update('identities', updatedIdentities, vscode.ConfigurationTarget.Global);
324+
await config.update('identities', updatedIdentities, vs.ConfigurationTarget.Global);
290325

291326
// Log security event
292327
securityLogger.logConfigChange('identities');

0 commit comments

Comments
 (0)