feat(FN-1598): merge fusion/fn-1598
This commit is contained in:
@@ -810,17 +810,17 @@ describe("detectResolvableConflicts", () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns empty array when no conflicts exist", () => {
|
it("returns empty array when no conflicts exist", async () => {
|
||||||
mockedExecSync.mockReturnValue(""); // Empty output = no conflicts
|
mockedExecSync.mockReturnValue(""); // Empty output = no conflicts
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects package-lock.json as auto-resolvable with 'theirs' strategy", () => {
|
it("detects package-lock.json as auto-resolvable with 'theirs' strategy", async () => {
|
||||||
mockedExecSync.mockReturnValue("package-lock.json\n");
|
mockedExecSync.mockReturnValue("package-lock.json\n");
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(result[0]).toMatchObject({
|
expect(result[0]).toMatchObject({
|
||||||
filePath: "package-lock.json",
|
filePath: "package-lock.json",
|
||||||
@@ -830,10 +830,10 @@ describe("detectResolvableConflicts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects pnpm-lock.yaml as lock file with 'ours' strategy", () => {
|
it("detects pnpm-lock.yaml as lock file with 'ours' strategy", async () => {
|
||||||
mockedExecSync.mockReturnValue("pnpm-lock.yaml\n");
|
mockedExecSync.mockReturnValue("pnpm-lock.yaml\n");
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(result[0]).toMatchObject({
|
expect(result[0]).toMatchObject({
|
||||||
filePath: "pnpm-lock.yaml",
|
filePath: "pnpm-lock.yaml",
|
||||||
@@ -843,10 +843,10 @@ describe("detectResolvableConflicts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects yarn.lock as lock file with 'ours' strategy", () => {
|
it("detects yarn.lock as lock file with 'ours' strategy", async () => {
|
||||||
mockedExecSync.mockReturnValue("yarn.lock\n");
|
mockedExecSync.mockReturnValue("yarn.lock\n");
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result[0]).toMatchObject({
|
expect(result[0]).toMatchObject({
|
||||||
autoResolvable: true,
|
autoResolvable: true,
|
||||||
strategy: "ours",
|
strategy: "ours",
|
||||||
@@ -854,10 +854,10 @@ describe("detectResolvableConflicts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects Gemfile.lock as lock file with 'ours' strategy", () => {
|
it("detects Gemfile.lock as lock file with 'ours' strategy", async () => {
|
||||||
mockedExecSync.mockReturnValue("Gemfile.lock\n");
|
mockedExecSync.mockReturnValue("Gemfile.lock\n");
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result[0]).toMatchObject({
|
expect(result[0]).toMatchObject({
|
||||||
autoResolvable: true,
|
autoResolvable: true,
|
||||||
strategy: "ours",
|
strategy: "ours",
|
||||||
@@ -865,10 +865,10 @@ describe("detectResolvableConflicts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects .gen.ts files as generated files with 'theirs' strategy", () => {
|
it("detects .gen.ts files as generated files with 'theirs' strategy", async () => {
|
||||||
mockedExecSync.mockReturnValue("src/types.gen.ts\n");
|
mockedExecSync.mockReturnValue("src/types.gen.ts\n");
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result[0]).toMatchObject({
|
expect(result[0]).toMatchObject({
|
||||||
autoResolvable: true,
|
autoResolvable: true,
|
||||||
strategy: "theirs",
|
strategy: "theirs",
|
||||||
@@ -876,10 +876,10 @@ describe("detectResolvableConflicts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects dist/ paths as generated files with 'theirs' strategy", () => {
|
it("detects dist/ paths as generated files with 'theirs' strategy", async () => {
|
||||||
mockedExecSync.mockReturnValue("dist/index.js\n");
|
mockedExecSync.mockReturnValue("dist/index.js\n");
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result[0]).toMatchObject({
|
expect(result[0]).toMatchObject({
|
||||||
autoResolvable: true,
|
autoResolvable: true,
|
||||||
strategy: "theirs",
|
strategy: "theirs",
|
||||||
@@ -887,14 +887,14 @@ describe("detectResolvableConflicts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects coverage/ paths as generated files with 'theirs' strategy", () => {
|
it("detects coverage/ paths as generated files with 'theirs' strategy", async () => {
|
||||||
mockedExecSync.mockImplementation((cmd: any) => {
|
mockedExecSync.mockImplementation((cmd: any) => {
|
||||||
const cmdStr = String(cmd);
|
const cmdStr = String(cmd);
|
||||||
if (cmdStr.includes("git diff --name-only")) return "coverage/lcov.info\n";
|
if (cmdStr.includes("git diff --name-only")) return "coverage/lcov.info\n";
|
||||||
return Buffer.from("");
|
return Buffer.from("");
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result[0]).toMatchObject({
|
expect(result[0]).toMatchObject({
|
||||||
autoResolvable: true,
|
autoResolvable: true,
|
||||||
strategy: "theirs",
|
strategy: "theirs",
|
||||||
@@ -902,7 +902,7 @@ describe("detectResolvableConflicts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("marks regular source files as complex conflicts", () => {
|
it("marks regular source files as complex conflicts", async () => {
|
||||||
mockedExecSync.mockImplementation((cmd: any) => {
|
mockedExecSync.mockImplementation((cmd: any) => {
|
||||||
const cmdStr = String(cmd);
|
const cmdStr = String(cmd);
|
||||||
if (cmdStr.includes("git diff --name-only")) return "src/components/App.tsx\n";
|
if (cmdStr.includes("git diff --name-only")) return "src/components/App.tsx\n";
|
||||||
@@ -911,7 +911,7 @@ describe("detectResolvableConflicts", () => {
|
|||||||
return Buffer.from("");
|
return Buffer.from("");
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result[0]).toMatchObject({
|
expect(result[0]).toMatchObject({
|
||||||
filePath: "src/components/App.tsx",
|
filePath: "src/components/App.tsx",
|
||||||
autoResolvable: false,
|
autoResolvable: false,
|
||||||
@@ -919,7 +919,7 @@ describe("detectResolvableConflicts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles multiple conflicted files with mixed categories", () => {
|
it("handles multiple conflicted files with mixed categories", async () => {
|
||||||
mockedExecSync.mockImplementation((cmd: any) => {
|
mockedExecSync.mockImplementation((cmd: any) => {
|
||||||
const cmdStr = String(cmd);
|
const cmdStr = String(cmd);
|
||||||
if (cmdStr.includes("git diff --name-only"))
|
if (cmdStr.includes("git diff --name-only"))
|
||||||
@@ -929,7 +929,7 @@ describe("detectResolvableConflicts", () => {
|
|||||||
return Buffer.from("");
|
return Buffer.from("");
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result).toHaveLength(3);
|
expect(result).toHaveLength(3);
|
||||||
|
|
||||||
const lockFile = result.find((r) => r.filePath === "package-lock.json");
|
const lockFile = result.find((r) => r.filePath === "package-lock.json");
|
||||||
@@ -941,12 +941,12 @@ describe("detectResolvableConflicts", () => {
|
|||||||
expect(distFile).toMatchObject({ autoResolvable: true, reason: "generated-file" });
|
expect(distFile).toMatchObject({ autoResolvable: true, reason: "generated-file" });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns empty array on git command failure", () => {
|
it("returns empty array on git command failure", async () => {
|
||||||
mockedExecSync.mockImplementation(() => {
|
mockedExecSync.mockImplementation(() => {
|
||||||
throw new Error("git command failed");
|
throw new Error("git command failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -958,8 +958,8 @@ describe("autoResolveFile", () => {
|
|||||||
mockedExecSync.mockReturnValue(Buffer.from(""));
|
mockedExecSync.mockReturnValue(Buffer.from(""));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls git checkout --theirs for 'theirs' resolution", () => {
|
it("calls git checkout --theirs for 'theirs' resolution", async () => {
|
||||||
autoResolveFile("package-lock.json", "theirs", "/tmp/root");
|
await autoResolveFile("package-lock.json", "theirs", "/tmp/root");
|
||||||
|
|
||||||
const checkoutCall = mockedExecSync.mock.calls.find((call) =>
|
const checkoutCall = mockedExecSync.mock.calls.find((call) =>
|
||||||
String(call[0]).includes("git checkout --theirs"),
|
String(call[0]).includes("git checkout --theirs"),
|
||||||
@@ -968,8 +968,8 @@ describe("autoResolveFile", () => {
|
|||||||
expect(String(checkoutCall![0])).toContain("package-lock.json");
|
expect(String(checkoutCall![0])).toContain("package-lock.json");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls git checkout --ours for 'ours' resolution", () => {
|
it("calls git checkout --ours for 'ours' resolution", async () => {
|
||||||
autoResolveFile("config.json", "ours", "/tmp/root");
|
await autoResolveFile("config.json", "ours", "/tmp/root");
|
||||||
|
|
||||||
const checkoutCall = mockedExecSync.mock.calls.find((call) =>
|
const checkoutCall = mockedExecSync.mock.calls.find((call) =>
|
||||||
String(call[0]).includes("git checkout --ours"),
|
String(call[0]).includes("git checkout --ours"),
|
||||||
@@ -978,8 +978,8 @@ describe("autoResolveFile", () => {
|
|||||||
expect(String(checkoutCall![0])).toContain("config.json");
|
expect(String(checkoutCall![0])).toContain("config.json");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stages the resolved file with git add", () => {
|
it("stages the resolved file with git add", async () => {
|
||||||
autoResolveFile("package-lock.json", "theirs", "/tmp/root");
|
await autoResolveFile("package-lock.json", "theirs", "/tmp/root");
|
||||||
|
|
||||||
const addCall = mockedExecSync.mock.calls.find((call) =>
|
const addCall = mockedExecSync.mock.calls.find((call) =>
|
||||||
String(call[0]).includes("git add"),
|
String(call[0]).includes("git add"),
|
||||||
@@ -988,7 +988,7 @@ describe("autoResolveFile", () => {
|
|||||||
expect(String(addCall![0])).toContain("package-lock.json");
|
expect(String(addCall![0])).toContain("package-lock.json");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws error when git checkout fails", () => {
|
it("throws error when git checkout fails", async () => {
|
||||||
mockedExecSync.mockImplementation((cmd: any) => {
|
mockedExecSync.mockImplementation((cmd: any) => {
|
||||||
if (String(cmd).includes("checkout")) {
|
if (String(cmd).includes("checkout")) {
|
||||||
throw new Error("checkout failed");
|
throw new Error("checkout failed");
|
||||||
@@ -996,7 +996,7 @@ describe("autoResolveFile", () => {
|
|||||||
return Buffer.from("");
|
return Buffer.from("");
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(() => autoResolveFile("file.ts", "theirs", "/tmp/root")).toThrow(
|
await expect(autoResolveFile("file.ts", "theirs", "/tmp/root")).rejects.toThrow(
|
||||||
"Failed to auto-resolve",
|
"Failed to auto-resolve",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1009,14 +1009,14 @@ describe("resolveConflicts", () => {
|
|||||||
mockedExecSync.mockReturnValue(Buffer.from(""));
|
mockedExecSync.mockReturnValue(Buffer.from(""));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves lock files and returns remaining complex conflicts", () => {
|
it("resolves lock files and returns remaining complex conflicts", async () => {
|
||||||
const categories: ConflictCategory[] = [
|
const categories: ConflictCategory[] = [
|
||||||
{ filePath: "package-lock.json", autoResolvable: true, strategy: "ours", reason: "lock-file" },
|
{ filePath: "package-lock.json", autoResolvable: true, strategy: "ours", reason: "lock-file" },
|
||||||
{ filePath: "src/App.tsx", autoResolvable: false, reason: "complex" },
|
{ filePath: "src/App.tsx", autoResolvable: false, reason: "complex" },
|
||||||
{ filePath: "dist/bundle.js", autoResolvable: true, strategy: "ours", reason: "generated-file" },
|
{ filePath: "dist/bundle.js", autoResolvable: true, strategy: "ours", reason: "generated-file" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const remaining = resolveConflicts(categories, "/tmp/root");
|
const remaining = await resolveConflicts(categories, "/tmp/root");
|
||||||
|
|
||||||
// Should have resolved package-lock.json and dist/bundle.js
|
// Should have resolved package-lock.json and dist/bundle.js
|
||||||
expect(remaining).toEqual(["src/App.tsx"]);
|
expect(remaining).toEqual(["src/App.tsx"]);
|
||||||
@@ -1028,13 +1028,13 @@ describe("resolveConflicts", () => {
|
|||||||
expect(checkoutCalls).toHaveLength(2);
|
expect(checkoutCalls).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns all files when none are auto-resolvable", () => {
|
it("returns all files when none are auto-resolvable", async () => {
|
||||||
const categories: ConflictCategory[] = [
|
const categories: ConflictCategory[] = [
|
||||||
{ filePath: "src/App.tsx", autoResolvable: false, reason: "complex" },
|
{ filePath: "src/App.tsx", autoResolvable: false, reason: "complex" },
|
||||||
{ filePath: "src/utils.ts", autoResolvable: false, reason: "complex" },
|
{ filePath: "src/utils.ts", autoResolvable: false, reason: "complex" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const remaining = resolveConflicts(categories, "/tmp/root");
|
const remaining = await resolveConflicts(categories, "/tmp/root");
|
||||||
|
|
||||||
expect(remaining).toEqual(["src/App.tsx", "src/utils.ts"]);
|
expect(remaining).toEqual(["src/App.tsx", "src/utils.ts"]);
|
||||||
// No checkout calls should be made
|
// No checkout calls should be made
|
||||||
@@ -1044,13 +1044,13 @@ describe("resolveConflicts", () => {
|
|||||||
expect(checkoutCalls).toHaveLength(0);
|
expect(checkoutCalls).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns empty array when all conflicts are resolved", () => {
|
it("returns empty array when all conflicts are resolved", async () => {
|
||||||
const categories: ConflictCategory[] = [
|
const categories: ConflictCategory[] = [
|
||||||
{ filePath: "package-lock.json", autoResolvable: true, strategy: "ours", reason: "lock-file" },
|
{ filePath: "package-lock.json", autoResolvable: true, strategy: "ours", reason: "lock-file" },
|
||||||
{ filePath: "yarn.lock", autoResolvable: true, strategy: "ours", reason: "lock-file" },
|
{ filePath: "yarn.lock", autoResolvable: true, strategy: "ours", reason: "lock-file" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const remaining = resolveConflicts(categories, "/tmp/root");
|
const remaining = await resolveConflicts(categories, "/tmp/root");
|
||||||
|
|
||||||
expect(remaining).toEqual([]);
|
expect(remaining).toEqual([]);
|
||||||
});
|
});
|
||||||
@@ -1063,7 +1063,7 @@ describe("trivial conflict detection (isTrivialWhitespaceConflict via detectReso
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("detects whitespace-only conflicts as trivial", () => {
|
it("detects whitespace-only conflicts as trivial", async () => {
|
||||||
mockedExecSync.mockImplementation((cmd: any) => {
|
mockedExecSync.mockImplementation((cmd: any) => {
|
||||||
const cmdStr = String(cmd);
|
const cmdStr = String(cmd);
|
||||||
if (cmdStr.includes("git diff --name-only")) return "src/utils.ts\n";
|
if (cmdStr.includes("git diff --name-only")) return "src/utils.ts\n";
|
||||||
@@ -1072,7 +1072,7 @@ describe("trivial conflict detection (isTrivialWhitespaceConflict via detectReso
|
|||||||
return Buffer.from("");
|
return Buffer.from("");
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(result[0]).toMatchObject({
|
expect(result[0]).toMatchObject({
|
||||||
filePath: "src/utils.ts",
|
filePath: "src/utils.ts",
|
||||||
@@ -1082,7 +1082,7 @@ describe("trivial conflict detection (isTrivialWhitespaceConflict via detectReso
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("marks conflicts with actual content differences as complex", () => {
|
it("marks conflicts with actual content differences as complex", async () => {
|
||||||
mockedExecSync.mockImplementation((cmd: any) => {
|
mockedExecSync.mockImplementation((cmd: any) => {
|
||||||
const cmdStr = String(cmd);
|
const cmdStr = String(cmd);
|
||||||
if (cmdStr.includes("git diff --name-only")) return "src/utils.ts\n";
|
if (cmdStr.includes("git diff --name-only")) return "src/utils.ts\n";
|
||||||
@@ -1091,7 +1091,7 @@ describe("trivial conflict detection (isTrivialWhitespaceConflict via detectReso
|
|||||||
return Buffer.from("");
|
return Buffer.from("");
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(result[0]).toMatchObject({
|
expect(result[0]).toMatchObject({
|
||||||
filePath: "src/utils.ts",
|
filePath: "src/utils.ts",
|
||||||
@@ -1100,7 +1100,7 @@ describe("trivial conflict detection (isTrivialWhitespaceConflict via detectReso
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles multiple conflict sections - one non-trivial makes complex", () => {
|
it("handles multiple conflict sections - one non-trivial makes complex", async () => {
|
||||||
mockedExecSync.mockImplementation((cmd: any) => {
|
mockedExecSync.mockImplementation((cmd: any) => {
|
||||||
const cmdStr = String(cmd);
|
const cmdStr = String(cmd);
|
||||||
if (cmdStr.includes("git diff --name-only")) return "src/utils.ts\n";
|
if (cmdStr.includes("git diff --name-only")) return "src/utils.ts\n";
|
||||||
@@ -1109,7 +1109,7 @@ describe("trivial conflict detection (isTrivialWhitespaceConflict via detectReso
|
|||||||
return Buffer.from("");
|
return Buffer.from("");
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(result[0]).toMatchObject({
|
expect(result[0]).toMatchObject({
|
||||||
autoResolvable: false,
|
autoResolvable: false,
|
||||||
@@ -1117,7 +1117,7 @@ describe("trivial conflict detection (isTrivialWhitespaceConflict via detectReso
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles git command errors as complex conflicts", () => {
|
it("handles git command errors as complex conflicts", async () => {
|
||||||
mockedExecSync.mockImplementation((cmd: any) => {
|
mockedExecSync.mockImplementation((cmd: any) => {
|
||||||
const cmdStr = String(cmd);
|
const cmdStr = String(cmd);
|
||||||
if (cmdStr.includes("git diff --name-only")) return "src/utils.ts\n";
|
if (cmdStr.includes("git diff --name-only")) return "src/utils.ts\n";
|
||||||
@@ -1125,7 +1125,7 @@ describe("trivial conflict detection (isTrivialWhitespaceConflict via detectReso
|
|||||||
return Buffer.from("");
|
return Buffer.from("");
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = detectResolvableConflicts("/tmp/root");
|
const result = await detectResolvableConflicts("/tmp/root");
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(result[0]).toMatchObject({
|
expect(result[0]).toMatchObject({
|
||||||
autoResolvable: false,
|
autoResolvable: false,
|
||||||
@@ -1228,6 +1228,13 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
|
|||||||
return "src/file.ts\n";
|
return "src/file.ts\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// git diff-tree for trivial whitespace detection - return real changes (non-trivial)
|
||||||
|
if (cmdStr.includes("diff-tree")) {
|
||||||
|
const error = new Error("exit code 1") as any;
|
||||||
|
error.stdout = "+const x = 2;\n-const x = 1;";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
// Staged changes check after merge (conflicts present but not staged)
|
// Staged changes check after merge (conflicts present but not staged)
|
||||||
if (cmdStr.includes("diff --cached --quiet")) {
|
if (cmdStr.includes("diff --cached --quiet")) {
|
||||||
return "1"; // Has staged changes from the merge
|
return "1"; // Has staged changes from the merge
|
||||||
@@ -1337,6 +1344,13 @@ describe("aiMergeTask — retry logic with escalating strategies", () => {
|
|||||||
return hasConflicts ? "src/complex.ts\n" : "";
|
return hasConflicts ? "src/complex.ts\n" : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// git diff-tree for trivial whitespace detection - return real changes (non-trivial)
|
||||||
|
if (cmdStr.includes("diff-tree")) {
|
||||||
|
const error = new Error("exit code 1") as any;
|
||||||
|
error.stdout = "+const x = 2;\n-const x = 1;";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
if (cmdStr.includes("diff --cached --quiet")) return hasConflicts ? "1" : "0";
|
if (cmdStr.includes("diff --cached --quiet")) return hasConflicts ? "1" : "0";
|
||||||
if (cmdStr.includes("git commit")) return Buffer.from("");
|
if (cmdStr.includes("git commit")) return Buffer.from("");
|
||||||
if (cmdStr.includes("branch -d")) return Buffer.from("");
|
if (cmdStr.includes("branch -d")) return Buffer.from("");
|
||||||
@@ -1461,59 +1475,70 @@ describe("classifyConflict", () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies package-lock.json as 'lockfile-ours'", () => {
|
it("classifies package-lock.json as 'lockfile-ours'", async () => {
|
||||||
const result = classifyConflict("package-lock.json", "/tmp/root");
|
const result = await classifyConflict("package-lock.json", "/tmp/root");
|
||||||
expect(result).toBe("lockfile-ours");
|
expect(result).toBe("lockfile-ours");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies pnpm-lock.yaml as 'lockfile-ours'", () => {
|
it("classifies pnpm-lock.yaml as 'lockfile-ours'", async () => {
|
||||||
const result = classifyConflict("pnpm-lock.yaml", "/tmp/root");
|
const result = await classifyConflict("pnpm-lock.yaml", "/tmp/root");
|
||||||
expect(result).toBe("lockfile-ours");
|
expect(result).toBe("lockfile-ours");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies yarn.lock as 'lockfile-ours'", () => {
|
it("classifies yarn.lock as 'lockfile-ours'", async () => {
|
||||||
const result = classifyConflict("yarn.lock", "/tmp/root");
|
const result = await classifyConflict("yarn.lock", "/tmp/root");
|
||||||
expect(result).toBe("lockfile-ours");
|
expect(result).toBe("lockfile-ours");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies Gemfile.lock as 'lockfile-ours'", () => {
|
it("classifies Gemfile.lock as 'lockfile-ours'", async () => {
|
||||||
const result = classifyConflict("Gemfile.lock", "/tmp/root");
|
const result = await classifyConflict("Gemfile.lock", "/tmp/root");
|
||||||
expect(result).toBe("lockfile-ours");
|
expect(result).toBe("lockfile-ours");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies bun.lockb as 'lockfile-ours'", () => {
|
it("classifies bun.lockb as 'lockfile-ours'", async () => {
|
||||||
const result = classifyConflict("bun.lockb", "/tmp/root");
|
const result = await classifyConflict("bun.lockb", "/tmp/root");
|
||||||
expect(result).toBe("lockfile-ours");
|
expect(result).toBe("lockfile-ours");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies go.sum as 'lockfile-ours'", () => {
|
it("classifies go.sum as 'lockfile-ours'", async () => {
|
||||||
const result = classifyConflict("go.sum", "/tmp/root");
|
const result = await classifyConflict("go.sum", "/tmp/root");
|
||||||
expect(result).toBe("lockfile-ours");
|
expect(result).toBe("lockfile-ours");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies *.gen.ts files as 'generated-theirs'", () => {
|
it("classifies *.gen.ts files as 'generated-theirs'", async () => {
|
||||||
const result = classifyConflict("src/types.gen.ts", "/tmp/root");
|
const result = await classifyConflict("src/types.gen.ts", "/tmp/root");
|
||||||
expect(result).toBe("generated-theirs");
|
expect(result).toBe("generated-theirs");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies dist/* files as 'generated-theirs'", () => {
|
it("classifies dist/* files as 'generated-theirs'", async () => {
|
||||||
const result = classifyConflict("dist/bundle.js", "/tmp/root");
|
const result = await classifyConflict("dist/bundle.js", "/tmp/root");
|
||||||
expect(result).toBe("generated-theirs");
|
expect(result).toBe("generated-theirs");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies build/* files as 'generated-theirs'", () => {
|
it("classifies build/* files as 'generated-theirs'", async () => {
|
||||||
const result = classifyConflict("build/index.html", "/tmp/root");
|
const result = await classifyConflict("build/index.html", "/tmp/root");
|
||||||
expect(result).toBe("generated-theirs");
|
expect(result).toBe("generated-theirs");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies *.min.js files as 'generated-theirs'", () => {
|
it("classifies *.min.js files as 'generated-theirs'", async () => {
|
||||||
const result = classifyConflict("app.min.js", "/tmp/root");
|
const result = await classifyConflict("app.min.js", "/tmp/root");
|
||||||
expect(result).toBe("generated-theirs");
|
expect(result).toBe("generated-theirs");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("classifies regular source files as 'complex'", () => {
|
it("classifies regular source files as 'complex'", async () => {
|
||||||
|
// Mock git diff-tree to return actual content changes (non-trivial)
|
||||||
|
mockedExecSync.mockImplementation(() => {
|
||||||
|
const error = new Error("exit code 1") as any;
|
||||||
|
error.stdout = `diff --git a/src/components/App.tsx b/src/components/App.tsx
|
||||||
|
--- a/src/components/App.tsx
|
||||||
|
+++ b/src/components/App.tsx
|
||||||
|
@@ -1 +1 @@
|
||||||
|
-const x = 1;
|
||||||
|
+const x = 2;`;
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
mockedReadFileSync.mockReturnValue("const x = 1;");
|
mockedReadFileSync.mockReturnValue("const x = 1;");
|
||||||
const result = classifyConflict("src/components/App.tsx", "/tmp/root");
|
const result = await classifyConflict("src/components/App.tsx", "/tmp/root");
|
||||||
expect(result).toBe("complex");
|
expect(result).toBe("complex");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1523,26 +1548,26 @@ describe("getConflictedFiles", () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns array of conflicted file paths", () => {
|
it("returns array of conflicted file paths", async () => {
|
||||||
mockedExecSync.mockReturnValue("package-lock.json\nsrc/index.ts\n");
|
mockedExecSync.mockReturnValue("package-lock.json\nsrc/index.ts\n");
|
||||||
|
|
||||||
const result = getConflictedFiles("/tmp/root");
|
const result = await getConflictedFiles("/tmp/root");
|
||||||
expect(result).toEqual(["package-lock.json", "src/index.ts"]);
|
expect(result).toEqual(["package-lock.json", "src/index.ts"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns empty array when no conflicts", () => {
|
it("returns empty array when no conflicts", async () => {
|
||||||
mockedExecSync.mockReturnValue("");
|
mockedExecSync.mockReturnValue("");
|
||||||
|
|
||||||
const result = getConflictedFiles("/tmp/root");
|
const result = await getConflictedFiles("/tmp/root");
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns empty array on git error", () => {
|
it("returns empty array on git error", async () => {
|
||||||
mockedExecSync.mockImplementation(() => {
|
mockedExecSync.mockImplementation(() => {
|
||||||
throw new Error("git error");
|
throw new Error("git error");
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = getConflictedFiles("/tmp/root");
|
const result = await getConflictedFiles("/tmp/root");
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1553,8 +1578,8 @@ describe("resolveWithOurs", () => {
|
|||||||
mockedExecSync.mockReturnValue(Buffer.from(""));
|
mockedExecSync.mockReturnValue(Buffer.from(""));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls git checkout --ours and git add", () => {
|
it("calls git checkout --ours and git add", async () => {
|
||||||
resolveWithOurs("package-lock.json", "/tmp/root");
|
await resolveWithOurs("package-lock.json", "/tmp/root");
|
||||||
|
|
||||||
const checkoutCall = mockedExecSync.mock.calls.find((call) =>
|
const checkoutCall = mockedExecSync.mock.calls.find((call) =>
|
||||||
String(call[0]).includes("checkout --ours"),
|
String(call[0]).includes("checkout --ours"),
|
||||||
@@ -1568,12 +1593,12 @@ describe("resolveWithOurs", () => {
|
|||||||
expect(String(checkoutCall![0])).toContain("package-lock.json");
|
expect(String(checkoutCall![0])).toContain("package-lock.json");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws on git error", () => {
|
it("throws on git error", async () => {
|
||||||
mockedExecSync.mockImplementation(() => {
|
mockedExecSync.mockImplementation(() => {
|
||||||
throw new Error("checkout failed");
|
throw new Error("checkout failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(() => resolveWithOurs("file.ts", "/tmp/root")).toThrow(
|
await expect(resolveWithOurs("file.ts", "/tmp/root")).rejects.toThrow(
|
||||||
"Failed to auto-resolve",
|
"Failed to auto-resolve",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1585,8 +1610,8 @@ describe("resolveWithTheirs", () => {
|
|||||||
mockedExecSync.mockReturnValue(Buffer.from(""));
|
mockedExecSync.mockReturnValue(Buffer.from(""));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls git checkout --theirs and git add", () => {
|
it("calls git checkout --theirs and git add", async () => {
|
||||||
resolveWithTheirs("dist/bundle.js", "/tmp/root");
|
await resolveWithTheirs("dist/bundle.js", "/tmp/root");
|
||||||
|
|
||||||
const checkoutCall = mockedExecSync.mock.calls.find((call) =>
|
const checkoutCall = mockedExecSync.mock.calls.find((call) =>
|
||||||
String(call[0]).includes("checkout --theirs"),
|
String(call[0]).includes("checkout --theirs"),
|
||||||
@@ -1600,12 +1625,12 @@ describe("resolveWithTheirs", () => {
|
|||||||
expect(String(checkoutCall![0])).toContain("dist/bundle.js");
|
expect(String(checkoutCall![0])).toContain("dist/bundle.js");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws on git error", () => {
|
it("throws on git error", async () => {
|
||||||
mockedExecSync.mockImplementation(() => {
|
mockedExecSync.mockImplementation(() => {
|
||||||
throw new Error("checkout failed");
|
throw new Error("checkout failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(() => resolveWithTheirs("file.ts", "/tmp/root")).toThrow(
|
await expect(resolveWithTheirs("file.ts", "/tmp/root")).rejects.toThrow(
|
||||||
"Failed to auto-resolve",
|
"Failed to auto-resolve",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1617,8 +1642,8 @@ describe("resolveTrivialWhitespace", () => {
|
|||||||
mockedExecSync.mockReturnValue(Buffer.from(""));
|
mockedExecSync.mockReturnValue(Buffer.from(""));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls git add to resolve trivial whitespace conflict", () => {
|
it("calls git add to resolve trivial whitespace conflict", async () => {
|
||||||
resolveTrivialWhitespace("src/utils.ts", "/tmp/root");
|
await resolveTrivialWhitespace("src/utils.ts", "/tmp/root");
|
||||||
|
|
||||||
const addCall = mockedExecSync.mock.calls.find((call) =>
|
const addCall = mockedExecSync.mock.calls.find((call) =>
|
||||||
String(call[0]).includes("git add"),
|
String(call[0]).includes("git add"),
|
||||||
@@ -1628,12 +1653,12 @@ describe("resolveTrivialWhitespace", () => {
|
|||||||
expect(String(addCall![0])).toContain("src/utils.ts");
|
expect(String(addCall![0])).toContain("src/utils.ts");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws on git error", () => {
|
it("throws on git error", async () => {
|
||||||
mockedExecSync.mockImplementation(() => {
|
mockedExecSync.mockImplementation(() => {
|
||||||
throw new Error("add failed");
|
throw new Error("add failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(() => resolveTrivialWhitespace("file.ts", "/tmp/root")).toThrow(
|
await expect(resolveTrivialWhitespace("file.ts", "/tmp/root")).rejects.toThrow(
|
||||||
"Failed to auto-resolve",
|
"Failed to auto-resolve",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1669,17 +1694,17 @@ describe("isTrivialWhitespaceConflict", () => {
|
|||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns true when diff contains only whitespace changes", () => {
|
it("returns true when diff contains only whitespace changes", async () => {
|
||||||
// Mock git diff-tree to return empty diff (no content changes)
|
// Mock git diff-tree to return empty diff (no content changes)
|
||||||
mockedExecSync.mockReturnValue(
|
mockedExecSync.mockReturnValue(
|
||||||
"diff --git a/file.ts b/file.ts\nindex 123..456 100644\n--- a/file.ts\n+++ b/file.ts\n"
|
"diff --git a/file.ts b/file.ts\nindex 123..456 100644\n--- a/file.ts\n+++ b/file.ts\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = isTrivialWhitespaceConflict("src/file.ts", "/tmp/root");
|
const result = await isTrivialWhitespaceConflict("src/file.ts", "/tmp/root");
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns false when diff contains content changes", () => {
|
it("returns false when diff contains content changes", async () => {
|
||||||
// Mock git diff-tree to return diff with actual content changes
|
// Mock git diff-tree to return diff with actual content changes
|
||||||
mockedExecSync.mockImplementation(() => {
|
mockedExecSync.mockImplementation(() => {
|
||||||
const error = new Error("exit code 1") as any;
|
const error = new Error("exit code 1") as any;
|
||||||
@@ -1692,35 +1717,35 @@ describe("isTrivialWhitespaceConflict", () => {
|
|||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = isTrivialWhitespaceConflict("src/file.ts", "/tmp/root");
|
const result = await isTrivialWhitespaceConflict("src/file.ts", "/tmp/root");
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns true when only line endings differ (CRLF vs LF)", () => {
|
it("returns true when only line endings differ (CRLF vs LF)", async () => {
|
||||||
// Mock git diff-tree -w to show no content changes (whitespace ignored)
|
// Mock git diff-tree -w to show no content changes (whitespace ignored)
|
||||||
mockedExecSync.mockReturnValue(
|
mockedExecSync.mockReturnValue(
|
||||||
"diff --git a/file.ts b/file.ts\nindex 123..456 100644\n--- a/file.ts\n+++ b/file.ts\n"
|
"diff --git a/file.ts b/file.ts\nindex 123..456 100644\n--- a/file.ts\n+++ b/file.ts\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = isTrivialWhitespaceConflict("src/file.ts", "/tmp/root");
|
const result = await isTrivialWhitespaceConflict("src/file.ts", "/tmp/root");
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns false when git diff-tree fails unexpectedly", () => {
|
it("returns false when git diff-tree fails unexpectedly", async () => {
|
||||||
mockedExecSync.mockImplementation(() => {
|
mockedExecSync.mockImplementation(() => {
|
||||||
throw new Error("fatal: not a git repository");
|
throw new Error("fatal: not a git repository");
|
||||||
});
|
});
|
||||||
// Mock readFileSync for the fallback
|
// Mock readFileSync for the fallback
|
||||||
mockedReadFileSync.mockReturnValue("content without conflict markers");
|
mockedReadFileSync.mockReturnValue("content without conflict markers");
|
||||||
|
|
||||||
const result = isTrivialWhitespaceConflict("src/file.ts", "/tmp/root");
|
const result = await isTrivialWhitespaceConflict("src/file.ts", "/tmp/root");
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls git diff-tree with correct index references (:2: and :3:)", () => {
|
it("calls git diff-tree with correct index references (:2: and :3:)", async () => {
|
||||||
mockedExecSync.mockReturnValue("");
|
mockedExecSync.mockReturnValue("");
|
||||||
|
|
||||||
isTrivialWhitespaceConflict("src/utils.ts", "/tmp/root");
|
await isTrivialWhitespaceConflict("src/utils.ts", "/tmp/root");
|
||||||
|
|
||||||
const call = mockedExecSync.mock.calls.find((call) =>
|
const call = mockedExecSync.mock.calls.find((call) =>
|
||||||
String(call[0]).includes("git diff-tree")
|
String(call[0]).includes("git diff-tree")
|
||||||
@@ -3356,6 +3381,12 @@ describe("aiMergeTask — context limit recovery with truncation", () => {
|
|||||||
throw new Error("merge conflict");
|
throw new Error("merge conflict");
|
||||||
}
|
}
|
||||||
if (cmdStr.includes("diff --name-only --diff-filter=U")) return "src/file.ts";
|
if (cmdStr.includes("diff --name-only --diff-filter=U")) return "src/file.ts";
|
||||||
|
// git diff-tree for trivial whitespace detection - return real changes (non-trivial)
|
||||||
|
if (cmdStr.includes("diff-tree")) {
|
||||||
|
const error = new Error("exit code 1") as any;
|
||||||
|
error.stdout = "+const x = 2;\n-const x = 1;";
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
if (cmdStr.includes("reset --merge")) return Buffer.from("");
|
if (cmdStr.includes("reset --merge")) return Buffer.from("");
|
||||||
return Buffer.from("");
|
return Buffer.from("");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -140,13 +140,13 @@ function matchGlob(path: string, pattern: string): boolean {
|
|||||||
return regex.test(fileName) || regex.test(path);
|
return regex.test(fileName) || regex.test(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getStagedFiles(cwd: string): string[] {
|
export async function getStagedFiles(cwd: string): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const output = execSync("git diff --cached --name-only", {
|
const { stdout } = await execAsync("git diff --cached --name-only", {
|
||||||
cwd,
|
cwd,
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
stdio: "pipe",
|
});
|
||||||
}).trim();
|
const output = stdout.trim();
|
||||||
return output ? output.split("\n").filter(Boolean) : [];
|
return output ? output.split("\n").filter(Boolean) : [];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
@@ -544,12 +544,13 @@ export async function validateDiffScope(
|
|||||||
* Get list of conflicted files from git.
|
* Get list of conflicted files from git.
|
||||||
* Runs `git diff --name-only --diff-filter=U` and returns array of file paths.
|
* Runs `git diff --name-only --diff-filter=U` and returns array of file paths.
|
||||||
*/
|
*/
|
||||||
export function getConflictedFiles(cwd: string): string[] {
|
export async function getConflictedFiles(cwd: string): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const output = execSync("git diff --name-only --diff-filter=U", {
|
const { stdout } = await execAsync("git diff --name-only --diff-filter=U", {
|
||||||
cwd,
|
cwd,
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
}).trim();
|
});
|
||||||
|
const output = stdout.trim();
|
||||||
|
|
||||||
if (!output) return [];
|
if (!output) return [];
|
||||||
return output.split("\n").filter(Boolean);
|
return output.split("\n").filter(Boolean);
|
||||||
@@ -562,19 +563,19 @@ export function getConflictedFiles(cwd: string): string[] {
|
|||||||
* Check if a file has only trivial whitespace conflicts using git.
|
* Check if a file has only trivial whitespace conflicts using git.
|
||||||
* Compares ours (:2) and theirs (:3) versions with whitespace ignored.
|
* Compares ours (:2) and theirs (:3) versions with whitespace ignored.
|
||||||
*/
|
*/
|
||||||
export function isTrivialWhitespaceConflict(filePath: string, cwd: string): boolean {
|
export async function isTrivialWhitespaceConflict(filePath: string, cwd: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// Use git diff-tree to compare index entries with whitespace ignored
|
// Use git diff-tree to compare index entries with whitespace ignored
|
||||||
// :2 = ours (current branch), :3 = theirs (incoming branch)
|
// :2 = ours (current branch), :3 = theirs (incoming branch)
|
||||||
// -w flag ignores whitespace
|
// -w flag ignores whitespace
|
||||||
const result = execSync(
|
const { stdout } = await execAsync(
|
||||||
`git diff-tree -p -w -- :2:"${filePath}" :3:"${filePath}"`,
|
`git diff-tree -p -w -- :2:"${filePath}" :3:"${filePath}"`,
|
||||||
{ cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }
|
{ cwd, encoding: "utf-8" }
|
||||||
);
|
);
|
||||||
|
|
||||||
// If the diff output is empty or contains no actual changes, it's trivial
|
// If the diff output is empty or contains no actual changes, it's trivial
|
||||||
// The diff output will have headers but no +/- content lines for whitespace-only changes
|
// The diff output will have headers but no +/- content lines for whitespace-only changes
|
||||||
const lines = result.split("\n");
|
const lines = stdout.split("\n");
|
||||||
const contentChanges = lines.filter(
|
const contentChanges = lines.filter(
|
||||||
(line: string) => (line.startsWith("+") || line.startsWith("-")) &&
|
(line: string) => (line.startsWith("+") || line.startsWith("-")) &&
|
||||||
!line.startsWith("+++") && !line.startsWith("---")
|
!line.startsWith("+++") && !line.startsWith("---")
|
||||||
@@ -601,7 +602,7 @@ export function isTrivialWhitespaceConflict(filePath: string, cwd: string): bool
|
|||||||
* Classify a single conflicted file for auto-resolution.
|
* Classify a single conflicted file for auto-resolution.
|
||||||
* Returns one of: 'lockfile-ours', 'generated-theirs', 'trivial-whitespace', 'complex'
|
* Returns one of: 'lockfile-ours', 'generated-theirs', 'trivial-whitespace', 'complex'
|
||||||
*/
|
*/
|
||||||
export function classifyConflict(filePath: string, cwd: string): ConflictType {
|
export async function classifyConflict(filePath: string, cwd: string): Promise<ConflictType> {
|
||||||
// Check for lock files - always take "ours" (current branch's version)
|
// Check for lock files - always take "ours" (current branch's version)
|
||||||
if (LOCKFILE_PATTERNS.some((pattern) => matchGlob(filePath, pattern))) {
|
if (LOCKFILE_PATTERNS.some((pattern) => matchGlob(filePath, pattern))) {
|
||||||
return "lockfile-ours";
|
return "lockfile-ours";
|
||||||
@@ -613,7 +614,7 @@ export function classifyConflict(filePath: string, cwd: string): ConflictType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for trivial conflicts (whitespace-only)
|
// Check for trivial conflicts (whitespace-only)
|
||||||
if (isTrivialWhitespaceConflict(filePath, cwd)) {
|
if (await isTrivialWhitespaceConflict(filePath, cwd)) {
|
||||||
return "trivial-whitespace";
|
return "trivial-whitespace";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,10 +626,10 @@ export function classifyConflict(filePath: string, cwd: string): ConflictType {
|
|||||||
* Resolve a conflicted file using "ours" (current branch's version).
|
* Resolve a conflicted file using "ours" (current branch's version).
|
||||||
* Runs `git checkout --ours` and `git add`.
|
* Runs `git checkout --ours` and `git add`.
|
||||||
*/
|
*/
|
||||||
export function resolveWithOurs(filePath: string, cwd: string): void {
|
export async function resolveWithOurs(filePath: string, cwd: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
execSync(`git checkout --ours "${filePath}"`, { cwd, stdio: "pipe" });
|
await execAsync(`git checkout --ours "${filePath}"`, { cwd });
|
||||||
execSync(`git add "${filePath}"`, { cwd, stdio: "pipe" });
|
await execAsync(`git add "${filePath}"`, { cwd });
|
||||||
mergerLog.log(`Auto-resolved ${filePath} using --ours`);
|
mergerLog.log(`Auto-resolved ${filePath} using --ours`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to auto-resolve ${filePath} with ours: ${error}`);
|
throw new Error(`Failed to auto-resolve ${filePath} with ours: ${error}`);
|
||||||
@@ -639,10 +640,10 @@ export function resolveWithOurs(filePath: string, cwd: string): void {
|
|||||||
* Resolve a conflicted file using "theirs" (incoming branch's version).
|
* Resolve a conflicted file using "theirs" (incoming branch's version).
|
||||||
* Runs `git checkout --theirs` and `git add`.
|
* Runs `git checkout --theirs` and `git add`.
|
||||||
*/
|
*/
|
||||||
export function resolveWithTheirs(filePath: string, cwd: string): void {
|
export async function resolveWithTheirs(filePath: string, cwd: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
execSync(`git checkout --theirs "${filePath}"`, { cwd, stdio: "pipe" });
|
await execAsync(`git checkout --theirs "${filePath}"`, { cwd });
|
||||||
execSync(`git add "${filePath}"`, { cwd, stdio: "pipe" });
|
await execAsync(`git add "${filePath}"`, { cwd });
|
||||||
mergerLog.log(`Auto-resolved ${filePath} using --theirs`);
|
mergerLog.log(`Auto-resolved ${filePath} using --theirs`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to auto-resolve ${filePath} with theirs: ${error}`);
|
throw new Error(`Failed to auto-resolve ${filePath} with theirs: ${error}`);
|
||||||
@@ -653,9 +654,9 @@ export function resolveWithTheirs(filePath: string, cwd: string): void {
|
|||||||
* Resolve a trivial whitespace conflict.
|
* Resolve a trivial whitespace conflict.
|
||||||
* For trivial conflicts, we can just stage the file (git considers it resolved).
|
* For trivial conflicts, we can just stage the file (git considers it resolved).
|
||||||
*/
|
*/
|
||||||
export function resolveTrivialWhitespace(filePath: string, cwd: string): void {
|
export async function resolveTrivialWhitespace(filePath: string, cwd: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
execSync(`git add "${filePath}"`, { cwd, stdio: "pipe" });
|
await execAsync(`git add "${filePath}"`, { cwd });
|
||||||
mergerLog.log(`Auto-resolved ${filePath} (trivial whitespace)`);
|
mergerLog.log(`Auto-resolved ${filePath} (trivial whitespace)`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to auto-resolve ${filePath} trivial conflict: ${error}`);
|
throw new Error(`Failed to auto-resolve ${filePath} trivial conflict: ${error}`);
|
||||||
@@ -678,36 +679,42 @@ export interface ConflictCategory {
|
|||||||
* Detect and categorize merge conflicts. Delegates to the new classifyConflict API.
|
* Detect and categorize merge conflicts. Delegates to the new classifyConflict API.
|
||||||
* @deprecated Use getConflictedFiles() + classifyConflict() instead.
|
* @deprecated Use getConflictedFiles() + classifyConflict() instead.
|
||||||
*/
|
*/
|
||||||
export function detectResolvableConflicts(rootDir: string): ConflictCategory[] {
|
export async function detectResolvableConflicts(rootDir: string): Promise<ConflictCategory[]> {
|
||||||
const files = getConflictedFiles(rootDir);
|
const files = await getConflictedFiles(rootDir);
|
||||||
return files.map((filePath): ConflictCategory => {
|
const results: ConflictCategory[] = [];
|
||||||
const type = classifyConflict(filePath, rootDir);
|
for (const filePath of files) {
|
||||||
|
const type = await classifyConflict(filePath, rootDir);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "lockfile-ours":
|
case "lockfile-ours":
|
||||||
return { filePath, autoResolvable: true, strategy: "ours", reason: "lock-file" };
|
results.push({ filePath, autoResolvable: true, strategy: "ours", reason: "lock-file" });
|
||||||
|
break;
|
||||||
case "generated-theirs":
|
case "generated-theirs":
|
||||||
return { filePath, autoResolvable: true, strategy: "theirs", reason: "generated-file" };
|
results.push({ filePath, autoResolvable: true, strategy: "theirs", reason: "generated-file" });
|
||||||
|
break;
|
||||||
case "trivial-whitespace":
|
case "trivial-whitespace":
|
||||||
return { filePath, autoResolvable: true, strategy: "ours", reason: "trivial" };
|
results.push({ filePath, autoResolvable: true, strategy: "ours", reason: "trivial" });
|
||||||
|
break;
|
||||||
case "complex":
|
case "complex":
|
||||||
return { filePath, autoResolvable: false, reason: "complex" };
|
results.push({ filePath, autoResolvable: false, reason: "complex" });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-resolve a single file using git checkout --ours or --theirs.
|
* Auto-resolve a single file using git checkout --ours or --theirs.
|
||||||
* @deprecated Use resolveWithOurs() or resolveWithTheirs() instead.
|
* @deprecated Use resolveWithOurs() or resolveWithTheirs() instead.
|
||||||
*/
|
*/
|
||||||
export function autoResolveFile(
|
export async function autoResolveFile(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
resolution: ConflictResolution,
|
resolution: ConflictResolution,
|
||||||
rootDir: string,
|
rootDir: string,
|
||||||
): void {
|
): Promise<void> {
|
||||||
if (resolution === "ours") {
|
if (resolution === "ours") {
|
||||||
resolveWithOurs(filePath, rootDir);
|
await resolveWithOurs(filePath, rootDir);
|
||||||
} else {
|
} else {
|
||||||
resolveWithTheirs(filePath, rootDir);
|
await resolveWithTheirs(filePath, rootDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,14 +722,14 @@ export function autoResolveFile(
|
|||||||
* Auto-resolve all resolvable conflicts from the categorization.
|
* Auto-resolve all resolvable conflicts from the categorization.
|
||||||
* @deprecated Use classifyConflict + resolveWithOurs/resolveWithTheirs instead.
|
* @deprecated Use classifyConflict + resolveWithOurs/resolveWithTheirs instead.
|
||||||
*/
|
*/
|
||||||
export function resolveConflicts(
|
export async function resolveConflicts(
|
||||||
categories: ConflictCategory[],
|
categories: ConflictCategory[],
|
||||||
rootDir: string,
|
rootDir: string,
|
||||||
): string[] {
|
): Promise<string[]> {
|
||||||
const remainingComplex: string[] = [];
|
const remainingComplex: string[] = [];
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
if (category.autoResolvable && category.strategy) {
|
if (category.autoResolvable && category.strategy) {
|
||||||
autoResolveFile(category.filePath, category.strategy, rootDir);
|
await autoResolveFile(category.filePath, category.strategy, rootDir);
|
||||||
} else {
|
} else {
|
||||||
remainingComplex.push(category.filePath);
|
remainingComplex.push(category.filePath);
|
||||||
}
|
}
|
||||||
@@ -996,9 +1003,8 @@ export async function aiMergeTask(
|
|||||||
}).trim().replace(/^origin\//, "");
|
}).trim().replace(/^origin\//, "");
|
||||||
if (currentBranch !== mainBranch) {
|
if (currentBranch !== mainBranch) {
|
||||||
mergerLog.log(`${taskId}: rootDir on '${currentBranch}', checking out '${mainBranch}' before merge`);
|
mergerLog.log(`${taskId}: rootDir on '${currentBranch}', checking out '${mainBranch}' before merge`);
|
||||||
execSync(`git checkout "${mainBranch}"`, {
|
await execAsync(`git checkout "${mainBranch}"`, {
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
stdio: "pipe",
|
|
||||||
});
|
});
|
||||||
// Audit trail: record git checkout (FN-1404)
|
// Audit trail: record git checkout (FN-1404)
|
||||||
await audit.git({ type: "branch:checkout", target: mainBranch });
|
await audit.git({ type: "branch:checkout", target: mainBranch });
|
||||||
@@ -1006,7 +1012,7 @@ export async function aiMergeTask(
|
|||||||
} catch {
|
} catch {
|
||||||
// Fallback: try checking out main directly
|
// Fallback: try checking out main directly
|
||||||
try {
|
try {
|
||||||
execSync("git checkout main", { cwd: rootDir, stdio: "pipe" });
|
await execAsync("git checkout main", { cwd: rootDir });
|
||||||
// Audit trail: record git checkout (FN-1404)
|
// Audit trail: record git checkout (FN-1404)
|
||||||
await audit.git({ type: "branch:checkout", target: "main" });
|
await audit.git({ type: "branch:checkout", target: "main" });
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1018,22 +1024,25 @@ export async function aiMergeTask(
|
|||||||
let commitLog = "";
|
let commitLog = "";
|
||||||
let diffStat = "";
|
let diffStat = "";
|
||||||
try {
|
try {
|
||||||
commitLog = execSync(`git log HEAD..${branch} --format="- %s"`, {
|
const { stdout: logOutput } = await execAsync(`git log HEAD..${branch} --format="- %s"`, {
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
}).trim();
|
});
|
||||||
|
commitLog = logOutput.trim();
|
||||||
} catch {
|
} catch {
|
||||||
commitLog = "(unable to read commit log)";
|
commitLog = "(unable to read commit log)";
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const mergeBase = execSync(`git merge-base HEAD ${branch}`, {
|
const { stdout: mergeBaseOutput } = await execAsync(`git merge-base HEAD ${branch}`, {
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
}).trim();
|
});
|
||||||
diffStat = execSync(`git diff ${mergeBase}..${branch} --stat`, {
|
const mergeBase = mergeBaseOutput.trim();
|
||||||
|
const { stdout: diffOutput } = await execAsync(`git diff ${mergeBase}..${branch} --stat`, {
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
}).trim();
|
});
|
||||||
|
diffStat = diffOutput.trim();
|
||||||
} catch {
|
} catch {
|
||||||
diffStat = "(unable to read diff)";
|
diffStat = "(unable to read diff)";
|
||||||
}
|
}
|
||||||
@@ -1182,12 +1191,11 @@ export async function aiMergeTask(
|
|||||||
let deletions: number | undefined;
|
let deletions: number | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const statsOutput = execSync("git show --shortstat --format= HEAD", {
|
const { stdout: statsOutput } = await execAsync("git show --shortstat --format= HEAD", {
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
stdio: "pipe",
|
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
}).trim();
|
});
|
||||||
const normalized = statsOutput.replace(/\n/g, " ");
|
const normalized = statsOutput.trim().replace(/\n/g, " ");
|
||||||
const filesMatch = normalized.match(/(\d+) files? changed/);
|
const filesMatch = normalized.match(/(\d+) files? changed/);
|
||||||
const insertionsMatch = normalized.match(/(\d+) insertions?\(\+\)/);
|
const insertionsMatch = normalized.match(/(\d+) insertions?\(\+\)/);
|
||||||
const deletionsMatch = normalized.match(/(\d+) deletions?\(-\)/);
|
const deletionsMatch = normalized.match(/(\d+) deletions?\(-\)/);
|
||||||
@@ -1218,13 +1226,13 @@ export async function aiMergeTask(
|
|||||||
|
|
||||||
// 6. Delete branch
|
// 6. Delete branch
|
||||||
try {
|
try {
|
||||||
execSync(`git branch -d "${branch}"`, { cwd: rootDir, stdio: "pipe" });
|
await execAsync(`git branch -d "${branch}"`, { cwd: rootDir });
|
||||||
result.branchDeleted = true;
|
result.branchDeleted = true;
|
||||||
// Audit trail: record branch deletion (FN-1404)
|
// Audit trail: record branch deletion (FN-1404)
|
||||||
await audit.git({ type: "branch:delete", target: branch });
|
await audit.git({ type: "branch:delete", target: branch });
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
try {
|
||||||
execSync(`git branch -D "${branch}"`, { cwd: rootDir, stdio: "pipe" });
|
await execAsync(`git branch -D "${branch}"`, { cwd: rootDir });
|
||||||
result.branchDeleted = true;
|
result.branchDeleted = true;
|
||||||
// Audit trail: record branch deletion (force) (FN-1404)
|
// Audit trail: record branch deletion (force) (FN-1404)
|
||||||
await audit.git({ type: "branch:delete", target: branch, metadata: { force: true } });
|
await audit.git({ type: "branch:delete", target: branch, metadata: { force: true } });
|
||||||
@@ -1242,9 +1250,8 @@ export async function aiMergeTask(
|
|||||||
result.worktreeRemoved = false;
|
result.worktreeRemoved = false;
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
execSync(`git worktree remove "${worktreePath}" --force`, {
|
await execAsync(`git worktree remove "${worktreePath}" --force`, {
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
stdio: "pipe",
|
|
||||||
});
|
});
|
||||||
// Audit trail: record worktree removal (FN-1404)
|
// Audit trail: record worktree removal (FN-1404)
|
||||||
await audit.git({ type: "worktree:remove", target: worktreePath });
|
await audit.git({ type: "worktree:remove", target: worktreePath });
|
||||||
@@ -1372,9 +1379,8 @@ async function executeMergeAttempt(
|
|||||||
// This is expected - we catch it and proceed with auto-resolution
|
// This is expected - we catch it and proceed with auto-resolution
|
||||||
let mergeExitedWithConflicts = false;
|
let mergeExitedWithConflicts = false;
|
||||||
try {
|
try {
|
||||||
execSync(`git merge --squash "${branch}"`, {
|
await execAsync(`git merge --squash "${branch}"`, {
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
stdio: "pipe",
|
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// Merge exits with code 1 when conflicts exist - this is expected
|
// Merge exits with code 1 when conflicts exist - this is expected
|
||||||
@@ -1382,13 +1388,14 @@ async function executeMergeAttempt(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use new API: get conflicted files and classify them
|
// Use new API: get conflicted files and classify them
|
||||||
const conflictedFiles = getConflictedFiles(rootDir);
|
const conflictedFiles = await getConflictedFiles(rootDir);
|
||||||
if (conflictedFiles.length > 0 || mergeExitedWithConflicts) {
|
if (conflictedFiles.length > 0 || mergeExitedWithConflicts) {
|
||||||
// Classify each conflicted file
|
// Classify each conflicted file
|
||||||
const classified = conflictedFiles.map((file) => ({
|
const classified: { file: string; type: ConflictType }[] = [];
|
||||||
file,
|
for (const file of conflictedFiles) {
|
||||||
type: classifyConflict(file, rootDir),
|
const type = await classifyConflict(file, rootDir);
|
||||||
}));
|
classified.push({ file, type });
|
||||||
|
}
|
||||||
|
|
||||||
const autoResolvable = classified.filter(
|
const autoResolvable = classified.filter(
|
||||||
(c) => c.type !== "complex",
|
(c) => c.type !== "complex",
|
||||||
@@ -1405,11 +1412,11 @@ async function executeMergeAttempt(
|
|||||||
for (const { file, type } of autoResolvable) {
|
for (const { file, type } of autoResolvable) {
|
||||||
try {
|
try {
|
||||||
if (type === "lockfile-ours") {
|
if (type === "lockfile-ours") {
|
||||||
resolveWithOurs(file, rootDir);
|
await resolveWithOurs(file, rootDir);
|
||||||
} else if (type === "generated-theirs") {
|
} else if (type === "generated-theirs") {
|
||||||
resolveWithTheirs(file, rootDir);
|
await resolveWithTheirs(file, rootDir);
|
||||||
} else if (type === "trivial-whitespace") {
|
} else if (type === "trivial-whitespace") {
|
||||||
resolveTrivialWhitespace(file, rootDir);
|
await resolveTrivialWhitespace(file, rootDir);
|
||||||
}
|
}
|
||||||
result.autoResolvedCount = (result.autoResolvedCount || 0) + 1;
|
result.autoResolvedCount = (result.autoResolvedCount || 0) + 1;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1431,9 +1438,9 @@ async function executeMergeAttempt(
|
|||||||
if (staged !== "0") {
|
if (staged !== "0") {
|
||||||
const escapedLog = commitLog.replace(/"/g, '\\"');
|
const escapedLog = commitLog.replace(/"/g, '\\"');
|
||||||
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
||||||
execSync(
|
await execAsync(
|
||||||
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"`,
|
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"`,
|
||||||
{ cwd: rootDir, stdio: "pipe" },
|
{ cwd: rootDir },
|
||||||
);
|
);
|
||||||
mergerLog.log(`${taskId}: committed after auto-resolving all conflicts`);
|
mergerLog.log(`${taskId}: committed after auto-resolving all conflicts`);
|
||||||
}
|
}
|
||||||
@@ -1465,9 +1472,8 @@ async function executeMergeAttempt(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Attempt 1: Standard merge
|
// Attempt 1: Standard merge
|
||||||
execSync(`git merge --squash "${branch}"`, {
|
await execAsync(`git merge --squash "${branch}"`, {
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
stdio: "pipe",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if squash is empty
|
// Check if squash is empty
|
||||||
@@ -1504,7 +1510,7 @@ async function executeMergeAttempt(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (buildCommand) {
|
if (buildCommand) {
|
||||||
const stagedFiles = getStagedFiles(rootDir);
|
const stagedFiles = await getStagedFiles(rootDir);
|
||||||
if (shouldSyncDependenciesForMerge(stagedFiles, hasInstallState(rootDir))) {
|
if (shouldSyncDependenciesForMerge(stagedFiles, hasInstallState(rootDir))) {
|
||||||
await syncDependenciesForMerge(store, rootDir, taskId);
|
await syncDependenciesForMerge(store, rootDir, taskId);
|
||||||
}
|
}
|
||||||
@@ -1583,9 +1589,8 @@ async function attemptWithTheirsStrategy(params: MergeAttemptParams): Promise<bo
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Use -X theirs to auto-resolve conflicts favoring the incoming branch
|
// Use -X theirs to auto-resolve conflicts favoring the incoming branch
|
||||||
execSync(`git merge -X theirs --squash "${branch}"`, {
|
await execAsync(`git merge -X theirs --squash "${branch}"`, {
|
||||||
cwd: rootDir,
|
cwd: rootDir,
|
||||||
stdio: "pipe",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check if there are still conflicts (some types can't be auto-resolved)
|
// Check if there are still conflicts (some types can't be auto-resolved)
|
||||||
@@ -1617,9 +1622,9 @@ async function attemptWithTheirsStrategy(params: MergeAttemptParams): Promise<bo
|
|||||||
// Commit with fallback message
|
// Commit with fallback message
|
||||||
const escapedLog = commitLog.replace(/"/g, '\\"');
|
const escapedLog = commitLog.replace(/"/g, '\\"');
|
||||||
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
||||||
execSync(
|
await execAsync(
|
||||||
`git commit -m "${fallbackPrefix}: merge ${branch} (auto-resolved)" -m "${escapedLog}"`,
|
`git commit -m "${fallbackPrefix}: merge ${branch} (auto-resolved)" -m "${escapedLog}"`,
|
||||||
{ cwd: rootDir, stdio: "pipe" },
|
{ cwd: rootDir },
|
||||||
);
|
);
|
||||||
mergerLog.log(`${taskId}: committed with -X theirs auto-resolution`);
|
mergerLog.log(`${taskId}: committed with -X theirs auto-resolution`);
|
||||||
|
|
||||||
@@ -1882,9 +1887,9 @@ async function runAiAgentForCommit(params: AiAgentParams): Promise<{ success: bo
|
|||||||
mergerLog.log("Agent didn't commit — committing with fallback message");
|
mergerLog.log("Agent didn't commit — committing with fallback message");
|
||||||
const escapedLog = commitLog.replace(/"/g, '\\"');
|
const escapedLog = commitLog.replace(/"/g, '\\"');
|
||||||
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
const fallbackPrefix = includeTaskId ? `feat(${taskId})` : "feat";
|
||||||
execSync(
|
await execAsync(
|
||||||
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"`,
|
`git commit -m "${fallbackPrefix}: merge ${branch}" -m "${escapedLog}"`,
|
||||||
{ cwd: rootDir, stdio: "pipe" },
|
{ cwd: rootDir },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// Build command was configured but agent didn't commit and didn't report failure
|
// Build command was configured but agent didn't commit and didn't report failure
|
||||||
|
|||||||
Reference in New Issue
Block a user