feat(FN-4522): complete Step 1 — fix rename/copy parsing
Fusion-Task-Id: FN-4522 Fusion-Task-Lineage: fbaedf11-6bc6-4c77-91b0-6b1ef606d81f
This commit is contained in:
@@ -1978,6 +1978,45 @@ describe("GET /tasks/:id/diff", () => {
|
|||||||
expect(res.body.stats).toHaveProperty("filesChanged");
|
expect(res.body.stats).toHaveProperty("filesChanged");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("uses destination path for rename entries in active-task name-status parsing", async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "kb-dashboard-rename-"));
|
||||||
|
try {
|
||||||
|
execFileSync("git", ["init", "--initial-branch=main", root], { stdio: "pipe" });
|
||||||
|
execFileSync("git", ["-C", root, "config", "user.email", "kb-tests@example.com"], { stdio: "pipe" });
|
||||||
|
execFileSync("git", ["-C", root, "config", "user.name", "KB Tests"], { stdio: "pipe" });
|
||||||
|
writeFileSync(join(root, "old.ts"), "export const value = 1;\n");
|
||||||
|
execFileSync("git", ["-C", root, "add", "old.ts"], { stdio: "pipe" });
|
||||||
|
execFileSync("git", ["-C", root, "commit", "-m", "add old"], { stdio: "pipe" });
|
||||||
|
const baseSha = execFileSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf-8", stdio: "pipe" }).trim();
|
||||||
|
execFileSync("git", ["-C", root, "mv", "old.ts", "new.ts"], { stdio: "pipe" });
|
||||||
|
writeFileSync(join(root, "new.ts"), "export const value = 2;\n");
|
||||||
|
|
||||||
|
const localStore = createMockStore({ getRootDir: vi.fn().mockReturnValue(root) });
|
||||||
|
(localStore.getTask as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||||
|
...FAKE_TASK_DETAIL,
|
||||||
|
id: "FN-001",
|
||||||
|
column: "in-progress",
|
||||||
|
branch: "main",
|
||||||
|
worktree: root,
|
||||||
|
baseCommitSha: baseSha,
|
||||||
|
});
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use("/api", createApiRoutes(localStore));
|
||||||
|
|
||||||
|
const res = await GET(app, "/api/tasks/FN-001/diff");
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.files.map((file: { path: string }) => file.path)).toContain("new.ts");
|
||||||
|
expect(res.body.files.map((file: { path: string }) => file.path)).not.toContain("old.ts");
|
||||||
|
expect(res.body.stats.filesChanged).toBe(1);
|
||||||
|
expect(Number.isInteger(res.body.stats.additions)).toBe(true);
|
||||||
|
expect(Number.isInteger(res.body.stats.deletions)).toBe(true);
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("GET /tasks/:id/file-diffs", () => {
|
describe("GET /tasks/:id/file-diffs", () => {
|
||||||
|
|||||||
@@ -263,17 +263,31 @@ function parseStatusCode(statusCode: string): DoneTaskFileStatus {
|
|||||||
return "modified";
|
return "modified";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a single `git diff --name-status` line.
|
||||||
|
*
|
||||||
|
* Rename/copy entries are detected by `R*`/`C*` status prefixes. Their
|
||||||
|
* destination path is `parts[2]` in normal output, with a defensive `parts[3]`
|
||||||
|
* fallback for variants that split score fields across extra tab columns.
|
||||||
|
*/
|
||||||
|
function parseNameStatusLine(line: string): { statusCode: string; path: string; oldPath?: string } | null {
|
||||||
|
const parts = line.split("\t");
|
||||||
|
const statusCode = parts[0] ?? "M";
|
||||||
|
const isRenameLike = statusCode.startsWith("R") || statusCode.startsWith("C");
|
||||||
|
const oldPath = isRenameLike ? (parts[1] ?? "") : undefined;
|
||||||
|
const path = isRenameLike ? (parts.length > 3 ? (parts[3] ?? "") : (parts[2] ?? "")) : (parts[1] ?? "");
|
||||||
|
if (!path) return null;
|
||||||
|
return oldPath ? { statusCode, path, oldPath } : { statusCode, path };
|
||||||
|
}
|
||||||
|
|
||||||
async function collectDoneRangeFiles(range: string, rootDir: string): Promise<AggregatedDoneTaskFile[]> {
|
async function collectDoneRangeFiles(range: string, rootDir: string): Promise<AggregatedDoneTaskFile[]> {
|
||||||
const nameStatus = (await runGitCommand(["diff", "--name-status", "-M", range], rootDir, 10000)).trim();
|
const nameStatus = (await runGitCommand(["diff", "--name-status", "-M", range], rootDir, 10000)).trim();
|
||||||
const files: AggregatedDoneTaskFile[] = [];
|
const files: AggregatedDoneTaskFile[] = [];
|
||||||
|
|
||||||
for (const line of nameStatus.split("\n").filter(Boolean)) {
|
for (const line of nameStatus.split("\n").filter(Boolean)) {
|
||||||
const parts = line.split("\t");
|
const parsed = parseNameStatusLine(line);
|
||||||
const statusCode = parts[0] ?? "M";
|
if (!parsed) continue;
|
||||||
const isRenameLike = statusCode.startsWith("R") || statusCode.startsWith("C");
|
const { statusCode, path: filePath, oldPath } = parsed;
|
||||||
const oldPath = isRenameLike ? (parts[1] ?? "") : undefined;
|
|
||||||
const filePath = isRenameLike ? (parts[2] ?? parts[1] ?? "") : (parts[1] ?? "");
|
|
||||||
if (!filePath) continue;
|
|
||||||
|
|
||||||
let patch = "";
|
let patch = "";
|
||||||
try {
|
try {
|
||||||
@@ -628,8 +642,9 @@ export function registerSessionDiffRoutes(router: Router, deps: SessionDiffRoute
|
|||||||
try {
|
try {
|
||||||
const committedOutput = (await runGitCommand(["diff", "--name-status", `${diffBase}..HEAD`], cwd, 10000)).trim();
|
const committedOutput = (await runGitCommand(["diff", "--name-status", `${diffBase}..HEAD`], cwd, 10000)).trim();
|
||||||
for (const line of committedOutput.split("\n").filter(Boolean)) {
|
for (const line of committedOutput.split("\n").filter(Boolean)) {
|
||||||
const parts = line.split("\t");
|
const parsed = parseNameStatusLine(line);
|
||||||
fileMap.set(parts[1] ?? "", parts[0] ?? "M");
|
if (!parsed) continue;
|
||||||
|
fileMap.set(parsed.path, parsed.statusCode);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// committed diff failed
|
// committed diff failed
|
||||||
@@ -639,11 +654,9 @@ export function registerSessionDiffRoutes(router: Router, deps: SessionDiffRoute
|
|||||||
try {
|
try {
|
||||||
const stagedOutput = (await runGitCommand(["diff", "--cached", "--name-status"], cwd, 10000)).trim();
|
const stagedOutput = (await runGitCommand(["diff", "--cached", "--name-status"], cwd, 10000)).trim();
|
||||||
for (const line of stagedOutput.split("\n").filter(Boolean)) {
|
for (const line of stagedOutput.split("\n").filter(Boolean)) {
|
||||||
const parts = line.split("\t");
|
const parsed = parseNameStatusLine(line);
|
||||||
const filePath = parts[1] ?? "";
|
if (!parsed || fileMap.has(parsed.path)) continue;
|
||||||
if (filePath && !fileMap.has(filePath)) {
|
fileMap.set(parsed.path, parsed.statusCode);
|
||||||
fileMap.set(filePath, parts[0] ?? "M");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// staged diff failed
|
// staged diff failed
|
||||||
@@ -652,11 +665,9 @@ export function registerSessionDiffRoutes(router: Router, deps: SessionDiffRoute
|
|||||||
try {
|
try {
|
||||||
const workingTreeOutput = (await runGitCommand(["diff", "--name-status"], cwd, 10000)).trim();
|
const workingTreeOutput = (await runGitCommand(["diff", "--name-status"], cwd, 10000)).trim();
|
||||||
for (const line of workingTreeOutput.split("\n").filter(Boolean)) {
|
for (const line of workingTreeOutput.split("\n").filter(Boolean)) {
|
||||||
const parts = line.split("\t");
|
const parsed = parseNameStatusLine(line);
|
||||||
const filePath = parts[1] ?? "";
|
if (!parsed || fileMap.has(parsed.path)) continue;
|
||||||
if (filePath && !fileMap.has(filePath)) {
|
fileMap.set(parsed.path, parsed.statusCode);
|
||||||
fileMap.set(filePath, parts[0] ?? "M");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// working tree diff failed
|
// working tree diff failed
|
||||||
|
|||||||
Reference in New Issue
Block a user