Files
fusion/packages/engine/src/merger-git-parse.ts
gsxdsm 06a5fd813d refactor: package code organization waves 6–7 (#2166)
## Summary

Waves 6–7 of package code organization (plan:
`docs/plans/2026-07-14-001-refactor-package-code-organization-plan.md`),
after #2148.

### Wave 6
| New module | Parent |
|---|---|
| `merger-autostash-labels.ts` | `merger.ts` |
| `types/agent-state.ts` | `types.ts` |
| `app/api/tasks-lifecycle.ts` | `legacy.ts` |
| `task-store/task-row-mappers.ts` | `remaining-ops-3.ts` (rename) |

### Wave 7
| New module | Parent |
|---|---|
| `self-healing-optional-step-revision.ts` | `self-healing.ts` |
| `self-healing-path-utils.ts` | `self-healing.ts` |
| `merger-git-parse` (+ `quoteArg`, `getBranchChangedFiles`) |
`merger.ts` |
| `app/api/settings.ts` | `legacy.ts` |

Public import paths stay stable via re-exports.

## Test plan
- [x] engine + dashboard typecheck (incl. app)
- [x] eslint on touched modules
- [x] merger-autostash / parse-porcelain / getBranchChanged / api-tasks
- [ ] CI merge gate

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a dedicated task lifecycle API client (task promotion, merge,
retry/reset/duplicate, pause/unpause, archive/unarchive, revert, plan
approve/reject) plus branch-group and planner oversight actions.
* Added a settings/config API service (effective task settings, update
check/refresh/install).
* Introduced standardized agent lifecycle states with identity/ephemeral
detection helpers.
* **Bug Fixes**
* Improved autostash label compatibility and NUL-delimited changed-file
detection for branch diffs.
* **Refactor**
* Modularized merger labeling/parsing, self-healing helpers, and
lifecycle/type wiring while keeping behavior consistent.
* **Tests**
  * Updated merger verification tests for `git diff -z` output handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-16 15:42:22 -07:00

104 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* FNXC:CodeOrganization 2026-07-16-00:30:
* Pure git/test-output parse helpers peeled from merger.ts (string parsing only).
*
* FNXC:CodeOrganization 2026-07-16-14:00:
* Also hosts getBranchChangedFiles + quoteArg (shell-safe git name-only diff).
*/
import { execFileSync } from "node:child_process";
export function parseFailingFilesFromOutput(output: string): string[] {
const paths = new Set<string>();
for (const line of output.split("\n")) {
// jest/vitest: "FAIL packages/engine/src/__tests__/foo.test.ts"
const failMatch = line.match(/^FAIL\s+(\S+)/);
if (failMatch && failMatch[1]) {
paths.add(failMatch[1]);
continue;
}
// vitest summary: " ❯ packages/engine/src/__tests__/foo.test.ts (2 tests | 1 failed)"
const vitestSummaryMatch = line.match(/^\s*[❯>]\s+(\S+\.(?:test|spec)\.[jt]sx?)\s/);
if (vitestSummaryMatch && vitestSummaryMatch[1]) {
paths.add(vitestSummaryMatch[1]);
continue;
}
// vitest: " × src/__tests__/foo.test.ts > some test name"
const crossMatch = line.match(/^\s*[×✕✗]\s+(\S+\.(?:test|spec)\.[jt]sx?)\s/);
if (crossMatch && crossMatch[1]) {
paths.add(crossMatch[1]);
}
}
return Array.from(paths);
}
/** Parse `git status -z --porcelain` into a Set of paths.
*
* Format per entry: `XY <space> <path>\0` where X = staged status, Y =
* unstaged status. Renames and copies are special: they emit TWO
* NUL-separated entries, `R <new>\0<old>\0` (or `C <new>\0<old>\0`).
* We must consume the trailing `<old>` entry without treating it as a
* separate path, otherwise observability code over-reports "cleared
* paths" with the historical names of renames. */
export function parsePorcelainZ(raw: string): Set<string> {
const paths = new Set<string>();
const entries = raw.split("\0");
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
if (!entry) continue;
if (entry.length < 4) continue;
const status = entry.slice(0, 2);
const path = entry.slice(3);
if (!path) continue;
paths.add(path);
// Rename/copy: the very next entry is the old path — skip it so it
// isn't mistaken for an independent dirty path.
if (status.charAt(0) === "R" || status.charAt(0) === "C") {
i++;
}
}
return paths;
}
export function parseShortstatSummary(statsOutput: string): { filesChanged: number; insertions: number; deletions: number } {
const normalized = statsOutput.trim().replace(/\n/g, " ");
const filesMatch = normalized.match(/(\d+) files? changed/);
const insertionsMatch = normalized.match(/(\d+) insertions?\(\+\)/);
const deletionsMatch = normalized.match(/(\d+) deletions?\(-\)/);
return {
filesChanged: filesMatch ? Number.parseInt(filesMatch[1], 10) : 0,
insertions: insertionsMatch ? Number.parseInt(insertionsMatch[1], 10) : 0,
deletions: deletionsMatch ? Number.parseInt(deletionsMatch[1], 10) : 0,
};
}
export function quoteArg(value: string): string {
return `"${value.replace(/(["\\$`])/g, "\\$1")}"`;
}
/**
* Get the set of files changed in the branch relative to the base branch.
* Uses `git diff --name-only -z <baseBranch>...HEAD` (three-dot range so it
* computes the diff from the merge-base, not the current HEAD of baseBranch).
*
* FNXC:CodeOrganization 2026-07-16-16:00:
* Invoke git via execFileSync argv (no shell) so branch names cannot break
* quoting on Windows cmd.exe, and parse NUL-delimited paths so whitespace/
* newlines in filenames are preserved. Empty array on git errors (unknown).
*
* @internal Exported for testing only.
*/
export function getBranchChangedFiles(rootDir: string, baseBranch: string, branch: string): string[] {
try {
const headRef = branch === "HEAD" ? "HEAD" : branch;
const output = execFileSync(
"git",
["diff", "--name-only", "-z", `${baseBranch}...${headRef}`],
{ cwd: rootDir, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] },
);
return String(output).split("\0").map((f) => f.trim()).filter(Boolean);
} catch {
return [];
}
}