diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index d04925039a9..ba0dcfa465e 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -365,6 +365,26 @@ export const createSettingsRouter = () => { }; }), + getDeleteLocalBranch: publicProcedure.query(() => { + const row = getSettings(); + return row.deleteLocalBranch ?? false; + }), + + setDeleteLocalBranch: publicProcedure + .input(z.object({ enabled: z.boolean() })) + .mutation(({ input }) => { + localDb + .insert(settings) + .values({ id: 1, deleteLocalBranch: input.enabled }) + .onConflictDoUpdate({ + target: settings.id, + set: { deleteLocalBranch: input.enabled }, + }) + .run(); + + return { success: true }; + }), + getNotificationSoundsMuted: publicProcedure.query(() => { const row = getSettings(); return row.notificationSoundsMuted ?? false; diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts index bf8b60b42a2..a150bb9a455 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts @@ -17,6 +17,7 @@ import { updateActiveWorkspaceIfRemoved, } from "../utils/db-helpers"; import { + deleteLocalBranch, hasUncommittedChanges, hasUnpushedCommits, worktreeExists, @@ -148,7 +149,9 @@ export const createDeleteProcedures = () => { }), delete: publicProcedure - .input(z.object({ id: z.string() })) + .input( + z.object({ id: z.string(), deleteLocalBranch: z.boolean().optional() }), + ) .mutation(async ({ input }) => { const workspace = getWorkspace(input.id); @@ -247,6 +250,20 @@ export const createDeleteProcedures = () => { } finally { workspaceInitManager.releaseProjectLock(project.id); } + + if (input.deleteLocalBranch && workspace.branch) { + try { + await deleteLocalBranch({ + mainRepoPath: project.mainRepoPath, + branch: workspace.branch, + }); + } catch (error) { + console.error( + `[workspace/delete] Branch cleanup failed (non-blocking):`, + error instanceof Error ? error.message : String(error), + ); + } + } } deleteWorkspace(input.id); 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 991574d5ca8..5dcc4347beb 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts @@ -632,6 +632,32 @@ export async function createWorktreeFromExistingBranch({ } } +export async function deleteLocalBranch({ + mainRepoPath, + branch, +}: { + mainRepoPath: string; + branch: string; +}): Promise { + const env = await getGitEnv(); + + try { + await execFileAsync("git", ["-C", mainRepoPath, "branch", "-D", branch], { + env, + timeout: 10_000, + }); + console.log(`[workspace/delete] Deleted local branch "${branch}"`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error( + `[workspace/delete] Failed to delete local branch "${branch}": ${errorMessage}`, + ); + throw new Error( + `Failed to delete local branch "${branch}": ${errorMessage}`, + ); + } +} + export async function removeWorktree( mainRepoPath: string, worktreePath: string, diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx index 56addf8f714..f28e73c8ef1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx @@ -28,6 +28,10 @@ export function BehaviorSettings({ visibleItems }: BehaviorSettingsProps) { SETTING_ITEM_ID.BEHAVIOR_CONFIRM_QUIT, visibleItems, ); + const showDeleteLocalBranch = isItemVisible( + SETTING_ITEM_ID.BEHAVIOR_DELETE_LOCAL_BRANCH, + visibleItems, + ); const showBranchPrefix = isItemVisible( SETTING_ITEM_ID.BEHAVIOR_BRANCH_PREFIX, visibleItems, @@ -62,6 +66,33 @@ export function BehaviorSettings({ visibleItems }: BehaviorSettingsProps) { setConfirmOnQuit.mutate({ enabled }); }; + const { data: deleteLocalBranch, isLoading: isDeleteBranchLoading } = + electronTrpc.settings.getDeleteLocalBranch.useQuery(); + const setDeleteLocalBranch = + electronTrpc.settings.setDeleteLocalBranch.useMutation({ + onMutate: async ({ enabled }) => { + await utils.settings.getDeleteLocalBranch.cancel(); + const previous = utils.settings.getDeleteLocalBranch.getData(); + utils.settings.getDeleteLocalBranch.setData(undefined, enabled); + return { previous }; + }, + onError: (_err, _vars, context) => { + if (context?.previous !== undefined) { + utils.settings.getDeleteLocalBranch.setData( + undefined, + context.previous, + ); + } + }, + onSettled: () => { + utils.settings.getDeleteLocalBranch.invalidate(); + }, + }); + + const handleDeleteBranchToggle = (enabled: boolean) => { + setDeleteLocalBranch.mutate({ enabled }); + }; + // TODO: remove telemetry query/mutation/handler once telemetry procedures are removed const { data: telemetryEnabled, isLoading: isTelemetryLoading } = electronTrpc.settings.getTelemetryEnabled.useQuery(); @@ -171,6 +202,29 @@ export function BehaviorSettings({ visibleItems }: BehaviorSettingsProps) { )} + {showDeleteLocalBranch && ( +
+
+ +

+ Also delete the local git branch when deleting a worktree + workspace +

+
+ +
+ )} + {showBranchPrefix && (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts index 5200d4706a9..c9632ae229a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts @@ -21,6 +21,7 @@ export const SETTING_ITEM_ID = { KEYBOARD_SHORTCUTS: "keyboard-shortcuts", BEHAVIOR_CONFIRM_QUIT: "behavior-confirm-quit", + BEHAVIOR_DELETE_LOCAL_BRANCH: "behavior-delete-local-branch", BEHAVIOR_BRANCH_PREFIX: "behavior-branch-prefix", BEHAVIOR_TELEMETRY: "behavior-telemetry", @@ -308,6 +309,24 @@ export const SETTINGS_ITEMS: SettingsItem[] = [ "unsaved", ], }, + { + id: SETTING_ITEM_ID.BEHAVIOR_DELETE_LOCAL_BRANCH, + section: "behavior", + title: "Delete local branch on workspace removal", + description: + "Also delete the local git branch when deleting a worktree workspace", + keywords: [ + "features", + "delete", + "branch", + "local", + "worktree", + "workspace", + "remove", + "cleanup", + "git", + ], + }, { id: SETTING_ITEM_ID.BEHAVIOR_BRANCH_PREFIX, section: "behavior", diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx index 8d919f1d8ee..9c8b9b51ff5 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx @@ -9,6 +9,7 @@ import { import { Button } from "@superset/ui/button"; import { toast } from "@superset/ui/sonner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { useState } from "react"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { useCloseWorkspace, @@ -33,6 +34,18 @@ export function DeleteWorkspaceDialog({ const isBranch = workspaceType === "branch"; const deleteWorkspace = useDeleteWorkspace(); const closeWorkspace = useCloseWorkspace(); + const setDeleteLocalBranchSetting = + electronTrpc.settings.setDeleteLocalBranch.useMutation(); + + const { data: deleteLocalBranchDefault } = + electronTrpc.settings.getDeleteLocalBranch.useQuery(undefined, { + enabled: open && !isBranch, + }); + const [deleteLocalBranch, setDeleteLocalBranch] = useState( + null, + ); + const deleteLocalBranchChecked = + deleteLocalBranch ?? deleteLocalBranchDefault ?? false; const { data: gitStatusData, isLoading: isLoadingGitStatus } = electronTrpc.workspaces.canDelete.useQuery( @@ -85,13 +98,22 @@ export function DeleteWorkspaceDialog({ const handleDelete = () => { onOpenChange(false); + setDeleteLocalBranchSetting.mutate({ + enabled: deleteLocalBranchChecked, + }); + toast.promise( - deleteWorkspace.mutateAsync({ id: workspaceId }).then((result) => { - if (!result.success) { - throw new Error(result.error ?? "Failed to delete"); - } - return result; - }), + deleteWorkspace + .mutateAsync({ + id: workspaceId, + deleteLocalBranch: deleteLocalBranchChecked, + }) + .then((result) => { + if (!result.success) { + throw new Error(result.error ?? "Failed to delete"); + } + return result; + }), { loading: `Deleting "${workspaceName}"...`, success: (result) => { @@ -183,7 +205,7 @@ export function DeleteWorkspaceDialog({ {!isLoading && canDelete && hasWarnings && (
-
+
{hasChanges && hasUnpushedCommits ? "Has uncommitted changes and unpushed commits" : hasChanges @@ -193,6 +215,19 @@ export function DeleteWorkspaceDialog({
)} + {!isLoading && canDelete && ( +
+ +
+ )} +