fix(FN-4409): add merge-base fallback for contamination audit
Fusion-Task-Id: FN-4409 Fusion-Task-Lineage: c608c942-fec7-4489-9970-b8dc314f8be2
This commit is contained in:
@@ -21,8 +21,6 @@ test("flags branch as tainted when foreign task commits are present", () => {
|
||||
fs.writeFileSync(path.join(dir, "README.md"), "base\n");
|
||||
run(dir, "git", ["add", "README.md"]);
|
||||
run(dir, "git", ["commit", "-m", "chore: base"]);
|
||||
const base = run(dir, "git", ["rev-parse", "HEAD"]);
|
||||
|
||||
run(dir, "git", ["checkout", "-b", "fusion/fn-0001"]);
|
||||
fs.writeFileSync(path.join(dir, "feature.txt"), "foreign\n");
|
||||
run(dir, "git", ["add", "feature.txt"]);
|
||||
@@ -32,7 +30,7 @@ test("flags branch as tainted when foreign task commits are present", () => {
|
||||
fs.mkdirSync(fusionDir, { recursive: true });
|
||||
const dbPath = path.join(fusionDir, "fusion.db");
|
||||
run(dir, "sqlite3", [dbPath, `CREATE TABLE tasks (id TEXT PRIMARY KEY, title TEXT, branch TEXT, baseCommitSha TEXT, "column" TEXT);`]);
|
||||
run(dir, "sqlite3", [dbPath, `INSERT INTO tasks (id, title, branch, baseCommitSha, "column") VALUES ('FN-0001', 'Task 1', 'fusion/fn-0001', '${base}', 'in-progress');`]);
|
||||
run(dir, "sqlite3", [dbPath, `INSERT INTO tasks (id, title, branch, baseCommitSha, "column") VALUES ('FN-0001', 'Task 1', 'fusion/fn-0001', NULL, 'in-progress');`]);
|
||||
|
||||
const report = auditBranchCrossContamination({ projectRoot: dir });
|
||||
assert.equal(report.taintedTaskCount, 1);
|
||||
@@ -40,6 +38,7 @@ test("flags branch as tainted when foreign task commits are present", () => {
|
||||
assert.ok(task);
|
||||
assert.equal(task.tainted, true);
|
||||
assert.equal(task.recommendation, "force-reset");
|
||||
assert.equal(task.baseResolutionSource.startsWith("merge-base("), true);
|
||||
assert.equal(task.taintedCommits[0].foreignTaskId, "FN-0002");
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
|
||||
@@ -60,6 +60,25 @@ function branchExists(projectRoot, branchName) {
|
||||
return runGit(projectRoot, ["rev-parse", "--verify", `refs/heads/${branchName}`], { allowFailure: true }) !== null;
|
||||
}
|
||||
|
||||
function resolveMainRef(projectRoot) {
|
||||
if (runGit(projectRoot, ["rev-parse", "--verify", "origin/main"], { allowFailure: true })) {
|
||||
return "origin/main";
|
||||
}
|
||||
return "main";
|
||||
}
|
||||
|
||||
function resolveBaseCommit(projectRoot, branchName, taskBaseCommitSha) {
|
||||
if (taskBaseCommitSha && String(taskBaseCommitSha).trim()) {
|
||||
return { baseCommitSha: String(taskBaseCommitSha).trim(), source: "task.baseCommitSha" };
|
||||
}
|
||||
const mainRef = resolveMainRef(projectRoot);
|
||||
const fallback = runGit(projectRoot, ["merge-base", mainRef, branchName], { allowFailure: true });
|
||||
if (fallback) {
|
||||
return { baseCommitSha: fallback.trim(), source: `merge-base(${mainRef},${branchName})` };
|
||||
}
|
||||
return { baseCommitSha: null, source: "unresolved" };
|
||||
}
|
||||
|
||||
function parseCommits(raw) {
|
||||
if (!raw) return [];
|
||||
const lines = raw.split("\n").map((line) => line.trim()).filter(Boolean);
|
||||
@@ -97,20 +116,8 @@ export function auditBranchCrossContamination({ projectRoot = process.cwd() } =
|
||||
for (const task of taskRows) {
|
||||
const taskId = String(task.id).toUpperCase();
|
||||
const branchName = expectedBranch(taskId, task.branch);
|
||||
const baseCommitSha = task.baseCommitSha ? String(task.baseCommitSha).trim() : null;
|
||||
|
||||
if (!baseCommitSha) {
|
||||
report.tasks.push({
|
||||
taskId,
|
||||
title: task.title,
|
||||
branchName,
|
||||
baseCommitSha: null,
|
||||
column: task.columnName,
|
||||
skipped: true,
|
||||
reason: "missing-baseCommitSha",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const baseResolution = resolveBaseCommit(projectRoot, branchName, task.baseCommitSha);
|
||||
const baseCommitSha = baseResolution.baseCommitSha;
|
||||
|
||||
if (!branchExists(projectRoot, branchName)) {
|
||||
report.missingBranchCount += 1;
|
||||
@@ -127,6 +134,20 @@ export function auditBranchCrossContamination({ projectRoot = process.cwd() } =
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!baseCommitSha) {
|
||||
report.tasks.push({
|
||||
taskId,
|
||||
title: task.title,
|
||||
branchName,
|
||||
baseCommitSha: null,
|
||||
baseResolutionSource: baseResolution.source,
|
||||
column: task.columnName,
|
||||
skipped: true,
|
||||
reason: "missing-baseCommitSha",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawLog = runGit(projectRoot, ["log", `${baseCommitSha}..${branchName}`, "--format=%H%x1f%s%x1f%b"]);
|
||||
const commits = parseCommits(rawLog);
|
||||
const taintedCommits = commits.filter((commit) => commit.attributedTaskId && commit.attributedTaskId !== taskId);
|
||||
@@ -140,6 +161,7 @@ export function auditBranchCrossContamination({ projectRoot = process.cwd() } =
|
||||
title: task.title,
|
||||
branchName,
|
||||
baseCommitSha,
|
||||
baseResolutionSource: baseResolution.source,
|
||||
column: task.columnName,
|
||||
totalCommits: commits.length,
|
||||
taskAttributedCommitCount: ownCommits.length,
|
||||
|
||||
Reference in New Issue
Block a user