Skip to content

fix(projects): adding a project through a symlink alias reuses the existing project - #29651

Merged
nwparker merged 2 commits into
mainfrom
nwparker/symlink-alias-project-dedupe
Oct 16, 2026
Merged

nwparker merged 2 commits into
mainfrom
nwparker/symlink-alias-project-dedupe

Conversation

@nwparker

@nwparker nwparker commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 2 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​206 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​206
Prod 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​58 $\color{#cf222e}{\Huge{\mathbf{−}}}$​8 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​50

ELI5

If a repository on a remote machine can be reached by two names, for example repo and a shortcut repo-link that 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 ~/repo and then ~/repo-link (a symlink to ~/repo) left two projects in the sidebar for one checkout. Each had its own terminals and the same main worktree. Re-adding ~/repo itself 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 with fs.realpath on that host:

  • The new path's real location is git's --show-toplevel for a Git project (git already resolves symlinks there) or the folder's real path for a folder workspace.
  • Each project already registered for that host is resolved the same way, so an alias stored by an older build is still matched.
  • Rows that belong to another SSH machine are skipped, so a remote path can never be resolved against this machine's disk.
  • realpath errors (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 real git. 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.ts and src/main/ipc/repos-add-linked-worktree.test.ts pass 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 ~/real as 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 called git through child_process instead of @orca/process-host), fixed; re-review found nothing.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

The cost is one realpath for the new path plus one per project already registered on that host, and only when the string comparison found no match. Windows: realpath handles junctions, and paths are compared with the existing case-insensitive Windows rules.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…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.
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

📝 Walkthrough

Runtime and local repository registration now check real-path identity when matching existing projects. Runtime registration validates Git repositories and limits matching to eligible execution-host projects. Tests cover symlink aliases, separate checkouts, and SSH-hosted project scope.

Priority: ➖ Normal

Merge Risk: 🔵 Low · up to 63190

Adding a project through a symlink alias now reuses the existing project on SSH hosts and for local folders. One narrow gap remains: if the same folder and its alias are added at almost the same moment, two projects can still be created. This is unlikely in normal use and can be fixed by removing the duplicate.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description check Passed The description is complete and relevant. It covers the user impact, implementation, rationale, linked issue, visual proof, testing, compatibility considerations, review status, and checklist.
Title check Passed The title clearly and concisely describes the primary change: reusing an existing project when it is added through a symlink alias.
Linked Issues check Passed Issue #29637 requires deduplication for a Git repository added through a symlinked SSH host path. RuntimeRepositoryRegistrationController.add validates Git input, compares same-host projects, and co…
Out of Scope Changes check Passed The changes stay within the stated real-path deduplication scope. The shared helper supports the required host-side check and the related local folder-workspace case described in the PR. The added tes…
Full details: Linked Issues check

Explanation

Issue #29637 requires deduplication for a Git repository added through a symlinked SSH host path. RuntimeRepositoryRegistrationController.add validates Git input, compares same-host projects, and compares real paths using the host-side findProjectAtSameRealPath helper. The lookup excludes SSH rows from local real-path resolution. The changed tests cover both add orders and preserve separate checkouts. These changes meet the linked coding requirement.

Full details: Out of Scope Changes check

Explanation

The changes stay within the stated real-path deduplication scope. The shared helper supports the required host-side check and the related local folder-workspace case described in the PR. The added tests verify aliases, separate checkouts, and SSH-row isolation. No remote folder-browser changes or unrelated product behavior appear in the reviewed changes.

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 60cece60-f37f-4784-a985-fb6599f6cd18
📥 Commits

Reviewing files that changed from the base of the PR and between 9c03c68 and 63190cd.

📒 Files selected for processing: 5
  • src/main/ipc/repos/local-repo-registration.test.ts
  • src/main/ipc/repos/local-repo-registration.ts
  • src/main/project-realpath-identity.ts
  • src/main/runtime/runtime-repository-registration-controller.test.ts
  • src/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.

Comment on lines +53 to +61
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
))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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-based raceWinner check 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

@nwparker
nwparker merged commit 62bf1d6 into main Oct 16, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SSH: adding a repo through a symlinked path creates a duplicate project for the same repository

1 participant