feat(projects): relocate project when directory moved#1941
feat(projects): relocate project when directory moved#1941webdo wants to merge 4 commits intogeneralaction:mainfrom
Conversation
Adds Relocate action in path-not-found panel and sidebar context menu. Validates new path is a git repo, recomputes baseRef, preserves project id and tasks.
Greptile SummaryThis PR adds a Relocate Project feature that lets users re-point a local project to a new directory after it has been moved on disk, preserving tasks, settings, and worktrees. The backend validates the new path as a git repo, guards against path conflicts, closes the existing provider, and updates the DB row; the renderer transitions project state and remounts.
Confidence Score: 3/5Needs fixes in the backend operation and the sidebar error path before merging. The backend closes the provider then updates the DB without guarding the src/main/core/projects/operations/relocateProject.ts and src/renderer/features/sidebar/project-item.tsx
|
| Filename | Overview |
|---|---|
| src/main/core/projects/operations/relocateProject.ts | New backend RPC: validates path, checks for git repo, guards path conflicts, closes provider, then updates DB — but row from .returning() is unguarded (undefined if deleted concurrently) and closeProject's error result is silently discarded. |
| src/renderer/features/projects/stores/project-manager.ts | Adds relocateLocalProject store method that calls the RPC, transitions project state, awaits any in-flight mount, then remounts — state management mirrors the existing updateProjectConnection pattern and looks correct. |
| src/renderer/features/projects/components/main-panel/main-panel.tsx | Adds Relocate button to the path-not-found panel with loading state, inline error display, and a now-secondary Remove link — solid UX upgrade for the primary recovery flow. |
| src/renderer/features/sidebar/project-item.tsx | Adds Relocate context menu item for local projects, but errors are only logged to console — users get no visual feedback when relocation fails from the sidebar. |
| src/main/core/projects/controller.ts | Registers relocateLocalProject in the RPC controller — trivial wiring change. |
Sequence Diagram
sequenceDiagram
participant U as User
participant UI as Renderer (Panel / Sidebar)
participant Store as ProjectManagerStore
participant RPC as IPC/RPC
participant BE as relocateLocalProject (main)
participant PM as ProjectManager (main)
participant DB as Database
U->>UI: Click Relocate Project
UI->>UI: openSelectDirectoryDialog to newPath
UI->>Store: relocateLocalProject(projectId, newPath)
Store->>RPC: rpc.projects.relocateLocalProject(projectId, newPath)
RPC->>BE: relocateLocalProject(projectId, newPath)
BE->>DB: getProjectById(projectId)
BE->>BE: checkIsValidDirectory(newPath)
BE->>BE: git.detectInfo() to rootPath, baseRef, isGitRepo
BE->>DB: "SELECT id WHERE path = resolvedPath (collision check)"
BE->>BE: resolveBaseRef(git, gitInfo.baseRef)
BE->>PM: closeProject(projectId)
PM-->>BE: ok / err (result discarded)
BE->>DB: "UPDATE projects SET path, baseRef WHERE id = projectId RETURNING"
DB-->>BE: row (may be undefined in race)
BE-->>RPC: ok(LocalProject)
RPC-->>Store: result
Store->>Store: runInAction to transition state to opening
Store->>Store: await inFlight mount (if any)
Store->>Store: mountProject(projectId)
Store-->>UI: done
UI-->>U: Project remounted at new path
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
src/main/core/projects/operations/relocateProject.ts:88-106
**Unguarded `row` after UPDATE `.returning()`**
The `db.update(...).returning()` call can return an empty array if the project row was deleted in a race between the `getProjectById` check at line 42 and this update (e.g., a concurrent `deleteProject` call). Destructuring into `const [row]` would leave `row` as `undefined`, and the immediately following `row.id` access would throw a `TypeError` that bypasses the typed `Result` return, leaving the project in a closed-but-not-relocated limbo on the backend. Add an explicit guard: if `!row`, return `err({ type: 'not-found', message: '...' })` before the `ok(...)` call.
### Issue 2 of 3
src/renderer/features/sidebar/project-item.tsx:107-108
**Relocation errors silently swallowed in sidebar**
When relocation fails from the context menu (e.g., not a git repo, path conflict, network error), the error is only sent to `console.error` and the user receives no visual feedback. In contrast, `ProjectPathNotFoundPanel` renders the error message inline. Users who trigger **Relocate Project** from the sidebar and hit a validation error will see the spinner disappear with no indication of what went wrong. The component should surface the error (e.g., a toast notification, an inline label, or a tooltip on the menu item).
### Issue 3 of 3
src/main/core/projects/operations/relocateProject.ts:86
**`closeProject` result discarded**
`projectManager.closeProject` returns a `Result<void, ProviderLifecycleError>` and can time out (60 s teardown budget) or fail. The result is awaited but never inspected, so a teardown failure is silently ignored and the code proceeds to mutate the DB with the old provider potentially still running. A check like `const closeResult = await projectManager.closeProject(projectId); if (!closeResult.ok) { /* log or surface the error */ }` would at minimum make teardown failures observable.
Reviews (1): Last reviewed commit: "feat(projects): relocate project when di..." | Re-trigger Greptile
Guard !row after db.update().returning(), inspect closeProject Result and return error on teardown failure, surface relocate errors via toast in sidebar instead of console.error.
Hides the Relocate context-menu item for healthy local projects.
Run `git worktree repair` from the new project root after a successful DB update. Fixes the bidirectional .git pointers when the main repo moves but the worktrees stay at their original location. Failure is non-fatal — logged and ignored.
Summary
Relocate Projectbutton (with folder-input icon) + secondaryRemove Projectlink.path_not_foundstate (hidden for healthy projects, hidden for SSH).relocateLocalProject(projectId, newPath)RPC. Validates the new path is a directory + git repo, guards against the unique-path constraint colliding with another project, closes the existing provider (and surfaces teardown failures), recomputesbaseRef, updates the DB row (with!rowguard against concurrent deletion), returns the updatedLocalProject. Renderer remounts the project after the swap.useToast(success + destructive variants) instead of being swallowed byconsole.error.path-not-foundstate isn't currently surfaced anywhere in the UI and can be added in a follow-up.Screenshots
Commits
feat(projects): relocate project when directory movedfix(projects): address greptile review on relocate—!rowguard, inspectcloseProjectResult, toast-based error surface in sidebarfix(projects): only show sidebar Relocate when path-not-foundTest plan
path-not-foundis not currently surfaced in the UI.Local gate
format,lint,typecheck,testall pass on the changed files. Pre-existing upstream failures inapp-menu-events.tsx,featurebase-issue-provider.test.ts, andlegacy-port/importers/relational/relational.test.tsare unrelated and present onupstream/mainwithout this change.