mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-06 08:24:46 +00:00
fix(projects): strengthen grouping identity
This commit is contained in:
@@ -21,6 +21,8 @@ function makeProject(overrides: Partial<ProjectSummary>): ProjectSummary {
|
||||
function makeHost(overrides: Partial<ProjectSummary["hosts"][number]>) {
|
||||
return {
|
||||
serverId: "host-1",
|
||||
projectName: "Project",
|
||||
projectCustomName: null,
|
||||
serverName: "Host 1",
|
||||
isOnline: true,
|
||||
repoRoot: "/tmp/project",
|
||||
|
||||
@@ -242,7 +242,8 @@ function ProjectSettingsBody({
|
||||
projectKey={project.projectKey}
|
||||
/>
|
||||
<ProjectNameEditor
|
||||
project={project}
|
||||
projectName={selectedHost.projectName}
|
||||
projectCustomName={selectedHost.projectCustomName}
|
||||
projectId={selectedHost.projectId ?? project.projectKey}
|
||||
client={client}
|
||||
/>
|
||||
@@ -793,17 +794,23 @@ function ResolveSpinnerColor(): string {
|
||||
}
|
||||
|
||||
interface ProjectNameEditorProps {
|
||||
project: ProjectSummary;
|
||||
projectName: string;
|
||||
projectCustomName: string | null;
|
||||
projectId: string;
|
||||
client: DaemonClient;
|
||||
}
|
||||
|
||||
function ProjectNameEditor({ project, projectId, client }: ProjectNameEditorProps) {
|
||||
function ProjectNameEditor({
|
||||
projectName,
|
||||
projectCustomName,
|
||||
projectId,
|
||||
client,
|
||||
}: ProjectNameEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const toast = useToast();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [value, setValue] = useState(project.projectCustomName ?? "");
|
||||
const [value, setValue] = useState(projectCustomName ?? "");
|
||||
|
||||
const renameMutation = useMutation({
|
||||
mutationFn: (customName: string | null) => client.renameProject(projectId, customName),
|
||||
@@ -820,24 +827,24 @@ function ProjectNameEditor({ project, projectId, client }: ProjectNameEditorProp
|
||||
});
|
||||
|
||||
const handleStartEdit = useCallback(() => {
|
||||
setValue(project.projectCustomName ?? "");
|
||||
setValue(projectCustomName ?? "");
|
||||
setIsEditing(true);
|
||||
}, [project.projectCustomName]);
|
||||
}, [projectCustomName]);
|
||||
|
||||
const handleCancel = useCallback(() => {
|
||||
setIsEditing(false);
|
||||
setValue(project.projectCustomName ?? "");
|
||||
}, [project.projectCustomName]);
|
||||
setValue(projectCustomName ?? "");
|
||||
}, [projectCustomName]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const trimmed = value.trim();
|
||||
const next = trimmed.length === 0 ? null : trimmed;
|
||||
if (next === (project.projectCustomName ?? null)) {
|
||||
if (next === projectCustomName) {
|
||||
setIsEditing(false);
|
||||
return;
|
||||
}
|
||||
renameMutation.mutate(next);
|
||||
}, [value, project.projectCustomName, renameMutation]);
|
||||
}, [value, projectCustomName, renameMutation]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
renameMutation.mutate(null);
|
||||
@@ -847,7 +854,7 @@ function ProjectNameEditor({ project, projectId, client }: ProjectNameEditorProp
|
||||
return (
|
||||
<View style={styles.nameEditorRow}>
|
||||
<Text style={styles.projectTitle} numberOfLines={1}>
|
||||
{project.projectName}
|
||||
{projectName}
|
||||
</Text>
|
||||
<Pressable
|
||||
testID="project-name-edit-button"
|
||||
@@ -858,7 +865,7 @@ function ProjectNameEditor({ project, projectId, client }: ProjectNameEditorProp
|
||||
>
|
||||
<Pencil size={ICON_SIZE} color={styles.iconColor.color} />
|
||||
</Pressable>
|
||||
{project.projectCustomName ? (
|
||||
{projectCustomName ? (
|
||||
<Pressable
|
||||
testID="project-name-reset-button"
|
||||
accessibilityLabel={t("settings.project.rename.resetLabel")}
|
||||
@@ -881,7 +888,7 @@ function ProjectNameEditor({ project, projectId, client }: ProjectNameEditorProp
|
||||
accessibilityLabel={t("settings.project.rename.projectNameLabel")}
|
||||
value={value}
|
||||
onChangeText={setValue}
|
||||
placeholder={project.projectName}
|
||||
placeholder={projectName}
|
||||
placeholderTextColor={styles.placeholderColor.color}
|
||||
autoFocus
|
||||
style={styles.nameEditorInput}
|
||||
|
||||
@@ -221,6 +221,8 @@ function workspaceSummary(overrides: Partial<WorkspaceSummary> = {}): WorkspaceS
|
||||
function hostEntry(overrides: Partial<ProjectHostEntry> = {}): ProjectHostEntry {
|
||||
return {
|
||||
serverId: "host-a",
|
||||
projectName: "Project",
|
||||
projectCustomName: null,
|
||||
serverName: "alpha",
|
||||
isOnline: true,
|
||||
repoRoot: "/home/me/proj",
|
||||
|
||||
@@ -121,6 +121,37 @@ describe("buildProjects", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses one ambiguity scope across every host", () => {
|
||||
const projectGroupKey = "remote:github.com/acme/app";
|
||||
const result = buildProjects({
|
||||
hosts: [
|
||||
{
|
||||
serverId: "desktop",
|
||||
serverName: "Desktop",
|
||||
isOnline: true,
|
||||
workspaces: [
|
||||
workspace({ id: "clone-a", repoRoot: "/a", projectId: "prj_a", projectGroupKey }),
|
||||
workspace({ id: "clone-b", repoRoot: "/b", projectId: "prj_b", projectGroupKey }),
|
||||
],
|
||||
},
|
||||
{
|
||||
serverId: "laptop",
|
||||
serverName: "Laptop",
|
||||
isOnline: true,
|
||||
workspaces: [
|
||||
workspace({ id: "clone-c", repoRoot: "/c", projectId: "prj_c", projectGroupKey }),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.projects.map((project) => project.projectKey).sort()).toEqual([
|
||||
"host:desktop:project:prj_a",
|
||||
"host:desktop:project:prj_b",
|
||||
"host:laptop:project:prj_c",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps host-local metadata when the grouping key differs from the project ID", () => {
|
||||
const projectGroupKey = "remote:github.com/acme/app";
|
||||
const result = buildProjects({
|
||||
@@ -150,6 +181,49 @@ describe("buildProjects", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps custom-name state on the host that owns it", () => {
|
||||
const projectGroupKey = "remote:github.com/acme/app";
|
||||
const result = buildProjects({
|
||||
hosts: [
|
||||
{
|
||||
serverId: "desktop",
|
||||
serverName: "Desktop",
|
||||
isOnline: true,
|
||||
workspaces: [
|
||||
workspace({
|
||||
id: "desktop-main",
|
||||
repoRoot: "/desktop/app",
|
||||
projectId: "prj_desktop",
|
||||
projectGroupKey,
|
||||
projectName: "My App",
|
||||
projectCustomName: "My App",
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
serverId: "laptop",
|
||||
serverName: "Laptop",
|
||||
isOnline: true,
|
||||
workspaces: [
|
||||
workspace({
|
||||
id: "laptop-main",
|
||||
repoRoot: "/laptop/app",
|
||||
projectId: "prj_laptop",
|
||||
projectGroupKey,
|
||||
projectName: "acme/app",
|
||||
projectCustomName: null,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.projects[0]?.hosts).toMatchObject([
|
||||
{ serverId: "desktop", projectName: "My App", projectCustomName: "My App" },
|
||||
{ serverId: "laptop", projectName: "acme/app", projectCustomName: null },
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves the root of a grouped project without workspaces", () => {
|
||||
const result = buildProjects({
|
||||
hosts: [
|
||||
@@ -162,8 +236,8 @@ describe("buildProjects", () => {
|
||||
{
|
||||
projectId: "prj_app",
|
||||
projectGroupKey: "remote:github.com/acme/app",
|
||||
projectDisplayName: "acme/app",
|
||||
projectCustomName: null,
|
||||
projectDisplayName: "My Empty Project",
|
||||
projectCustomName: "My Empty Project",
|
||||
projectRootPath: "/repo/app",
|
||||
projectKind: "git",
|
||||
},
|
||||
@@ -174,6 +248,8 @@ describe("buildProjects", () => {
|
||||
|
||||
expect(result.projects[0]?.hosts[0]).toMatchObject({
|
||||
projectId: "prj_app",
|
||||
projectName: "My Empty Project",
|
||||
projectCustomName: "My Empty Project",
|
||||
repoRoot: "/repo/app",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface WorkspaceSummary {
|
||||
export interface ProjectHostEntry {
|
||||
serverId: string;
|
||||
projectId?: string;
|
||||
projectName: string;
|
||||
projectCustomName: string | null;
|
||||
serverName: string;
|
||||
isOnline: boolean;
|
||||
repoRoot: string;
|
||||
@@ -58,6 +60,8 @@ const GITHUB_PROJECT_KEY_PATTERN = /^remote:github\.com\/([^/]+)\/([^/]+)$/;
|
||||
interface HostGroup {
|
||||
serverId: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
projectCustomName: string | null;
|
||||
serverName: string;
|
||||
isOnline: boolean;
|
||||
workspaces: WorkspaceDescriptor[];
|
||||
@@ -73,31 +77,35 @@ interface ProjectGroup {
|
||||
hostsByServerId: Map<string, HostGroup>;
|
||||
}
|
||||
|
||||
function findProjectCustomName(
|
||||
workspaces: WorkspaceDescriptor[],
|
||||
function findProjectMetadata(
|
||||
host: ProjectHost,
|
||||
projectKey: string,
|
||||
): { customName: string; displayName: string } | null {
|
||||
for (const workspace of workspaces) {
|
||||
if (workspace.projectId === projectKey && workspace.projectCustomName) {
|
||||
): { customName: string | null; displayName: string } | null {
|
||||
for (const workspace of host.workspaces) {
|
||||
if (workspace.projectId === projectKey) {
|
||||
return {
|
||||
customName: workspace.projectCustomName,
|
||||
customName: workspace.projectCustomName ?? null,
|
||||
displayName: workspace.projectDisplayName,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
const emptyProject = host.emptyProjects?.find((project) => project.projectId === projectKey);
|
||||
return emptyProject
|
||||
? {
|
||||
customName: emptyProject.projectCustomName ?? null,
|
||||
displayName: emptyProject.projectDisplayName,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
function buildHostProjectEntries(host: ProjectHost): HostProjectListItem[] {
|
||||
function buildHostProjectEntries(hosts: ProjectHost[]): HostProjectListItem[] {
|
||||
return buildHostProjectList({
|
||||
projects: buildWorkspaceStructureProjects({
|
||||
sessions: [
|
||||
{
|
||||
serverId: host.serverId,
|
||||
workspaces: host.workspaces,
|
||||
emptyProjects: host.emptyProjects,
|
||||
},
|
||||
],
|
||||
sessions: hosts.map((host) => ({
|
||||
serverId: host.serverId,
|
||||
workspaces: host.workspaces,
|
||||
emptyProjects: host.emptyProjects,
|
||||
})),
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -141,6 +149,8 @@ function toHostEntry(group: HostGroup): ProjectHostEntry {
|
||||
return {
|
||||
serverId: group.serverId,
|
||||
projectId: group.projectId,
|
||||
projectName: group.projectName,
|
||||
projectCustomName: group.projectCustomName,
|
||||
serverName: group.serverName,
|
||||
isOnline: group.isOnline,
|
||||
repoRoot,
|
||||
@@ -175,67 +185,84 @@ function toProjectSummary(draft: ProjectGroup): ProjectSummary {
|
||||
};
|
||||
}
|
||||
|
||||
function addHostProjects(
|
||||
groups: Map<string, ProjectGroup>,
|
||||
host: ProjectHost,
|
||||
hostProjects: HostProjectListItem[],
|
||||
): void {
|
||||
const emptyRepoRootByProjectId = new Map(
|
||||
(host.emptyProjects ?? []).map((project) => [project.projectId, project.projectRootPath]),
|
||||
);
|
||||
|
||||
for (const hostProject of hostProjects) {
|
||||
const placement = hostProject.hosts.find((entry) => entry.serverId === host.serverId);
|
||||
if (!placement) continue;
|
||||
const projectId = placement.projectId ?? hostProject.projectKey;
|
||||
const customName = findProjectMetadata(host, projectId);
|
||||
let group = groups.get(hostProject.projectKey);
|
||||
if (!group) {
|
||||
group = {
|
||||
projectKey: hostProject.projectKey,
|
||||
projectName: customName?.displayName ?? hostProject.projectName,
|
||||
projectCustomName: customName?.customName ?? null,
|
||||
hostsByServerId: new Map(),
|
||||
};
|
||||
groups.set(hostProject.projectKey, group);
|
||||
} else if (customName?.customName && !group.projectCustomName) {
|
||||
group.projectCustomName = customName.customName;
|
||||
group.projectName = customName.displayName;
|
||||
}
|
||||
|
||||
if (!group.hostsByServerId.has(host.serverId)) {
|
||||
group.hostsByServerId.set(host.serverId, {
|
||||
serverId: host.serverId,
|
||||
projectId,
|
||||
projectName: customName?.displayName ?? hostProject.projectName,
|
||||
projectCustomName: customName?.customName ?? null,
|
||||
serverName: host.serverName,
|
||||
isOnline: host.isOnline,
|
||||
workspaces: [],
|
||||
fallbackRepoRoot: emptyRepoRootByProjectId.get(projectId) ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function attachHostWorkspaces(
|
||||
groups: Map<string, ProjectGroup>,
|
||||
host: ProjectHost,
|
||||
hostProjects: HostProjectListItem[],
|
||||
): void {
|
||||
const projectKeyByProjectId = new Map(
|
||||
hostProjects.flatMap((project) =>
|
||||
project.hosts
|
||||
.filter((placement) => placement.serverId === host.serverId && placement.projectId)
|
||||
.map((placement) => [placement.projectId!, project.projectKey] as const),
|
||||
),
|
||||
);
|
||||
|
||||
for (const workspace of host.workspaces) {
|
||||
const key =
|
||||
projectKeyByProjectId.get(workspace.projectId) ??
|
||||
resolveProjectGroupKey({
|
||||
serverId: host.serverId,
|
||||
projectId: workspace.projectId,
|
||||
projectGroupKey: workspace.projectGroupKey,
|
||||
});
|
||||
groups.get(key)?.hostsByServerId.get(host.serverId)?.workspaces.push(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
export function buildProjects(input: BuildProjectsInput): BuildProjectsResult {
|
||||
const groups = new Map<string, ProjectGroup>();
|
||||
const projectEntries = buildHostProjectEntries(input.hosts);
|
||||
|
||||
for (const host of input.hosts) {
|
||||
const emptyRepoRootByProjectKey = new Map<string, string>();
|
||||
for (const emptyProject of host.emptyProjects ?? []) {
|
||||
emptyRepoRootByProjectKey.set(emptyProject.projectId, emptyProject.projectRootPath);
|
||||
}
|
||||
|
||||
const hostProjects = buildHostProjectEntries(host);
|
||||
const projectKeyByProjectId = new Map(
|
||||
hostProjects.flatMap((project) =>
|
||||
project.hosts
|
||||
.filter((placement) => placement.serverId === host.serverId && placement.projectId)
|
||||
.map((placement) => [placement.projectId!, project.projectKey] as const),
|
||||
),
|
||||
const hostProjects = projectEntries.filter((project) =>
|
||||
project.hosts.some((placement) => placement.serverId === host.serverId),
|
||||
);
|
||||
for (const hostProject of hostProjects) {
|
||||
const placement = hostProject.hosts.find((entry) => entry.serverId === host.serverId);
|
||||
if (!placement) continue;
|
||||
const projectId = placement.projectId ?? hostProject.projectKey;
|
||||
const customName = findProjectCustomName(host.workspaces, projectId);
|
||||
let group = groups.get(hostProject.projectKey);
|
||||
if (!group) {
|
||||
group = {
|
||||
projectKey: hostProject.projectKey,
|
||||
projectName: customName?.displayName ?? hostProject.projectName,
|
||||
projectCustomName: customName?.customName ?? null,
|
||||
hostsByServerId: new Map(),
|
||||
};
|
||||
groups.set(hostProject.projectKey, group);
|
||||
} else if (customName && !group.projectCustomName) {
|
||||
group.projectCustomName = customName.customName;
|
||||
group.projectName = customName.displayName;
|
||||
}
|
||||
|
||||
if (!group.hostsByServerId.has(host.serverId)) {
|
||||
group.hostsByServerId.set(host.serverId, {
|
||||
serverId: host.serverId,
|
||||
projectId,
|
||||
serverName: host.serverName,
|
||||
isOnline: host.isOnline,
|
||||
workspaces: [],
|
||||
fallbackRepoRoot: emptyRepoRootByProjectKey.get(projectId) ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const workspace of host.workspaces) {
|
||||
const key =
|
||||
projectKeyByProjectId.get(workspace.projectId) ??
|
||||
resolveProjectGroupKey({
|
||||
serverId: host.serverId,
|
||||
projectId: workspace.projectId,
|
||||
projectGroupKey: workspace.projectGroupKey,
|
||||
});
|
||||
const group = groups.get(key);
|
||||
const hostGroup = group?.hostsByServerId.get(host.serverId);
|
||||
if (!hostGroup) continue;
|
||||
hostGroup.workspaces.push(workspace);
|
||||
}
|
||||
addHostProjects(groups, host, hostProjects);
|
||||
attachHostWorkspaces(groups, host, hostProjects);
|
||||
}
|
||||
|
||||
const projects = Array.from(groups.values()).map(toProjectSummary);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import path from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { deriveProjectGroupKey } from "./project-group-key.js";
|
||||
|
||||
describe("deriveProjectGroupKey", () => {
|
||||
test("preserves an explicit remote port", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
|
||||
expect(
|
||||
deriveProjectGroupKey({
|
||||
rootPath,
|
||||
remoteUrl: "https://git.example.com:8443/acme/app.git",
|
||||
worktreeRoot: rootPath,
|
||||
mainRepoRoot: null,
|
||||
}),
|
||||
).toBe("remote:git.example.com:8443/acme/app");
|
||||
});
|
||||
|
||||
test("includes the selected path within a repository", () => {
|
||||
const worktreeRoot = path.resolve("repo");
|
||||
const rootPath = path.join(worktreeRoot, "packages", "app");
|
||||
|
||||
expect(
|
||||
deriveProjectGroupKey({
|
||||
rootPath,
|
||||
remoteUrl: "git@github.com:getpaseo/paseo.git",
|
||||
worktreeRoot,
|
||||
mainRepoRoot: null,
|
||||
}),
|
||||
).toBe("remote:github.com/getpaseo/paseo#subdir:packages/app");
|
||||
});
|
||||
|
||||
test("keeps repository-root keys stable", () => {
|
||||
const rootPath = path.resolve("repo");
|
||||
|
||||
expect(
|
||||
deriveProjectGroupKey({
|
||||
rootPath,
|
||||
remoteUrl: "git@github.com:getpaseo/paseo.git",
|
||||
worktreeRoot: rootPath,
|
||||
mainRepoRoot: null,
|
||||
}),
|
||||
).toBe("remote:github.com/getpaseo/paseo");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { resolve } from "node:path";
|
||||
import { isAbsolute, relative, resolve, sep } from "node:path";
|
||||
|
||||
/**
|
||||
* Derives the persisted, opaque equivalence key used to group projects across hosts.
|
||||
@@ -7,11 +7,29 @@ import { resolve } from "node:path";
|
||||
export function deriveProjectGroupKey(input: {
|
||||
rootPath: string;
|
||||
remoteUrl: string | null;
|
||||
worktreeRoot: string | null;
|
||||
mainRepoRoot: string | null;
|
||||
}): string {
|
||||
return (
|
||||
deriveRemoteProjectGroupKey(input.remoteUrl) ?? resolve(input.mainRepoRoot ?? input.rootPath)
|
||||
);
|
||||
const remoteKey = deriveRemoteProjectGroupKey(input.remoteUrl);
|
||||
if (!remoteKey) return resolve(input.mainRepoRoot ?? input.rootPath);
|
||||
|
||||
const selectedPath = deriveSelectedPath(input.rootPath, input.worktreeRoot);
|
||||
return selectedPath ? `${remoteKey}#subdir:${selectedPath}` : remoteKey;
|
||||
}
|
||||
|
||||
function deriveSelectedPath(rootPath: string, worktreeRoot: string | null): string | null {
|
||||
if (!worktreeRoot) return null;
|
||||
const selectedPath = relative(resolve(worktreeRoot), resolve(rootPath));
|
||||
if (
|
||||
!selectedPath ||
|
||||
selectedPath === "." ||
|
||||
selectedPath === ".." ||
|
||||
selectedPath.startsWith(`..${sep}`) ||
|
||||
isAbsolute(selectedPath)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return selectedPath.split(sep).map(encodeURIComponent).join("/");
|
||||
}
|
||||
|
||||
function deriveRemoteProjectGroupKey(remoteUrl: string | null): string | null {
|
||||
@@ -27,7 +45,7 @@ function deriveRemoteProjectGroupKey(remoteUrl: string | null): string | null {
|
||||
} else if (trimmed.includes("://")) {
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
host = parsed.hostname || null;
|
||||
host = parsed.host || null;
|
||||
remotePath = parsed.pathname ? parsed.pathname.replace(/^\/+/, "") : null;
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
+6
-1
@@ -206,6 +206,10 @@ test("reopening archived exact-root records restores the fresh Git project", asy
|
||||
displayName: "repo",
|
||||
timestamp: ARCHIVED_AT,
|
||||
});
|
||||
await projectRegistry.upsert({
|
||||
...project,
|
||||
projectGroupKey: "remote:github.com/acme/old-repo",
|
||||
});
|
||||
const workspace = createPersistedWorkspaceRecord({
|
||||
workspaceId: "ws-archived-root",
|
||||
projectId: project.projectId,
|
||||
@@ -227,7 +231,7 @@ test("reopening archived exact-root records restores the fresh Git project", asy
|
||||
cwd,
|
||||
isGit: true,
|
||||
currentBranch: "main",
|
||||
remoteUrl: null,
|
||||
remoteUrl: "https://github.com/acme/new-repo.git",
|
||||
worktreeRoot: cwd,
|
||||
isPaseoOwnedWorktree: false,
|
||||
mainRepoRoot: null,
|
||||
@@ -244,6 +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",
|
||||
archivedAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
+21
-2
@@ -166,6 +166,7 @@ export function createWorkspaceProvisioningService(deps: {
|
||||
projectGroupKey: deriveProjectGroupKey({
|
||||
rootPath,
|
||||
remoteUrl: checkout.remoteUrl,
|
||||
worktreeRoot: checkout.worktreeRoot,
|
||||
mainRepoRoot: checkout.mainRepoRoot,
|
||||
}),
|
||||
timestamp,
|
||||
@@ -270,6 +271,7 @@ export function createWorkspaceProvisioningService(deps: {
|
||||
projectGroupKey: deriveProjectGroupKey({
|
||||
rootPath: input.repoRoot,
|
||||
remoteUrl: checkout.remoteUrl,
|
||||
worktreeRoot: checkout.worktreeRoot,
|
||||
mainRepoRoot: checkout.mainRepoRoot,
|
||||
}),
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -346,8 +348,24 @@ export function createWorkspaceProvisioningService(deps: {
|
||||
? checkout
|
||||
: await workspaceGitService.getCheckout(project.rootPath);
|
||||
const kind = projectCheckout.isGit ? "git" : "non_git";
|
||||
if (project.archivedAt || project.kind !== kind) {
|
||||
await projectRegistry.upsert({ ...project, kind, archivedAt: null, updatedAt: timestamp });
|
||||
const projectGroupKey = deriveProjectGroupKey({
|
||||
rootPath: project.rootPath,
|
||||
remoteUrl: projectCheckout.remoteUrl,
|
||||
worktreeRoot: projectCheckout.worktreeRoot,
|
||||
mainRepoRoot: projectCheckout.mainRepoRoot,
|
||||
});
|
||||
if (
|
||||
project.archivedAt ||
|
||||
project.kind !== kind ||
|
||||
project.projectGroupKey !== projectGroupKey
|
||||
) {
|
||||
await projectRegistry.upsert({
|
||||
...project,
|
||||
kind,
|
||||
projectGroupKey,
|
||||
archivedAt: null,
|
||||
updatedAt: timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!next) return workspace;
|
||||
@@ -386,6 +404,7 @@ export function createWorkspaceProvisioningService(deps: {
|
||||
const projectGroupKey = deriveProjectGroupKey({
|
||||
rootPath: project.rootPath,
|
||||
remoteUrl: projectCheckout.remoteUrl,
|
||||
worktreeRoot: projectCheckout.worktreeRoot,
|
||||
mainRepoRoot: projectCheckout.mainRepoRoot,
|
||||
});
|
||||
if (project.kind === kind && project.projectGroupKey === projectGroupKey) return project;
|
||||
|
||||
@@ -345,6 +345,7 @@ export class WorkspaceReconciliationService {
|
||||
const projectGroupKey = deriveProjectGroupKey({
|
||||
rootPath: project.rootPath,
|
||||
remoteUrl: currentGit.remoteUrl,
|
||||
worktreeRoot: currentGit.worktreeRoot,
|
||||
mainRepoRoot: currentGit.mainRepoRoot,
|
||||
});
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export function classifyDirectoryForProjectMembership(input: {
|
||||
const projectKey = deriveProjectGroupKey({
|
||||
rootPath: checkout.worktreeRoot ?? cwd,
|
||||
remoteUrl: checkout.remoteUrl,
|
||||
worktreeRoot: checkout.worktreeRoot,
|
||||
mainRepoRoot: checkout.mainRepoRoot,
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user