zephyr-agent
Version:
Zephyr plugin agent
124 lines (123 loc) • 5.21 kB
JavaScript
import "node:module";
import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { unknownOrigin } from "./git-ai.mjs";
import { comparisonFile, loadSourceRecord } from "./source.mjs";
function originAt(file, line) {
return file?.attribution.find((range)=>range.start <= line && range.end >= line)?.origin ?? unknownOrigin;
}
function compareSourceRecords(directory, before, after) {
const left = loadSourceRecord(directory, before);
const right = loadSourceRecord(directory, after);
return compareSourceContents(left, right, directory);
}
function compareSourceContents(left, right, directory) {
const changes = [];
const temporary = mkdtempSync(join(tmpdir(), 'zephyr-source-diff-'));
try {
for (const file of [
...new Set([
...Object.keys(left.files),
...Object.keys(right.files)
])
].sort()){
if (left.files[file]?.hash === right.files[file]?.hash && left.files[file]?.mode === right.files[file]?.mode) continue;
const oldFile = directory ? comparisonFile(directory, left, file) : left.files[file];
const newFile = directory ? comparisonFile(directory, right, file) : right.files[file];
const oldContent = Buffer.from(oldFile?.content ?? '', 'base64');
const newContent = Buffer.from(newFile?.content ?? '', 'base64');
writeFileSync(join(temporary, 'before'), oldContent, {
mode: 384
});
writeFileSync(join(temporary, 'after'), newContent, {
mode: 384
});
const result = spawnSync('git', [
'diff',
'--no-index',
'--no-ext-diff',
'--no-textconv',
'--no-color',
'--unified=3',
'--',
'before',
'after'
], {
cwd: temporary,
encoding: 'utf8',
timeout: 5000,
maxBuffer: 67108864
});
if (result.error || 0 !== result.status && 1 !== result.status) throw result.error ?? new Error(result.stderr);
const lines = [];
let oldLine = 0;
let newLine = 0;
let inHunk = false;
for (const line of result.stdout.split('\n')){
const hunk = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
if (hunk) {
oldLine = Number(hunk[1]);
newLine = Number(hunk[2]);
inHunk = true;
continue;
}
if (inHunk) {
if (line.startsWith('+')) lines.push({
operation: 'add',
line: newLine++,
text: line.slice(1),
origin: originAt(newFile, newLine - 1)
});
else if (line.startsWith('-')) lines.push({
operation: 'remove',
line: oldLine++,
text: line.slice(1),
origin: originAt(oldFile, oldLine - 1)
});
else if (line.startsWith(' ')) {
oldLine++;
newLine++;
}
}
}
const patchLines = result.stdout.split('\n');
const firstHunk = patchLines.findIndex((entry)=>entry.startsWith('@@ '));
changes.push({
file,
status: oldFile ? newFile ? 'modified' : 'deleted' : 'added',
binary: oldContent.includes(0) || newContent.includes(0),
beforeMode: oldFile?.mode,
afterMode: newFile?.mode,
patch: patchLines.map((line, index)=>{
if (firstHunk >= 0 && index >= firstHunk) return line;
if (line.startsWith('diff --git ') || line.startsWith('--- ') || line.startsWith('+++ ')) {
if ('diff --git a/before b/after' === line) return `diff --git a/${file} b/${file}`;
if ('--- a/before' === line) return `--- a/${file}`;
if ('+++ b/after' === line) return `+++ b/${file}`;
}
if ('Binary files a/before and b/after differ' === line) return `Binary files a/${file} and b/${file} differ`;
return line;
}).join('\n'),
lines
});
}
} finally{
rmSync(temporary, {
recursive: true,
force: true
});
}
return {
schemaVersion: 1,
before: left.id,
after: right.id,
beforeFingerprint: left.fingerprint,
afterFingerprint: right.fingerprint,
sameCommit: left.baseCommit === right.baseCommit,
changes
};
}
export { compareSourceContents, compareSourceRecords };
//# sourceMappingURL=compare.mjs.map