fix(projects): preserve remote path semantics

This commit is contained in:
Mohamed Boudra
2026-07-29 00:43:52 +00:00
parent a3188a7699
commit d853df73a4
4 changed files with 55 additions and 6 deletions

View File

@@ -107,8 +107,8 @@ describe("buildWorktreeSetupCalloutPolicy", () => {
repoRoot: "/repo/project-1",
}),
).toEqual({
id: "worktree-setup-missing:project-1",
dismissalKey: "worktree-setup-missing:project-1",
id: "worktree-setup-missing:host:server-1:project:project-1",
dismissalKey: "worktree-setup-missing:host:server-1:project:project-1",
priority: 100,
title: "Set up worktree scripts",
description:
@@ -130,6 +130,23 @@ describe("buildWorktreeSetupCalloutPolicy", () => {
).toBe("/settings/projects/host%3Aserver-1%3Aproject%3Aprj_local");
});
it("keeps dismissals scoped to the host placement", () => {
const hostA = buildWorktreeSetupCalloutPolicy({
serverId: "host-a",
projectId: "project-a",
projectKey: "remote:github.com/acme/project",
repoRoot: "/host-a/project",
});
const hostB = buildWorktreeSetupCalloutPolicy({
serverId: "host-b",
projectId: "project-b",
projectKey: "remote:github.com/acme/project",
repoRoot: "/host-b/project",
});
expect(hostA.dismissalKey).not.toBe(hostB.dismissalKey);
});
it("scopes retained legacy project IDs to the active host", () => {
expect(
buildWorktreeSetupCalloutPolicy({

View File

@@ -68,11 +68,11 @@ export function shouldShowWorktreeSetupCallout(readResult: ReadProjectConfigResu
export function buildWorktreeSetupCalloutPolicy(
project: ActiveGitWorkspaceProject,
): WorktreeSetupCalloutPolicy {
const calloutKey = `worktree-setup-missing:${project.projectKey}`;
const projectSettingsKey = resolveHostProjectSettingsRouteKey({
serverId: project.serverId,
projectId: project.projectId,
});
const calloutKey = `worktree-setup-missing:${projectSettingsKey ?? project.projectKey}`;
return {
id: calloutKey,

View File

@@ -57,6 +57,32 @@ describe("deriveProjectGroupKey", () => {
).toBe("remote:github.com/getpaseo/paseo");
});
test("distinguishes absolute and home-relative SCP paths", () => {
const rootPath = path.resolve("repo");
const derive = (remoteUrl: string) =>
deriveProjectGroupKey({
rootPath,
remoteUrl,
worktreeRoot: rootPath,
mainRepoRoot: null,
});
expect(derive("example.com:srv/repo.git")).not.toBe(derive("example.com:/srv/repo.git"));
});
test.each(["git+ssh:", "ssh+git:"])("normalizes SSH alias default ports for %s", (scheme) => {
const rootPath = path.resolve("repo");
expect(
deriveProjectGroupKey({
rootPath,
remoteUrl: `${scheme}//git@github.com:22/getpaseo/paseo.git`,
worktreeRoot: rootPath,
mainRepoRoot: null,
}),
).toBe("remote:github.com/getpaseo/paseo");
});
test("accepts an SCP-style remote with a bracketed IPv6 host", () => {
const rootPath = path.resolve("repo");

View File

@@ -37,6 +37,7 @@ function deriveRemoteProjectGroupKey(remoteUrl: string | null): string | null {
let host: string | null = null;
let remotePath: string | null = null;
let preserveLeadingSlash = false;
const scpLike =
!trimmed.includes("://") && !/^[A-Za-z]:[\\/]/.test(trimmed)
? trimmed.match(/^(?:[^@/:]+@)?(\[[^\]]+\]|[^/:]+):(.+)$/)
@@ -44,6 +45,7 @@ function deriveRemoteProjectGroupKey(remoteUrl: string | null): string | null {
if (scpLike) {
host = scpLike[1] ?? null;
remotePath = scpLike[2] ?? null;
preserveLeadingSlash = remotePath?.startsWith("/") ?? false;
} else if (trimmed.includes("://")) {
try {
const parsed = new URL(trimmed);
@@ -55,13 +57,15 @@ function deriveRemoteProjectGroupKey(remoteUrl: string | null): string | null {
}
if (!host || !remotePath) return null;
const cleanedPath = normalizeRemotePath(remotePath);
const cleanedPath = normalizeRemotePath(remotePath, preserveLeadingSlash);
if (!cleanedPath) return null;
return `remote:${host.toLowerCase()}/${cleanedPath}`;
}
function normalizeRemotePath(remotePath: string): string {
const segments = remotePath.trim().replace(/^\/+/, "").replace(/\/+$/, "").split("/");
function normalizeRemotePath(remotePath: string, preserveLeadingSlash: boolean): string {
const trimmedPath = remotePath.trim().replace(/\/+$/, "");
const pathForEncoding = preserveLeadingSlash ? trimmedPath : trimmedPath.replace(/^\/+/, "");
const segments = pathForEncoding.split("/");
const decodedSegments = segments.map((segment) => {
try {
return decodeURIComponent(segment);
@@ -78,7 +82,9 @@ function normalizeRemotePath(remotePath: string): string {
function deriveRemoteHost(remoteUrl: URL): string | null {
const defaultPorts: Partial<Record<string, string>> = {
"git:": "9418",
"git+ssh:": "22",
"ssh:": "22",
"ssh+git:": "22",
};
if (remoteUrl.port === defaultPorts[remoteUrl.protocol]) return remoteUrl.hostname || null;
return remoteUrl.host || null;