diff --git a/apps/desktop/src/lib/trpc/routers/changes/branches.ts b/apps/desktop/src/lib/trpc/routers/changes/branches.ts index 792d40f5929..2aebd3da028 100644 --- a/apps/desktop/src/lib/trpc/routers/changes/branches.ts +++ b/apps/desktop/src/lib/trpc/routers/changes/branches.ts @@ -4,6 +4,11 @@ import { localDb } from "main/lib/local-db"; import simpleGit from "simple-git"; import { z } from "zod"; import { publicProcedure, router } from "../.."; +import { + getBranchBaseConfig, + setBranchBaseConfig, + unsetBranchBaseConfig, +} from "../workspaces/utils/base-branch-config"; import { getCurrentBranch } from "../workspaces/utils/git"; import { assertRegisteredWorktree, @@ -31,12 +36,25 @@ export const createBranchesRouter = () => { const branchSummary = await git.branch(["-a"]); const currentBranch = await getCurrentBranch(input.worktreePath); - - const gitConfigBase = currentBranch - ? await git - .raw(["config", `branch.${currentBranch}.base`]) - .catch(() => "") - : ""; + const { baseBranch: configuredBaseBranch } = currentBranch + ? await getBranchBaseConfig({ + repoPath: input.worktreePath, + branch: currentBranch, + }) + : { baseBranch: null }; + const persistedWorktree = localDb + .select({ + branch: worktrees.branch, + baseBranch: worktrees.baseBranch, + }) + .from(worktrees) + .where(eq(worktrees.path, input.worktreePath)) + .get(); + const persistedBaseBranch = + persistedWorktree && + (!currentBranch || persistedWorktree.branch === currentBranch) + ? (persistedWorktree.baseBranch?.trim() ?? null) + : null; const localBranches: string[] = []; const remote: string[] = []; @@ -63,7 +81,7 @@ export const createBranchesRouter = () => { remote: remote.sort(), defaultBranch, checkedOutBranches, - worktreeBaseBranch: gitConfigBase.trim() || null, + worktreeBaseBranch: configuredBaseBranch ?? persistedBaseBranch, }; }, ), @@ -87,6 +105,7 @@ export const createBranchesRouter = () => { .update(worktrees) .set({ branch: input.branch, + baseBranch: null, gitStatus, }) .where(eq(worktrees.path, input.worktreePath)) @@ -105,24 +124,31 @@ export const createBranchesRouter = () => { .mutation(async ({ input }): Promise<{ success: boolean }> => { assertRegisteredWorktree(input.worktreePath); - const git = simpleGit(input.worktreePath); const currentBranch = await getCurrentBranch(input.worktreePath); if (!currentBranch) { throw new Error("Could not determine current branch"); } if (input.baseBranch) { - await git.raw([ - "config", - `branch.${currentBranch}.base`, - input.baseBranch, - ]); + await setBranchBaseConfig({ + repoPath: input.worktreePath, + branch: currentBranch, + baseBranch: input.baseBranch, + isExplicit: true, + }); } else { - await git - .raw(["config", "--unset", `branch.${currentBranch}.base`]) - .catch(() => {}); + await unsetBranchBaseConfig({ + repoPath: input.worktreePath, + branch: currentBranch, + }); } + localDb + .update(worktrees) + .set({ baseBranch: input.baseBranch }) + .where(eq(worktrees.path, input.worktreePath)) + .run(); + return { success: true }; }), }); diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index 7bca2aa2fe5..d1b668298ee 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -812,6 +812,7 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { .optional(), branchPrefixMode: z.enum(BRANCH_PREFIX_MODES).nullable().optional(), branchPrefixCustom: z.string().nullable().optional(), + workspaceBaseBranch: z.string().nullable().optional(), hideImage: z.boolean().optional(), defaultApp: z.enum(EXTERNAL_APPS).nullable().optional(), }), @@ -840,6 +841,9 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { ...(input.patch.branchPrefixCustom !== undefined && { branchPrefixCustom: input.patch.branchPrefixCustom, }), + ...(input.patch.workspaceBaseBranch !== undefined && { + workspaceBaseBranch: input.patch.workspaceBaseBranch, + }), ...(input.patch.hideImage !== undefined && { hideImage: input.patch.hideImage, }), diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts index 9b8b8d62651..90abbcd4741 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts @@ -6,9 +6,10 @@ import { track } from "main/lib/analytics"; import { localDb } from "main/lib/local-db"; import { workspaceInitManager } from "main/lib/workspace-init-manager"; import { SUPERSET_DIR_NAME, WORKTREES_DIR_NAME } from "shared/constants"; -import simpleGit from "simple-git"; import { z } from "zod"; import { publicProcedure, router } from "../../.."; +import { resolveWorkspaceBaseBranch } from "../utils/base-branch"; +import { setBranchBaseConfig } from "../utils/base-branch-config"; import { activateProject, findOrphanedWorktreeByBranch, @@ -170,6 +171,21 @@ interface HandleNewWorktreeParams { workspaceName: string; } +async function getKnownBranchesSafe( + repoPath: string, +): Promise { + try { + const { local, remote } = await listBranches(repoPath); + return [...local, ...remote]; + } catch (error) { + console.warn( + `[workspaces/create] Failed to list branches for ${repoPath}:`, + error, + ); + return undefined; + } +} + async function handleNewWorktree({ project, prInfo, @@ -206,7 +222,12 @@ async function handleNewWorktree({ localBranchName, }); - const defaultBranch = project.defaultBranch || "main"; + const knownBranches = await getKnownBranchesSafe(project.mainRepoPath); + const baseBranch = resolveWorkspaceBaseBranch({ + workspaceBaseBranch: project.workspaceBaseBranch, + defaultBranch: project.defaultBranch, + knownBranches, + }); const worktree = localDb .insert(worktrees) @@ -214,7 +235,7 @@ async function handleNewWorktree({ projectId: project.id, path: worktreePath, branch: localBranchName, - baseBranch: defaultBranch, + baseBranch, gitStatus: null, }) .returning() @@ -233,14 +254,18 @@ async function handleNewWorktree({ workspace_id: workspace.id, project_id: project.id, branch: localBranchName, + base_branch: baseBranch, source: "pr", pr_number: prInfo.number, is_fork: prInfo.isCrossRepository, }); - await simpleGit(project.mainRepoPath) - .raw(["config", `branch.${localBranchName}.base`, defaultBranch]) - .catch(() => {}); + await setBranchBaseConfig({ + repoPath: project.mainRepoPath, + branch: localBranchName, + baseBranch, + isExplicit: false, + }); workspaceInitManager.startJob(workspace.id, project.id); initializeWorkspaceWorktree({ @@ -420,8 +445,12 @@ export const createCreateProcedures = () => { branch, ); - const defaultBranch = project.defaultBranch || "main"; - const targetBranch = input.baseBranch || defaultBranch; + const targetBranch = resolveWorkspaceBaseBranch({ + explicitBaseBranch: input.baseBranch, + workspaceBaseBranch: project.workspaceBaseBranch, + defaultBranch: project.defaultBranch, + knownBranches: existingBranches, + }); const worktree = localDb .insert(worktrees) @@ -462,9 +491,12 @@ export const createCreateProcedures = () => { use_existing_branch: input.useExistingBranch ?? false, }); - await simpleGit(project.mainRepoPath) - .raw(["config", `branch.${branch}.base`, targetBranch]) - .catch(() => {}); + await setBranchBaseConfig({ + repoPath: project.mainRepoPath, + branch, + baseBranch: targetBranch, + isExplicit: Boolean(input.baseBranch?.trim()), + }); workspaceInitManager.startJob(workspace.id, input.projectId); initializeWorkspaceWorktree({ @@ -805,14 +837,20 @@ export const createCreateProcedures = () => { }; } - const defaultBranch = project.defaultBranch || "main"; + const knownBranches = await getKnownBranchesSafe(project.mainRepoPath); + const baseBranch = resolveWorkspaceBaseBranch({ + workspaceBaseBranch: project.workspaceBaseBranch, + defaultBranch: project.defaultBranch, + knownBranches, + }); + const worktree = localDb .insert(worktrees) .values({ projectId: input.projectId, path: input.worktreePath, branch: input.branch, - baseBranch: defaultBranch, + baseBranch, gitStatus: { branch: input.branch, needsRebase: false, @@ -841,10 +879,6 @@ export const createCreateProcedures = () => { setLastActiveWorkspace(workspace.id); activateProject(project); - await simpleGit(project.mainRepoPath) - .raw(["config", `branch.${input.branch}.base`, defaultBranch]) - .catch(() => {}); - copySupersetConfigToWorktree(project.mainRepoPath, input.worktreePath); const setupConfig = loadSetupConfig({ mainRepoPath: project.mainRepoPath, @@ -856,9 +890,17 @@ export const createCreateProcedures = () => { workspace_id: workspace.id, project_id: project.id, branch: input.branch, + base_branch: baseBranch, source: "external_import", }); + await setBranchBaseConfig({ + repoPath: project.mainRepoPath, + branch: input.branch, + baseBranch, + isExplicit: false, + }); + return { workspace, initialCommands: setupConfig?.setup || null, @@ -933,6 +975,12 @@ export const createCreateProcedures = () => { if (!project) { throw new Error(`Project ${input.projectId} not found`); } + const knownBranches = await getKnownBranchesSafe(project.mainRepoPath); + const baseBranch = resolveWorkspaceBaseBranch({ + workspaceBaseBranch: project.workspaceBaseBranch, + defaultBranch: project.defaultBranch, + knownBranches, + }); let imported = 0; @@ -982,7 +1030,6 @@ export const createCreateProcedures = () => { project.mainRepoPath, ); const trackedPaths = new Set(projectWorktrees.map((wt) => wt.path)); - const defaultBranch = project.defaultBranch || "main"; const externalWorktrees = allExternalWorktrees.filter((wt) => { if (wt.path === project.mainRepoPath) return false; @@ -1003,7 +1050,7 @@ export const createCreateProcedures = () => { projectId: input.projectId, path: ext.path, branch, - baseBranch: defaultBranch, + baseBranch, gitStatus: { branch, needsRebase: false, @@ -1028,6 +1075,13 @@ export const createCreateProcedures = () => { }) .run(); + await setBranchBaseConfig({ + repoPath: project.mainRepoPath, + branch, + baseBranch, + isExplicit: false, + }); + copySupersetConfigToWorktree(project.mainRepoPath, ext.path); imported++; } diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/base-branch-config.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/base-branch-config.ts new file mode 100644 index 00000000000..cc012481aa8 --- /dev/null +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/base-branch-config.ts @@ -0,0 +1,80 @@ +import simpleGit from "simple-git"; + +interface BranchConfigParams { + repoPath: string; + branch: string; +} + +interface SetBranchBaseConfigParams extends BranchConfigParams { + baseBranch: string; + isExplicit: boolean; +} + +interface BranchBaseConfig { + baseBranch: string | null; + isExplicit: boolean; +} + +function parseBooleanConfig(value: string): boolean { + const normalized = value.trim().toLowerCase(); + return ( + normalized === "true" || + normalized === "yes" || + normalized === "on" || + normalized === "1" + ); +} + +export async function getBranchBaseConfig({ + repoPath, + branch, +}: BranchConfigParams): Promise { + const git = simpleGit(repoPath); + const [baseOutput, explicitOutput] = await Promise.all([ + git.raw(["config", `branch.${branch}.base`]).catch(() => ""), + git + .raw(["config", "--bool", `branch.${branch}.base-explicit`]) + .catch(() => ""), + ]); + + return { + baseBranch: baseOutput.trim() || null, + isExplicit: parseBooleanConfig(explicitOutput), + }; +} + +export async function setBranchBaseConfig({ + repoPath, + branch, + baseBranch, + isExplicit, +}: SetBranchBaseConfigParams): Promise { + const git = simpleGit(repoPath); + + await git + .raw(["config", `branch.${branch}.base`, baseBranch]) + .catch(() => {}); + if (isExplicit) { + await git + .raw(["config", "--bool", `branch.${branch}.base-explicit`, "true"]) + .catch(() => {}); + return; + } + + await git + .raw(["config", "--unset", `branch.${branch}.base-explicit`]) + .catch(() => {}); +} + +export async function unsetBranchBaseConfig({ + repoPath, + branch, +}: BranchConfigParams): Promise { + const git = simpleGit(repoPath); + await Promise.all([ + git.raw(["config", "--unset", `branch.${branch}.base`]).catch(() => {}), + git + .raw(["config", "--unset", `branch.${branch}.base-explicit`]) + .catch(() => {}), + ]); +} diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/base-branch.test.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/base-branch.test.ts new file mode 100644 index 00000000000..a4b11befd0e --- /dev/null +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/base-branch.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from "bun:test"; +import { resolveWorkspaceBaseBranch } from "./base-branch"; + +describe("resolveWorkspaceBaseBranch", () => { + test("uses explicit base branch when provided", () => { + const resolved = resolveWorkspaceBaseBranch({ + explicitBaseBranch: "release/2026-q1", + workspaceBaseBranch: "feature/long-lived", + defaultBranch: "main", + knownBranches: ["main", "feature/long-lived"], + }); + + expect(resolved).toBe("release/2026-q1"); + }); + + test("falls back to project workspace base branch when explicit is absent", () => { + const resolved = resolveWorkspaceBaseBranch({ + workspaceBaseBranch: "feature/long-lived", + defaultBranch: "main", + knownBranches: ["main", "feature/long-lived"], + }); + + expect(resolved).toBe("feature/long-lived"); + }); + + test("falls back to repository default branch when project preference is absent", () => { + const resolved = resolveWorkspaceBaseBranch({ + defaultBranch: "main", + knownBranches: ["main", "feature/long-lived"], + }); + + expect(resolved).toBe("main"); + }); + + test("falls back to repository default when stored preference is stale", () => { + const resolved = resolveWorkspaceBaseBranch({ + workspaceBaseBranch: "feature/deleted", + defaultBranch: "main", + knownBranches: ["main", "feature/long-lived"], + }); + + expect(resolved).toBe("main"); + }); + + test("uses workspace base branch when knownBranches is unavailable (offline)", () => { + const resolved = resolveWorkspaceBaseBranch({ + workspaceBaseBranch: "feature/long-lived", + defaultBranch: "main", + }); + expect(resolved).toBe("feature/long-lived"); + }); + test('falls back to "main" when no defaultBranch or workspaceBaseBranch is provided', () => { + const resolved = resolveWorkspaceBaseBranch({}); + expect(resolved).toBe("main"); + }); +}); diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/base-branch.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/base-branch.ts new file mode 100644 index 00000000000..6c50d7d82b0 --- /dev/null +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/base-branch.ts @@ -0,0 +1,38 @@ +interface ResolveWorkspaceBaseBranchParams { + explicitBaseBranch?: string; + workspaceBaseBranch?: string | null; + defaultBranch?: string | null; + knownBranches?: string[]; +} + +function normalizeBranch(value?: string | null): string | undefined { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +export function resolveWorkspaceBaseBranch({ + explicitBaseBranch, + workspaceBaseBranch, + defaultBranch, + knownBranches, +}: ResolveWorkspaceBaseBranchParams): string { + const fallbackBranch = normalizeBranch(defaultBranch) ?? "main"; + const explicit = normalizeBranch(explicitBaseBranch); + if (explicit) { + return explicit; + } + + const preferred = normalizeBranch(workspaceBaseBranch); + if (!preferred) { + return fallbackBranch; + } + + if (knownBranches?.length) { + const knownBranchSet = new Set(knownBranches); + if (!knownBranchSet.has(preferred)) { + return fallbackBranch; + } + } + + return preferred; +} diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts index cd33faa4e22..200b7756a79 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts @@ -4,7 +4,8 @@ import { track } from "main/lib/analytics"; import { localDb } from "main/lib/local-db"; import { workspaceInitManager } from "main/lib/workspace-init-manager"; import type { WorkspaceInitStep } from "shared/types/workspace-init"; -import simpleGit from "simple-git"; +import { resolveWorkspaceBaseBranch } from "./base-branch"; +import { getBranchBaseConfig, setBranchBaseConfig } from "./base-branch-config"; import { branchExistsOnRemote, createWorktree, @@ -65,12 +66,17 @@ export async function initializeWorkspaceWorktree({ .where(eq(projects.id, projectId)) .get(); - const gitConfigBase = await simpleGit(mainRepoPath) - .raw(["config", `branch.${branch}.base`]) - .catch(() => ""); - const baseBranchWasConfigured = !!gitConfigBase.trim(); + const { baseBranch: gitConfigBase, isExplicit: baseBranchWasExplicit } = + await getBranchBaseConfig({ + repoPath: mainRepoPath, + branch, + }); let effectiveBaseBranch = - gitConfigBase.trim() || project?.defaultBranch || "main"; + gitConfigBase || + resolveWorkspaceBaseBranch({ + workspaceBaseBranch: project?.workspaceBaseBranch, + defaultBranch: project?.defaultBranch, + }); if (useExistingBranch) { if (skipWorktreeCreation) { @@ -198,7 +204,7 @@ export async function initializeWorkspaceWorktree({ return { ref: effectiveBaseBranch }; } - if (baseBranchWasConfigured) { + if (baseBranchWasExplicit) { console.log( `[workspace-init] ${reason}. Base branch "${effectiveBaseBranch}" was explicitly set, not using fallback.`, ); @@ -246,9 +252,17 @@ export async function initializeWorkspaceWorktree({ `[workspace-init] Updating baseBranch from "${originalBranch}" to "${result.fallbackBranch}" for workspace ${workspaceId}`, ); effectiveBaseBranch = result.fallbackBranch; - await simpleGit(mainRepoPath) - .raw(["config", `branch.${branch}.base`, result.fallbackBranch]) - .catch(() => {}); + await setBranchBaseConfig({ + repoPath: mainRepoPath, + branch, + baseBranch: result.fallbackBranch, + isExplicit: false, + }); + localDb + .update(worktrees) + .set({ baseBranch: result.fallbackBranch }) + .where(eq(worktrees.id, worktreeId)) + .run(); manager.updateProgress( workspaceId, progressStep, @@ -299,7 +313,7 @@ export async function initializeWorkspaceWorktree({ workspaceId, "failed", "No local reference available", - baseBranchWasConfigured + baseBranchWasExplicit ? `${failureDetail} and branch "${effectiveBaseBranch}" doesn't exist locally.${isNetworkError ? " Please check your network connection and try again." : " Please try again with a different base branch."}` : `${failureDetail} and no local ref for "${effectiveBaseBranch}" exists.${isNetworkError ? " Please check your network connection and try again." : ""}`, ); @@ -318,7 +332,7 @@ export async function initializeWorkspaceWorktree({ workspaceId, "failed", "No local reference available", - baseBranchWasConfigured + baseBranchWasExplicit ? `No remote configured and branch "${effectiveBaseBranch}" doesn't exist locally.` : `No remote configured and no local ref for "${effectiveBaseBranch}" exists.`, ); diff --git a/apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx b/apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx index f3d537e19f8..fdec50533bf 100644 --- a/apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx +++ b/apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx @@ -39,6 +39,7 @@ import { import { LuFolderOpen } from "react-icons/lu"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { formatRelativeTime } from "renderer/lib/formatRelativeTime"; +import { resolveEffectiveWorkspaceBaseBranch } from "renderer/lib/workspaceBaseBranch"; import { useOpenProject } from "renderer/react-query/projects"; import { useCreateWorkspace } from "renderer/react-query/workspaces"; import { @@ -126,7 +127,12 @@ export function NewWorkspaceModal() { } }, [isOpen, selectedProjectId, preSelectedProjectId]); - const effectiveBaseBranch = baseBranch ?? branchData?.defaultBranch ?? null; + const effectiveBaseBranch = resolveEffectiveWorkspaceBaseBranch({ + explicitBaseBranch: baseBranch, + workspaceBaseBranch: project?.workspaceBaseBranch, + defaultBranch: branchData?.defaultBranch, + branches: branchData?.branches, + }); // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally reset when project changes useEffect(() => { @@ -227,7 +233,7 @@ export function NewWorkspaceModal() { projectId: selectedProjectId, name: workspaceName, branchName: branchSlug || undefined, - baseBranch: effectiveBaseBranch || undefined, + baseBranch: baseBranch || undefined, applyPrefix, }); @@ -355,7 +361,7 @@ export function NewWorkspaceModal() { {branchPreview || "branch-name"} - from {effectiveBaseBranch} + from {effectiveBaseBranch ?? "..."}

)} diff --git a/apps/desktop/src/renderer/lib/workspaceBaseBranch/index.ts b/apps/desktop/src/renderer/lib/workspaceBaseBranch/index.ts new file mode 100644 index 00000000000..96c9a161289 --- /dev/null +++ b/apps/desktop/src/renderer/lib/workspaceBaseBranch/index.ts @@ -0,0 +1 @@ +export { resolveEffectiveWorkspaceBaseBranch } from "./workspaceBaseBranch"; diff --git a/apps/desktop/src/renderer/lib/workspaceBaseBranch/workspaceBaseBranch.test.ts b/apps/desktop/src/renderer/lib/workspaceBaseBranch/workspaceBaseBranch.test.ts new file mode 100644 index 00000000000..92c4d76783f --- /dev/null +++ b/apps/desktop/src/renderer/lib/workspaceBaseBranch/workspaceBaseBranch.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, test } from "bun:test"; +import { resolveEffectiveWorkspaceBaseBranch } from "./workspaceBaseBranch"; + +describe("resolveEffectiveWorkspaceBaseBranch", () => { + test("prefers explicit base branch", () => { + const resolved = resolveEffectiveWorkspaceBaseBranch({ + explicitBaseBranch: "release/2026-q1", + workspaceBaseBranch: "feature/preferred", + defaultBranch: "main", + branches: [{ name: "main" }, { name: "feature/preferred" }], + }); + + expect(resolved).toBe("release/2026-q1"); + }); + + test("uses workspace base branch when branch exists", () => { + const resolved = resolveEffectiveWorkspaceBaseBranch({ + workspaceBaseBranch: "feature/preferred", + defaultBranch: "main", + branches: [{ name: "main" }, { name: "feature/preferred" }], + }); + + expect(resolved).toBe("feature/preferred"); + }); + + test("falls back to default branch when workspace branch is stale", () => { + const resolved = resolveEffectiveWorkspaceBaseBranch({ + workspaceBaseBranch: "feature/deleted", + defaultBranch: "main", + branches: [{ name: "main" }, { name: "feature/preferred" }], + }); + + expect(resolved).toBe("main"); + }); + + test("returns null when nothing resolves", () => { + const resolved = resolveEffectiveWorkspaceBaseBranch({}); + + expect(resolved).toBeNull(); + }); + + test("trusts workspace base branch when branches are undefined (offline/loading)", () => { + const resolved = resolveEffectiveWorkspaceBaseBranch({ + workspaceBaseBranch: "develop", + defaultBranch: "main", + branches: undefined, + }); + + expect(resolved).toBe("develop"); + }); + + test("falls back to default branch when branches array is empty", () => { + const resolved = resolveEffectiveWorkspaceBaseBranch({ + workspaceBaseBranch: "develop", + defaultBranch: "main", + branches: [], + }); + + expect(resolved).toBe("main"); + }); + + test("ignores empty string explicit base branch", () => { + const resolved = resolveEffectiveWorkspaceBaseBranch({ + explicitBaseBranch: "", + workspaceBaseBranch: "develop", + defaultBranch: "main", + branches: [{ name: "main" }, { name: "develop" }], + }); + + expect(resolved).toBe("develop"); + }); +}); diff --git a/apps/desktop/src/renderer/lib/workspaceBaseBranch/workspaceBaseBranch.ts b/apps/desktop/src/renderer/lib/workspaceBaseBranch/workspaceBaseBranch.ts new file mode 100644 index 00000000000..bfce5c50571 --- /dev/null +++ b/apps/desktop/src/renderer/lib/workspaceBaseBranch/workspaceBaseBranch.ts @@ -0,0 +1,39 @@ +interface BranchLike { + name: string; +} + +interface ResolveEffectiveWorkspaceBaseBranchParams { + explicitBaseBranch?: string | null; + workspaceBaseBranch?: string | null; + defaultBranch?: string | null; + branches?: BranchLike[]; +} + +function normalizeBranch(value?: string | null): string | null { + const trimmed = value?.trim(); + return trimmed ? trimmed : null; +} + +export function resolveEffectiveWorkspaceBaseBranch({ + explicitBaseBranch, + workspaceBaseBranch, + defaultBranch, + branches, +}: ResolveEffectiveWorkspaceBaseBranchParams): string | null { + const explicit = normalizeBranch(explicitBaseBranch); + if (explicit) { + return explicit; + } + + const preferred = normalizeBranch(workspaceBaseBranch); + if (preferred) { + if (!branches) { + return preferred; + } + if (branches.some((branch) => branch.name === preferred)) { + return preferred; + } + } + + return normalizeBranch(defaultBranch); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/project/$projectId/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/project/$projectId/page.tsx index bb52a707360..4ec263e0c91 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/project/$projectId/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/project/$projectId/page.tsx @@ -18,6 +18,7 @@ import { HiCheck, HiChevronDown, HiChevronUpDown } from "react-icons/hi2"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { formatRelativeTime } from "renderer/lib/formatRelativeTime"; import { electronTrpcClient as trpcClient } from "renderer/lib/trpc-client"; +import { resolveEffectiveWorkspaceBaseBranch } from "renderer/lib/workspaceBaseBranch"; import { useCreateWorkspace } from "renderer/react-query/workspaces"; import { NotFound } from "renderer/routes/not-found"; import { sanitizeSegment } from "shared/utils/branch"; @@ -102,7 +103,12 @@ function ProjectPage() { ); }, [branchData?.branches, branchSearch]); - const effectiveBaseBranch = baseBranch ?? branchData?.defaultBranch ?? null; + const effectiveBaseBranch = resolveEffectiveWorkspaceBaseBranch({ + explicitBaseBranch: baseBranch, + workspaceBaseBranch: project?.workspaceBaseBranch, + defaultBranch: branchData?.defaultBranch, + branches: branchData?.branches, + }); useEffect(() => { const timer = setTimeout(() => { @@ -130,7 +136,7 @@ function ProjectPage() { projectId, name: workspaceName, branchName: generatedBranchName || undefined, - baseBranch: effectiveBaseBranch || undefined, + baseBranch: baseBranch || undefined, }); toast.success("Workspace created", { diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/list-projects.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/list-projects.ts index 5e04117be60..4456aa594e0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/list-projects.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useCommandWatcher/tools/list-projects.ts @@ -21,6 +21,7 @@ async function execute( name: p.name, mainRepoPath: p.mainRepoPath, defaultBranch: p.defaultBranch, + workspaceBaseBranch: p.workspaceBaseBranch, color: p.color, lastOpenedAt: p.lastOpenedAt, tabOrder: p.tabOrder, diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx index 1620f51f84e..e63b06f7989 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx @@ -24,6 +24,8 @@ import { ClickablePath } from "../../../../components/ClickablePath"; import { BRANCH_PREFIX_MODE_LABELS_WITH_DEFAULT } from "../../../../utils/branch-prefix"; import { ScriptsEditor } from "./components/ScriptsEditor"; +const REPO_DEFAULT_BASE_BRANCH = "__repo_default__"; + export function SettingsSection({ icon, title, @@ -60,6 +62,11 @@ export function ProjectSettings({ projectId }: ProjectSettingsProps) { const { data: project } = electronTrpc.projects.get.useQuery({ id: projectId, }); + const { data: branchData, isLoading: isBranchDataLoading } = + electronTrpc.projects.getBranches.useQuery( + { projectId }, + { enabled: !!projectId }, + ); const { data: gitAuthor } = electronTrpc.projects.getGitAuthor.useQuery({ id: projectId, }); @@ -155,6 +162,15 @@ export function ProjectSettings({ projectId }: ProjectSettingsProps) { }); }; + const handleWorkspaceBaseBranchChange = (value: string) => { + updateProject.mutate({ + id: projectId, + patch: { + workspaceBaseBranch: value === REPO_DEFAULT_BASE_BRANCH ? null : value, + }, + }); + }; + const getPreviewPrefix = ( mode: BranchPrefixMode | "default", ): string | null => { @@ -182,6 +198,17 @@ export function ProjectSettings({ projectId }: ProjectSettingsProps) { const currentMode = project.branchPrefixMode ?? "default"; const previewPrefix = getPreviewPrefix(currentMode); + const repoDefaultBranch = + branchData?.defaultBranch ?? project.defaultBranch ?? "main"; + const workspaceBaseBranchValue = + project.workspaceBaseBranch ?? REPO_DEFAULT_BASE_BRANCH; + const workspaceBaseBranchMissing = + !isBranchDataLoading && + !!project.workspaceBaseBranch && + !!branchData && + !branchData.branches.some( + (branch) => branch.name === project.workspaceBaseBranch, + ); return (
@@ -244,6 +271,56 @@ export function ProjectSettings({ projectId }: ProjectSettingsProps) {
+ } + title="Workspace Base Branch" + description="Set the default base branch for new workspaces in this repository." + > +
+
+ +

+ Used when creating a workspace unless you choose a one-off base + branch. +

+
+ +
+ {workspaceBaseBranchMissing && ( +

+ Branch "{project.workspaceBaseBranch}" no longer exists. New + workspaces will fall back to "{repoDefaultBranch}". +

+ )} +
+
diff --git a/biome.jsonc b/biome.jsonc index 101e9c7eaf5..0f49d9ccf3c 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.4.2/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.8/schema.json", "vcs": { "enabled": true, "clientKind": "git", diff --git a/packages/local-db/drizzle/0029_add_workspace_base_branch.sql b/packages/local-db/drizzle/0029_add_workspace_base_branch.sql new file mode 100644 index 00000000000..08d855a7b68 --- /dev/null +++ b/packages/local-db/drizzle/0029_add_workspace_base_branch.sql @@ -0,0 +1 @@ +ALTER TABLE `projects` ADD `workspace_base_branch` text; \ No newline at end of file diff --git a/packages/local-db/drizzle/meta/0029_snapshot.json b/packages/local-db/drizzle/meta/0029_snapshot.json new file mode 100644 index 00000000000..6c6a4afd754 --- /dev/null +++ b/packages/local-db/drizzle/meta/0029_snapshot.json @@ -0,0 +1,1224 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "63568533-c315-4206-a426-d69f23b77874", + "prevId": "64e86972-e7d7-435c-8d6e-402b74ba1432", + "tables": { + "browser_history": { + "name": "browser_history", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "favicon_url": { + "name": "favicon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_visited_at": { + "name": "last_visited_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visit_count": { + "name": "visit_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": { + "browser_history_url_unique": { + "name": "browser_history_url_unique", + "columns": [ + "url" + ], + "isUnique": true + }, + "browser_history_url_idx": { + "name": "browser_history_url_idx", + "columns": [ + "url" + ], + "isUnique": false + }, + "browser_history_last_visited_at_idx": { + "name": "browser_history_last_visited_at_idx", + "columns": [ + "last_visited_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization_members": { + "name": "organization_members", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organization_members_organization_id_idx": { + "name": "organization_members_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "organization_members_user_id_idx": { + "name": "organization_members_user_id_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "organization_members_organization_id_organizations_id_fk": { + "name": "organization_members_organization_id_organizations_id_fk", + "tableFrom": "organization_members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_members_user_id_users_id_fk": { + "name": "organization_members_user_id_users_id_fk", + "tableFrom": "organization_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organizations": { + "name": "organizations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "clerk_org_id": { + "name": "clerk_org_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "github_org": { + "name": "github_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "organizations_clerk_org_id_unique": { + "name": "organizations_clerk_org_id_unique", + "columns": [ + "clerk_org_id" + ], + "isUnique": true + }, + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "organizations_slug_idx": { + "name": "organizations_slug_idx", + "columns": [ + "slug" + ], + "isUnique": false + }, + "organizations_clerk_org_id_idx": { + "name": "organizations_clerk_org_id_idx", + "columns": [ + "clerk_org_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "projects": { + "name": "projects", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "main_repo_path": { + "name": "main_repo_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_opened_at": { + "name": "last_opened_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "config_toast_dismissed": { + "name": "config_toast_dismissed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "workspace_base_branch": { + "name": "workspace_base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_owner": { + "name": "github_owner", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_mode": { + "name": "branch_prefix_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_custom": { + "name": "branch_prefix_custom", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "hide_image": { + "name": "hide_image", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "neon_project_id": { + "name": "neon_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "default_app": { + "name": "default_app", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "projects_main_repo_path_idx": { + "name": "projects_main_repo_path_idx", + "columns": [ + "main_repo_path" + ], + "isUnique": false + }, + "projects_last_opened_at_idx": { + "name": "projects_last_opened_at_idx", + "columns": [ + "last_opened_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "settings": { + "name": "settings", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "last_active_workspace_id": { + "name": "last_active_workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_presets": { + "name": "terminal_presets", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_presets_initialized": { + "name": "terminal_presets_initialized", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selected_ringtone_id": { + "name": "selected_ringtone_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirm_on_quit": { + "name": "confirm_on_quit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_link_behavior": { + "name": "terminal_link_behavior", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "persist_terminal": { + "name": "persist_terminal", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": true + }, + "auto_apply_default_preset": { + "name": "auto_apply_default_preset", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_mode": { + "name": "branch_prefix_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch_prefix_custom": { + "name": "branch_prefix_custom", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "notification_sounds_muted": { + "name": "notification_sounds_muted", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "delete_local_branch": { + "name": "delete_local_branch", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "file_open_mode": { + "name": "file_open_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_presets_bar": { + "name": "show_presets_bar", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_font_family": { + "name": "terminal_font_family", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "terminal_font_size": { + "name": "terminal_font_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "editor_font_family": { + "name": "editor_font_family", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "editor_font_size": { + "name": "editor_font_size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "show_resource_monitor": { + "name": "show_resource_monitor", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tasks": { + "name": "tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status_color": { + "name": "status_color", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_type": { + "name": "status_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_position": { + "name": "status_position", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "repository_id": { + "name": "repository_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "assignee_id": { + "name": "assignee_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "estimate": { + "name": "estimate", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "due_date": { + "name": "due_date", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "labels": { + "name": "labels", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_provider": { + "name": "external_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_key": { + "name": "external_key", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "external_url": { + "name": "external_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tasks_slug_unique": { + "name": "tasks_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + }, + "tasks_slug_idx": { + "name": "tasks_slug_idx", + "columns": [ + "slug" + ], + "isUnique": false + }, + "tasks_organization_id_idx": { + "name": "tasks_organization_id_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "tasks_assignee_id_idx": { + "name": "tasks_assignee_id_idx", + "columns": [ + "assignee_id" + ], + "isUnique": false + }, + "tasks_status_idx": { + "name": "tasks_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "tasks_created_at_idx": { + "name": "tasks_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tasks_organization_id_organizations_id_fk": { + "name": "tasks_organization_id_organizations_id_fk", + "tableFrom": "tasks", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_assignee_id_users_id_fk": { + "name": "tasks_assignee_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_creator_id_users_id_fk": { + "name": "tasks_creator_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "clerk_id": { + "name": "clerk_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_clerk_id_unique": { + "name": "users_clerk_id_unique", + "columns": [ + "clerk_id" + ], + "isUnique": true + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "users_email_idx": { + "name": "users_email_idx", + "columns": [ + "email" + ], + "isUnique": false + }, + "users_clerk_id_idx": { + "name": "users_clerk_id_idx", + "columns": [ + "clerk_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "workspaces": { + "name": "workspaces", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worktree_id": { + "name": "worktree_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tab_order": { + "name": "tab_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_opened_at": { + "name": "last_opened_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_unread": { + "name": "is_unread", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "is_unnamed": { + "name": "is_unnamed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "deleting_at": { + "name": "deleting_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "port_base": { + "name": "port_base", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "workspaces_project_id_idx": { + "name": "workspaces_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "workspaces_worktree_id_idx": { + "name": "workspaces_worktree_id_idx", + "columns": [ + "worktree_id" + ], + "isUnique": false + }, + "workspaces_last_opened_at_idx": { + "name": "workspaces_last_opened_at_idx", + "columns": [ + "last_opened_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "workspaces_project_id_projects_id_fk": { + "name": "workspaces_project_id_projects_id_fk", + "tableFrom": "workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspaces_worktree_id_worktrees_id_fk": { + "name": "workspaces_worktree_id_worktrees_id_fk", + "tableFrom": "workspaces", + "tableTo": "worktrees", + "columnsFrom": [ + "worktree_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "worktrees": { + "name": "worktrees", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "git_status": { + "name": "git_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_status": { + "name": "github_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "worktrees_project_id_idx": { + "name": "worktrees_project_id_idx", + "columns": [ + "project_id" + ], + "isUnique": false + }, + "worktrees_branch_idx": { + "name": "worktrees_branch_idx", + "columns": [ + "branch" + ], + "isUnique": false + } + }, + "foreignKeys": { + "worktrees_project_id_projects_id_fk": { + "name": "worktrees_project_id_projects_id_fk", + "tableFrom": "worktrees", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/packages/local-db/drizzle/meta/_journal.json b/packages/local-db/drizzle/meta/_journal.json index 3ba8be371e9..6612d90fdbb 100644 --- a/packages/local-db/drizzle/meta/_journal.json +++ b/packages/local-db/drizzle/meta/_journal.json @@ -204,6 +204,13 @@ "when": 1771539756028, "tag": "0028_add_show_resource_monitor_setting", "breakpoints": true + }, + { + "idx": 29, + "version": "6", + "when": 1771547603399, + "tag": "0029_add_workspace_base_branch", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/local-db/src/schema/schema.ts b/packages/local-db/src/schema/schema.ts index 5d615ad1195..1c617ef78ba 100644 --- a/packages/local-db/src/schema/schema.ts +++ b/packages/local-db/src/schema/schema.ts @@ -35,6 +35,7 @@ export const projects = sqliteTable( mode: "boolean", }), defaultBranch: text("default_branch"), + workspaceBaseBranch: text("workspace_base_branch"), githubOwner: text("github_owner"), branchPrefixMode: text("branch_prefix_mode").$type(), branchPrefixCustom: text("branch_prefix_custom"),