* feat(server): derive owner/repo display name for any remote host Generalize deriveProjectGroupingName so any remote:<host>/<segments...> project key returns the last two path segments (owner/repo) instead of just the trailing segment. Path-fallback for non-remote keys is unchanged. Brings GitLab, Gitea, Bitbucket, and self-hosted remotes to parity with the prior github.com-only behavior — no separate special-case needed. * feat(app): show projects from any git remote in Projects settings Remove the isSupportedProjectKey filter so workspaces with non-GitHub remotes (GitLab, Gitea, Bitbucket, self-hosted, ssh-style) appear in the Projects list and route to the project settings screen. The daemon RPCs, config schema, and registry were already host-agnostic; this lifts a client-side filter that hid them. Also remove the now-vestigial hiddenUnsupportedRemoteCount field from ProjectSummary, BuildProjectsResult, and UseProjectsResult — once the filter is gone, the count is always zero and the field is dead state. This is an internal app-package type, not a wire schema, so deletion is safe. * chore(app): simplify Projects empty state to "No projects yet" Drop the "Non-GitHub remote projects aren't supported yet" empty-state branch — it can no longer fire now that any git remote is shown. The empty state is unconditional now. * test(app): cover non-GitHub remote in projects-settings e2e Add a fixture that creates a temp git repo with origin pointing at a gitlab.com URL and exercises the same paseo.json read/edit/save flow already covered for local repos. Verifies the project surfaces with the "acme/app" display name and that the round-trip persists correctly. Extracted the remote-setup branch in createTempGitRepo into a small configureRemote helper to keep the main function under the cyclomatic complexity limit. --------- Co-authored-by: Mathias Kurz <mkurz@stamus-networks.com>
172 lines
4.6 KiB
TypeScript
172 lines
4.6 KiB
TypeScript
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
|
|
|
export interface WorkspaceSummary {
|
|
id: string;
|
|
name: string;
|
|
workspaceKind: WorkspaceDescriptor["workspaceKind"];
|
|
status: WorkspaceDescriptor["status"];
|
|
currentBranch: string | null;
|
|
}
|
|
|
|
export interface ProjectHostEntry {
|
|
serverId: string;
|
|
serverName: string;
|
|
isOnline: boolean;
|
|
repoRoot: string;
|
|
workspaceCount: number;
|
|
workspaces: WorkspaceSummary[];
|
|
gitRuntime?: WorkspaceDescriptor["gitRuntime"];
|
|
githubRuntime?: WorkspaceDescriptor["githubRuntime"];
|
|
}
|
|
|
|
export interface ProjectSummary {
|
|
projectKey: string;
|
|
projectName: string;
|
|
hosts: ProjectHostEntry[];
|
|
totalWorkspaceCount: number;
|
|
hostCount: number;
|
|
onlineHostCount: number;
|
|
githubUrl?: string;
|
|
}
|
|
|
|
export interface ProjectHost {
|
|
serverId: string;
|
|
serverName: string;
|
|
isOnline: boolean;
|
|
workspaces: WorkspaceDescriptor[];
|
|
}
|
|
|
|
export interface BuildProjectsInput {
|
|
hosts: ProjectHost[];
|
|
}
|
|
|
|
export interface BuildProjectsResult {
|
|
projects: ProjectSummary[];
|
|
}
|
|
|
|
const GITHUB_PROJECT_KEY_PATTERN = /^remote:github\.com\/([^/]+)\/([^/]+)$/;
|
|
|
|
interface HostGroup {
|
|
serverId: string;
|
|
serverName: string;
|
|
isOnline: boolean;
|
|
workspaces: WorkspaceDescriptor[];
|
|
}
|
|
|
|
interface ProjectGroup {
|
|
projectKey: string;
|
|
projectName: string;
|
|
hostsByServerId: Map<string, HostGroup>;
|
|
}
|
|
|
|
function deriveGithubUrl(projectKey: string): string | undefined {
|
|
const match = projectKey.match(GITHUB_PROJECT_KEY_PATTERN);
|
|
if (!match) {
|
|
return undefined;
|
|
}
|
|
return `https://github.com/${match[1]}/${match[2]}`;
|
|
}
|
|
|
|
function resolveHostRepoRoot(workspaces: WorkspaceDescriptor[]): string {
|
|
for (const workspace of workspaces) {
|
|
const mainRepoRoot = workspace.project?.checkout.mainRepoRoot;
|
|
if (mainRepoRoot) {
|
|
return mainRepoRoot;
|
|
}
|
|
}
|
|
return workspaces[0]?.projectRootPath ?? "";
|
|
}
|
|
|
|
function toWorkspaceSummary(workspace: WorkspaceDescriptor): WorkspaceSummary {
|
|
return {
|
|
id: workspace.id,
|
|
name: workspace.name,
|
|
workspaceKind: workspace.workspaceKind,
|
|
status: workspace.status,
|
|
currentBranch: workspace.gitRuntime?.currentBranch ?? null,
|
|
};
|
|
}
|
|
|
|
function toHostEntry(group: HostGroup): ProjectHostEntry {
|
|
const repoRoot = resolveHostRepoRoot(group.workspaces);
|
|
const canonical =
|
|
group.workspaces.find((workspace) => workspace.projectRootPath === repoRoot) ??
|
|
group.workspaces[0];
|
|
return {
|
|
serverId: group.serverId,
|
|
serverName: group.serverName,
|
|
isOnline: group.isOnline,
|
|
repoRoot,
|
|
workspaceCount: group.workspaces.length,
|
|
workspaces: group.workspaces.map(toWorkspaceSummary),
|
|
gitRuntime: canonical?.gitRuntime,
|
|
githubRuntime: canonical?.githubRuntime,
|
|
};
|
|
}
|
|
|
|
function compareHosts(left: ProjectHostEntry, right: ProjectHostEntry): number {
|
|
const name = left.serverName.localeCompare(right.serverName);
|
|
if (name !== 0) {
|
|
return name;
|
|
}
|
|
return left.serverId.localeCompare(right.serverId);
|
|
}
|
|
|
|
function toProjectSummary(draft: ProjectGroup): ProjectSummary {
|
|
const hosts = Array.from(draft.hostsByServerId.values()).map(toHostEntry).sort(compareHosts);
|
|
const totalWorkspaceCount = hosts.reduce((sum, host) => sum + host.workspaceCount, 0);
|
|
const onlineHostCount = hosts.filter((host) => host.isOnline).length;
|
|
return {
|
|
projectKey: draft.projectKey,
|
|
projectName: draft.projectName,
|
|
hosts,
|
|
totalWorkspaceCount,
|
|
hostCount: hosts.length,
|
|
onlineHostCount,
|
|
githubUrl: deriveGithubUrl(draft.projectKey),
|
|
};
|
|
}
|
|
|
|
export function buildProjects(input: BuildProjectsInput): BuildProjectsResult {
|
|
const groups = new Map<string, ProjectGroup>();
|
|
|
|
for (const host of input.hosts) {
|
|
for (const workspace of host.workspaces) {
|
|
const projectKey = workspace.projectId;
|
|
|
|
let group = groups.get(projectKey);
|
|
if (!group) {
|
|
group = {
|
|
projectKey,
|
|
projectName: workspace.projectDisplayName,
|
|
hostsByServerId: new Map(),
|
|
};
|
|
groups.set(projectKey, group);
|
|
}
|
|
|
|
let hostGroup = group.hostsByServerId.get(host.serverId);
|
|
if (!hostGroup) {
|
|
hostGroup = {
|
|
serverId: host.serverId,
|
|
serverName: host.serverName,
|
|
isOnline: host.isOnline,
|
|
workspaces: [],
|
|
};
|
|
group.hostsByServerId.set(host.serverId, hostGroup);
|
|
}
|
|
hostGroup.workspaces.push(workspace);
|
|
}
|
|
}
|
|
|
|
const projects = Array.from(groups.values()).map(toProjectSummary);
|
|
projects.sort((left, right) => {
|
|
const name = left.projectName.localeCompare(right.projectName);
|
|
if (name !== 0) {
|
|
return name;
|
|
}
|
|
return left.projectKey.localeCompare(right.projectKey);
|
|
});
|
|
|
|
return { projects };
|
|
}
|