mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-03 02:21:44 +00:00
refactor(analyze): move detected-branch sanitization off CLI config (#3325)
* refactor(analyze): load detected-branch sanitization from core git-ref Keep the never-throw helper next to validateBranchName so run-analyze no longer imports CLI config parsing. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(analyze): warn once when a checkout name cannot label the index After the write lock settles, emit a single onLog warning and keep writing the workspace slot. Pin that run-analyze does not import CLI analyze-config. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(analyze): escape hidden checkout names in the detect-reject warning Keep the rejected ref visible in onLog without replaying bidi or quote characters, and document that sanitizeDetectedBranch rethrows unexpected errors. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(analyze): keep detect-reject warnings on one line (#3325) Git-legal U+2028/U+2029 checkout names were rejected as whitespace but left raw in the new onLog warning, so the message split across two lines. Escape those code points in the formatter without changing validateBranchName. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(autofix): apply prettier + eslint fixes via /autofix command * fix(analyze): keep C1 and Unicode spaces in detect-reject warnings Escape NEL and remaining whitespace as \uXXXX so stripControlCharacters cannot drop or disguise the rejected checkout name. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
9a68183c99
commit
9d95af9fc3
7 changed files with 377 additions and 27 deletions
|
|
@ -33,8 +33,10 @@ import path from 'node:path';
|
|||
import { readRepoControlFile } from '../config/repo-control-file.js';
|
||||
import {
|
||||
InvalidBranchError,
|
||||
sanitizeDetectedBranch,
|
||||
validateBranchName as validateBranchNameCore,
|
||||
} from '../core/git-ref.js';
|
||||
export { sanitizeDetectedBranch };
|
||||
import type { AnalyzeOptions } from './analyze-options.js';
|
||||
|
||||
export const GITNEXUS_RC_FILENAME = '.gitnexusrc';
|
||||
|
|
@ -172,20 +174,6 @@ export function validateBranchName(value: string, source: string): string {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort validation for an auto-detected branch (from git). Never throws —
|
||||
* returns `undefined` for anything unusable so the resolver falls back to the
|
||||
* next precedence tier.
|
||||
*/
|
||||
export function sanitizeDetectedBranch(value: string | null | undefined): string | undefined {
|
||||
if (!value) return undefined;
|
||||
try {
|
||||
return validateBranchName(value, 'detected branch');
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeValue = (kind: ValueKind, value: unknown, key: string): unknown => {
|
||||
const source = `${GITNEXUS_RC_FILENAME} "${key}"`;
|
||||
switch (kind) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
/**
|
||||
* Git ref-name validation used by both the CLI and the HTTP analyze route.
|
||||
*
|
||||
* Lives in `core/` so `server/api.ts` does not import `cli/analyze-config`
|
||||
* (that import closed a cli → server → cli cycle: `cli/serve.ts` already
|
||||
* imports `createServer`). The CLI keeps a thin wrapper that rethrows
|
||||
* {@link InvalidBranchError} as `GitNexusRcError`.
|
||||
* Lives in `core/` so `server/api.ts` and `run-analyze.ts` do not import
|
||||
* `cli/analyze-config` (that import closed a cli → server → cli cycle:
|
||||
* `cli/serve.ts` already imports `createServer`). The CLI keeps a thin
|
||||
* wrapper that rethrows {@link InvalidBranchError} as `GitNexusRcError`.
|
||||
*/
|
||||
|
||||
/** Git refs longer than this are almost certainly a mistake / injection attempt. */
|
||||
|
|
@ -126,3 +126,49 @@ export function validateBranchName(value: string, source: string): string {
|
|||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort validation for an auto-detected branch (from git). Returns the
|
||||
* trimmed name, or `undefined` for anything unusable so callers fall back to
|
||||
* the next precedence tier or leave the index unlabeled. Swallows
|
||||
* {@link InvalidBranchError} only; unexpected errors are rethrown.
|
||||
*/
|
||||
export function sanitizeDetectedBranch(value: string | null | undefined): string | undefined {
|
||||
if (!value) return undefined;
|
||||
try {
|
||||
return validateBranchName(value, 'detected branch');
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidBranchError) return undefined;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a rejected checkout name for `onLog`. Hidden / bidi / control code
|
||||
* points become `\uXXXX` so a git-legal U+202E name cannot reverse the
|
||||
* warning in a terminal. C1 controls (U+0080–U+009F, including NEL U+0085)
|
||||
* and remaining Unicode whitespace (`/\s/` — NBSP, U+2028/U+2029, ideographic
|
||||
* space, etc.) are not all in {@link isHiddenOrControl}; escape them the same
|
||||
* way so the ASCII escape survives `stripControlCharacters` and the warning
|
||||
* stays one line. ASCII `"` is escaped; other characters (including backticks)
|
||||
* stay visible.
|
||||
*/
|
||||
export function formatRejectedBranchForLog(value: string): string {
|
||||
const shouldEscapeRejectedBranchChar = (cp: number, ch: string): boolean =>
|
||||
isHiddenOrControl(cp) || (cp >= 0x80 && cp <= 0x9f) || /\s/.test(ch);
|
||||
|
||||
let out = '';
|
||||
for (const ch of value) {
|
||||
const cp = ch.codePointAt(0);
|
||||
if (cp !== undefined && shouldEscapeRejectedBranchChar(cp, ch)) {
|
||||
out += `\\u${cp.toString(16).padStart(4, '0')}`;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
out += '\\"';
|
||||
continue;
|
||||
}
|
||||
out += ch;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ import {
|
|||
type CachedEmbeddingsSnapshot,
|
||||
} from './embeddings/embedding-restore-spill.js';
|
||||
import { generateAIContextFiles } from '../cli/ai-context.js';
|
||||
import { sanitizeDetectedBranch } from '../cli/analyze-config.js';
|
||||
import { formatRejectedBranchForLog, sanitizeDetectedBranch } from './git-ref.js';
|
||||
import {
|
||||
EMBEDDING_TABLE_NAME,
|
||||
EMBEDDING_DIMS,
|
||||
|
|
@ -1052,12 +1052,16 @@ export const pdgModeMismatch = (recorded: RepoMeta['pdg'], options: PdgOptions):
|
|||
* directory (#2658). `metaDir` — not `getStoragePaths(repoPath, options.branch)`
|
||||
* — is the lock scope: a `--branch X` that owns the flat slot resolves to the
|
||||
* flat `.gitnexus`, so scoping off the raw option would lock the wrong dir.
|
||||
* `rejectedDetectedBranch` is log-only (the detect-reject warning after lock
|
||||
* settle); it does not change placement.
|
||||
*/
|
||||
interface WriteTarget {
|
||||
storagePath: string;
|
||||
repoHasGit: boolean;
|
||||
currentCommit: string;
|
||||
checkedOutBranch: string | null;
|
||||
/** Raw checkout name when git returned one the branch-name rules reject. */
|
||||
rejectedDetectedBranch: string | null;
|
||||
branchLabel: string | null;
|
||||
placement: { branch?: string };
|
||||
lbugPath: string;
|
||||
|
|
@ -1089,9 +1093,12 @@ async function resolveWriteTarget(repoPath: string, options: AnalyzeOptions): Pr
|
|||
// validated (#2106 R1): a git ref the branch-name rules forbid becomes `null`
|
||||
// → the flat slot, matching that a later `--branch <that-ref>` query would
|
||||
// also be rejected. A normal ref round-trips index-time/query-time labels.
|
||||
const checkedOutBranch = repoHasGit
|
||||
? (sanitizeDetectedBranch(getCurrentBranch(repoPath)) ?? null)
|
||||
: null;
|
||||
// Keep the raw rejected name so `runFullAnalysis` can warn once after the
|
||||
// lock settles. Detached / non-git / empty detect stay `null` here and silent.
|
||||
const rawDetectedBranch = repoHasGit ? getCurrentBranch(repoPath) : null;
|
||||
const checkedOutBranch = sanitizeDetectedBranch(rawDetectedBranch) ?? null;
|
||||
const rejectedDetectedBranch =
|
||||
rawDetectedBranch != null && checkedOutBranch === null ? rawDetectedBranch : null;
|
||||
// Analyze indexes the working tree, not an arbitrary ref. An explicit
|
||||
// `--branch X` while a DIFFERENT branch Y is checked out would write Y's
|
||||
// content into X's slot, corrupting X (#2106). Refuse the mismatch. Detached
|
||||
|
|
@ -1112,6 +1119,7 @@ async function resolveWriteTarget(repoPath: string, options: AnalyzeOptions): Pr
|
|||
repoHasGit,
|
||||
currentCommit,
|
||||
checkedOutBranch,
|
||||
rejectedDetectedBranch,
|
||||
branchLabel,
|
||||
placement,
|
||||
lbugPath,
|
||||
|
|
@ -1222,6 +1230,11 @@ export async function runFullAnalysis(
|
|||
);
|
||||
}
|
||||
}
|
||||
if (writeTarget.rejectedDetectedBranch) {
|
||||
log(
|
||||
`Warning: checkout "${formatRejectedBranchForLog(writeTarget.rejectedDetectedBranch)}" is not a usable index label; continuing.`,
|
||||
);
|
||||
}
|
||||
return await runFullAnalysisInner(
|
||||
repoPath,
|
||||
options,
|
||||
|
|
|
|||
6
gitnexus/test/helpers/detect-reject-warning.ts
Normal file
6
gitnexus/test/helpers/detect-reject-warning.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Matches the detect-reject warning emitted by `run-analyze` after lock settle.
|
||||
* Shared so unit and integration tests keep one regex.
|
||||
*/
|
||||
export const isDetectRejectWarning = (message: string): boolean =>
|
||||
/^Warning:.*not a usable index label.*continuing\.$/.test(message);
|
||||
|
|
@ -5,6 +5,7 @@ import path from 'path';
|
|||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { getStoragePaths, loadMeta, listRegisteredRepos } from '../../src/storage/repo-manager.js';
|
||||
import { createTempDir } from '../helpers/test-db.js';
|
||||
import { isDetectRejectWarning } from '../helpers/detect-reject-warning.js';
|
||||
|
||||
/**
|
||||
* #2106/#2354 — branch handling end-to-end. Proves that a plain analyze
|
||||
|
|
@ -220,7 +221,12 @@ describe('multi-branch analyze (#2106)', () => {
|
|||
execFileSync('git', ['branch', '-M', 'feat`x'], { cwd: repo, stdio: 'pipe' });
|
||||
|
||||
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
|
||||
await runFullAnalysis(repo, {}, { onProgress: () => {} });
|
||||
const logs: string[] = [];
|
||||
await runFullAnalysis(
|
||||
repo,
|
||||
{},
|
||||
{ onProgress: () => {}, onLog: (message) => logs.push(message) },
|
||||
);
|
||||
|
||||
// The forbidden ref was normalized to null → flat slot, no branch field,
|
||||
// and no branches/ sub-directory created for an unqueryable slug.
|
||||
|
|
@ -228,6 +234,9 @@ describe('multi-branch analyze (#2106)', () => {
|
|||
expect(existsSync(flat.lbugPath)).toBe(true);
|
||||
expect((await loadMeta(flat.storagePath))?.branch).toBeUndefined();
|
||||
expect(existsSync(path.join(flat.storagePath, 'branches'))).toBe(false);
|
||||
const warnings = logs.filter(isDetectRejectWarning);
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0]).toContain('feat`x');
|
||||
} finally {
|
||||
await tmp.cleanup();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { InvalidBranchError, validateBranchName } from '../../src/core/git-ref.js';
|
||||
import {
|
||||
InvalidBranchError,
|
||||
formatRejectedBranchForLog,
|
||||
sanitizeDetectedBranch,
|
||||
validateBranchName,
|
||||
} from '../../src/core/git-ref.js';
|
||||
|
||||
describe('core/git-ref', () => {
|
||||
it('throws InvalidBranchError with name "InvalidBranchError"', () => {
|
||||
|
|
@ -12,4 +17,46 @@ describe('core/git-ref', () => {
|
|||
expect((err as Error).name).toBe('InvalidBranchError');
|
||||
}
|
||||
});
|
||||
|
||||
it('sanitizeDetectedBranch returns the trimmed name for a legal branch', () => {
|
||||
expect(sanitizeDetectedBranch('develop')).toBe('develop');
|
||||
expect(sanitizeDetectedBranch(' feature/foo-bar ')).toBe('feature/foo-bar');
|
||||
});
|
||||
|
||||
it('sanitizeDetectedBranch returns undefined for null, empty, or whitespace', () => {
|
||||
expect(sanitizeDetectedBranch(null)).toBeUndefined();
|
||||
expect(sanitizeDetectedBranch(undefined)).toBeUndefined();
|
||||
expect(sanitizeDetectedBranch('')).toBeUndefined();
|
||||
expect(sanitizeDetectedBranch(' ')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sanitizeDetectedBranch swallows InvalidBranchError and does not throw', () => {
|
||||
expect(sanitizeDetectedBranch('feat`x')).toBeUndefined();
|
||||
expect(sanitizeDetectedBranch('main`evil')).toBeUndefined();
|
||||
expect(sanitizeDetectedBranch('HEAD')).toBeUndefined();
|
||||
expect(() => sanitizeDetectedBranch('feat`x')).not.toThrow();
|
||||
});
|
||||
|
||||
it('formatRejectedBranchForLog keeps backticks visible and escapes bidi, quotes, and line separators', () => {
|
||||
expect(formatRejectedBranchForLog('feat`x')).toBe('feat`x');
|
||||
expect(formatRejectedBranchForLog('a"b')).toBe('a\\"b');
|
||||
expect(formatRejectedBranchForLog(`ok${'\u202e'}bad`)).toBe('ok\\u202ebad');
|
||||
expect(formatRejectedBranchForLog(`zw${'\u200b'}sp`)).toBe('zw\\u200bsp');
|
||||
expect(formatRejectedBranchForLog(`foo${'\u2028'}bar`)).toBe('foo\\u2028bar');
|
||||
expect(formatRejectedBranchForLog(`foo${'\u2029'}bar`)).toBe('foo\\u2029bar');
|
||||
});
|
||||
|
||||
it('formatRejectedBranchForLog escapes NBSP, NEL, and other C1 as \\uXXXX', () => {
|
||||
expect(formatRejectedBranchForLog(`foo${'\u00a0'}bar`)).toBe('foo\\u00a0bar');
|
||||
const nelBacktick = formatRejectedBranchForLog(`feat${'\u0085'}\``);
|
||||
expect(nelBacktick).toContain('\\u0085');
|
||||
expect(nelBacktick).toContain('`');
|
||||
expect(nelBacktick).not.toContain('\u0085');
|
||||
expect(formatRejectedBranchForLog(`x${'\u009f'}y`)).toBe('x\\u009fy');
|
||||
});
|
||||
|
||||
it('sanitizeDetectedBranch rejects git-legal U+2028/U+2029 as whitespace', () => {
|
||||
expect(sanitizeDetectedBranch(`foo${'\u2028'}bar`)).toBeUndefined();
|
||||
expect(sanitizeDetectedBranch(`foo${'\u2029'}bar`)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { execSync } from 'child_process';
|
||||
import { execFileSync, execSync } from 'child_process';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { pathToFileURL } from 'url';
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
import { SCHEMA_FINGERPRINT } from '../../src/core/lbug/schema.js';
|
||||
import { taintModelVersion } from '../../src/core/ingestion/taint/typescript-model.js';
|
||||
import { createTempDir } from '../helpers/test-db.js';
|
||||
import { isDetectRejectWarning } from '../helpers/detect-reject-warning.js';
|
||||
import { readEmbeddingNodeIds } from '../helpers/embedding-seed.js';
|
||||
import { getIndexIncompleteReasons } from '../../src/core/index-freshness.js';
|
||||
import { CLASS_FRAMEWORK_ANNOTATIONS_FEATURE } from '../../src/core/analysis-features.js';
|
||||
|
|
@ -40,6 +41,14 @@ describe('run-analyze module', () => {
|
|||
expect(typeof mod.runFullAnalysis).toBe('function');
|
||||
});
|
||||
|
||||
it('does not import cli/analyze-config', async () => {
|
||||
const source = await fs.readFile(
|
||||
path.resolve(__dirname, '../../src/core/run-analyze.ts'),
|
||||
'utf8',
|
||||
);
|
||||
expect(source).not.toMatch(/cli\/analyze-config/);
|
||||
});
|
||||
|
||||
it('exports PHASE_LABELS', async () => {
|
||||
const mod = await import('../../src/core/run-analyze.js');
|
||||
expect(mod.PHASE_LABELS).toBeDefined();
|
||||
|
|
@ -1015,18 +1024,26 @@ describe('run-analyze module', () => {
|
|||
|
||||
// Detached HEAD → branchLabel is null → the restamp block must not
|
||||
// fire: the existing stamp survives, mirroring the end-of-run write.
|
||||
// Null detect (detached / getCurrentBranch null / non-git) must stay
|
||||
// silent — it is not a rejected checkout name.
|
||||
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
|
||||
const result = await runFullAnalysis(tmpRepo.dbPath, {}, { onProgress: () => {} });
|
||||
const logs: string[] = [];
|
||||
const result = await runFullAnalysis(
|
||||
tmpRepo.dbPath,
|
||||
{},
|
||||
{ onProgress: () => {}, onLog: (message) => logs.push(message) },
|
||||
);
|
||||
expect(result.alreadyUpToDate).toBe(true);
|
||||
const flatMeta = await loadMeta(flat.storagePath);
|
||||
expect(flatMeta?.branch).toBe('main');
|
||||
expect(logs.filter(isDetectRejectWarning)).toEqual([]);
|
||||
} finally {
|
||||
if (savedHome === undefined) delete process.env.GITNEXUS_HOME;
|
||||
else process.env.GITNEXUS_HOME = savedHome;
|
||||
await tmpHome.cleanup();
|
||||
await tmpRepo.cleanup();
|
||||
}
|
||||
});
|
||||
}, 180_000);
|
||||
|
||||
it('reports isPrimaryBranch false for an up-to-date explicit --branch run (#2106 R2)', async () => {
|
||||
const tmpRepo = await createTempDir('gitnexus-run-analyze-nonprimary-');
|
||||
|
|
@ -1102,6 +1119,230 @@ describe('run-analyze module', () => {
|
|||
await tmpRepo.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it('warns once when the checkout name is not a usable index label', async () => {
|
||||
const tmpRepo = await createTempDir('gitnexus-run-analyze-detect-reject-');
|
||||
try {
|
||||
execSync('git init', { cwd: tmpRepo.dbPath, stdio: 'pipe' });
|
||||
execSync('git -c user.name=test -c user.email=test@test commit --allow-empty -m init', {
|
||||
cwd: tmpRepo.dbPath,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
execFileSync('git', ['branch', '-M', 'feat`x'], { cwd: tmpRepo.dbPath, stdio: 'pipe' });
|
||||
const currentCommit = execSync('git rev-parse HEAD', {
|
||||
cwd: tmpRepo.dbPath,
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
const { storagePath } = getStoragePaths(tmpRepo.dbPath);
|
||||
await saveMeta(storagePath, {
|
||||
repoPath: tmpRepo.dbPath,
|
||||
lastCommit: currentCommit,
|
||||
indexedAt: new Date().toISOString(),
|
||||
schemaFingerprint: SCHEMA_FINGERPRINT,
|
||||
analysisFeatures: CURRENT_ANALYSIS_FEATURES,
|
||||
runnerIdentity: currentRunnerIdentity(),
|
||||
});
|
||||
|
||||
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
|
||||
const logs: string[] = [];
|
||||
const result = await runFullAnalysis(
|
||||
tmpRepo.dbPath,
|
||||
{},
|
||||
{ onProgress: () => {}, onLog: (message) => logs.push(message) },
|
||||
);
|
||||
|
||||
expect(result.alreadyUpToDate).toBe(true);
|
||||
expect((await loadMeta(storagePath))?.branch).toBeUndefined();
|
||||
await expect(fs.access(path.join(storagePath, 'branches'))).rejects.toThrow();
|
||||
const warnings = logs.filter(isDetectRejectWarning);
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0]).toContain('feat`x');
|
||||
expect(warnings[0]).toMatch(/^Warning:.*continuing\.$/);
|
||||
expect(warnings[0]).not.toMatch(/stamp|cleared|unlabeled/i);
|
||||
} finally {
|
||||
await tmpRepo.cleanup();
|
||||
}
|
||||
}, 180_000);
|
||||
|
||||
it('applies an explicit --branch on a rejected checkout and still warns', async () => {
|
||||
const tmpRepo = await createTempDir('gitnexus-run-analyze-detect-reject-pin-');
|
||||
try {
|
||||
execSync('git init', { cwd: tmpRepo.dbPath, stdio: 'pipe' });
|
||||
execSync('git -c user.name=test -c user.email=test@test commit --allow-empty -m init', {
|
||||
cwd: tmpRepo.dbPath,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
execFileSync('git', ['branch', '-M', 'feat`x'], { cwd: tmpRepo.dbPath, stdio: 'pipe' });
|
||||
const currentCommit = execSync('git rev-parse HEAD', {
|
||||
cwd: tmpRepo.dbPath,
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
const { storagePath } = getStoragePaths(tmpRepo.dbPath);
|
||||
await saveMeta(storagePath, {
|
||||
repoPath: tmpRepo.dbPath,
|
||||
lastCommit: currentCommit,
|
||||
indexedAt: new Date().toISOString(),
|
||||
schemaFingerprint: SCHEMA_FINGERPRINT,
|
||||
analysisFeatures: CURRENT_ANALYSIS_FEATURES,
|
||||
runnerIdentity: currentRunnerIdentity(),
|
||||
});
|
||||
|
||||
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
|
||||
const logs: string[] = [];
|
||||
const result = await runFullAnalysis(
|
||||
tmpRepo.dbPath,
|
||||
{ branch: 'main' },
|
||||
{ onProgress: () => {}, onLog: (message) => logs.push(message) },
|
||||
);
|
||||
|
||||
expect(result.alreadyUpToDate).toBe(true);
|
||||
expect((await loadMeta(storagePath))?.branch).toBe('main');
|
||||
const warnings = logs.filter(isDetectRejectWarning);
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0]).toContain('feat`x');
|
||||
} finally {
|
||||
await tmpRepo.cleanup();
|
||||
}
|
||||
}, 180_000);
|
||||
|
||||
it('keeps the detect-reject warning on one line for U+2028 checkout names', async () => {
|
||||
const tmpRepo = await createTempDir('gitnexus-run-analyze-detect-reject-ls-');
|
||||
try {
|
||||
execSync('git init', { cwd: tmpRepo.dbPath, stdio: 'pipe' });
|
||||
execSync('git -c user.name=test -c user.email=test@test commit --allow-empty -m init', {
|
||||
cwd: tmpRepo.dbPath,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
execFileSync('git', ['branch', '-M', 'foo\u2028bar'], {
|
||||
cwd: tmpRepo.dbPath,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
const currentCommit = execSync('git rev-parse HEAD', {
|
||||
cwd: tmpRepo.dbPath,
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
const { storagePath } = getStoragePaths(tmpRepo.dbPath);
|
||||
await saveMeta(storagePath, {
|
||||
repoPath: tmpRepo.dbPath,
|
||||
lastCommit: currentCommit,
|
||||
indexedAt: new Date().toISOString(),
|
||||
schemaFingerprint: SCHEMA_FINGERPRINT,
|
||||
analysisFeatures: CURRENT_ANALYSIS_FEATURES,
|
||||
runnerIdentity: currentRunnerIdentity(),
|
||||
});
|
||||
|
||||
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
|
||||
const logs: string[] = [];
|
||||
const result = await runFullAnalysis(
|
||||
tmpRepo.dbPath,
|
||||
{},
|
||||
{ onProgress: () => {}, onLog: (message) => logs.push(message) },
|
||||
);
|
||||
|
||||
expect(result.alreadyUpToDate).toBe(true);
|
||||
const warnings = logs.filter(isDetectRejectWarning);
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0]).toContain('\\u2028');
|
||||
expect(warnings[0]).not.toContain('\u2028');
|
||||
expect(warnings[0].split(/\n|\r|\u2028|\u2029/)).toHaveLength(1);
|
||||
expect(warnings[0]).toMatch(/^Warning:.*continuing\.$/);
|
||||
} finally {
|
||||
await tmpRepo.cleanup();
|
||||
}
|
||||
}, 180_000);
|
||||
|
||||
it('keeps the detect-reject warning on one line for NEL+backtick checkout names', async () => {
|
||||
const tmpRepo = await createTempDir('gitnexus-run-analyze-detect-reject-nel-');
|
||||
try {
|
||||
execSync('git init', { cwd: tmpRepo.dbPath, stdio: 'pipe' });
|
||||
execSync('git -c user.name=test -c user.email=test@test commit --allow-empty -m init', {
|
||||
cwd: tmpRepo.dbPath,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
execFileSync('git', ['branch', '-M', 'feat\u0085`'], {
|
||||
cwd: tmpRepo.dbPath,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
const currentCommit = execSync('git rev-parse HEAD', {
|
||||
cwd: tmpRepo.dbPath,
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
const { storagePath } = getStoragePaths(tmpRepo.dbPath);
|
||||
await saveMeta(storagePath, {
|
||||
repoPath: tmpRepo.dbPath,
|
||||
lastCommit: currentCommit,
|
||||
indexedAt: new Date().toISOString(),
|
||||
schemaFingerprint: SCHEMA_FINGERPRINT,
|
||||
analysisFeatures: CURRENT_ANALYSIS_FEATURES,
|
||||
runnerIdentity: currentRunnerIdentity(),
|
||||
});
|
||||
|
||||
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
|
||||
const logs: string[] = [];
|
||||
const result = await runFullAnalysis(
|
||||
tmpRepo.dbPath,
|
||||
{},
|
||||
{ onProgress: () => {}, onLog: (message) => logs.push(message) },
|
||||
);
|
||||
|
||||
expect(result.alreadyUpToDate).toBe(true);
|
||||
const warnings = logs.filter(isDetectRejectWarning);
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0]).toContain('\\u0085');
|
||||
expect(warnings[0]).not.toContain('\u0085');
|
||||
expect(warnings[0]).toContain('`');
|
||||
expect(warnings[0].split(/\n|\r|\u2028|\u2029/)).toHaveLength(1);
|
||||
expect(warnings[0]).toMatch(/^Warning:.*continuing\.$/);
|
||||
} finally {
|
||||
await tmpRepo.cleanup();
|
||||
}
|
||||
}, 180_000);
|
||||
|
||||
it('keeps the detect-reject warning on one line for NBSP checkout names', async () => {
|
||||
const tmpRepo = await createTempDir('gitnexus-run-analyze-detect-reject-nbsp-');
|
||||
try {
|
||||
execSync('git init', { cwd: tmpRepo.dbPath, stdio: 'pipe' });
|
||||
execSync('git -c user.name=test -c user.email=test@test commit --allow-empty -m init', {
|
||||
cwd: tmpRepo.dbPath,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
execFileSync('git', ['branch', '-M', 'foo\u00a0bar'], {
|
||||
cwd: tmpRepo.dbPath,
|
||||
stdio: 'pipe',
|
||||
});
|
||||
const currentCommit = execSync('git rev-parse HEAD', {
|
||||
cwd: tmpRepo.dbPath,
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
const { storagePath } = getStoragePaths(tmpRepo.dbPath);
|
||||
await saveMeta(storagePath, {
|
||||
repoPath: tmpRepo.dbPath,
|
||||
lastCommit: currentCommit,
|
||||
indexedAt: new Date().toISOString(),
|
||||
schemaFingerprint: SCHEMA_FINGERPRINT,
|
||||
analysisFeatures: CURRENT_ANALYSIS_FEATURES,
|
||||
runnerIdentity: currentRunnerIdentity(),
|
||||
});
|
||||
|
||||
const { runFullAnalysis } = await import('../../src/core/run-analyze.js');
|
||||
const logs: string[] = [];
|
||||
const result = await runFullAnalysis(
|
||||
tmpRepo.dbPath,
|
||||
{},
|
||||
{ onProgress: () => {}, onLog: (message) => logs.push(message) },
|
||||
);
|
||||
|
||||
expect(result.alreadyUpToDate).toBe(true);
|
||||
const warnings = logs.filter(isDetectRejectWarning);
|
||||
expect(warnings).toHaveLength(1);
|
||||
expect(warnings[0]).toContain('\\u00a0');
|
||||
expect(warnings[0]).not.toContain('\u00a0');
|
||||
expect(warnings[0].split(/\n|\r|\u2028|\u2029/)).toHaveLength(1);
|
||||
expect(warnings[0]).toMatch(/^Warning:.*continuing\.$/);
|
||||
} finally {
|
||||
await tmpRepo.cleanup();
|
||||
}
|
||||
}, 180_000);
|
||||
});
|
||||
|
||||
describe('collectBranchCacheKeys (#2106 R6)', () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue