mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-10 10:25:03 +00:00
fix(projects): restore project key as grouping identity
Random host-local project IDs had replaced the persisted cross-host value carried by projectKey. Keep projectId for routing while reconciliation owns and persists projectKey for grouping.
This commit is contained in:
@@ -75,7 +75,7 @@ test("creates a worktree and registers it in the source workspace project withou
|
||||
expect(deps.workspaceGitService.getSnapshot).not.toHaveBeenCalled();
|
||||
expect(deps.projects.get(sourceProject.projectId)).toEqual({
|
||||
...sourceProject,
|
||||
projectGroupKey: path.resolve(result.repoRoot),
|
||||
projectKey: path.resolve(result.repoRoot),
|
||||
updatedAt: expect.any(String),
|
||||
});
|
||||
expect(events).toEqual([`workspace:${result.workspace.workspaceId}`]);
|
||||
@@ -442,7 +442,7 @@ test("an explicit project FK remains unchanged when its worktree comes from anot
|
||||
expect(result.workspace.projectId).toBe(project.projectId);
|
||||
expect(deps.projects.get(project.projectId)).toEqual({
|
||||
...project,
|
||||
projectGroupKey: path.resolve(project.rootPath),
|
||||
projectKey: path.resolve(project.rootPath),
|
||||
updatedAt: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
+27
-27
@@ -2,14 +2,14 @@ import { mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { deriveProjectGroupKey, deriveProjectGroupingDisplayName } from "./project-group-key.js";
|
||||
import { deriveProjectKey, deriveProjectGroupingDisplayName } from "./project-key.js";
|
||||
|
||||
describe("deriveProjectGroupKey", () => {
|
||||
describe("deriveProjectKey", () => {
|
||||
test("preserves an explicit remote port", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
|
||||
expect(
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl: "https://git.example.com:8443/acme/app.git",
|
||||
worktreeRoot: rootPath,
|
||||
@@ -21,7 +21,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
test("normalizes the default SSH port", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (remoteUrl: string) =>
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl,
|
||||
worktreeRoot: rootPath,
|
||||
@@ -37,7 +37,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
|
||||
expect(
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl: "https://git.example.com/repo.git",
|
||||
worktreeRoot: rootPath,
|
||||
@@ -49,7 +49,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
test("preserves meaningful dot-git suffixes on unknown Git servers", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (remoteUrl: string) =>
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl,
|
||||
worktreeRoot: rootPath,
|
||||
@@ -62,7 +62,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
test("normalizes dot-git suffixes on known cloud forges", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (remoteUrl: string) =>
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl,
|
||||
worktreeRoot: rootPath,
|
||||
@@ -75,7 +75,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
test("normalizes GitHub owner and repository casing", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (remoteUrl: string) =>
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl,
|
||||
worktreeRoot: rootPath,
|
||||
@@ -92,7 +92,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
(remoteUrl) => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (value: string) =>
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl: value,
|
||||
worktreeRoot: rootPath,
|
||||
@@ -107,7 +107,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
|
||||
expect(
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl: "github.com:getpaseo/paseo.git",
|
||||
worktreeRoot: rootPath,
|
||||
@@ -119,7 +119,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
test("distinguishes absolute and home-relative SCP paths", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (remoteUrl: string) =>
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl,
|
||||
worktreeRoot: rootPath,
|
||||
@@ -132,7 +132,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
test("distinguishes absolute SSH URL paths from home-relative SCP paths", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (remoteUrl: string) =>
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl,
|
||||
worktreeRoot: rootPath,
|
||||
@@ -147,7 +147,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
test("distinguishes SSH users for home-relative SCP paths", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (remoteUrl: string) =>
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl,
|
||||
worktreeRoot: rootPath,
|
||||
@@ -162,7 +162,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
test("keeps percent sequences literal in SCP paths", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (remoteUrl: string) =>
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl,
|
||||
worktreeRoot: rootPath,
|
||||
@@ -177,7 +177,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
test("preserves leading whitespace in SCP repository paths", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (remoteUrl: string) =>
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl,
|
||||
worktreeRoot: rootPath,
|
||||
@@ -193,7 +193,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
|
||||
expect(
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl: "C:repo",
|
||||
worktreeRoot: rootPath,
|
||||
@@ -205,7 +205,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
test.each(["git+ssh:", "ssh+git:"])("normalizes SSH alias default ports for %s", (scheme) => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (remoteUrl: string) =>
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl,
|
||||
worktreeRoot: rootPath,
|
||||
@@ -221,7 +221,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
|
||||
expect(
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl: "git@[2001:db8::1]:getpaseo/paseo.git",
|
||||
worktreeRoot: rootPath,
|
||||
@@ -235,7 +235,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
const rootPath = path.join(worktreeRoot, "packages", "app");
|
||||
|
||||
expect(
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl: "git@github.com:getpaseo/paseo.git",
|
||||
worktreeRoot,
|
||||
@@ -246,13 +246,13 @@ describe("deriveProjectGroupKey", () => {
|
||||
|
||||
test("keeps a selected path distinct from remote path syntax", () => {
|
||||
const worktreeRoot = path.resolve("repo");
|
||||
const selectedKey = deriveProjectGroupKey({
|
||||
const selectedKey = deriveProjectKey({
|
||||
rootPath: path.join(worktreeRoot, "packages", "app"),
|
||||
remoteUrl: "example.com:acme/repo.git",
|
||||
worktreeRoot,
|
||||
mainRepoRoot: null,
|
||||
});
|
||||
const remoteSyntaxKey = deriveProjectGroupKey({
|
||||
const remoteSyntaxKey = deriveProjectKey({
|
||||
rootPath: worktreeRoot,
|
||||
remoteUrl: "example.com:acme/repo#subdir:packages/app.git",
|
||||
worktreeRoot,
|
||||
@@ -266,7 +266,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
|
||||
expect(
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl: "git@github.com:getpaseo/paseo.git",
|
||||
worktreeRoot: rootPath,
|
||||
@@ -280,7 +280,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
const mainRepoRoot = path.resolve("main");
|
||||
|
||||
expect(
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath: path.join(worktreeRoot, "packages", "app"),
|
||||
remoteUrl: null,
|
||||
worktreeRoot,
|
||||
@@ -292,7 +292,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
test("keeps path-only project identities scoped to their host", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
const derive = (serverId: string) =>
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl: null,
|
||||
worktreeRoot: null,
|
||||
@@ -306,7 +306,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
|
||||
test("preserves selected-path casing across Windows and POSIX hosts", () => {
|
||||
expect(
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath: "c:\\repo\\Packages\\App",
|
||||
remoteUrl: "git@github.com:getpaseo/paseo.git",
|
||||
worktreeRoot: "C:\\Repo",
|
||||
@@ -318,7 +318,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"preserves a selected subproject reached through a symlink",
|
||||
() => {
|
||||
const tempDir = mkdtempSync(path.join(tmpdir(), "project-group-key-"));
|
||||
const tempDir = mkdtempSync(path.join(tmpdir(), "project-key-"));
|
||||
try {
|
||||
const worktreeRoot = path.join(tempDir, "repo");
|
||||
const selectedRoot = path.join(worktreeRoot, "packages", "app");
|
||||
@@ -327,7 +327,7 @@ describe("deriveProjectGroupKey", () => {
|
||||
symlinkSync(selectedRoot, linkedRoot, "dir");
|
||||
|
||||
expect(
|
||||
deriveProjectGroupKey({
|
||||
deriveProjectKey({
|
||||
rootPath: linkedRoot,
|
||||
remoteUrl: "git@github.com:getpaseo/paseo.git",
|
||||
worktreeRoot,
|
||||
+3
-3
@@ -23,14 +23,14 @@ const DEFAULT_REMOTE_PORTS: Partial<Record<string, string>> = {
|
||||
* Derives the persisted, opaque equivalence key used to group projects across hosts.
|
||||
* A Git remote is today's strongest shared fact, but callers treat the result as opaque.
|
||||
*/
|
||||
export function deriveProjectGroupKey(input: {
|
||||
export function deriveProjectKey(input: {
|
||||
rootPath: string;
|
||||
remoteUrl: string | null;
|
||||
worktreeRoot: string | null;
|
||||
mainRepoRoot: string | null;
|
||||
serverId?: string;
|
||||
}): string {
|
||||
const remoteKey = deriveRemoteProjectGroupKey(input.remoteUrl);
|
||||
const remoteKey = deriveRemoteProjectKey(input.remoteUrl);
|
||||
const selectedPath = deriveSelectedPath(input.rootPath, input.worktreeRoot);
|
||||
if (!remoteKey) {
|
||||
const localPath =
|
||||
@@ -54,7 +54,7 @@ function encodeSelectedPath(selectedPath: string): string {
|
||||
return selectedPath.split(/[\\/]/u).map(encodeURIComponent).join("/");
|
||||
}
|
||||
|
||||
function deriveRemoteProjectGroupKey(remoteUrl: string | null): string | null {
|
||||
function deriveRemoteProjectKey(remoteUrl: string | null): string | null {
|
||||
if (!remoteUrl?.trim()) return null;
|
||||
const remote = parseRemoteLocation(remoteUrl);
|
||||
if (!remote) return null;
|
||||
@@ -649,7 +649,7 @@ describe("project command-center RPCs", () => {
|
||||
rootPath: directoryPath,
|
||||
kind: "non_git",
|
||||
displayName: "new-project",
|
||||
projectGroupKey: directoryPath,
|
||||
projectKey: directoryPath,
|
||||
timestamp: expect.any(String),
|
||||
});
|
||||
expect(messages).toEqual([
|
||||
|
||||
@@ -1690,8 +1690,7 @@ export class Session {
|
||||
fallbackWorktreeRoot: snapshot?.git.repoRoot,
|
||||
});
|
||||
return {
|
||||
projectKey: project.projectId,
|
||||
...(project.projectGroupKey ? { projectGroupKey: project.projectGroupKey } : {}),
|
||||
projectKey: project.projectKey ?? project.projectId,
|
||||
projectName: resolveProjectDisplayName(project),
|
||||
workspaceName: resolveWorkspaceDisplayName(workspace),
|
||||
checkout,
|
||||
@@ -4290,7 +4289,7 @@ export class Session {
|
||||
return {
|
||||
id: result.workspace.workspaceId,
|
||||
projectId: result.workspace.projectId,
|
||||
...(projectRecord?.projectGroupKey ? { projectGroupKey: projectRecord.projectGroupKey } : {}),
|
||||
...(projectRecord?.projectKey ? { projectKey: projectRecord.projectKey } : {}),
|
||||
projectDisplayName: projectRecord
|
||||
? resolveProjectDisplayName(projectRecord)
|
||||
: result.workspace.projectId,
|
||||
@@ -4463,7 +4462,7 @@ export class Session {
|
||||
): WorkspaceProjectDescriptorPayload {
|
||||
return {
|
||||
projectId: project.projectId,
|
||||
...(project.projectGroupKey ? { projectGroupKey: project.projectGroupKey } : {}),
|
||||
...(project.projectKey ? { projectKey: project.projectKey } : {}),
|
||||
projectDisplayName: resolveProjectDisplayName(project),
|
||||
projectCustomName: project.customName ?? null,
|
||||
projectRootPath: project.rootPath,
|
||||
|
||||
@@ -3699,7 +3699,7 @@ test("create paseo worktree response preserves an explicit non-Git project", asy
|
||||
expect(response?.payload.error).toBeNull();
|
||||
expect(response?.payload.workspace).toMatchObject({
|
||||
projectId: explicitProject.projectId,
|
||||
projectGroupKey: explicitProject.rootPath,
|
||||
projectKey: explicitProject.rootPath,
|
||||
projectDisplayName: explicitProject.displayName,
|
||||
projectRootPath: explicitProject.rootPath,
|
||||
projectKind: "non_git",
|
||||
@@ -3713,7 +3713,7 @@ test("create paseo worktree response preserves an explicit non-Git project", asy
|
||||
expect(workspaces.has(response?.payload.workspace?.id ?? "")).toBe(true);
|
||||
expect(projects.get(explicitProject.projectId)).toEqual({
|
||||
...explicitProject,
|
||||
projectGroupKey: explicitProject.rootPath,
|
||||
projectKey: explicitProject.rootPath,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+2
-2
@@ -208,7 +208,7 @@ test("reopening archived exact-root records restores the fresh Git project", asy
|
||||
});
|
||||
await projectRegistry.upsert({
|
||||
...project,
|
||||
projectGroupKey: "remote:github.com/acme/old-repo",
|
||||
projectKey: "remote:github.com/acme/old-repo",
|
||||
});
|
||||
const workspace = createPersistedWorkspaceRecord({
|
||||
workspaceId: "ws-archived-root",
|
||||
@@ -248,7 +248,7 @@ test("reopening archived exact-root records restores the fresh Git project", asy
|
||||
});
|
||||
expect(await projectRegistry.get(project.projectId)).toMatchObject({
|
||||
kind: "git",
|
||||
projectGroupKey: "remote:github.com/acme/new-repo",
|
||||
projectKey: "remote:github.com/acme/new-repo",
|
||||
archivedAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
+9
-13
@@ -14,7 +14,7 @@ import {
|
||||
} from "../../workspace-registry.js";
|
||||
import type { WorkspaceGitService } from "../../workspace-git-service.js";
|
||||
import type { CreatePaseoWorktreeWorkflowResult } from "../../worktree-session.js";
|
||||
import { deriveProjectGroupKey } from "../../project-group-key.js";
|
||||
import { deriveProjectKey } from "../../project-key.js";
|
||||
import { areEquivalentPaths, createRealpathAwarePathMatcher } from "../../../utils/path.js";
|
||||
|
||||
export interface ResolveOrCreateWorkspaceIdInput {
|
||||
@@ -164,7 +164,7 @@ export function createWorkspaceProvisioningService(deps: {
|
||||
rootPath,
|
||||
kind: checkout.isGit ? "git" : "non_git",
|
||||
displayName: basename(rootPath) || rootPath,
|
||||
projectGroupKey: deriveProjectGroupKey({
|
||||
projectKey: deriveProjectKey({
|
||||
rootPath,
|
||||
remoteUrl: checkout.remoteUrl,
|
||||
worktreeRoot: checkout.worktreeRoot,
|
||||
@@ -270,7 +270,7 @@ export function createWorkspaceProvisioningService(deps: {
|
||||
rootPath: input.repoRoot,
|
||||
kind: "git",
|
||||
displayName: basename(input.repoRoot) || input.repoRoot,
|
||||
projectGroupKey: deriveProjectGroupKey({
|
||||
projectKey: deriveProjectKey({
|
||||
rootPath: input.repoRoot,
|
||||
remoteUrl: checkout.remoteUrl,
|
||||
worktreeRoot: checkout.worktreeRoot,
|
||||
@@ -351,22 +351,18 @@ export function createWorkspaceProvisioningService(deps: {
|
||||
? checkout
|
||||
: await workspaceGitService.getCheckout(project.rootPath);
|
||||
const kind = projectCheckout.isGit ? "git" : "non_git";
|
||||
const projectGroupKey = deriveProjectGroupKey({
|
||||
const projectKey = deriveProjectKey({
|
||||
rootPath: project.rootPath,
|
||||
remoteUrl: projectCheckout.remoteUrl,
|
||||
worktreeRoot: projectCheckout.worktreeRoot,
|
||||
mainRepoRoot: projectCheckout.mainRepoRoot,
|
||||
serverId,
|
||||
});
|
||||
if (
|
||||
project.archivedAt ||
|
||||
project.kind !== kind ||
|
||||
project.projectGroupKey !== projectGroupKey
|
||||
) {
|
||||
if (project.archivedAt || project.kind !== kind || project.projectKey !== projectKey) {
|
||||
await projectRegistry.upsert({
|
||||
...project,
|
||||
kind,
|
||||
projectGroupKey,
|
||||
projectKey,
|
||||
archivedAt: null,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
@@ -405,18 +401,18 @@ export function createWorkspaceProvisioningService(deps: {
|
||||
? workspaceCheckout
|
||||
: await workspaceGitService.getCheckout(project.rootPath);
|
||||
const kind: PersistedProjectRecord["kind"] = projectCheckout.isGit ? "git" : "non_git";
|
||||
const projectGroupKey = deriveProjectGroupKey({
|
||||
const projectKey = deriveProjectKey({
|
||||
rootPath: project.rootPath,
|
||||
remoteUrl: projectCheckout.remoteUrl,
|
||||
worktreeRoot: projectCheckout.worktreeRoot,
|
||||
mainRepoRoot: projectCheckout.mainRepoRoot,
|
||||
serverId,
|
||||
});
|
||||
if (project.kind === kind && project.projectGroupKey === projectGroupKey) return project;
|
||||
if (project.kind === kind && project.projectKey === projectKey) return project;
|
||||
const refreshed = {
|
||||
...project,
|
||||
kind,
|
||||
projectGroupKey,
|
||||
projectKey,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
await projectRegistry.upsert(refreshed);
|
||||
|
||||
@@ -260,7 +260,7 @@ function createNoopProjectRegistry(): ProjectRegistry {
|
||||
rootPath: input.rootPath,
|
||||
kind: input.kind,
|
||||
displayName: input.displayName,
|
||||
projectGroupKey: input.projectGroupKey ?? null,
|
||||
projectKey: input.projectKey ?? null,
|
||||
customName: null,
|
||||
createdAt: input.timestamp,
|
||||
updatedAt: input.timestamp,
|
||||
|
||||
@@ -502,7 +502,7 @@ export class WorkspaceDirectory {
|
||||
)
|
||||
.map((project) => ({
|
||||
projectId: project.projectId,
|
||||
projectGroupKey: project.projectGroupKey ?? undefined,
|
||||
projectKey: project.projectKey ?? undefined,
|
||||
projectDisplayName: resolveProjectDisplayName(project),
|
||||
projectCustomName: project.customName ?? null,
|
||||
projectRootPath: project.rootPath,
|
||||
|
||||
@@ -258,7 +258,7 @@ describe("WorkspaceReconciliationService", () => {
|
||||
kind: "project_updated",
|
||||
projectId: "p1",
|
||||
directory: projectRoot,
|
||||
fields: { projectGroupKey: projectRoot },
|
||||
fields: { projectKey: projectRoot },
|
||||
},
|
||||
]);
|
||||
expect(workspaces.get("w1")?.archivedAt).toBeNull();
|
||||
@@ -316,7 +316,7 @@ describe("WorkspaceReconciliationService", () => {
|
||||
kind: "project_updated",
|
||||
projectId: "p1",
|
||||
directory: projectRoot,
|
||||
fields: { projectGroupKey: projectRoot },
|
||||
fields: { projectKey: projectRoot },
|
||||
},
|
||||
]);
|
||||
expect(afterGitInit.changesApplied).toEqual([
|
||||
@@ -405,13 +405,13 @@ describe("WorkspaceReconciliationService", () => {
|
||||
kind: "project_updated",
|
||||
projectId: "p1",
|
||||
directory: projectRoot,
|
||||
fields: { projectGroupKey: projectRoot },
|
||||
fields: { projectKey: projectRoot },
|
||||
},
|
||||
{
|
||||
kind: "project_updated",
|
||||
projectId: "p2",
|
||||
directory: equivalentProjectRoot,
|
||||
fields: { projectGroupKey: projectRoot },
|
||||
fields: { projectKey: projectRoot },
|
||||
},
|
||||
]);
|
||||
expect(git.reads).toEqual([projectRoot, workspaceRoot]);
|
||||
@@ -472,7 +472,7 @@ describe("WorkspaceReconciliationService", () => {
|
||||
kind: "project_updated",
|
||||
projectId: "p1",
|
||||
directory: projectRoot,
|
||||
fields: { kind: "git", projectGroupKey: projectRoot },
|
||||
fields: { kind: "git", projectKey: projectRoot },
|
||||
},
|
||||
{
|
||||
kind: "workspace_updated",
|
||||
@@ -489,7 +489,7 @@ describe("WorkspaceReconciliationService", () => {
|
||||
expect(projects.get("p1")).toEqual({
|
||||
...originalProject,
|
||||
kind: "git",
|
||||
projectGroupKey: projectRoot,
|
||||
projectKey: projectRoot,
|
||||
updatedAt: expect.any(String),
|
||||
});
|
||||
expect(workspaces.get("w1")).toEqual({
|
||||
@@ -815,7 +815,7 @@ describe("WorkspaceReconciliationService", () => {
|
||||
rootPath: repoDir,
|
||||
displayName: "blank-dot-page/editor",
|
||||
customName: null,
|
||||
projectGroupKey: "remote:github.com/blank-dot-page/editor",
|
||||
projectKey: "remote:github.com/blank-dot-page/editor",
|
||||
archivedAt: null,
|
||||
});
|
||||
expect(projects.get(repoDir)).toMatchObject({
|
||||
@@ -823,7 +823,7 @@ describe("WorkspaceReconciliationService", () => {
|
||||
rootPath: repoDir,
|
||||
displayName: "editor",
|
||||
customName: "Editor",
|
||||
projectGroupKey: "remote:github.com/blank-dot-page/editor",
|
||||
projectKey: "remote:github.com/blank-dot-page/editor",
|
||||
archivedAt: null,
|
||||
});
|
||||
expect(workspaces.get("focused-bat")).toMatchObject({
|
||||
@@ -836,7 +836,7 @@ describe("WorkspaceReconciliationService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("updates the group key while keeping the project display name stable", async () => {
|
||||
test("updates the project key while keeping the project display name stable", async () => {
|
||||
const dir = createTempGitRepo("reconcile-remote-");
|
||||
tempDirs.push(dir);
|
||||
|
||||
@@ -889,10 +889,10 @@ describe("WorkspaceReconciliationService", () => {
|
||||
const result = await service.runOnce();
|
||||
|
||||
expect(result.changesApplied.find((c) => c.kind === "project_updated")).toMatchObject({
|
||||
fields: { projectGroupKey: "remote:github.com/new-owner/new-repo" },
|
||||
fields: { projectKey: "remote:github.com/new-owner/new-repo" },
|
||||
});
|
||||
expect(projects.get("p1")!.displayName).toBe("old-owner/old-repo");
|
||||
expect(projects.get("p1")!.projectGroupKey).toBe("remote:github.com/new-owner/new-repo");
|
||||
expect(projects.get("p1")!.projectKey).toBe("remote:github.com/new-owner/new-repo");
|
||||
});
|
||||
|
||||
test("falls back to the persisted root when a Git remote disappears", async () => {
|
||||
@@ -904,7 +904,7 @@ describe("WorkspaceReconciliationService", () => {
|
||||
"p1",
|
||||
createPersistedProjectRecord({
|
||||
projectId: "p1",
|
||||
projectGroupKey: "remote:github.com/acme/old-repo",
|
||||
projectKey: "remote:github.com/acme/old-repo",
|
||||
rootPath: dir,
|
||||
kind: "git",
|
||||
displayName: "acme/old-repo",
|
||||
@@ -941,7 +941,7 @@ describe("WorkspaceReconciliationService", () => {
|
||||
|
||||
await service.runOnce();
|
||||
|
||||
expect(projects.get("p1")?.projectGroupKey).toBe(path.resolve(dir));
|
||||
expect(projects.get("p1")?.projectKey).toBe(path.resolve(dir));
|
||||
});
|
||||
|
||||
test("keeps custom and default names stable when the remote changes", async () => {
|
||||
@@ -1121,7 +1121,7 @@ describe("WorkspaceReconciliationService", () => {
|
||||
kind: "project_updated",
|
||||
projectId: "p1",
|
||||
directory: projectRoot,
|
||||
fields: { kind: "git", projectGroupKey: projectRoot },
|
||||
fields: { kind: "git", projectKey: projectRoot },
|
||||
},
|
||||
{
|
||||
kind: "workspace_updated",
|
||||
@@ -1409,7 +1409,7 @@ describe("WorkspaceReconciliationService", () => {
|
||||
kind: "project_updated",
|
||||
projectId: "p1",
|
||||
directory: rootPath,
|
||||
fields: { projectGroupKey: path.resolve("/tmp/main-repo") },
|
||||
fields: { projectKey: path.resolve("/tmp/main-repo") },
|
||||
},
|
||||
{
|
||||
kind: "workspace_updated",
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
type MutableWorkspacePlacement,
|
||||
} from "./workspace-registry-model.js";
|
||||
import { workspaceIdsForProjects } from "./workspace-directory.js";
|
||||
import { deriveProjectGroupKey } from "./project-group-key.js";
|
||||
import { deriveProjectKey } from "./project-key.js";
|
||||
|
||||
const DEFAULT_RESCAN_INTERVAL_MS = 5 * 60_000;
|
||||
const DEFAULT_DEBOUNCE_MS = 100;
|
||||
@@ -67,7 +67,7 @@ export type ReconciliationChange =
|
||||
kind: "project_updated";
|
||||
projectId: string;
|
||||
directory: string;
|
||||
fields: Partial<Pick<PersistedProjectRecord, "kind" | "projectGroupKey">>;
|
||||
fields: Partial<Pick<PersistedProjectRecord, "kind" | "projectKey">>;
|
||||
}
|
||||
| {
|
||||
kind: "workspace_updated";
|
||||
@@ -343,9 +343,9 @@ export class WorkspaceReconciliationService {
|
||||
checkout: await readCheckout(workspace.cwd),
|
||||
})),
|
||||
);
|
||||
const projectUpdates: Partial<Pick<PersistedProjectRecord, "kind" | "projectGroupKey">> = {};
|
||||
const projectUpdates: Partial<Pick<PersistedProjectRecord, "kind" | "projectKey">> = {};
|
||||
const mappedKind = deriveProjectKind(currentGit);
|
||||
const projectGroupKey = deriveProjectGroupKey({
|
||||
const projectKey = deriveProjectKey({
|
||||
rootPath: project.rootPath,
|
||||
remoteUrl: currentGit.remoteUrl,
|
||||
worktreeRoot: currentGit.worktreeRoot,
|
||||
@@ -356,8 +356,8 @@ export class WorkspaceReconciliationService {
|
||||
if (project.kind !== mappedKind) {
|
||||
projectUpdates.kind = mappedKind;
|
||||
}
|
||||
if (project.projectGroupKey !== projectGroupKey) {
|
||||
projectUpdates.projectGroupKey = projectGroupKey;
|
||||
if (project.projectKey !== projectKey) {
|
||||
projectUpdates.projectKey = projectKey;
|
||||
}
|
||||
|
||||
if (Object.keys(projectUpdates).length > 0) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ProjectCheckoutLitePayload } from "@getpaseo/protocol/messages";
|
||||
|
||||
import { parseGitRevParsePath } from "../utils/git-rev-parse-path.js";
|
||||
import { getRealpathAwareRelativePath } from "../utils/path.js";
|
||||
import { deriveProjectGroupKey, deriveProjectGroupingDisplayName } from "./project-group-key.js";
|
||||
import { deriveProjectKey, deriveProjectGroupingDisplayName } from "./project-key.js";
|
||||
import {
|
||||
deriveProjectKind,
|
||||
deriveWorkspaceDisplayName,
|
||||
@@ -34,7 +34,7 @@ export function classifyDirectoryForProjectMembership(input: {
|
||||
}): DirectoryProjectMembership {
|
||||
const cwd = resolve(input.cwd);
|
||||
const checkout: ProjectCheckoutLitePayload = { ...input.checkout, cwd };
|
||||
const projectKey = deriveProjectGroupKey({
|
||||
const projectKey = deriveProjectKey({
|
||||
rootPath: cwd,
|
||||
remoteUrl: checkout.remoteUrl,
|
||||
worktreeRoot: checkout.worktreeRoot,
|
||||
|
||||
@@ -16,8 +16,8 @@ const PersistedProjectRecordSchema = z.object({
|
||||
rootPath: z.string(),
|
||||
kind: z.enum(["git", "non_git"]),
|
||||
displayName: z.string(),
|
||||
// COMPAT(projectGroupKey): added in v0.2.4 on 2026-07-28; remove optional after 2027-01-28.
|
||||
projectGroupKey: z
|
||||
// COMPAT(projectKey): added in v0.2.4 on 2026-07-28; remove optional after 2027-01-28.
|
||||
projectKey: z
|
||||
.string()
|
||||
.nullable()
|
||||
.optional()
|
||||
@@ -110,7 +110,7 @@ export interface ProjectRegistry {
|
||||
rootPath: string;
|
||||
kind: PersistedProjectKind;
|
||||
displayName: string;
|
||||
projectGroupKey?: string;
|
||||
projectKey?: string;
|
||||
timestamp: string;
|
||||
}): Promise<PersistedProjectRecord>;
|
||||
upsert(record: PersistedProjectRecord): Promise<void>;
|
||||
@@ -315,7 +315,7 @@ export class FileBackedProjectRegistry
|
||||
rootPath: string;
|
||||
kind: PersistedProjectKind;
|
||||
displayName: string;
|
||||
projectGroupKey?: string;
|
||||
projectKey?: string;
|
||||
timestamp: string;
|
||||
}): Promise<PersistedProjectRecord> {
|
||||
const previous = this.allocationQueue;
|
||||
@@ -333,15 +333,12 @@ export class FileBackedProjectRegistry
|
||||
left.projectId.localeCompare(right.projectId),
|
||||
)[0];
|
||||
if (active) {
|
||||
if (
|
||||
active.kind === input.kind &&
|
||||
active.projectGroupKey === (input.projectGroupKey ?? null)
|
||||
)
|
||||
if (active.kind === input.kind && active.projectKey === (input.projectKey ?? null))
|
||||
return active;
|
||||
const refreshed = {
|
||||
...active,
|
||||
kind: input.kind,
|
||||
projectGroupKey: input.projectGroupKey ?? null,
|
||||
projectKey: input.projectKey ?? null,
|
||||
updatedAt: input.timestamp,
|
||||
};
|
||||
await this.upsert(refreshed);
|
||||
@@ -356,7 +353,7 @@ export class FileBackedProjectRegistry
|
||||
rootPath: input.rootPath,
|
||||
kind: input.kind,
|
||||
displayName: input.displayName,
|
||||
projectGroupKey: input.projectGroupKey ?? null,
|
||||
projectKey: input.projectKey ?? null,
|
||||
createdAt: input.timestamp,
|
||||
updatedAt: input.timestamp,
|
||||
});
|
||||
@@ -477,7 +474,7 @@ export function createPersistedProjectRecord(input: {
|
||||
kind: PersistedProjectKind;
|
||||
displayName: string;
|
||||
customName?: string | null;
|
||||
projectGroupKey?: string | null;
|
||||
projectKey?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
archivedAt?: string | null;
|
||||
@@ -485,7 +482,7 @@ export function createPersistedProjectRecord(input: {
|
||||
return PersistedProjectRecordSchema.parse({
|
||||
...input,
|
||||
customName: input.customName ?? null,
|
||||
projectGroupKey: input.projectGroupKey ?? null,
|
||||
projectKey: input.projectKey ?? null,
|
||||
archivedAt: input.archivedAt ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user