fix(renderer): sanitize persisted snapshots#2199
Conversation
Greptile SummaryThis PR fixes two independent issues: worktree creation failing when the canonical pool path is a stale non-worktree directory, and snapshot persistence crashing when MobX state contains non-serializable values.
Confidence Score: 5/5Safe to merge — both fixes are well-scoped, the stale-directory path is preserved rather than deleted, and three integration tests exercise the new suffix-resolution logic end-to-end. The worktree suffix logic is straightforward and the regression tests directly cover the two reported failure modes (stale canonical path and suffix-collision with a different branch). The snapshot-registry change is defensive — it adds a try/catch around the clone and short-circuits on undefined, so the only behavioral change is that previously-crashing serializations now silently skip. No data loss, no silent misbehaviour in the success path. No files require special attention.
|
| Filename | Overview |
|---|---|
| src/main/core/projects/worktrees/worktree-service.ts | Adds resolveAvailableTargetPath to handle stale directories and suffix-path collisions, replaces the old delete-and-retry pattern, and simplifies getWorktree to rely entirely on the git worktree list. |
| src/main/core/projects/worktrees/worktree-service.test.ts | Adds three regression tests covering: stale-directory suffix fallback for checkoutBranchWorktree, the foo/foo-2 suffix-collision case, and stale-directory suffix fallback for checkoutExistingBranch. |
| src/renderer/lib/stores/snapshot-registry.ts | Introduces toCloneableSnapshot (structuredClone + toJS) to sanitize MobX observables before persisting; uncloneable snapshots are logged and silently skipped instead of causing IPC serialization errors. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[checkoutBranchWorktree / checkoutExistingBranch] --> B[findBranchAnywhere]
B -->|found valid worktree| C[return ok - existing path]
B -->|not found| D[targetPath = poolPath / branchName]
D --> E[resolveAvailableTargetPath]
E --> F{targetPath exists?}
F -->|No| G[return targetPath]
F -->|Yes| H{isValidWorktree AND branch matches?}
H -->|Yes| G
H -->|No| I[try suffix i=2..999]
I --> J{candidate exists?}
J -->|No| K[return candidate]
J -->|Yes| L{isValidWorktree AND branch matches?}
L -->|Yes| K
L -->|No| M[i++]
M --> I
G --> N{isValidWorktree at resolved path?}
K --> N
N -->|Yes| C
N -->|No| O[git worktree add resolved path branchName]
O --> P[return ok - new path]
Reviews (2): Last reviewed commit: "fix(worktrees): validate suffixed branch..." | Re-trigger Greptile
| private async resolveAvailableTargetPath(targetPath: string): Promise<string> { | ||
| if (!(await this.targetPathExists(targetPath))) return targetPath; | ||
| if (await this.isValidWorktree(targetPath)) return targetPath; | ||
|
|
||
| for (let i = 2; i < 1000; i++) { | ||
| const candidate = `${targetPath}-${i}`; | ||
| if (!(await this.targetPathExists(candidate))) return candidate; | ||
| if (await this.isValidWorktree(candidate)) return candidate; | ||
| } | ||
|
|
||
| throw new Error(`No available worktree path found for ${targetPath}`); | ||
| } |
There was a problem hiding this comment.
Wrong branch returned via suffix collision
When branch foo can't use its primary pool path (stale dir at poolPath/foo), resolveAvailableTargetPath tries poolPath/foo-2. If a separate branch literally named foo-2 already has a valid worktree at poolPath/foo-2, the function returns that path unchanged — and the callers (doCheckoutBranchWorktree, doCheckoutExistingBranch) immediately return ok(poolPath/foo-2) without verifying that the worktree there actually corresponds to foo. Tasks for branch foo then run against branch foo-2's working tree, silently mixing their content.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/main/core/projects/worktrees/worktree-service.ts
Line: 79-90
Comment:
**Wrong branch returned via suffix collision**
When branch `foo` can't use its primary pool path (stale dir at `poolPath/foo`), `resolveAvailableTargetPath` tries `poolPath/foo-2`. If a separate branch literally named `foo-2` already has a valid worktree at `poolPath/foo-2`, the function returns that path unchanged — and the callers (`doCheckoutBranchWorktree`, `doCheckoutExistingBranch`) immediately return `ok(poolPath/foo-2)` without verifying that the worktree there actually corresponds to `foo`. Tasks for branch `foo` then run against branch `foo-2`'s working tree, silently mixing their content.
How can I resolve this? If you propose a fix, please make it concise.
summary