Repository navigation
fix(projects): adding a project through a symlink alias reuses the existing project - #29651
Conversation
…isting project On a managed host, adding repo-link (-> repo) created a second project for the same checkout while re-adding repo was deduped. Compare the real path, resolved on the host that owns the files, so an alias finds the tracked project; distinct checkouts keep distinct real paths and stay separate. Local folder workspaces get the same check.
📝 Walkthrough
Priority: ➖ Normal Merge Risk: 🔵 Low · up to
|
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
60cece60-f37f-4784-a985-fb6599f6cd18
📒 Files selected for processing: 5
src/main/ipc/repos/local-repo-registration.test.tssrc/main/ipc/repos/local-repo-registration.tssrc/main/project-realpath-identity.tssrc/main/runtime/runtime-repository-registration-controller.test.tssrc/main/runtime/runtime-repository-registration-controller.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.
| const existing = | ||
| sameHostRepos.find((repo) => runtimePathsEqual(repo.path, path)) ?? | ||
| (await findProjectAtSameRealPath( | ||
| // Why: SSH rows name paths on another machine; resolving them here could match an unrelated local dir. | ||
| sameHostRepos.filter( | ||
| (repo) => parseExecutionHostId(getRepoExecutionHostId(repo))?.kind !== 'ssh' | ||
| ), | ||
| gitInfo?.rootPath ?? path | ||
| )) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Serialize real-path matching and project insertion. Concurrent additions of a directory and its alias can both find no match before either inserts a project. Both registration paths can then persist a duplicate.
src/main/runtime/runtime-repository-registration-controller.ts#L53-L61: keep the identity check and insertion in one serialized registration operation.src/main/ipc/repos/local-repo-registration.ts#L54-L59: use the same guarantee; the later string-basedraceWinnercheck cannot identify an alias inserted during icon detection.
📍 Affects 2 files
src/main/runtime/runtime-repository-registration-controller.ts#L53-L61(this comment)src/main/ipc/repos/local-repo-registration.ts#L54-L59
ELI5
If a repository on a remote machine can be reached by two names, for example
repoand a shortcutrepo-linkthat points at it, adding it under the second name created a second, duplicate project. Now Orca sees that both names are the same folder and selects the project you already have.What Changed
Before: on a managed SSH host, adding
~/repoand then~/repo-link(a symlink to~/repo) left two projects in the sidebar for one checkout. Each had its own terminals and the samemainworktree. Re-adding~/repoitself was already deduplicated.After: adding the alias selects the existing project. The order doesn't matter: adding the alias first and the real path second also gives a single project. Two genuinely different checkouts, such as a second clone of the same repository, still become two projects. Folder workspaces follow the same rule.
Mechanism: the host-side registration (
RuntimeRepositoryRegistrationController.add, which runs on the machine that owns the files) only compared path strings. It now also compares real paths, resolved withfs.realpathon that host:--show-toplevelfor a Git project (git already resolves symlinks there) or the folder's real path for a folder workspace.realpatherrors (a missing folder, a loop) simply mean "no match".The desktop's local registration (
addLocalRepoFromPath) already resolved Git projects to git's toplevel, but folder workspaces could still be duplicated through a symlink. It now uses the same shared check (src/main/project-realpath-identity.ts).Why
The real path is the identity that cannot be fooled by aliases, and two separate checkouts always have different real paths, so deliberately added clones or copies are never merged. We considered matching on git identity such as the remote URL or the common git dir. That would merge separate clones of the same remote, and it would merge a linked worktree with its main checkout, both of which users add on purpose. Resolving on the host that owns the path keeps this correct for SSH, WSL and managed hosts, where the desktop cannot see the remote disk. The stored path is unchanged (the path the user typed), so existing projects keep their names and IDs, and nothing changes on the wire.
Linked Issue
Fixes #29637
Visual Proof
N/A: no new UI. The second add now selects the existing project instead of creating a duplicate row. The live check on a real Linux host is described under Testing.
Testing
src/main/runtime/runtime-repository-registration-controller.test.ts(new) uses real temporary folders and realgit. It covers alias after real path, real path after alias, a folder workspace alias, two different clones that stay separate, and an SSH row that is never resolved locally. On main, the three alias cases fail.src/main/ipc/repos/local-repo-registration.test.ts(new) covers a local folder workspace alias (fails on main) and two different folders that stay separate.src/main/runtime/orca-runtime.test.tsandsrc/main/ipc/repos-add-linked-worktree.test.tspass unchanged.Live: verified on openclaw (Linux) under host isolation: a private user-level sshd and throwaway HOME in /tmp, an isolated hidden desktop built from this change, and a managed server auto-deployed from that build. Through the real Add Project UI: added
~/repo, then~/repo-link(symlink to it), which left one project (/tmp/symef/home/repo). Added~/repo-copy(a separate clone), which became a second project. Opened~/realas a folder and then~/linked(symlink to it) as a folder, which left one folder workspace. The alias-first order and the local-desktop folder case are verified only in unit tests.I manually tested these changes locally
Automated tests added/updated, or explained why not below
AI Disclosure
Review
Codex review (
gpt-6.1-sol, high): one P2 (test fixtures calledgitthroughchild_processinstead of@orca/process-host), fixed; re-review found nothing.Agent skill upstream boundary
Notes
The cost is one
realpathfor the new path plus one per project already registered on that host, and only when the string comparison found no match. Windows:realpathhandles junctions, and paths are compared with the existing case-insensitive Windows rules.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)