fix: resolve correct project path when opening existing worktrees in monorepos#67
Conversation
…monorepos When discovering existing worktrees via `git worktree list`, the returned paths are always the worktree root. In monorepo setups where the project is a subdirectory (e.g. /repo/packages/app), opening such a worktree would incorrectly set the project path to the worktree root instead of the matching subdirectory within it. Compute the subdirectory offset from the parent project's git root and append it to discovered worktree paths. Also fix the worktree list popover to correctly filter the main repo entry and match tracked worktrees in monorepo setups. Co-Authored-By: Claude Code
Co-Authored-By: Claude Code
There was a problem hiding this comment.
Pull request overview
Fixes incorrect project paths when opening existing git worktrees for monorepo subdirectory projects by ensuring discovered worktree roots are mapped to the equivalent subdirectory inside each worktree.
Changes:
- Compute a monorepo subdirectory offset from the parent project’s git root and apply it when adding discovered worktrees.
- Update the worktree list popover to (a) filter out the main repo by comparing against git root and (b) match tracked worktrees using the computed project path (with backwards-compat fallbacks).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
crates/okena-workspace/src/actions/project.rs |
Adjust discovered worktree project path to include monorepo subdir and add backwards-compat duplicate detection. |
crates/okena-views-sidebar/src/worktree_list.rs |
Store git root + subdir in popover state; filter main repo correctly; match tracked/untracked worktrees using computed project path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let parent_path = self.project(parent_id) | ||
| .map(|p| p.path.clone()) | ||
| .unwrap_or_default(); | ||
| let parent_pathbuf = std::path::PathBuf::from(&parent_path); | ||
| let git_root = okena_git::get_repo_root(&parent_pathbuf) | ||
| .unwrap_or_else(|| parent_pathbuf.clone()); | ||
| let subdir = parent_pathbuf.strip_prefix(&git_root) |
There was a problem hiding this comment.
add_discovered_worktree falls back to an empty parent_path when parent_id is missing, which can make get_repo_root run git -C "" ... (current process cwd) and compute an incorrect project_path. Prefer returning early if the parent project can't be found (or otherwise ensure the path used for repo detection is valid).
| let parent_path = self.project(parent_id) | |
| .map(|p| p.path.clone()) | |
| .unwrap_or_default(); | |
| let parent_pathbuf = std::path::PathBuf::from(&parent_path); | |
| let git_root = okena_git::get_repo_root(&parent_pathbuf) | |
| .unwrap_or_else(|| parent_pathbuf.clone()); | |
| let subdir = parent_pathbuf.strip_prefix(&git_root) | |
| // If the parent project can't be found, don't add this worktree. | |
| let parent_path = match self.project(parent_id) { | |
| Some(p) => p.path.clone(), | |
| None => return, | |
| }; | |
| let parent_pathbuf = std::path::PathBuf::from(&parent_path); | |
| let git_root = okena_git::get_repo_root(&parent_pathbuf) | |
| .unwrap_or_else(|| parent_pathbuf.clone()); | |
| let subdir = parent_pathbuf | |
| .strip_prefix(&git_root) |
| let git_root = okena_git::get_repo_root(&parent_pathbuf) | ||
| .unwrap_or_else(|| parent_pathbuf.clone()); | ||
| let subdir = parent_pathbuf.strip_prefix(&git_root) |
There was a problem hiding this comment.
strip_prefix is performed on parent_pathbuf vs git_root without normalizing; if the stored project path is relative or contains ./.., strip_prefix will fail (since get_repo_root returns an absolute path) and subdir becomes empty, reintroducing the monorepo bug. Normalize both paths (like okena_git::repository::normalize_path is used elsewhere) before computing subdir.
| let git_root = okena_git::get_repo_root(&parent_pathbuf) | |
| .unwrap_or_else(|| parent_pathbuf.clone()); | |
| let subdir = parent_pathbuf.strip_prefix(&git_root) | |
| // Normalize both the parent path and the git root before computing subdir | |
| let parent_norm = okena_git::repository::normalize_path(&parent_pathbuf); | |
| let git_root = okena_git::get_repo_root(&parent_norm) | |
| .unwrap_or_else(|| parent_norm.clone()); | |
| let git_root_norm = okena_git::repository::normalize_path(&git_root); | |
| let subdir = parent_norm | |
| .strip_prefix(&git_root_norm) |
| let project_path = workspace.read(cx).project(&project_id) | ||
| .map(|p| p.path.clone()) | ||
| .unwrap_or_default(); | ||
| let entries = okena_git::repository::list_git_worktrees( | ||
| std::path::Path::new(&project_path), | ||
| ); | ||
| let project_pathbuf = std::path::PathBuf::from(&project_path); | ||
| let git_root = okena_git::get_repo_root(&project_pathbuf) | ||
| .unwrap_or_else(|| project_pathbuf.clone()); | ||
| let subdir = project_pathbuf.strip_prefix(&git_root) |
There was a problem hiding this comment.
WorktreeListPopover::new uses unwrap_or_default() for project_path; if the project id is invalid, this can cause get_repo_root/git worktree list to run relative to the process cwd and show unrelated worktrees. Consider returning early (e.g., empty entries) when the project isn't found instead of defaulting to an empty path.
| let project_pathbuf = std::path::PathBuf::from(&project_path); | ||
| let git_root = okena_git::get_repo_root(&project_pathbuf) | ||
| .unwrap_or_else(|| project_pathbuf.clone()); | ||
| let subdir = project_pathbuf.strip_prefix(&git_root) |
There was a problem hiding this comment.
subdir is computed via project_pathbuf.strip_prefix(&git_root) without normalizing either path. This is inconsistent with other monorepo/worktree codepaths (e.g., WorktreeDialog) and can fail when the stored project path is relative or contains ./.., causing incorrect expected_path computation. Normalize both paths before strip_prefix.
| let subdir = project_pathbuf.strip_prefix(&git_root) | |
| // Normalize both paths before computing subdir to handle relative paths and `.`/`..`. | |
| let normalized_project = project_pathbuf | |
| .canonicalize() | |
| .unwrap_or_else(|_| project_pathbuf.clone()); | |
| let normalized_root = git_root | |
| .canonicalize() | |
| .unwrap_or_else(|_| git_root.clone()); | |
| let subdir = normalized_project | |
| .strip_prefix(&normalized_root) |
- Add `resolve_git_root_and_subdir()` to okena-git, replacing duplicated inline git_root + strip_prefix logic. Uses normalize_path for correctness with symlinks and relative path components. - Have `add_discovered_worktree` return the project ID instead of forcing caller to search by path. Remove unused `_main_repo_path` parameter. - Extract `find_tracked_project_id` helper in WorktreeListPopover to deduplicate the backwards-compat path matching logic. - Cache normalized git root in struct field instead of recomputing per render frame. Co-Authored-By: Claude Code
Summary
git worktree list, the returned paths are always the worktree root. In monorepo setups where the project is a subdirectory (e.g./repo/packages/app), opening such a worktree incorrectly set the project path to the worktree root instead of the matching subdirectory within it (e.g./repo-wt/featureinstead of/repo-wt/feature/packages/app).add_discovered_worktree.Follows up on #34 and #61 which fixed worktree creation and removal in monorepos — this PR fixes the remaining case of opening existing worktrees.
Test plan
git worktree add), then open it via the worktree list — verify terminal starts in the correct subdirectoryCo-Authored-By: Claude Code