fix(proximity): decode quoted paths with precise line ranges

This commit is contained in:
Huanyi Xie
2026-10-06 21:05:58 +03:00
parent 52cc44f40a
commit f7d7532d65
2 changed files with 76 additions and 7 deletions
+32 -5
View File
@@ -15,6 +15,34 @@ const { execFileSync } = require('child_process');
const { scanAirspace, buildProximityTriggers } = require('../agent-proximity');
const { buildDependencyGraph } = require('../agent-proximity/graph');
/** Decode Git C-style path escapes as bytes before UTF-8 decoding. */
function decodeGitPath(header) {
const value = header.replace(/\t$/, '');
if (!value.startsWith('"')) return value;
if (!value.endsWith('"')) return null;
const parts = [];
let literal = '';
const escaped = { a: 7, b: 8, t: 9, n: 10, v: 11, f: 12, r: 13, '"': 34, '\\': 92 };
for (let i = 1; i < value.length - 1; i += 1) {
const character = value[i];
if (character !== '\\') { literal += character; continue; }
if (literal) { parts.push(Buffer.from(literal, 'utf8')); literal = ''; }
const next = value[++i];
if (/[0-7]/.test(next || '')) {
let octal = next;
while (octal.length < 3 && /[0-7]/.test(value[i + 1] || '')) octal += value[++i];
const byte = parseInt(octal, 8);
if (byte > 255) return null;
parts.push(Buffer.from([byte]));
} else {
if (!Object.prototype.hasOwnProperty.call(escaped, next)) return null;
parts.push(Buffer.from([escaped[next]]));
}
}
if (literal) parts.push(Buffer.from(literal, 'utf8'));
return Buffer.concat(parts).toString('utf8');
}
/**
* Parse `git diff --unified=0` output into per-file NEW-side line ranges. Hunk
* headers look like `@@ -a,b +c,d @@`; we keep the +c,d (new) side so the overlap
@@ -32,10 +60,9 @@ function parseDiffRanges(diff) {
current = null;
continue;
}
const fileMatch = line.match(/^\+\+\+ b\/(.+)$/);
if (fileMatch) {
const name = fileMatch[1].replace(/\t$/, '');
current = name === '/dev/null' ? null : name;
if (line.startsWith('+++ ')) {
const gitPath = decodeGitPath(line.slice(4));
current = gitPath && gitPath.startsWith('b/') ? gitPath.slice(2) : null;
if (current && !byFile.has(current)) byFile.set(current, []);
continue;
}
@@ -60,7 +87,7 @@ function runGit(worktreePath, args) {
function runGitDiff(worktreePath, base, extraArgs) {
const mergeBase = runGit(worktreePath, ['merge-base', '--', base, 'HEAD']).trim();
return runGit(worktreePath, ['diff', ...extraArgs, mergeBase, '--']);
return runGit(worktreePath, ['-c', 'core.quotePath=false', 'diff', ...extraArgs, mergeBase, '--']);
}
function untrackedFiles(worktreePath) {
+44 -2
View File
@@ -148,7 +148,7 @@ test('quoted staged paths and untracked paths share identity, and untracked fail
fs.writeFileSync(path.join(repo, 'README'), 'base\n');
git(repo, 'add', '.');
git(repo, 'commit', '-qm', 'base');
const file = 'café"résumé.js';
const file = process.platform === 'win32' ? 'café-résumé.js' : 'café"résumé.js';
const worktrees = ['a', 'b'].map(name => {
const wt = path.join(dir, name);
git(repo, 'worktree', 'add', '-q', '-b', name, wt, 'main');
@@ -177,7 +177,7 @@ test('quoted staged paths and untracked paths share identity, and untracked fail
};
delete require.cache[modulePath];
const partial = require(modulePath).sessionsToAgents([worktrees[0]]);
assert.deepStrictEqual(partial[0].files.map(entry => entry.path), ['README']);
assert.deepStrictEqual(partial[0].files.map(entry => entry.path).sort(), ['README', file].sort());
}
} finally {
childProcess.execFileSync = originalExec;
@@ -241,6 +241,48 @@ test('diff headers isolate quoted/deleted hunks and preserve trailing-space path
assert.strictEqual(ranges.has('trailing'), false);
});
test('quoted Git headers decode UTF-8 octal bytes, quotes and control escapes into exact paths', () => {
const ranges = parseDiffRanges([
'diff --git "a/café.js" "b/café.js"',
'+++ "b/caf\\303\\251\\"r\\303\\251sum\\303\\251\\tname.js"', '@@ -4,2 +4,2 @@',
'diff --git a/other.js b/other.js', '+++ b/other.js', '@@ -8 +8 @@',
].join('\n'));
assert.deepStrictEqual(ranges.get('café"résumé\tname.js'), [[4, 5]]);
assert.deepStrictEqual(ranges.get('other.js'), [[8, 8]]);
});
test('real quoted filenames retain disjoint hunk ranges rather than whole-file collisions', () => {
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execFileSync } = require('child_process');
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-proximity-quoted-lines-'));
const repo = path.join(dir, 'repo');
const git = (cwd, ...args) => execFileSync('git', ['-C', cwd, ...args], { stdio: 'pipe' });
const file = process.platform === 'win32' ? 'café-résumé.js' : 'café"résumé\tname.js';
try {
fs.mkdirSync(repo);
git(repo, 'init', '-q', '-b', 'main');
git(repo, 'config', 'user.name', 'proximity-test');
git(repo, 'config', 'user.email', 'proximity-test@example.com');
const original = Array.from({ length: 40 }, (_, i) => `const line${i} = ${i};`);
fs.writeFileSync(path.join(repo, file), `${original.join('\n')}\n`);
git(repo, 'add', '.'); git(repo, 'commit', '-qm', 'base');
const sessions = ['a', 'b'].map((name, index) => {
const wt = path.join(dir, name);
git(repo, 'worktree', 'add', '-q', '-b', name, wt, 'main');
const lines = original.map((line, i) => i === index * 39 ? `${line} // edit` : line);
fs.writeFileSync(path.join(wt, file), `${lines.join('\n')}\n`);
return { id: name, worktree: { path: wt, base: 'main' } };
});
const agents = sessionsToAgents(sessions);
assert.deepStrictEqual(agents[0].files, [{ weight: 1, path: file, lines: [[1, 1]] }]);
assert.deepStrictEqual(agents[1].files, [{ weight: 1, path: file, lines: [[40, 40]] }]);
const snapshot = buildProximitySnapshot(sessions, { repoRoot: repo, graph: { adjacency: {} } });
assert.ok(!snapshot.advisories.some(advisory => advisory.level === 'resolution'));
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
});
test('line-range channel: same file but disjoint ranges ⇒ no resolution', () => {
// Two agents in the same file, far-apart functions. workingSetFor provides ranges.
const workingSetFor = s =>