mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
fix(projects): preserve exact placement identity
This commit is contained in:
@@ -13,28 +13,13 @@ function gitWorkspace(
|
||||
projectId: "project-1",
|
||||
projectKind: "git",
|
||||
projectRootPath: "/repo/project-1",
|
||||
project: { checkout: { mainRepoRoot: "/repo/main-project-1" } },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("selectActiveGitWorkspaceProject", () => {
|
||||
it("selects the active git workspace project from checkout metadata", () => {
|
||||
it("selects the exact active git workspace project root", () => {
|
||||
expect(selectActiveGitWorkspaceProject("server-1", gitWorkspace())).toEqual({
|
||||
serverId: "server-1",
|
||||
projectId: "project-1",
|
||||
projectKey: "project-1",
|
||||
repoRoot: "/repo/main-project-1",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the workspace project root when checkout metadata has no main root", () => {
|
||||
expect(
|
||||
selectActiveGitWorkspaceProject(
|
||||
"server-1",
|
||||
gitWorkspace({ project: { checkout: { mainRepoRoot: null } } }),
|
||||
),
|
||||
).toEqual({
|
||||
serverId: "server-1",
|
||||
projectId: "project-1",
|
||||
projectKey: "project-1",
|
||||
@@ -65,10 +50,7 @@ describe("selectActiveGitWorkspaceProject", () => {
|
||||
null,
|
||||
);
|
||||
expect(
|
||||
selectActiveGitWorkspaceProject(
|
||||
"server-1",
|
||||
gitWorkspace({ projectRootPath: " ", project: null }),
|
||||
),
|
||||
selectActiveGitWorkspaceProject("server-1", gitWorkspace({ projectRootPath: " " })),
|
||||
).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,11 +9,6 @@ export interface WorktreeSetupWorkspaceInput {
|
||||
projectGroupKey?: string | null;
|
||||
projectKind: string;
|
||||
projectRootPath: string;
|
||||
project?: {
|
||||
checkout?: {
|
||||
mainRepoRoot?: string | null;
|
||||
} | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface ActiveGitWorkspaceProject {
|
||||
@@ -53,7 +48,7 @@ export function selectActiveGitWorkspaceProject(
|
||||
projectId,
|
||||
projectGroupKey: workspace.projectGroupKey,
|
||||
});
|
||||
const repoRoot = (workspace.project?.checkout?.mainRepoRoot ?? workspace.projectRootPath).trim();
|
||||
const repoRoot = workspace.projectRootPath.trim();
|
||||
if (!projectId || !repoRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ export function resolveProjectGroupKey(input: {
|
||||
// COMPAT(projectGroupKey): added in v0.2.4 on 2026-07-28; remove after 2027-01-28.
|
||||
// Older daemons used remote/path-shaped project IDs as their grouping key. New opaque IDs
|
||||
// must remain host-local when the new field is absent.
|
||||
return input.projectId.startsWith("prj_")
|
||||
? `host:${input.serverId}:project:${input.projectId}`
|
||||
: input.projectId;
|
||||
return input.projectId.startsWith("prj_") ? frameHostProjectKey(input) : input.projectId;
|
||||
}
|
||||
|
||||
export function frameHostProjectKey(input: { serverId: string; projectId: string }): string {
|
||||
return `host:${encodeURIComponent(input.serverId)}:project:${encodeURIComponent(input.projectId)}`;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { frameHostProjectKey } from "./project-group-key";
|
||||
|
||||
interface ProjectSettingsTarget {
|
||||
projectKey: string;
|
||||
hosts: ReadonlyArray<{ serverId: string; projectId?: string; isOnline?: boolean }>;
|
||||
@@ -9,7 +11,7 @@ export function resolveHostProjectSettingsRouteKey(host: {
|
||||
}): string | null {
|
||||
const projectId = host.projectId?.trim();
|
||||
if (!projectId) return null;
|
||||
return `host:${encodeURIComponent(host.serverId)}:project:${encodeURIComponent(projectId)}`;
|
||||
return frameHostProjectKey({ serverId: host.serverId, projectId });
|
||||
}
|
||||
|
||||
export function resolveProjectSettingsRouteKey(project: ProjectSettingsTarget): string {
|
||||
|
||||
@@ -6,10 +6,11 @@ function workspace(input: {
|
||||
id: string;
|
||||
projectName: string;
|
||||
projectCustomName: string | null;
|
||||
projectId?: string;
|
||||
}): WorkspaceDescriptor {
|
||||
return {
|
||||
id: input.id,
|
||||
projectId: `project-${input.id}`,
|
||||
projectId: input.projectId ?? `project-${input.id}`,
|
||||
projectGroupKey: "remote:github.com/acme/app",
|
||||
projectDisplayName: input.projectName,
|
||||
projectCustomName: input.projectCustomName,
|
||||
@@ -50,4 +51,42 @@ describe("buildWorkspaceStructureProjects", () => {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("frames ambiguous host placements without alias collisions", () => {
|
||||
const projects = buildWorkspaceStructureProjects({
|
||||
sessions: [
|
||||
{
|
||||
serverId: "a",
|
||||
workspaces: [
|
||||
workspace({
|
||||
id: "a-1",
|
||||
projectId: "remote:x/foo:project:/c",
|
||||
projectName: "first",
|
||||
projectCustomName: null,
|
||||
}),
|
||||
workspace({
|
||||
id: "a-2",
|
||||
projectId: "other",
|
||||
projectName: "other",
|
||||
projectCustomName: null,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
serverId: "a:project:remote:x/foo",
|
||||
workspaces: [
|
||||
workspace({
|
||||
id: "b",
|
||||
projectId: "/c",
|
||||
projectName: "second",
|
||||
projectCustomName: null,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(projects).toHaveLength(3);
|
||||
expect(new Set(projects.map((project) => project.projectKey)).size).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { EmptyProjectDescriptor, WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { projectDisplayNameFromProjectId } from "@/utils/project-display-name";
|
||||
import { resolveProjectGroupKey } from "@/projects/project-group-key";
|
||||
import { frameHostProjectKey, resolveProjectGroupKey } from "@/projects/project-group-key";
|
||||
|
||||
export interface WorkspaceStructureHostPlacement {
|
||||
serverId: string;
|
||||
@@ -104,9 +104,7 @@ function resolveUnambiguousProjectGroupKey(input: {
|
||||
ambiguousGroupKeys: ReadonlySet<string>;
|
||||
}): string {
|
||||
const groupKey = resolveProjectGroupKey(input);
|
||||
return input.ambiguousGroupKeys.has(groupKey)
|
||||
? `host:${input.serverId}:project:${input.projectId}`
|
||||
: groupKey;
|
||||
return input.ambiguousGroupKeys.has(groupKey) ? frameHostProjectKey(input) : groupKey;
|
||||
}
|
||||
|
||||
export function buildWorkspaceStructureProjects(input: {
|
||||
|
||||
@@ -159,6 +159,21 @@ describe("deriveProjectGroupKey", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("preserves leading whitespace in SCP repository paths", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (remoteUrl: string) =>
|
||||
deriveProjectGroupKey({
|
||||
rootPath,
|
||||
remoteUrl,
|
||||
worktreeRoot: rootPath,
|
||||
mainRepoRoot: null,
|
||||
});
|
||||
|
||||
expect(derive("git@example.com: acme/repo.git")).not.toBe(
|
||||
derive("git@example.com:acme/repo.git"),
|
||||
);
|
||||
});
|
||||
|
||||
test("does not parse drive-relative Windows paths as SCP remotes", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
|
||||
|
||||
@@ -50,9 +50,8 @@ function encodeSelectedPath(selectedPath: string): string {
|
||||
}
|
||||
|
||||
function deriveRemoteProjectGroupKey(remoteUrl: string | null): string | null {
|
||||
const trimmed = remoteUrl?.trim();
|
||||
if (!trimmed) return null;
|
||||
const remote = parseRemoteLocation(trimmed);
|
||||
if (!remoteUrl?.trim()) return null;
|
||||
const remote = parseRemoteLocation(remoteUrl);
|
||||
if (!remote) return null;
|
||||
const cleanedPath = normalizeRemotePath(
|
||||
remote.path,
|
||||
@@ -143,7 +142,7 @@ function normalizeRemotePath(
|
||||
decodePercentEncoding: boolean,
|
||||
stripDotGitSuffix: boolean,
|
||||
): string {
|
||||
const trimmedPath = remotePath.trim().replace(/\/+$/, "");
|
||||
const trimmedPath = remotePath.replace(/\/+$/, "");
|
||||
const pathForEncoding = preserveLeadingSlash ? trimmedPath : trimmedPath.replace(/^\/+/, "");
|
||||
const segments = pathForEncoding.split("/");
|
||||
const decodedSegments = segments.map((segment) => {
|
||||
|
||||
Reference in New Issue
Block a user