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.
This commit is contained in:
Will Chen
2025-05-19 14:41:18 -07:00
committed by GitHub
parent bc8166c274
commit cbfe26bac1
6 changed files with 154 additions and 10 deletions

View File

@@ -20,11 +20,12 @@ import { IpcClient } from "@/ipc/ipc_client";
import { useRouter } from "@tanstack/react-router"; import { useRouter } from "@tanstack/react-router";
import { selectedChatIdAtom } from "@/atoms/chatAtoms"; import { selectedChatIdAtom } from "@/atoms/chatAtoms";
import { useChats } from "@/hooks/useChats"; import { useChats } from "@/hooks/useChats";
import { showError } from "@/lib/toast"; import { showError, showSuccess } from "@/lib/toast";
import { useEffect } from "react"; import { useEffect } from "react";
import { useStreamChat } from "@/hooks/useStreamChat"; import { useStreamChat } from "@/hooks/useStreamChat";
import { useCurrentBranch } from "@/hooks/useCurrentBranch"; import { useCurrentBranch } from "@/hooks/useCurrentBranch";
import { useCheckoutVersion } from "@/hooks/useCheckoutVersion"; import { useCheckoutVersion } from "@/hooks/useCheckoutVersion";
import { useRenameBranch } from "@/hooks/useRenameBranch";
interface ChatHeaderProps { interface ChatHeaderProps {
isVersionPaneOpen: boolean; isVersionPaneOpen: boolean;
@@ -53,6 +54,7 @@ export function ChatHeader({
} = useCurrentBranch(appId); } = useCurrentBranch(appId);
const { checkoutVersion, isCheckingOutVersion } = useCheckoutVersion(); const { checkoutVersion, isCheckingOutVersion } = useCheckoutVersion();
const { renameBranch, isRenamingBranch } = useRenameBranch();
useEffect(() => { useEffect(() => {
if (appId) { if (appId) {
@@ -65,6 +67,14 @@ export function ChatHeader({
await checkoutVersion({ appId, versionId: "main" }); 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 () => { const handleNewChat = async () => {
if (appId) { if (appId) {
try { try {
@@ -127,14 +137,27 @@ export function ChatHeader({
{branchInfoLoading && <span>Checking branch...</span>} {branchInfoLoading && <span>Checking branch...</span>}
</span> </span>
</div> </div>
<Button {currentBranchName === "master" ? (
variant="outline" <Button
size="sm" variant="outline"
onClick={handleCheckoutMainBranch} size="sm"
disabled={isCheckingOutVersion || branchInfoLoading} onClick={handleRenameMasterToMain}
> disabled={isRenamingBranch || branchInfoLoading}
{isCheckingOutVersion ? "Checking out..." : "Switch to main branch"} >
</Button> {isRenamingBranch ? "Renaming..." : "Rename master to main"}
</Button>
) : (
<Button
variant="outline"
size="sm"
onClick={handleCheckoutMainBranch}
disabled={isCheckingOutVersion || branchInfoLoading}
>
{isCheckingOutVersion
? "Checking out..."
: "Switch to main branch"}
</Button>
)}
</div> </div>
)} )}

View File

@@ -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<void, Error, RenameBranchParams>({
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<RenameBranchParams, "appId">) => {
if (!currentAppId) {
showError("No application selected.");
return;
}
return mutation.mutateAsync({ ...params, appId: currentAppId });
};
return {
renameBranch,
isRenamingBranch: mutation.isPending,
renameBranchError: mutation.error,
};
}

View File

@@ -2,7 +2,7 @@ import { ipcMain } from "electron";
import { db, getDatabasePath } from "../../db"; import { db, getDatabasePath } from "../../db";
import { apps, chats } from "../../db/schema"; import { apps, chats } from "../../db/schema";
import { desc, eq } from "drizzle-orm"; 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 fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { getDyadAppPath, getUserDataPath } from "../../paths/paths"; import { getDyadAppPath, getUserDataPath } from "../../paths/paths";
@@ -767,4 +767,55 @@ export function registerAppHandlers() {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
return { version: packageJson.version }; 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}`,
);
}
});
});
} }

View File

@@ -29,6 +29,7 @@ import type {
ApproveProposalResult, ApproveProposalResult,
ImportAppResult, ImportAppResult,
ImportAppParams, ImportAppParams,
RenameBranchParams,
} from "./ipc_types"; } from "./ipc_types";
import type { ProposalResult } from "@/lib/schemas"; import type { ProposalResult } from "@/lib/schemas";
import { showError } from "@/lib/toast"; import { showError } from "@/lib/toast";
@@ -816,4 +817,8 @@ export class IpcClient {
}): Promise<{ exists: boolean }> { }): Promise<{ exists: boolean }> {
return this.ipcRenderer.invoke("check-app-name", params); return this.ipcRenderer.invoke("check-app-name", params);
} }
public async renameBranch(params: RenameBranchParams): Promise<void> {
await this.ipcRenderer.invoke("rename-branch", params);
}
} }

View File

@@ -201,3 +201,9 @@ export interface ImportAppResult {
appId: number; appId: number;
chatId: number; chatId: number;
} }
export interface RenameBranchParams {
appId: number;
oldBranchName: string;
newBranchName: string;
}

View File

@@ -73,6 +73,7 @@ const validInvokeChannels = [
"check-ai-rules", "check-ai-rules",
"select-app-folder", "select-app-folder",
"check-app-name", "check-app-name",
"rename-branch",
] as const; ] as const;
// Add valid receive channels // Add valid receive channels