From cbfe26bac1c179b9b753dd08564f3229e977d4b3 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Mon, 19 May 2025 14:41:18 -0700 Subject: [PATCH] Rename branch (#199) This is more important now that import app is available and not every git repo will be initialized with `main` as the default branch. This handles the other common case which is the `master` branch. --- src/components/chat/ChatHeader.tsx | 41 ++++++++++++++++----- src/hooks/useRenameBranch.ts | 58 ++++++++++++++++++++++++++++++ src/ipc/handlers/app_handlers.ts | 53 ++++++++++++++++++++++++++- src/ipc/ipc_client.ts | 5 +++ src/ipc/ipc_types.ts | 6 ++++ src/preload.ts | 1 + 6 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 src/hooks/useRenameBranch.ts diff --git a/src/components/chat/ChatHeader.tsx b/src/components/chat/ChatHeader.tsx index c2dc6cb..7a75080 100644 --- a/src/components/chat/ChatHeader.tsx +++ b/src/components/chat/ChatHeader.tsx @@ -20,11 +20,12 @@ import { IpcClient } from "@/ipc/ipc_client"; import { useRouter } from "@tanstack/react-router"; import { selectedChatIdAtom } from "@/atoms/chatAtoms"; import { useChats } from "@/hooks/useChats"; -import { showError } from "@/lib/toast"; +import { showError, showSuccess } from "@/lib/toast"; import { useEffect } from "react"; import { useStreamChat } from "@/hooks/useStreamChat"; import { useCurrentBranch } from "@/hooks/useCurrentBranch"; import { useCheckoutVersion } from "@/hooks/useCheckoutVersion"; +import { useRenameBranch } from "@/hooks/useRenameBranch"; interface ChatHeaderProps { isVersionPaneOpen: boolean; @@ -53,6 +54,7 @@ export function ChatHeader({ } = useCurrentBranch(appId); const { checkoutVersion, isCheckingOutVersion } = useCheckoutVersion(); + const { renameBranch, isRenamingBranch } = useRenameBranch(); useEffect(() => { if (appId) { @@ -65,6 +67,14 @@ export function ChatHeader({ await checkoutVersion({ appId, versionId: "main" }); }; + const handleRenameMasterToMain = async () => { + if (!appId) return; + // If this throws, it will automatically show an error toast + await renameBranch({ oldBranchName: "master", newBranchName: "main" }); + + showSuccess("Master branch renamed to main"); + }; + const handleNewChat = async () => { if (appId) { try { @@ -127,14 +137,27 @@ export function ChatHeader({ {branchInfoLoading && Checking branch...} - + {currentBranchName === "master" ? ( + + ) : ( + + )} )} diff --git a/src/hooks/useRenameBranch.ts b/src/hooks/useRenameBranch.ts new file mode 100644 index 0000000..e700a5a --- /dev/null +++ b/src/hooks/useRenameBranch.ts @@ -0,0 +1,58 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { IpcClient } from "@/ipc/ipc_client"; +import { showError } from "@/lib/toast"; +import { selectedAppIdAtom } from "@/atoms/appAtoms"; +import { useAtomValue } from "jotai"; + +interface RenameBranchParams { + appId: number; + oldBranchName: string; + newBranchName: string; +} + +export function useRenameBranch() { + const queryClient = useQueryClient(); + const currentAppId = useAtomValue(selectedAppIdAtom); + + const mutation = useMutation({ + mutationFn: async (params: RenameBranchParams) => { + if (params.appId === null || params.appId === undefined) { + throw new Error("App ID is required to rename a branch."); + } + if (!params.oldBranchName) { + throw new Error("Old branch name is required."); + } + if (!params.newBranchName) { + throw new Error("New branch name is required."); + } + await IpcClient.getInstance().renameBranch(params); + }, + onSuccess: (_, variables) => { + // Invalidate queries that depend on branch information + queryClient.invalidateQueries({ + queryKey: ["currentBranch", variables.appId], + }); + queryClient.invalidateQueries({ + queryKey: ["versions", variables.appId], + }); + // Potentially show a success message or trigger other actions + }, + meta: { + showErrorToast: true, + }, + }); + + const renameBranch = async (params: Omit) => { + if (!currentAppId) { + showError("No application selected."); + return; + } + return mutation.mutateAsync({ ...params, appId: currentAppId }); + }; + + return { + renameBranch, + isRenamingBranch: mutation.isPending, + renameBranchError: mutation.error, + }; +} diff --git a/src/ipc/handlers/app_handlers.ts b/src/ipc/handlers/app_handlers.ts index 71332ba..5344437 100644 --- a/src/ipc/handlers/app_handlers.ts +++ b/src/ipc/handlers/app_handlers.ts @@ -2,7 +2,7 @@ import { ipcMain } from "electron"; import { db, getDatabasePath } from "../../db"; import { apps, chats } from "../../db/schema"; import { desc, eq } from "drizzle-orm"; -import type { App, CreateAppParams } from "../ipc_types"; +import type { App, CreateAppParams, RenameBranchParams } from "../ipc_types"; import fs from "node:fs"; import path from "node:path"; import { getDyadAppPath, getUserDataPath } from "../../paths/paths"; @@ -767,4 +767,55 @@ export function registerAppHandlers() { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); return { version: packageJson.version }; }); + + handle("rename-branch", async (_, params: RenameBranchParams) => { + const { appId, oldBranchName, newBranchName } = params; + const app = await db.query.apps.findFirst({ + where: eq(apps.id, appId), + }); + + if (!app) { + throw new Error("App not found"); + } + + const appPath = getDyadAppPath(app.path); + + return withLock(appId, async () => { + try { + // Check if the old branch exists + const branches = await git.listBranches({ fs, dir: appPath }); + if (!branches.includes(oldBranchName)) { + throw new Error(`Branch '${oldBranchName}' not found.`); + } + + // Check if the new branch name already exists + if (branches.includes(newBranchName)) { + // If newBranchName is 'main' and oldBranchName is 'master', + // and 'main' already exists, we might want to allow this if 'main' is the current branch + // and just switch to it, or delete 'master'. + // For now, let's keep it simple and throw an error. + throw new Error( + `Branch '${newBranchName}' already exists. Cannot rename.`, + ); + } + + await git.renameBranch({ + fs: fs, + dir: appPath, + oldref: oldBranchName, + ref: newBranchName, + }); + logger.info( + `Branch renamed from '${oldBranchName}' to '${newBranchName}' for app ${appId}`, + ); + } catch (error: any) { + logger.error( + `Failed to rename branch for app ${appId}: ${error.message}`, + ); + throw new Error( + `Failed to rename branch '${oldBranchName}' to '${newBranchName}': ${error.message}`, + ); + } + }); + }); } diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index 9da44de..db98a68 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -29,6 +29,7 @@ import type { ApproveProposalResult, ImportAppResult, ImportAppParams, + RenameBranchParams, } from "./ipc_types"; import type { ProposalResult } from "@/lib/schemas"; import { showError } from "@/lib/toast"; @@ -816,4 +817,8 @@ export class IpcClient { }): Promise<{ exists: boolean }> { return this.ipcRenderer.invoke("check-app-name", params); } + + public async renameBranch(params: RenameBranchParams): Promise { + await this.ipcRenderer.invoke("rename-branch", params); + } } diff --git a/src/ipc/ipc_types.ts b/src/ipc/ipc_types.ts index 6ad3f08..744cefc 100644 --- a/src/ipc/ipc_types.ts +++ b/src/ipc/ipc_types.ts @@ -201,3 +201,9 @@ export interface ImportAppResult { appId: number; chatId: number; } + +export interface RenameBranchParams { + appId: number; + oldBranchName: string; + newBranchName: string; +} diff --git a/src/preload.ts b/src/preload.ts index 4e53390..3e6110f 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -73,6 +73,7 @@ const validInvokeChannels = [ "check-ai-rules", "select-app-folder", "check-app-name", + "rename-branch", ] as const; // Add valid receive channels