diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index bd5da74b855..0a015043544 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -17,7 +17,11 @@ import { PROJECT_COLOR_VALUES } from "shared/constants/project-colors"; import simpleGit from "simple-git"; import { z } from "zod"; import { publicProcedure, router } from "../.."; -import { getDefaultBranch, getGitRoot } from "../workspaces/utils/git"; +import { + getDefaultBranch, + getGitRoot, + refreshDefaultBranch, +} from "../workspaces/utils/git"; import { assignRandomColor } from "./utils/colors"; type Project = SelectProject; @@ -253,10 +257,22 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { branches = branchList.map((name) => ({ name, lastCommitDate: 0 })); } - // Determine default branch - let defaultBranch = project.defaultBranch; - if (!defaultBranch) { - defaultBranch = await getDefaultBranch(project.mainRepoPath); + // Sync with remote in case the default branch changed (e.g. master -> main) + const remoteDefaultBranch = await refreshDefaultBranch( + project.mainRepoPath, + ); + + const defaultBranch = + remoteDefaultBranch || + project.defaultBranch || + (await getDefaultBranch(project.mainRepoPath)); + + if (defaultBranch !== project.defaultBranch) { + localDb + .update(projects) + .set({ defaultBranch }) + .where(eq(projects.id, input.projectId)) + .run(); } // Sort: default branch first, then by date @@ -581,6 +597,54 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { return { success: true }; }), + refreshDefaultBranch: publicProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input }) => { + const project = localDb + .select() + .from(projects) + .where(eq(projects.id, input.id)) + .get(); + + if (!project) { + throw new Error(`Project ${input.id} not found`); + } + + const remoteDefaultBranch = await refreshDefaultBranch( + project.mainRepoPath, + ); + + if ( + remoteDefaultBranch && + remoteDefaultBranch !== project.defaultBranch + ) { + localDb + .update(projects) + .set({ defaultBranch: remoteDefaultBranch }) + .where(eq(projects.id, input.id)) + .run(); + + return { + success: true, + defaultBranch: remoteDefaultBranch, + changed: true, + previousBranch: project.defaultBranch, + }; + } + + // Ensure we always return a valid default branch + const defaultBranch = + project.defaultBranch ?? + remoteDefaultBranch ?? + (await getDefaultBranch(project.mainRepoPath)); + + return { + success: true, + defaultBranch, + changed: false, + }; + }), + close: publicProcedure .input(z.object({ id: z.string() })) .mutation(async ({ input }) => { diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts index ddd43e51711..6e48931dcc0 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts @@ -322,6 +322,49 @@ export async function fetchDefaultBranch( return commit.trim(); } +/** + * Refreshes the local origin/HEAD symref from the remote and returns the current default branch. + * This detects when the remote repository's default branch has changed (e.g., master -> main). + * @param mainRepoPath - Path to the main repository + * @returns The current default branch name, or null if unable to determine + */ +export async function refreshDefaultBranch( + mainRepoPath: string, +): Promise { + const git = simpleGit(mainRepoPath); + + const hasRemote = await hasOriginRemote(mainRepoPath); + if (!hasRemote) { + return null; + } + + try { + // Git doesn't auto-update origin/HEAD on fetch, so we must explicitly + // sync it to detect when the remote's default branch changes + await git.remote(["set-head", "origin", "--auto"]); + + const headRef = await git.raw(["symbolic-ref", "refs/remotes/origin/HEAD"]); + const match = headRef.trim().match(/refs\/remotes\/origin\/(.+)/); + if (match) { + return match[1]; + } + } catch { + // set-head requires network access; fall back to ls-remote which may + // work in some edge cases or provide a more specific error + try { + const result = await git.raw(["ls-remote", "--symref", "origin", "HEAD"]); + const symrefMatch = result.match(/ref:\s+refs\/heads\/(.+?)\tHEAD/); + if (symrefMatch) { + return symrefMatch[1]; + } + } catch { + // Network unavailable - caller will use cached value + } + } + + return null; +} + export async function checkNeedsRebase( worktreePath: string, defaultBranch: string, diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts index 21705214431..bd729ed299e 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts @@ -27,6 +27,7 @@ import { hasUncommittedChanges, hasUnpushedCommits, listBranches, + refreshDefaultBranch, removeWorktree, safeCheckoutBranch, worktreeExists, @@ -67,11 +68,17 @@ export const createWorkspacesRouter = () => { branch, ); - // Get default branch (lazy migration for existing projects without defaultBranch) - let defaultBranch = project.defaultBranch; - if (!defaultBranch) { - defaultBranch = await getDefaultBranch(project.mainRepoPath); - // Save it for future use + // Sync with remote in case the default branch changed (e.g. master -> main) + const remoteDefaultBranch = await refreshDefaultBranch( + project.mainRepoPath, + ); + + const defaultBranch = + remoteDefaultBranch || + project.defaultBranch || + (await getDefaultBranch(project.mainRepoPath)); + + if (defaultBranch !== project.defaultBranch) { localDb .update(projects) .set({ defaultBranch }) @@ -79,7 +86,6 @@ export const createWorkspacesRouter = () => { .run(); } - // Use provided baseBranch or fall back to default const targetBranch = input.baseBranch || defaultBranch; // Check if this repo has a remote origin @@ -1058,11 +1064,20 @@ export const createWorkspacesRouter = () => { throw new Error(`Project ${workspace.projectId} not found`); } - // Get default branch (lazy migration for existing projects without defaultBranch) + // Sync with remote in case the default branch changed (e.g. master -> main) + const remoteDefaultBranch = await refreshDefaultBranch( + project.mainRepoPath, + ); + let defaultBranch = project.defaultBranch; if (!defaultBranch) { defaultBranch = await getDefaultBranch(project.mainRepoPath); - // Save it for future use + } + if (remoteDefaultBranch && remoteDefaultBranch !== defaultBranch) { + defaultBranch = remoteDefaultBranch; + } + + if (defaultBranch !== project.defaultBranch) { localDb .update(projects) .set({ defaultBranch }) @@ -1092,7 +1107,7 @@ export const createWorkspacesRouter = () => { .where(eq(worktrees.id, worktree.id)) .run(); - return { gitStatus }; + return { gitStatus, defaultBranch }; }), getGitHubStatus: publicProcedure