fix: handle Windows drive-letter paths across the codebase (#148)
* Add metrics collection and terminal performance tests
* fix: handle Windows drive-letter paths across the codebase
Windows paths like C:\Users\foo\project were broken in multiple places:
- agent-storage slugified D:\MyProject as D:-MyProject (illegal colon)
- terminal-manager rejected all non-/ paths as relative
- bootstrap parser misparsed drive colons as TCP host:port
- daemon/client connection helpers misclassified Windows paths
- CLI cwd filtering used hardcoded / separators
- checkout-git worktree detection used hardcoded / in path checks
- worktree archive used split("/").pop() instead of path.basename()
All path helpers now normalize separators and handle Windows
drive letters with case-insensitive comparison where needed.
This commit is contained in:
@@ -18,6 +18,9 @@ import {
|
||||
NotGitRepoError,
|
||||
pushCurrentBranch,
|
||||
resolveRepositoryDefaultBranch,
|
||||
parseWorktreeList,
|
||||
isPaseoWorktreePath,
|
||||
isDescendantPath,
|
||||
} from "./checkout-git.js";
|
||||
import { createWorktree } from "./worktree.js";
|
||||
import { getPaseoWorktreeMetadataPath } from "./worktree-metadata.js";
|
||||
@@ -812,4 +815,73 @@ const x = 1;
|
||||
).rejects.toThrow(/base/i);
|
||||
await expect(mergeToBase(worktree.worktreePath, {}, { paseoHome })).rejects.toThrow(/base/i);
|
||||
});
|
||||
|
||||
describe("parseWorktreeList", () => {
|
||||
it("parses porcelain worktree output", () => {
|
||||
const output = [
|
||||
"worktree /home/user/repo",
|
||||
"branch refs/heads/main",
|
||||
"",
|
||||
"worktree /home/user/.paseo/worktrees/feature",
|
||||
"branch refs/heads/feature",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const entries = parseWorktreeList(output);
|
||||
expect(entries).toHaveLength(2);
|
||||
expect(entries[0]).toEqual({ path: "/home/user/repo", branchRef: "refs/heads/main" });
|
||||
expect(entries[1]).toEqual({
|
||||
path: "/home/user/.paseo/worktrees/feature",
|
||||
branchRef: "refs/heads/feature",
|
||||
});
|
||||
});
|
||||
|
||||
it("detects bare repos", () => {
|
||||
const output = ["worktree /home/user/repo.git", "bare", ""].join("\n");
|
||||
const entries = parseWorktreeList(output);
|
||||
expect(entries).toHaveLength(1);
|
||||
expect(entries[0]?.isBare).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPaseoWorktreePath", () => {
|
||||
it("matches Unix .paseo/worktrees/ paths", () => {
|
||||
expect(isPaseoWorktreePath("/home/user/.paseo/worktrees/feature")).toBe(true);
|
||||
});
|
||||
|
||||
it("matches Windows .paseo\\worktrees\\ paths", () => {
|
||||
expect(isPaseoWorktreePath("C:\\Users\\dev\\.paseo\\worktrees\\feature")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects paths without .paseo/worktrees segment", () => {
|
||||
expect(isPaseoWorktreePath("/home/user/repo")).toBe(false);
|
||||
expect(isPaseoWorktreePath("C:\\Users\\dev\\repo")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isDescendantPath", () => {
|
||||
it("detects children with Unix separators", () => {
|
||||
expect(isDescendantPath("/home/user/repo/child", "/home/user/repo")).toBe(true);
|
||||
});
|
||||
|
||||
it("detects children with Windows separators", () => {
|
||||
expect(isDescendantPath("C:\\repos\\child", "C:\\repos")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects the parent itself", () => {
|
||||
expect(isDescendantPath("/home/user/repo", "/home/user/repo")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects siblings that share a prefix", () => {
|
||||
expect(isDescendantPath("/home/user/repo-extra", "/home/user/repo")).toBe(false);
|
||||
});
|
||||
|
||||
it("handles mixed separators", () => {
|
||||
expect(isDescendantPath("C:/repo/child", "C:\\repo")).toBe(true);
|
||||
});
|
||||
|
||||
it("is case insensitive on Windows paths", () => {
|
||||
expect(isDescendantPath("c:\\repo\\child", "C:\\repo")).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -603,20 +603,39 @@ async function getMainRepoRoot(cwd: string): Promise<string> {
|
||||
});
|
||||
const worktrees = parseWorktreeList(worktreeOut);
|
||||
const nonBareNonPaseo = worktrees.filter(
|
||||
(wt) => !wt.isBare && !wt.path.includes("/.paseo/worktrees/"),
|
||||
(wt) => !wt.isBare && !isPaseoWorktreePath(wt.path),
|
||||
);
|
||||
const childrenOfBareRepo = nonBareNonPaseo.filter((wt) => wt.path.startsWith(normalized + "/"));
|
||||
const childrenOfBareRepo = nonBareNonPaseo.filter((wt) => isDescendantPath(wt.path, normalized));
|
||||
const mainChild = childrenOfBareRepo.find((wt) => basename(wt.path) === "main");
|
||||
return mainChild?.path ?? childrenOfBareRepo[0]?.path ?? nonBareNonPaseo[0]?.path ?? normalized;
|
||||
}
|
||||
|
||||
type GitWorktreeEntry = {
|
||||
export type GitWorktreeEntry = {
|
||||
path: string;
|
||||
branchRef?: string;
|
||||
isBare?: boolean;
|
||||
};
|
||||
|
||||
function parseWorktreeList(output: string): GitWorktreeEntry[] {
|
||||
/** Check whether a path contains a `.paseo/worktrees/` segment (both `/` and `\`). */
|
||||
export function isPaseoWorktreePath(p: string): boolean {
|
||||
return /[/\\]\.paseo[/\\]worktrees[/\\]/.test(p);
|
||||
}
|
||||
|
||||
/** True when `child` is strictly inside `parent` (handles both `/` and `\`). */
|
||||
export function isDescendantPath(child: string, parent: string): boolean {
|
||||
let c = child.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
let p = parent.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
// Case-insensitive on Windows (drive letter like C: or D:)
|
||||
if (/^[A-Za-z]:/.test(c) || /^[A-Za-z]:/.test(p)) {
|
||||
c = c.toLowerCase();
|
||||
p = p.toLowerCase();
|
||||
}
|
||||
if (!c.startsWith(p)) return false;
|
||||
if (c.length === p.length) return false;
|
||||
return c[p.length] === "/";
|
||||
}
|
||||
|
||||
export function parseWorktreeList(output: string): GitWorktreeEntry[] {
|
||||
const entries: GitWorktreeEntry[] = [];
|
||||
let current: GitWorktreeEntry | null = null;
|
||||
for (const line of output.split("\n")) {
|
||||
|
||||
Reference in New Issue
Block a user