feat(FN-3773): fix no-op re-squash in merge finalize instead of refusing me
Fixes the merger to re-squash instead of refusing when a no-op merge verification occurs mid-merge, with regression tests added to prevent recurrence. Also replaces hardcoded color/spacing values in ProjectCard and ProjectOverview CSS with design tokens for theme consistency. Fusion-Task-Id: FN-3773
This commit is contained in:
5
.changeset/FN-3773-phantom-merge-noop-fix.md
Normal file
5
.changeset/FN-3773-phantom-merge-noop-fix.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Tasks no longer strand in In Review when an in-merge verification fix only rebuilds gitignored artifacts. The merger now restores squash state and commits the original branch content when no commit exists yet, while still refusing real phantom merges with no task content.
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
.project-card-errored {
|
.project-card-errored {
|
||||||
border-color: var(--color-error);
|
border-color: var(--color-error);
|
||||||
box-shadow: 0 0 0 1px var(--color-error);
|
box-shadow: 0 0 0 calc(var(--space-xs) / 4) var(--color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card-header {
|
.project-card-header {
|
||||||
@@ -51,8 +51,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 36px;
|
width: calc(var(--space-xl) + var(--space-md));
|
||||||
height: 36px;
|
height: calc(var(--space-xl) + var(--space-md));
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
color: var(--todo);
|
color: var(--todo);
|
||||||
@@ -64,11 +64,11 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card-name {
|
.project-card-name {
|
||||||
font-size: 15px;
|
font-size: calc(var(--space-md) + var(--space-xs) * 0.75);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.project-card-path {
|
.project-card-path {
|
||||||
font-size: 12px;
|
font-size: calc(var(--space-sm) + var(--space-xs));
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -91,10 +91,10 @@
|
|||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
padding: var(--space-xs) var(--space-sm);
|
padding: var(--space-xs) var(--space-sm);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
font-size: 11px;
|
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: calc(var(--space-xs) / 8);
|
||||||
border: 1px solid currentColor;
|
border: 1px solid currentColor;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@@ -110,11 +110,11 @@
|
|||||||
.project-card-metric {
|
.project-card-metric {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card-metric-value {
|
.project-card-metric-value {
|
||||||
font-size: 18px;
|
font-size: calc(var(--space-lg) + var(--space-xs) * 0.5);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
|
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: calc(var(--space-xs) / 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card-metric-empty {
|
.project-card-metric-empty {
|
||||||
@@ -143,8 +143,8 @@
|
|||||||
.project-card-activity {
|
.project-card-activity {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: calc(var(--space-sm) - var(--space-xs) * 0.5);
|
||||||
font-size: 12px;
|
font-size: calc(var(--space-sm) + var(--space-xs));
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-card-activity-label {
|
.project-card-activity-label {
|
||||||
@@ -165,12 +165,12 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
padding: 6px 10px;
|
padding: calc(var(--space-sm) - var(--space-xs) * 0.5) calc(var(--space-sm) + var(--space-xs) * 0.5);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 12px;
|
font-size: calc(var(--space-sm) + var(--space-xs));
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
@@ -224,7 +224,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.project-card-action-remove {
|
.project-card-action-remove {
|
||||||
padding: 6px;
|
padding: calc(var(--space-sm) - var(--space-xs) * 0.5);
|
||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
font-size: 20px;
|
font-size: calc(var(--space-lg) + var(--space-xs));
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -148,13 +148,13 @@
|
|||||||
.project-filter-tab {
|
.project-filter-tab {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: calc(var(--space-sm) - var(--space-xs) * 0.5);
|
||||||
padding: 6px 12px;
|
padding: calc(var(--space-sm) - var(--space-xs) * 0.5) var(--space-md);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 13px;
|
font-size: calc(var(--space-md) + var(--space-xs) * 0.25);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition:
|
transition:
|
||||||
@@ -194,10 +194,10 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 18px;
|
min-width: calc(var(--space-lg) + var(--space-xs) * 0.5);
|
||||||
height: 18px;
|
height: calc(var(--space-lg) + var(--space-xs) * 0.5);
|
||||||
padding: 0 5px;
|
padding: 0 var(--space-xs);
|
||||||
font-size: 11px;
|
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
background: var(--border);
|
background: var(--border);
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 13px;
|
font-size: calc(var(--space-md) + var(--space-xs) * 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-sort-select {
|
.project-sort-select {
|
||||||
@@ -231,8 +231,8 @@
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 13px;
|
font-size: calc(var(--space-md) + var(--space-xs) * 0.25);
|
||||||
padding: 6px 28px 6px 10px;
|
padding: calc(var(--space-sm) - var(--space-xs) * 0.5) calc(var(--space-xl) + var(--space-xs)) calc(var(--space-sm) - var(--space-xs) * 0.5) calc(var(--space-sm) + var(--space-xs) * 0.5);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: var(--font-primary);
|
font-family: var(--font-primary);
|
||||||
transition:
|
transition:
|
||||||
@@ -258,7 +258,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 13px;
|
font-size: calc(var(--space-md) + var(--space-xs) * 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-node-filter-select {
|
.project-node-filter-select {
|
||||||
@@ -267,8 +267,8 @@
|
|||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 13px;
|
font-size: calc(var(--space-md) + var(--space-xs) * 0.25);
|
||||||
padding: 6px 28px 6px 10px;
|
padding: calc(var(--space-sm) - var(--space-xs) * 0.5) calc(var(--space-xl) + var(--space-xs)) calc(var(--space-sm) - var(--space-xs) * 0.5) calc(var(--space-sm) + var(--space-xs) * 0.5);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: var(--font-primary);
|
font-family: var(--font-primary);
|
||||||
transition:
|
transition:
|
||||||
@@ -315,7 +315,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.project-overview__no-results p {
|
.project-overview__no-results p {
|
||||||
font-size: 15px;
|
font-size: calc(var(--space-md) + var(--space-xs) * 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Empty State --- */
|
/* --- Empty State --- */
|
||||||
@@ -333,8 +333,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 80px;
|
width: calc(var(--space-2xl) * 2.5);
|
||||||
height: 80px;
|
height: calc(var(--space-2xl) * 2.5);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 2px dashed var(--border);
|
border: 2px dashed var(--border);
|
||||||
border-radius: var(--radius-xl);
|
border-radius: var(--radius-xl);
|
||||||
@@ -342,17 +342,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.project-empty-state__title {
|
.project-empty-state__title {
|
||||||
font-size: 20px;
|
font-size: calc(var(--space-lg) + var(--space-xs));
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-empty-state__description {
|
.project-empty-state__description {
|
||||||
font-size: 14px;
|
font-size: calc(var(--space-md) + var(--space-xs) * 0.5);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
max-width: 380px;
|
max-width: calc(var(--space-2xl) * 11.875);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-empty-state__cta {
|
.project-empty-state__cta {
|
||||||
@@ -360,8 +360,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
margin-top: var(--space-sm);
|
margin-top: var(--space-sm);
|
||||||
padding: 10px 20px;
|
padding: calc(var(--space-sm) + var(--space-xs) * 0.5) calc(var(--space-lg) + var(--space-xs));
|
||||||
font-size: 14px;
|
font-size: calc(var(--space-md) + var(--space-xs) * 0.5);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +434,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.project-skeleton--tab {
|
.project-skeleton--tab {
|
||||||
width: 80px;
|
width: calc(var(--space-2xl) * 2.5);
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
||||||
@@ -559,7 +559,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.project-stat__value {
|
.project-stat__value {
|
||||||
font-size: 14px;
|
font-size: calc(var(--space-md) + var(--space-xs) * 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-stat__label {
|
.project-stat__label {
|
||||||
|
|||||||
105
packages/engine/src/__tests__/merger-no-op-fix-finalize.test.ts
Normal file
105
packages/engine/src/__tests__/merger-no-op-fix-finalize.test.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import { mkdtempSync, rmSync, writeFileSync, readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { execSync } from "node:child_process";
|
||||||
|
import { commitOrAmendMergeWithFixes } from "../merger.js";
|
||||||
|
import { DEFAULT_SETTINGS } from "@fusion/core";
|
||||||
|
|
||||||
|
function git(dir: string, cmd: string): string {
|
||||||
|
return execSync(cmd, { cwd: dir, stdio: "pipe" }).toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initRepo(dir: string): void {
|
||||||
|
git(dir, "git init -b main");
|
||||||
|
git(dir, 'git config user.email "test@example.com"');
|
||||||
|
git(dir, 'git config user.name "Test"');
|
||||||
|
git(dir, "git config commit.gpgsign false");
|
||||||
|
writeFileSync(join(dir, "README.md"), "# repo\n");
|
||||||
|
git(dir, "git add README.md");
|
||||||
|
git(dir, 'git commit -m "chore: initial"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function stageSquashThenClear(dir: string, branch: string, file: string, content: string): string {
|
||||||
|
git(dir, `git checkout -b ${branch}`);
|
||||||
|
writeFileSync(join(dir, file), content);
|
||||||
|
git(dir, `git add ${file}`);
|
||||||
|
git(dir, `git commit -m "feat: add ${file}"`);
|
||||||
|
git(dir, "git checkout main");
|
||||||
|
const preAttemptSha = git(dir, "git rev-parse HEAD");
|
||||||
|
git(dir, `git merge --squash ${branch}`);
|
||||||
|
// Simulate a no-op in-merge fix path where staged squash content gets cleared.
|
||||||
|
git(dir, "git reset HEAD -- .");
|
||||||
|
return preAttemptSha;
|
||||||
|
}
|
||||||
|
|
||||||
|
const STUB_SETTINGS = {
|
||||||
|
...DEFAULT_SETTINGS,
|
||||||
|
commitAuthorEnabled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("commitOrAmendMergeWithFixes no-op finalize", () => {
|
||||||
|
let dir: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dir = mkdtempSync(join(tmpdir(), "fn-noop-finalize-"));
|
||||||
|
initRepo(dir);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-squashes and commits when fix run is a no-op but branch content exists", async () => {
|
||||||
|
const preAttemptSha = stageSquashThenClear(dir, "feat/noop", "feature-a.ts", "export const a = 1;\n");
|
||||||
|
expect(git(dir, "git diff --cached --name-only")).toBe("");
|
||||||
|
expect(git(dir, "git rev-parse HEAD")).toBe(preAttemptSha);
|
||||||
|
expect(readFileSync(join(dir, "feature-a.ts"), "utf-8")).toBe("export const a = 1;\n");
|
||||||
|
|
||||||
|
const result = await commitOrAmendMergeWithFixes(
|
||||||
|
dir,
|
||||||
|
"FN-3773",
|
||||||
|
"feat/noop",
|
||||||
|
"- feat: add feature-a.ts",
|
||||||
|
false,
|
||||||
|
preAttemptSha,
|
||||||
|
"",
|
||||||
|
undefined,
|
||||||
|
STUB_SETTINGS,
|
||||||
|
undefined,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new Set<string>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
const committedFiles = git(dir, "git diff --name-only HEAD~1 HEAD").split("\n").filter(Boolean);
|
||||||
|
expect(committedFiles).toContain("feature-a.ts");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still refuses real phantom finalize when nothing staged and HEAD belongs to another task", async () => {
|
||||||
|
// Real phantom case: no current-task squash state and HEAD belongs to another task.
|
||||||
|
writeFileSync(join(dir, "outside.txt"), "outside\n");
|
||||||
|
git(dir, "git add outside.txt");
|
||||||
|
git(dir, 'git commit -m "feat: unrelated commit\n\nFusion-Task-Id: FN-OTHER"');
|
||||||
|
const preAttemptSha = git(dir, "git rev-parse HEAD");
|
||||||
|
|
||||||
|
const result = await commitOrAmendMergeWithFixes(
|
||||||
|
dir,
|
||||||
|
"FN-3773",
|
||||||
|
"feat/phantom",
|
||||||
|
"- feat: add feature-b.ts",
|
||||||
|
false,
|
||||||
|
preAttemptSha,
|
||||||
|
"",
|
||||||
|
undefined,
|
||||||
|
STUB_SETTINGS,
|
||||||
|
undefined,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new Set<string>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2680,19 +2680,44 @@ export async function commitOrAmendMergeWithFixes(
|
|||||||
const headMoved = currentHead !== preAttemptHeadSha;
|
const headMoved = currentHead !== preAttemptHeadSha;
|
||||||
|
|
||||||
if (!hasStaged && !headMoved) {
|
if (!hasStaged && !headMoved) {
|
||||||
// Defense-in-depth: if HEAD already carries this task's `Fusion-Task-Id`
|
// FN-1858 guardrail: never claim merge success when we cannot prove this
|
||||||
// trailer, the merge commit landed on a prior code path (e.g. AI commit
|
// task produced commit content. This finalize path distinguishes three
|
||||||
// in an earlier attempt) and there's simply nothing left for the fix to
|
// terminal states: (1) committed-by-AI (HEAD already has this task ID),
|
||||||
// fold in. Record success rather than tripping the phantom-merge guard
|
// (2) no-op fix where squash state was cleared and must be restored, and
|
||||||
// and stranding the task in In Review when the work is already on main.
|
// (3) real phantom where there is truly no task content to commit.
|
||||||
if (await headCarriesTaskIdTrailer(rootDir, taskId)) {
|
if (await headCarriesTaskIdTrailer(rootDir, taskId)) {
|
||||||
mergerLog.log(
|
mergerLog.log(
|
||||||
`${taskId}: HEAD already carries Fusion-Task-Id trailer — treating in-merge fix finalize as no-op success`,
|
`${taskId}: HEAD already carries Fusion-Task-Id trailer — treating in-merge fix finalize as no-op success`,
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Truly nothing happened — neither a commit nor staged changes. Refuse
|
|
||||||
// to fabricate a successful merge: the caller will report failure.
|
// No commit and no staged content can still be recoverable when the
|
||||||
|
// in-merge fix path cleared the previous squash index state. Rebuild the
|
||||||
|
// squash from branch -> preAttemptHeadSha and continue normally.
|
||||||
|
try {
|
||||||
|
await execAsync(`git reset --hard ${preAttemptHeadSha}`, {
|
||||||
|
cwd: rootDir,
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
await execAsync("git clean -fd", {
|
||||||
|
cwd: rootDir,
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
await execAsync(`git merge --squash ${branch}`, {
|
||||||
|
cwd: rootDir,
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
mergerLog.warn(`${taskId}: failed to restore squash state before finalize: ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stdout: restoredStagedOut } = await execAsync("git diff --cached --name-only", {
|
||||||
|
cwd: rootDir,
|
||||||
|
encoding: "utf-8",
|
||||||
|
});
|
||||||
|
if (restoredStagedOut.trim().length === 0) {
|
||||||
mergerLog.warn(
|
mergerLog.warn(
|
||||||
`${taskId}: refusing to record merge — no commit was created and no changes are staged. ` +
|
`${taskId}: refusing to record merge — no commit was created and no changes are staged. ` +
|
||||||
`This usually means the AI agent never ran git commit and the in-merge fix had nothing to add.`,
|
`This usually means the AI agent never ran git commit and the in-merge fix had nothing to add.`,
|
||||||
@@ -2700,6 +2725,9 @@ export async function commitOrAmendMergeWithFixes(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mergerLog.log(`${taskId}: restored squash state after no-op verification fix; proceeding to commit`);
|
||||||
|
}
|
||||||
|
|
||||||
// Build the message from the actual commit content rather than the
|
// Build the message from the actual commit content rather than the
|
||||||
// wide-range branch context that was gathered before merge. The
|
// wide-range branch context that was gathered before merge. The
|
||||||
// pre-merge commitLog/diffStat use `merge-base(branch, main)` as base,
|
// pre-merge commitLog/diffStat use `merge-base(branch, main)` as base,
|
||||||
|
|||||||
Reference in New Issue
Block a user