Skip to content

Commit 471fdd1

Browse files
nullvariantclaude
andauthored
perf(identity): add validation cache and remove deprecated aliases (#402)
* perf(identity): add validation cache and remove deprecated aliases Cache getIdentitiesWithValidation() results in a module-level variable to avoid redundant VS Code config reads and schema validation on every call (up to MAX_IDENTITIES=1000 per invocation). Cache is invalidated on configuration change via onDidChangeConfiguration. Also removes 6 deprecated aliases (isPathSafe, isSecurePath, validateNoControlChars, validateNoInvisibleUnicode, isKeyLoaded, getGitAuthorFlag) and fixes valuesEqual() to return false (safe-side) for large objects instead of a length-based heuristic. 🖥️ IDE: [Cursor](https://cursor.sh) 🔌 Extension: [Claude Code](https://claude.ai/download) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Model-Raw: claude-opus-4-6 * fix(test): invalidate identity cache in E2E test setup/teardown The identity cache introduced in the previous commit persists across test cases. E2E tests that set up mock VS Code configurations need to also call invalidateIdentityCache() alongside _resetCache() to ensure each test starts with a fresh cache. 🖥️ IDE: [Cursor](https://cursor.sh) 🔌 Extension: [Claude Code](https://claude.ai/download) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Model-Raw: claude-opus-4-6 * fix(ci): fix printf treating checklist items as options printf interprets '- [ ] ...' as an invalid option flag because it starts with a hyphen. Use '%s ' format string to pass the checklist text as an argument instead of a format string. 🖥️ IDE: [Cursor](https://cursor.sh) 🔌 Extension: [Claude Code](https://claude.ai/download) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Model-Raw: claude-opus-4-6 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0c67caa commit 471fdd1

25 files changed

Lines changed: 146 additions & 127 deletions

.github/workflows/blaze-bot.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,13 @@ jobs:
9999
printf '`%s` → `%s`\n\n' "$BASE_VER" "$HEAD_VER"
100100
if [ "$BUMP_TYPE" = "MAJOR" ]; then
101101
printf '🔥🔥🔥 MAJOR RELEASE?! LET'"'"'S GOOOO!!! But wait... did you:\n\n'
102-
printf '- [ ] Update the CHANGELOG?\n'
103-
printf '- [ ] Check for breaking changes?\n'
104-
printf '- [ ] Run the full test suite?\n\n'
102+
printf '%s\n' '- [ ] Update the CHANGELOG?'
103+
printf '%s\n' '- [ ] Check for breaking changes?'
104+
printf '%s\n\n' '- [ ] Run the full test suite?'
105105
elif [ "$BUMP_TYPE" = "MINOR" ]; then
106-
printf '🔥 New features incoming! Make sure:\n\n'
107-
printf '- [ ] Tests pass for the new features?\n'
108-
printf '- [ ] Documentation updated?\n\n'
106+
printf '%s\n\n' '🔥 New features incoming! Make sure:'
107+
printf '%s\n' '- [ ] Tests pass for the new features?'
108+
printf '%s\n\n' '- [ ] Documentation updated?'
109109
else
110110
printf '✨ Quick patch! Nice and clean.\n\n'
111111
fi

extensions/git-id-switcher/CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.18.0] - 2026-03-30
11+
12+
### Changed
13+
14+
- **Identity validation caching**: `getIdentitiesWithValidation()` now caches validated results in a module-level variable, avoiding redundant VS Code config reads and schema validation on every call. Cache is automatically invalidated on configuration change via `onDidChangeConfiguration`
15+
- **Config change detection**: `valuesEqual()` in `ConfigChangeDetector` now returns `false` (changed) for large objects exceeding `MAX_STRINGIFY_SIZE`, instead of the previous length-based heuristic that could miss same-length but different-content objects
16+
- **Renamed** `resetValidationNotificationFlag()` to `invalidateIdentityCache()` to reflect its expanded responsibility (cache invalidation + notification flag reset)
17+
18+
### Removed
19+
20+
- **Deprecated aliases removed** (BREAKING for consumers importing these symbols directly):
21+
- `isPathSafe` — use `isShellSafePath` instead
22+
- `isSecurePath` — use `validatePathSecurity` instead
23+
- `validateNoControlChars` — use `controlCharValidator` instead
24+
- `validateNoInvisibleUnicode` — use `invisibleUnicodeValidator` instead
25+
- `isKeyLoaded` — use `checkKeyLoadedInAgent` instead
26+
- `getGitAuthorFlag()` — use `formatGitAuthor()` with template literal instead
27+
28+
### Refactored
29+
30+
- **UI module split** (via #401): Split `identityManager.ts` (1181 lines) into 4 responsibility-based modules: `identityAddForm.ts`, `identityEditFlow.ts`, `identityFormUtils.ts`, `identityFormValidation.ts`
31+
1032
## [0.17.1] - 2026-03-18
1133

1234
### Fixed

extensions/git-id-switcher/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "git-id-switcher",
33
"displayName": "%extension.displayName%",
44
"description": "%extension.description%",
5-
"version": "0.17.1",
5+
"version": "0.18.0",
66
"publisher": "nullvariant",
77
"icon": "images/icon.png",
88
"engines": {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,12 @@ export class ConfigChangeDetector {
139139
const aStr = JSON.stringify(a);
140140
const bStr = JSON.stringify(b);
141141

142-
// If strings are too large, use length-based comparison as fallback
142+
// Large objects: report as changed to guarantee config change detection
143143
if (
144144
aStr.length > LIMITS.MAX_STRINGIFY_SIZE ||
145145
bStr.length > LIMITS.MAX_STRINGIFY_SIZE
146146
) {
147-
return aStr.length === bStr.length && typeof a === typeof b;
147+
return false;
148148
}
149149

150150
return aStr === bStr;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import * as vscode from 'vscode';
8-
import { Identity, getIdentitiesWithValidation, resetValidationNotificationFlag } from '../identity/identity';
8+
import { Identity, getIdentitiesWithValidation, invalidateIdentityCache } from '../identity/identity';
99
import { createStatusBar, IdentityStatusBar } from '../ui/identityStatusBar';
1010
import { showDocumentation } from '../ui/documentationPublic';
1111
import { securityLogger } from '../security/securityLogger';
@@ -169,7 +169,7 @@ async function performTrustedInitialization(context: vscode.ExtensionContext): P
169169
}
170170
}
171171

172-
resetValidationNotificationFlag();
172+
invalidateIdentityCache();
173173
initializeState(context)
174174
.then(() => performSyncCheck())
175175
.catch(error => {

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

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
// Type-only import for TypeScript (stripped at compile time)
1515
import type * as vscodeTypes from 'vscode';
1616
import { getVSCode, getWorkspace } from './vscodeLoader';
17-
import { Identity, formatGitAuthor, getIdentitiesWithValidation } from '../identity/identity';
17+
import { Identity, getIdentitiesWithValidation } from '../identity/identity';
1818
import { gitExec, secureExec } from '../security/secureExec';
1919
import { validateIdentity } from '../identity/inputValidator';
2020
import {
@@ -290,24 +290,6 @@ export async function detectCurrentIdentity(
290290
return identities.find(i => i.email === config.userEmail);
291291
}
292292

293-
/**
294-
* Get Git author string for commit --author flag
295-
*
296-
* @security This function builds a shell-quoted `--author` flag using template literals.
297-
* The return value MUST be passed as an element of the args array to execFile(),
298-
* never concatenated into a shell command string. Shell concatenation would enable
299-
* command injection via crafted identity fields.
300-
*
301-
* @deprecated Prefer building the args array directly with `formatGitAuthor()`:
302-
* ```ts
303-
* await secureExec('git', ['commit', `--author=${formatGitAuthor(identity)}`]);
304-
* ```
305-
* This avoids the embedded double-quotes that are unnecessary when using execFile().
306-
*/
307-
export function getGitAuthorFlag(identity: Identity): string {
308-
return `--author="${formatGitAuthor(identity)}"`;
309-
}
310-
311293
/**
312294
* Verify Git is available
313295
*/

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

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ import { isUnderSshDirectory } from '../security/pathUtils';
3737
*/
3838
let hasShownValidationError = false;
3939

40+
/**
41+
* Cache for validated identities.
42+
*
43+
* Invalidated on configuration change via invalidateIdentityCache().
44+
* Avoids redundant VS Code config reads + schema validation on every call
45+
* (up to MAX_IDENTITIES=1000 validateIdentitySchema() per invocation).
46+
*/
47+
let identityCache: Identity[] | null = null;
48+
4049
/**
4150
* Show one-time validation error notification to user.
4251
*
@@ -324,6 +333,10 @@ export function getFieldMetadata(key: keyof Identity): FieldMetadata | undefined
324333
*
325334
* WARNING: This function does not validate identities.
326335
* Use getIdentitiesWithValidation() for security-critical code paths.
336+
*
337+
* NOTE: Always reads from VS Code configuration directly, bypassing the
338+
* identity cache. This is intentional for config mutation code paths
339+
* (add/delete/update/move) that need the latest raw configuration.
327340
*/
328341
export function getIdentities(): Identity[] {
329342
const vs = getVSCode();
@@ -344,6 +357,11 @@ export function getIdentities(): Identity[] {
344357
* @returns Array of valid identities (invalid ones are filtered out)
345358
*/
346359
export function getIdentitiesWithValidation(): Identity[] {
360+
// Return cached result if available (shallow copy to prevent callers from mutating cache)
361+
if (identityCache !== null) {
362+
return [...identityCache];
363+
}
364+
347365
const vs = getVSCode();
348366
if (!vs) {
349367
return [];
@@ -363,8 +381,9 @@ export function getIdentitiesWithValidation(): Identity[] {
363381
return [];
364382
}
365383

366-
// Early return for empty array (valid state)
384+
// Early return for empty array (valid state, cache to avoid repeated config reads)
367385
if (rawIdentities.length === 0) {
386+
identityCache = [];
368387
return [];
369388
}
370389

@@ -376,7 +395,8 @@ export function getIdentitiesWithValidation(): Identity[] {
376395
`Array length exceeds maximum (${MAX_IDENTITIES})`,
377396
rawIdentities.length
378397
);
379-
// Return empty array to fail securely
398+
// Cache and return empty array to fail securely (avoids repeated security log spam)
399+
identityCache = [];
380400
return [];
381401
}
382402

@@ -426,16 +446,19 @@ export function getIdentitiesWithValidation(): Identity[] {
426446
showValidationErrorNotification(invalidIndices.length);
427447
}
428448

429-
return validIdentities;
449+
identityCache = validIdentities;
450+
return [...validIdentities];
430451
}
431452

432453
/**
433-
* Reset the validation error notification flag
454+
* Invalidate the identity cache and reset the validation notification flag.
434455
*
435-
* Used when configuration changes to allow re-notification of validation errors.
456+
* Must be called on configuration change (onDidChangeConfiguration)
457+
* to force re-validation on next getIdentitiesWithValidation() call.
436458
* Also exposed for testing purposes.
437459
*/
438-
export function resetValidationNotificationFlag(): void {
460+
export function invalidateIdentityCache(): void {
461+
identityCache = null;
439462
hasShownValidationError = false;
440463
}
441464

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -310,8 +310,3 @@ export function isShellSafePath(path: string): boolean {
310310

311311
return true;
312312
}
313-
314-
/**
315-
* @deprecated Use `isShellSafePath` instead. This alias will be removed in a future version.
316-
*/
317-
export const isPathSafe = isShellSafePath;

extensions/git-id-switcher/src/security/commandAllowlist.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import { validatePathSecurity, isPathArgument } from './pathValidator';
1111
import { validateCombinedFlags } from './flagValidator';
1212

13-
// Re-export for backwards compatibility
14-
export { validatePathSecurity, isSecurePath, isPathArgument, SecurePathResult } from './pathValidator';
13+
// Re-export path security utilities
14+
export { validatePathSecurity, isPathArgument, SecurePathResult } from './pathValidator';
1515
export { validateCombinedFlags, CombinedFlagResult } from './flagValidator';
1616

1717
/**

extensions/git-id-switcher/src/security/pathUnicodeDetector.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,6 @@ export const controlCharValidator = createControlCharValidator();
7272
*/
7373
export const invisibleUnicodeValidator = createInvisibleUnicodeValidator();
7474

75-
/**
76-
* @deprecated Use `controlCharValidator` instead. This alias will be removed in a future version.
77-
*/
78-
export const validateNoControlChars = controlCharValidator;
79-
80-
/**
81-
* @deprecated Use `invisibleUnicodeValidator` instead. This alias will be removed in a future version.
82-
*/
83-
export const validateNoInvisibleUnicode = invisibleUnicodeValidator;
84-
8575
/**
8676
* Normalizes Unicode to NFC for consistent comparison
8777
*/

0 commit comments

Comments
 (0)