GitNexus/gitnexus/test/unit/detect-changes-eol.test.ts
auyua9 a4769439dd
fix(parse): retain metadata-only diff files (#3251)
* fix(parse): retain metadata-only diff files

* Address PR review feedback (#3251)

Keep detect_changes honest on C-quoted, TAB-terminated, and ambiguous
diff --git dests, and fail closed when a header cannot be parsed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify parseDiffHunks dest-prefix helper (#3251)

Drop the unused a/ prefix arm and the redundant empty-parse unparsed-header check.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(parse): ignore +++ inside hunks and accept mixed-quoted git headers

Stop treating hunk-body lines as file headers, and parse independently
C-quoted src/dest tokens on diff --git.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: ming <silverchris@foxmail.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 18:27:00 +01:00

108 lines
4.1 KiB
TypeScript

import { execFileSync } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { buildDetectChangesDiffArgs } from '../../src/mcp/local/local-backend.js';
import { parseDiffHunks } from '../../src/storage/git.js';
import { diffArgsFor } from '../helpers/detect-changes-diff-args.js';
import { commitAll, initGitRepo } from '../helpers/temp-git-repo.js';
/** Flags every scope carries, ahead of its own ref/staging arguments. */
const GUARD_FLAGS = [
'-c',
'core.quotePath=false',
'diff',
'--ignore-cr-at-eol',
'--no-ext-diff',
'--color=never',
'--src-prefix=a/',
'--dst-prefix=b/',
];
describe('detect_changes EOL filtering', () => {
it.each([
['unstaged', undefined, [...GUARD_FLAGS, '-U0']],
['staged', undefined, [...GUARD_FLAGS, '--staged', '-U0']],
['all', undefined, [...GUARD_FLAGS, 'HEAD', '-U0']],
['compare', 'main', [...GUARD_FLAGS, 'main', '-U0']],
])('adds the EOL and prefix guards for %s scope', (scope, baseRef, expected) => {
expect(buildDetectChangesDiffArgs(scope, baseRef)).toEqual(expected);
});
it('requires a base ref for compare scope', () => {
expect(buildDetectChangesDiffArgs('compare')).toBeNull();
});
it('suppresses CRLF-only changes but retains other whitespace changes', () => {
const repoDir = mkdtempSync(path.join(tmpdir(), 'gitnexus-detect-eol-'));
try {
initGitRepo(repoDir);
writeFileSync(path.join(repoDir, 'sample.ts'), 'const first = 1;\r\nconst second = 2;\r\n');
commitAll(repoDir, 'initial');
writeFileSync(path.join(repoDir, 'sample.ts'), 'const first = 1;\nconst second = 2;\n');
const diffArgs = diffArgsFor('unstaged');
expect(
execFileSync('git', diffArgs, {
cwd: repoDir,
encoding: 'utf8',
}),
).toBe('');
writeFileSync(path.join(repoDir, 'sample.ts'), 'const first = 1;\n const second = 2;\n');
expect(
execFileSync('git', diffArgs, {
cwd: repoDir,
encoding: 'utf8',
}),
).toContain('+ const second = 2;');
} finally {
rmSync(repoDir, { recursive: true, force: true });
}
});
});
/**
* #2915 — the user's own git config could turn the pre-commit gate into a
* silent all-clear.
*
* `parseDiffHunks` recognises a file by its `+++ b/` header, and git only emits
* that prefix by default: `diff.noprefix` emits `+++ sample.py` and
* `diff.mnemonicPrefix` emits `+++ w/sample.py`. Either one parses to ZERO
* files, which `detect_changes` reported as "No changes detected." with exit 0
* and no `partial`. The flags pin the prefixes the parser matches.
*/
describe('detect_changes diff prefix pinning', () => {
it.each([
['diff.noprefix', '+++ sample.py'],
['diff.mnemonicPrefix', '+++ w/sample.py'],
])('parses the diff even with %s configured', (configKey, hostileHeader) => {
const repoDir = mkdtempSync(path.join(tmpdir(), 'gitnexus-detect-prefix-'));
try {
initGitRepo(repoDir);
writeFileSync(path.join(repoDir, 'sample.py'), 'def hello():\n return 1\n');
commitAll(repoDir, 'initial');
execFileSync('git', ['config', configKey, 'true'], { cwd: repoDir });
writeFileSync(path.join(repoDir, 'sample.py'), 'def hello():\n return 2\n');
// The config really is hostile: without the prefix flags git relabels the
// headers and the whole diff parses to nothing.
const unguarded = execFileSync('git', ['diff', '--ignore-cr-at-eol', '-U0'], {
cwd: repoDir,
encoding: 'utf8',
});
expect(unguarded).toContain(hostileHeader);
expect(parseDiffHunks(unguarded)).toEqual([]);
// Source line 2 is the edit; the hunk header is git's own 1-based space.
expect(
parseDiffHunks(
execFileSync('git', diffArgsFor('unstaged'), { cwd: repoDir, encoding: 'utf8' }),
),
).toEqual([{ filePath: 'sample.py', hunks: [{ startLine: 2, endLine: 2 }] }]);
} finally {
rmSync(repoDir, { recursive: true, force: true });
}
});
});