Unverified 提交 6b0d6ef8 authored 作者: wwwillchen-bot's avatar wwwillchen-bot 提交者: GitHub

fix: handle branch deletion when branch doesn't exist locally (#2910)

## Summary - Check if branch exists locally before attempting to delete it - If branch only exists on remote, inform user to delete on GitHub - If branch doesn't exist anywhere, treat as success (already deleted) ## Test plan - Try deleting a branch that exists locally - should work as before - Try deleting a branch that only exists on remote - should get helpful error message - Try deleting a branch that doesn't exist anywhere - should succeed silently 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2910" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> --------- Co-authored-by: 's avatarWill Chen <willchen90@gmail.com> Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com> Co-authored-by: 's avatarclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
上级 e3baf852
import { describe, it, expect, vi, beforeEach } from "vitest";
import { IpcMainInvokeEvent } from "electron";
vi.mock("../ipc/utils/git_utils", () => ({
gitListBranches: vi.fn(),
gitListRemoteBranches: vi.fn(),
gitDeleteBranch: vi.fn(),
gitMergeAbort: vi.fn(),
gitFetch: vi.fn(),
gitPull: vi.fn(),
gitCreateBranch: vi.fn(),
gitCheckout: vi.fn(),
gitMerge: vi.fn(),
gitCurrentBranch: vi.fn(),
gitRenameBranch: vi.fn(),
GitStateError: vi.fn(),
GIT_ERROR_CODES: {},
isGitMergeInProgress: vi.fn(),
isGitRebaseInProgress: vi.fn(),
getGitUncommittedFilesWithStatus: vi.fn(),
gitAddAll: vi.fn(),
gitCommit: vi.fn(),
}));
vi.mock("../paths/paths", () => ({
getDyadAppPath: vi.fn((p: string) => `/mock/apps/${p}`),
}));
vi.mock("../db", () => ({
db: {
query: {
apps: {
findFirst: vi.fn(),
},
},
},
}));
vi.mock("../db/schema", () => ({
apps: { id: "id" },
}));
vi.mock("drizzle-orm", async (importOriginal) => {
const actual = await importOriginal<typeof import("drizzle-orm")>();
return {
...actual,
eq: vi.fn(),
};
});
vi.mock("electron-log", () => ({
default: {
scope: () => ({
info: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
}),
},
}));
vi.mock("../ipc/utils/lock_utils", () => ({
withLock: vi.fn(),
}));
vi.mock("../ipc/handlers/github_handlers", () => ({
updateAppGithubRepo: vi.fn(),
ensureCleanWorkspace: vi.fn(),
}));
vi.mock("../ipc/handlers/base", () => ({
createTypedHandler: vi.fn(),
}));
vi.mock("../ipc/types/github", () => ({
githubContracts: {},
gitContracts: {},
}));
vi.mock("../main/settings", () => ({
readSettings: vi.fn(),
}));
import { handleDeleteBranch } from "../ipc/handlers/git_branch_handlers";
import {
gitListBranches,
gitListRemoteBranches,
gitDeleteBranch,
} from "../ipc/utils/git_utils";
import { db } from "../db";
const mockEvent = {} as IpcMainInvokeEvent;
const mockApp = {
id: 1,
path: "test-app",
githubOrg: "test-org",
githubRepo: "test-repo",
};
describe("handleDeleteBranch", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(db.query.apps.findFirst).mockResolvedValue(mockApp as any);
});
it("deletes branch when it exists locally", async () => {
vi.mocked(gitListBranches).mockResolvedValue(["main", "feature"]);
vi.mocked(gitDeleteBranch).mockResolvedValue(undefined);
await handleDeleteBranch(mockEvent, { appId: 1, branch: "feature" });
expect(gitDeleteBranch).toHaveBeenCalledWith({
path: "/mock/apps/test-app",
branch: "feature",
});
expect(gitListRemoteBranches).not.toHaveBeenCalled();
});
it("throws error when branch only exists on remote with GitHub URL", async () => {
vi.mocked(gitListBranches).mockResolvedValue(["main"]);
vi.mocked(gitListRemoteBranches).mockResolvedValue(["main", "feature"]);
await expect(
handleDeleteBranch(mockEvent, { appId: 1, branch: "feature" }),
).rejects.toThrow(
/only exists on the remote.*https:\/\/github\.com\/test-org\/test-repo\/branches/,
);
});
it("succeeds silently when branch doesn't exist locally or remotely", async () => {
vi.mocked(gitListBranches).mockResolvedValue(["main"]);
vi.mocked(gitListRemoteBranches).mockResolvedValue(["main"]);
await handleDeleteBranch(mockEvent, { appId: 1, branch: "nonexistent" });
expect(gitDeleteBranch).not.toHaveBeenCalled();
});
it("throws error when branch doesn't exist locally and remote listing fails", async () => {
vi.mocked(gitListBranches).mockResolvedValue(["main"]);
vi.mocked(gitListRemoteBranches).mockRejectedValue(
new Error("network error"),
);
await expect(
handleDeleteBranch(mockEvent, { appId: 1, branch: "feature" }),
).rejects.toThrow(
/does not exist locally and remote branches could not be checked/,
);
});
it("throws generic error when branch only exists on remote for non-GitHub app", async () => {
const nonGithubApp = { id: 1, path: "test-app", githubOrg: null, githubRepo: null };
vi.mocked(db.query.apps.findFirst).mockResolvedValue(nonGithubApp as any);
vi.mocked(gitListBranches).mockResolvedValue(["main"]);
vi.mocked(gitListRemoteBranches).mockResolvedValue(["main", "feature"]);
await expect(
handleDeleteBranch(mockEvent, { appId: 1, branch: "feature" }),
).rejects.toThrow(
/only exists on the remote and cannot be deleted locally.*remote Git hosting provider/,
);
});
it("throws when app not found", async () => {
vi.mocked(db.query.apps.findFirst).mockResolvedValue(undefined);
await expect(
handleDeleteBranch(mockEvent, { appId: 999, branch: "feature" }),
).rejects.toThrow("App not found");
});
});
...@@ -107,7 +107,7 @@ async function handleCreateBranch( ...@@ -107,7 +107,7 @@ async function handleCreateBranch(
}); });
} }
async function handleDeleteBranch( export async function handleDeleteBranch(
event: IpcMainInvokeEvent, event: IpcMainInvokeEvent,
{ appId, branch }: GitBranchParams, { appId, branch }: GitBranchParams,
): Promise<void> { ): Promise<void> {
...@@ -115,10 +115,50 @@ async function handleDeleteBranch( ...@@ -115,10 +115,50 @@ async function handleDeleteBranch(
if (!app) throw new Error("App not found"); if (!app) throw new Error("App not found");
const appPath = getDyadAppPath(app.path); const appPath = getDyadAppPath(app.path);
// Check if branch exists locally
const localBranches = await gitListBranches({ path: appPath });
const existsLocally = localBranches.includes(branch);
if (existsLocally) {
// Delete local branch
await gitDeleteBranch({ await gitDeleteBranch({
path: appPath, path: appPath,
branch, branch,
}); });
} else {
// Branch doesn't exist locally - it may only exist on remote
// or has already been deleted. Check if it exists remotely.
let remoteBranches: string[];
try {
remoteBranches = await gitListRemoteBranches({ path: appPath });
} catch (error) {
logger.warn(
`Failed to list remote branches while checking for branch '${branch}' to delete.`,
error,
);
throw new Error(
`Branch '${branch}' does not exist locally and remote branches could not be checked. Please try again later.`,
);
}
if (!remoteBranches.includes(branch)) {
// Branch doesn't exist locally or remotely - it's already been deleted
logger.info(
`Branch '${branch}' not found locally or remotely - may have already been deleted`,
);
return; // Success - nothing to delete
}
// Branch only exists remotely - inform user they need to delete it on GitHub
if (app.githubOrg && app.githubRepo) {
throw new Error(
`Branch '${branch}' only exists on the remote. To delete it, please delete the branch on GitHub directly. Visit https://github.com/${app.githubOrg}/${app.githubRepo}/branches to manage remote branches.`,
);
}
throw new Error(
`Branch '${branch}' only exists on the remote and cannot be deleted locally. Please delete it from your remote Git hosting provider.`,
);
}
} }
async function handleSwitchBranch( async function handleSwitchBranch(
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论