Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { workspaces } from "@superset/local-db";
import { projects, workspaces } from "@superset/local-db";
import { and, eq, isNull } from "drizzle-orm";
import { localDb } from "main/lib/local-db";
import { z } from "zod";
Expand All @@ -8,6 +8,7 @@ import {
setLastActiveWorkspace,
touchWorkspace,
} from "../utils/db-helpers";
import { getOriginRemoteUrl, parseGitRemoteUrl } from "../utils/git";

export const createStatusProcedures = () => {
return router({
Expand Down Expand Up @@ -100,6 +101,82 @@ export const createStatusProcedures = () => {
return { success: true, isUnread: input.isUnread };
}),

linkToCloud: publicProcedure
.input(z.object({ id: z.string(), cloudWorkspaceId: z.string().uuid() }))
.mutation(({ input }) => {
const workspace = getWorkspaceNotDeleting(input.id);
if (!workspace) {
throw new Error(
`Workspace ${input.id} not found or is being deleted`,
);
}

localDb
.update(workspaces)
.set({ cloudWorkspaceId: input.cloudWorkspaceId })
.where(eq(workspaces.id, input.id))
.run();

return { success: true, cloudWorkspaceId: input.cloudWorkspaceId };
}),

unlinkFromCloud: publicProcedure
.input(z.object({ id: z.string() }))
.mutation(({ input }) => {
const workspace = getWorkspaceNotDeleting(input.id);
if (!workspace) {
throw new Error(
`Workspace ${input.id} not found or is being deleted`,
);
}

localDb
.update(workspaces)
.set({ cloudWorkspaceId: null })
.where(eq(workspaces.id, input.id))
.run();

return { success: true };
}),

getRepoInfo: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
const workspace = getWorkspaceNotDeleting(input.id);
if (!workspace) {
throw new Error(
`Workspace ${input.id} not found or is being deleted`,
);
}

const project = localDb
.select()
.from(projects)
.where(eq(projects.id, workspace.projectId))
.get();

if (!project) {
throw new Error(`Project not found for workspace ${input.id}`);
}

const remoteUrl = await getOriginRemoteUrl(project.mainRepoPath);
if (!remoteUrl) {
return { hasRemote: false as const };
}

const parsed = parseGitRemoteUrl(remoteUrl);
if (!parsed) {
return { hasRemote: false as const };
}

return {
hasRemote: true as const,
repoOwner: parsed.owner,
repoName: parsed.repo,
repoUrl: parsed.repoUrl,
};
}),

setActive: publicProcedure
.input(z.object({ workspaceId: z.string() }))
.mutation(({ input }) => {
Expand Down
81 changes: 81 additions & 0 deletions apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,87 @@ export async function hasOriginRemote(mainRepoPath: string): Promise<boolean> {
}
}

/**
* Gets the origin remote URL for a repository.
* @param repoPath - Path to the repository
* @returns The origin remote URL, or null if not found
*/
export async function getOriginRemoteUrl(
repoPath: string,
): Promise<string | null> {
try {
const git = simpleGit(repoPath);
const remotes = await git.getRemotes(true);
const origin = remotes.find((r) => r.name === "origin");
return origin?.refs?.fetch ?? origin?.refs?.push ?? null;
} catch {
return null;
}
}

/**
* Parses a git remote URL to extract owner and repo name.
* Supports formats:
* - https://github.com/owner/repo.git
* - https://github.com/owner/repo
* - git@github.com:owner/repo.git
* - git@github.com:owner/repo
* - ssh://git@github.com/owner/repo.git
*/
export function parseGitRemoteUrl(url: string): {
owner: string;
repo: string;
repoUrl: string;
} | null {
// Normalize the URL
let normalized = url.trim();

// Remove .git suffix if present
if (normalized.endsWith(".git")) {
normalized = normalized.slice(0, -4);
}

// Handle SSH format: git@github.com:owner/repo
const sshMatch = normalized.match(/^git@([^:]+):(.+)\/(.+)$/);
if (sshMatch) {
const [, host, owner, repo] = sshMatch;
return {
owner,
repo,
repoUrl: `https://${host}/${owner}/${repo}`,
};
}

// Handle SSH URL format: ssh://git@github.com/owner/repo
const sshUrlMatch = normalized.match(/^ssh:\/\/git@([^/]+)\/(.+)\/(.+)$/);
if (sshUrlMatch) {
const [, host, owner, repo] = sshUrlMatch;
return {
owner,
repo,
repoUrl: `https://${host}/${owner}/${repo}`,
};
}

// Handle HTTPS format: https://github.com/owner/repo
try {
const urlObj = new URL(normalized);
const pathParts = urlObj.pathname.split("/").filter(Boolean);
if (pathParts.length >= 2) {
const [owner, repo] = pathParts;
return {
owner,
repo,
repoUrl: `${urlObj.protocol}//${urlObj.host}/${owner}/${repo}`,
};
}
} catch {
// Not a valid URL
}

return null;
}

export async function getDefaultBranch(mainRepoPath: string): Promise<string> {
const git = simpleGit(mainRepoPath);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
useNewWorkspaceModalOpen,
usePreSelectedProjectId,
} from "renderer/stores/new-workspace-modal";
import { ENABLE_CLOUD_WORKSPACES } from "shared/constants";
import { sanitizeBranchName, sanitizeSegment } from "shared/utils/branch";
import { ExistingWorktreesList } from "./components/ExistingWorktreesList";

Expand Down Expand Up @@ -300,17 +301,19 @@ export function NewWorkspaceModal() {
>
Existing
</button>
<button
type="button"
onClick={() => setMode("cloud")}
className={`flex-1 px-3 py-1 text-xs font-medium rounded-sm transition-colors ${
mode === "cloud"
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
Cloud
</button>
{ENABLE_CLOUD_WORKSPACES && (
<button
type="button"
onClick={() => setMode("cloud")}
className={`flex-1 px-3 py-1 text-xs font-medium rounded-sm transition-colors ${
mode === "cloud"
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
}`}
>
Cloud
</button>
)}
</div>
</div>

Expand Down Expand Up @@ -481,7 +484,7 @@ export function NewWorkspaceModal() {
onOpenSuccess={handleClose}
/>
)}
{mode === "cloud" && (
{ENABLE_CLOUD_WORKSPACES && mode === "cloud" && (
<div className="flex flex-col items-center justify-center py-8 text-center">
<div className="text-sm font-medium text-foreground mb-1">
Cloud Workspaces
Expand Down
Loading