| author | |
| committer | |
| log | 7de054fa4964e5cc6c406f39551dc62a2c214b38 |
| tree | 34992cfb350c573dc4b7fcdfbc4ecb0a3664870a |
| parent | bb9371d3c9500bf035b7df5da0969b82aac5a1fc |
- wrap `git worktree remove` errors with command and cwd context
- add server test covering failed worktree removal messaging
- show an error toast and log details when orphaned worktree cleanup fails3 files changed, 43 insertions(+), 6 deletions(-)
apps/server/src/git.test.ts+18| ... | @@ -760,6 +760,24 @@ describe("git integration", () => { | ... | @@ -760,6 +760,24 @@ describe("git integration", () => { |
| 760 | expect(dirty.hasWorkingTreeChanges).toBe(true); | 760 | expect(dirty.hasWorkingTreeChanges).toBe(true); |
| 761 | }); | 761 | }); |
| 762 | 762 | ||
| 763 | it("includes command context when worktree removal fails", async () => { | ||
| 764 | await using tmp = await makeTmpDir(); | ||
| 765 | await initRepoWithCommit(tmp.path); | ||
| 766 | const core = new GitCoreService(); | ||
| 767 | const missingWorktreePath = path.join(tmp.path, "missing-worktree"); | ||
| 768 | |||
| 769 | const error = await core | ||
| 770 | .removeWorktree({ cwd: tmp.path, path: missingWorktreePath }) | ||
| 771 | .then(() => null) | ||
| 772 | .catch((err: unknown) => err); | ||
| 773 | |||
| 774 | expect(error).toBeInstanceOf(Error); | ||
| 775 | const message = (error as Error).message; | ||
| 776 | expect(message).toContain("git worktree remove"); | ||
| 777 | expect(message).toContain(`cwd: ${tmp.path}`); | ||
| 778 | expect(message).toContain(missingWorktreePath); | ||
| 779 | }); | ||
| 780 | |||
| 763 | it("refreshes upstream before statusDetails so behind count reflects remote updates", async () => { | 781 | it("refreshes upstream before statusDetails so behind count reflects remote updates", async () => { |
| 764 | await using remote = await makeTmpDir(); | 782 | await using remote = await makeTmpDir(); |
| 765 | await using source = await makeTmpDir(); | 783 | await using source = await makeTmpDir(); |
apps/server/src/git.ts+12-4| ... | @@ -546,10 +546,18 @@ export class GitCoreService { | ... | @@ -546,10 +546,18 @@ export class GitCoreService { |
| 546 | } | 546 | } |
| 547 | 547 | ||
| 548 | async removeWorktree(input: GitRemoveWorktreeInput): Promise<void> { | 548 | async removeWorktree(input: GitRemoveWorktreeInput): Promise<void> { |
| 549 | await executeGit(input.cwd, ["worktree", "remove", input.path], { | 549 | const args = ["worktree", "remove", input.path] as const; |
| 550 | timeoutMs: 15_000, | 550 | try { |
| 551 | fallbackErrorMessage: "git worktree remove failed", | 551 | await executeGit(input.cwd, args, { |
| 552 | }); | 552 | timeoutMs: 15_000, |
| 553 | fallbackErrorMessage: "git worktree remove failed", | ||
| 554 | }); | ||
| 555 | } catch (error) { | ||
| 556 | const detail = error instanceof Error ? error.message : String(error); | ||
| 557 | throw new Error(`${commandLabel(args)} failed (cwd: ${input.cwd}): ${detail}`, { | ||
| 558 | cause: error, | ||
| 559 | }); | ||
| 560 | } | ||
| 553 | } | 561 | } |
| 554 | 562 | ||
| 555 | async createBranch(input: GitCreateBranchInput): Promise<void> { | 563 | async createBranch(input: GitCreateBranchInput): Promise<void> { |
apps/web/src/components/Sidebar.tsx+13-2| ... | @@ -336,8 +336,19 @@ export default function Sidebar() { | ... | @@ -336,8 +336,19 @@ export default function Sidebar() { |
| 336 | cwd: threadProject.cwd, | 336 | cwd: threadProject.cwd, |
| 337 | path: orphanedWorktreePath, | 337 | path: orphanedWorktreePath, |
| 338 | }); | 338 | }); |
| 339 | } catch { | 339 | } catch (error) { |
| 340 | // Worktree deletion is best-effort and should not block thread deletion. | 340 | const message = error instanceof Error ? error.message : "Unknown error removing worktree."; |
| 341 | console.error("Failed to remove orphaned worktree after thread deletion", { | ||
| 342 | threadId, | ||
| 343 | projectCwd: threadProject.cwd, | ||
| 344 | worktreePath: orphanedWorktreePath, | ||
| 345 | error, | ||
| 346 | }); | ||
| 347 | toastManager.add({ | ||
| 348 | type: "error", | ||
| 349 | title: "Thread deleted, but worktree removal failed", | ||
| 350 | description: `Could not remove ${displayWorktreePath ?? orphanedWorktreePath}. ${message}`, | ||
| 351 | }); | ||
| 341 | } | 352 | } |
| 342 | }, | 353 | }, |
| 343 | [api, dispatch, removeWorktreeMutation, state.projects, state.threads], | 354 | [api, dispatch, removeWorktreeMutation, state.projects, state.threads], |