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
Expand Up @@ -6,6 +6,7 @@ describe("git-operations error handling", () => {
const upstreamDeletedMessages = [
"Your configuration specifies to merge with the ref 'refs/heads/feature-branch' from the remote, but no such ref was fetched.",
"fatal: couldn't find remote ref refs/heads/deleted-branch",
"fatal: kitenite/dont-hide-changes-tab cannot be resolved to branch",
"There is no tracking information for the current branch",
];

Expand Down
95 changes: 89 additions & 6 deletions apps/desktop/src/lib/trpc/routers/changes/git-operations.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TRPCError } from "@trpc/server";
import { shell } from "electron";
import simpleGit from "simple-git";
import { z } from "zod";
Expand All @@ -22,7 +23,68 @@ async function fetchCurrentBranch(
git: ReturnType<typeof simpleGit>,
): Promise<void> {
const branch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim();
await git.fetch(["origin", branch]);
try {
await git.fetch(["origin", branch]);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (isUpstreamMissingError(message)) {
try {
await git.fetch(["origin"]);
} catch (fallbackError) {
const fallbackMessage =
fallbackError instanceof Error
? fallbackError.message
: String(fallbackError);
if (!isUpstreamMissingError(fallbackMessage)) {
console.error(
`[git/fetch] failed fallback fetch for branch ${branch}:`,
fallbackError,
);
throw fallbackError;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return;
}
throw error;
}
}

async function pushWithSetUpstream({
git,
branch,
}: {
git: ReturnType<typeof simpleGit>;
branch: string;
}): Promise<void> {
const trimmedBranch = branch.trim();
if (!trimmedBranch || trimmedBranch === "HEAD") {
throw new TRPCError({
code: "BAD_REQUEST",
message:
"Cannot push from detached HEAD. Please checkout a branch and try again.",
});
}

// Use HEAD refspec to avoid resolving the branch name as a local ref.
// This is more reliable for worktrees where upstream tracking isn't set yet.
await git.push([
"--set-upstream",
"origin",
`HEAD:refs/heads/${trimmedBranch}`,
]);
}

function shouldRetryPushWithUpstream(message: string): boolean {
const lowerMessage = message.toLowerCase();
return (
lowerMessage.includes("no upstream branch") ||
lowerMessage.includes("no tracking information") ||
lowerMessage.includes(
"upstream branch of your current branch does not match",
) ||
lowerMessage.includes("cannot be resolved to branch") ||
lowerMessage.includes("couldn't find remote ref")
);
}

export const createGitOperationsRouter = () => {
Expand Down Expand Up @@ -62,9 +124,20 @@ export const createGitOperationsRouter = () => {

if (input.setUpstream && !hasUpstream) {
const branch = await git.revparse(["--abbrev-ref", "HEAD"]);
await git.push(["--set-upstream", "origin", branch.trim()]);
await pushWithSetUpstream({ git, branch });
} else {
await git.push();
try {
await git.push();
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
if (shouldRetryPushWithUpstream(message)) {
const branch = await git.revparse(["--abbrev-ref", "HEAD"]);
await pushWithSetUpstream({ git, branch });
} else {
throw error;
}
}
}
await fetchCurrentBranch(git);
return { success: true };
Expand Down Expand Up @@ -112,7 +185,7 @@ export const createGitOperationsRouter = () => {
error instanceof Error ? error.message : String(error);
if (isUpstreamMissingError(message)) {
const branch = await git.revparse(["--abbrev-ref", "HEAD"]);
await git.push(["--set-upstream", "origin", branch.trim()]);
await pushWithSetUpstream({ git, branch });
await fetchCurrentBranch(git);
return { success: true };
}
Expand Down Expand Up @@ -148,10 +221,20 @@ export const createGitOperationsRouter = () => {

// Ensure branch is pushed first
if (!hasUpstream) {
await git.push(["--set-upstream", "origin", branch]);
await pushWithSetUpstream({ git, branch });
} else {
// Push any unpushed commits
await git.push();
try {
await git.push();
} catch (error) {
const message =
error instanceof Error ? error.message : String(error);
if (shouldRetryPushWithUpstream(message)) {
await pushWithSetUpstream({ git, branch });
} else {
throw error;
}
}
}

// Get the remote URL to construct the GitHub compare URL
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/lib/trpc/routers/changes/git-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export function isUpstreamMissingError(message: string): boolean {
return (
message.includes("no such ref was fetched") ||
message.includes("no tracking information") ||
message.includes("couldn't find remote ref")
message.includes("couldn't find remote ref") ||
message.includes("cannot be resolved to branch")
);
}
3 changes: 0 additions & 3 deletions apps/desktop/src/renderer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,18 @@ const router = createRouter({
},
});

// Track pageviews on navigation
const unsubscribe = router.subscribe("onResolved", (event) => {
posthog.capture("$pageview", {
$current_url: event.toLocation.pathname,
});
});

// Handle deep link navigation from main process
const handleDeepLink = (path: string) => {
console.log("[deep-link] Navigating to:", path);
router.navigate({ to: path });
};
window.ipcRenderer.on("deep-link-navigate", handleDeepLink);

// Clean up subscription on HMR
if (import.meta.hot) {
import.meta.hot.dispose(() => {
unsubscribe();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export function useCreateFromPr(options?: MutationOptions) {
return electronTrpc.workspaces.createFromPr.useMutation({
...options,
onSuccess: async (data, ...rest) => {
// Set optimistic progress before navigation for new workspaces
if (!data.wasExisting && data.initialCommands) {
const optimisticProgress: WorkspaceInitProgress = {
workspaceId: data.workspace.id,
Expand All @@ -30,7 +29,6 @@ export function useCreateFromPr(options?: MutationOptions) {
updateProgress(optimisticProgress);
}

// Setup terminal if there are initial commands
if (data.initialCommands) {
addPendingTerminalSetup({
workspaceId: data.workspace.id,
Expand All @@ -41,7 +39,6 @@ export function useCreateFromPr(options?: MutationOptions) {

await utils.workspaces.invalidate();

// Navigate to the workspace
navigateToWorkspace(data.workspace.id, navigate);

await options?.onSuccess?.(data, ...rest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,9 @@ export function useDeleteWorkspace(
return electronTrpc.workspaces.delete.useMutation({
...options,
onMutate: async ({ id }) => {
// Check if we're viewing the workspace being deleted
const wasViewingDeleted = params.workspaceId === id;
let navigatedTo: string | null = null;

// If viewing deleted workspace, get navigation target BEFORE optimistic update
if (wasViewingDeleted) {
const prevWorkspaceId =
await utils.workspaces.getPreviousWorkspace.fetch({ id });
Expand All @@ -55,7 +53,6 @@ export function useDeleteWorkspace(
}
}

// Cancel outgoing queries and get snapshots
await Promise.all([
utils.workspaces.getAll.cancel(),
utils.workspaces.getAllGrouped.cancel(),
Expand All @@ -64,7 +61,6 @@ export function useDeleteWorkspace(
const previousGrouped = utils.workspaces.getAllGrouped.getData();
const previousAll = utils.workspaces.getAll.getData();

// Optimistic update: remove workspace from cache
if (previousGrouped) {
utils.workspaces.getAllGrouped.setData(
undefined,
Expand Down Expand Up @@ -96,11 +92,9 @@ export function useDeleteWorkspace(
await options?.onSettled?.(...args);
},
onSuccess: async (data, variables, ...rest) => {
// Navigation already handled in onMutate (optimistic)
await options?.onSuccess?.(data, variables, ...rest);
},
onError: async (_err, variables, context, ...rest) => {
// Rollback optimistic cache updates
if (context?.previousGrouped !== undefined) {
utils.workspaces.getAllGrouped.setData(
undefined,
Expand All @@ -111,7 +105,6 @@ export function useDeleteWorkspace(
utils.workspaces.getAll.setData(undefined, context.previousAll);
}

// If we optimistically navigated away, navigate back to the deleted workspace
if (context?.wasViewingDeleted) {
navigateToWorkspace(variables.id, navigate);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,18 @@ export function useOpenWorktree(
return electronTrpc.workspaces.openWorktree.useMutation({
...options,
onSuccess: async (data, ...rest) => {
// Auto-invalidate all workspace queries
await utils.workspaces.invalidate();
// Invalidate project queries since openWorktree updates project metadata
await utils.projects.getRecents.invalidate();

const initialCommands =
Array.isArray(data.initialCommands) && data.initialCommands.length > 0
? data.initialCommands
: undefined;

// Always create a terminal tab when opening a worktree
const { tabId, paneId } = addTab(data.workspace.id);
if (initialCommands) {
setTabAutoTitle(tabId, "Workspace Setup");
}
// Pre-create terminal session (with initial commands if present)
// Terminal component will attach to this session when it mounts
createOrAttach.mutate({
paneId,
tabId,
Expand All @@ -53,7 +48,6 @@ export function useOpenWorktree(
});

if (!initialCommands) {
// Show config toast if no setup commands
toast.info("No setup script configured", {
description: "Automate workspace setup with a config.json file",
action: {
Expand All @@ -66,10 +60,8 @@ export function useOpenWorktree(
});
}

// Navigate to the opened workspace
navigateToWorkspace(data.workspace.id, navigate);

// Call user's onSuccess if provided
await options?.onSuccess?.(data, ...rest);
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function useHybridSearch<T extends SearchableTask>(tasks: T[]) {
}),
[tasks],
);
// TODO(satya): Replace this with embeddings search

const fuzzyFuse = useMemo(
() =>
new Fuse(tasks, {
Expand Down
10 changes: 2 additions & 8 deletions apps/web/src/app/auth/desktop/success/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ export default async function DesktopSuccessPage({
);
}

// Get session from Better Auth
let session: Awaited<ReturnType<typeof auth.api.getSession>> | null = null;
try {
session = await auth.api.getSession({ headers: await headers() });
Expand All @@ -50,24 +49,19 @@ export default async function DesktopSuccessPage({
);
}

// Create a separate session for the desktop app instead of reusing the browser session
// This ensures desktop and web have independent sessions with separate activeOrganizationId
// Desktop and web need independent sessions with separate activeOrganizationId
const headersObj = await headers();
const userAgent = headersObj.get("user-agent") || "Superset Desktop App";
const ipAddress =
headersObj.get("x-forwarded-for")?.split(",")[0] ||
headersObj.get("x-real-ip") ||
undefined;

// Generate a unique session token for the desktop app
const crypto = await import("node:crypto");
const token = crypto.randomBytes(32).toString("base64url");
const now = new Date();
const expiresAt = new Date(
Date.now() + 60 * 60 * 24 * 30 * 1000, // 30 days (matching auth config)
);
const expiresAt = new Date(Date.now() + 60 * 60 * 24 * 30 * 1000);

// Create a new session record in the database
await db.insert(sessions).values({
token,
userId: session.user.id,
Expand Down
4 changes: 0 additions & 4 deletions apps/web/src/app/oauth/consent/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export default async function ConsentPage({ searchParams }: ConsentPageProps) {
headers: await headers(),
});

// Redirect to sign-in if not authenticated
if (!session) {
const params = await searchParams;
const returnUrl = `/oauth/consent?${new URLSearchParams(params as Record<string, string>).toString()}`;
Expand All @@ -29,7 +28,6 @@ export default async function ConsentPage({ searchParams }: ConsentPageProps) {

const { consent_code, client_id, scope } = await searchParams;

// Validate required parameters
if (!consent_code || !client_id) {
return (
<div className="relative flex min-h-screen flex-col">
Expand Down Expand Up @@ -62,11 +60,9 @@ export default async function ConsentPage({ searchParams }: ConsentPageProps) {

const scopes = scope?.split(" ").filter(Boolean) ?? ["openid"];

// Fetch user's organization memberships via tRPC
const trpc = await api();
const userOrganizations = await trpc.user.myOrganizations.query();

// Get active organization from session or default to first
const extendedSession = session.session as typeof session.session & {
activeOrganizationId?: string | null;
};
Expand Down