feat(FN-2944): merge fusion/fn-2944

- test(FN-2944): cover already checked out worktree conflict recovery
- fix(FN-2944): recognize git already checked out worktree conflict
- fix(engine): auto-recover from squash-merge orphan rebase failures

Fusion-Task-Id: FN-2944
This commit is contained in:
Fusion
2026-04-29 07:10:52 -07:00
committed by gsxdsm
parent a74b77cec5
commit ba3d868c2d
46 changed files with 454 additions and 163 deletions

View File

@@ -0,0 +1,92 @@
export interface PathBreadcrumb {
label: string;
path: string;
}
export function normalizeDisplayPath(path: string): string {
return path.replace(/\\/g, "/");
}
function trimTrailingSeparators(path: string): string {
return path.replace(/[\\/]+$/g, "");
}
export function splitPathSegments(path: string): string[] {
const normalized = normalizeDisplayPath(trimTrailingSeparators(path));
if (!normalized) return [];
return normalized.split("/").filter(Boolean);
}
export function getPathBasename(path: string): string {
const normalized = normalizeDisplayPath(trimTrailingSeparators(path));
if (!normalized) return path;
const segments = normalized.split("/").filter(Boolean);
return segments[segments.length - 1] || normalized;
}
export function getTrailingPath(path: string, count: number): string {
const segments = splitPathSegments(path);
if (segments.length === 0) {
return normalizeDisplayPath(path);
}
return segments.slice(-count).join("/");
}
export function getDisplayDirname(path: string): string {
const normalized = normalizeDisplayPath(trimTrailingSeparators(path));
const lastSeparator = normalized.lastIndexOf("/");
if (lastSeparator < 0) return "";
return normalized.slice(0, lastSeparator + 1);
}
export function joinDisplayPath(basePath: string, name: string): string {
if (!basePath || basePath === ".") {
return name;
}
const segments = splitPathSegments(basePath);
return [...segments, name].join("/");
}
export function getParentDisplayPath(path: string): string {
const segments = splitPathSegments(path);
if (segments.length === 0) {
return ".";
}
segments.pop();
return segments.length === 0 ? "." : segments.join("/");
}
export function getPathBreadcrumbs(path: string): PathBreadcrumb[] {
const normalized = normalizeDisplayPath(path);
const winMatch = normalized.match(/^([A-Za-z]:)(?:\/(.*))?$/);
if (winMatch) {
const root = `${winMatch[1]}/`;
const segments = (winMatch[2] ?? "").split("/").filter(Boolean);
const breadcrumbs: PathBreadcrumb[] = [{ label: winMatch[1], path: root }];
for (let i = 0; i < segments.length; i += 1) {
breadcrumbs.push({
label: segments[i]!,
path: `${root}${segments.slice(0, i + 1).join("/")}`,
});
}
return breadcrumbs;
}
if (normalized.startsWith("/")) {
const segments = normalized.split("/").filter(Boolean);
const breadcrumbs: PathBreadcrumb[] = [{ label: "/", path: "/" }];
for (let i = 0; i < segments.length; i += 1) {
breadcrumbs.push({
label: segments[i]!,
path: `/${segments.slice(0, i + 1).join("/")}`,
});
}
return breadcrumbs;
}
const segments = normalized.split("/").filter(Boolean);
return segments.map((segment, index) => ({
label: segment,
path: segments.slice(0, index + 1).join("/"),
}));
}

View File

@@ -1,4 +1,5 @@
import type { Task } from "@fusion/core";
import { getPathBasename } from "./pathDisplay";
export interface WorktreeGroupData {
label: string;
@@ -11,9 +12,7 @@ export interface WorktreeGroupData {
* e.g. ".worktrees/FN-001" → "FN-001", "/path/to/fn/fn-001" → "fn-001"
*/
export function getWorktreeLabel(worktreePath: string): string {
// Take the last segment of the path
const segments = worktreePath.replace(/\/+$/, "").split("/");
return segments[segments.length - 1] || worktreePath;
return getPathBasename(worktreePath) || worktreePath;
}
/**