From 72c50f880d0b992e839b73839fba28fec7b7498f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Thu, 26 Mar 2026 13:05:48 +0100 Subject: [PATCH 1/2] fix: resolve worktree root before removal in monorepos In monorepo setups the project path points to a subdirectory inside the worktree checkout (e.g. /repo-wt-branch/app), causing `git worktree remove` to fail with "is not a working tree". Use `get_repo_root()` to resolve the actual worktree root before passing it to the remove command. Closes #60 Co-Authored-By: Claude Code --- crates/okena-workspace/src/actions/project.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/okena-workspace/src/actions/project.rs b/crates/okena-workspace/src/actions/project.rs index c5d26592..5ec31087 100644 --- a/crates/okena-workspace/src/actions/project.rs +++ b/crates/okena-workspace/src/actions/project.rs @@ -659,9 +659,11 @@ impl Workspace { let project_hooks = project.hooks.clone(); let project_name = project.name.clone(); let project_path = project.path.clone(); - // Use the stored worktree path (repo-level checkout), falling back to project path - // for backwards compatibility with worktrees created before monorepo support - let worktree_path = std::path::PathBuf::from(&project_path); + // For monorepos the project path is a subdirectory inside the worktree checkout. + // Resolve the actual worktree root via git so `git worktree remove` gets the right path. + let project_pathbuf = std::path::PathBuf::from(&project_path); + let worktree_path = okena_git::get_repo_root(&project_pathbuf) + .unwrap_or(project_pathbuf); // Resolve branch BEFORE removal (git worktree remove deletes the checkout) let branch = okena_git::get_current_branch(&worktree_path).unwrap_or_default(); From dda5c44ea53196ef40c0e62cd6b23024ba2ec4ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Thu, 26 Mar 2026 13:12:44 +0100 Subject: [PATCH 2/2] test: add tests for get_repo_root with worktrees and subdirectories Verify that get_repo_root correctly resolves the worktree root (not a subdirectory) when called from a nested path inside a worktree. This covers the monorepo worktree removal fix. Co-Authored-By: Claude Code --- crates/okena-git/Cargo.toml | 3 ++ crates/okena-git/src/repository.rs | 64 ++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/crates/okena-git/Cargo.toml b/crates/okena-git/Cargo.toml index 61f99b67..2256b056 100644 --- a/crates/okena-git/Cargo.toml +++ b/crates/okena-git/Cargo.toml @@ -12,3 +12,6 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" uuid = { version = "1.10", features = ["v4"] } log = "0.4" + +[dev-dependencies] +tempfile = "3" diff --git a/crates/okena-git/src/repository.rs b/crates/okena-git/src/repository.rs index f817b763..8f4505bf 100644 --- a/crates/okena-git/src/repository.rs +++ b/crates/okena-git/src/repository.rs @@ -983,6 +983,70 @@ mod tests { assert_paths_eq(&proj, &expected); } + // ─── get_repo_root worktree / monorepo tests ─────────────────────── + + /// Helper: initialise a throwaway git repo with one commit so worktrees can + /// be created from it. + fn init_temp_repo() -> (tempfile::TempDir, PathBuf) { + let tmp = tempfile::tempdir().expect("create temp dir"); + let repo = tmp.path().to_path_buf(); + let r = |args: &[&str]| { + std::process::Command::new("git") + .args(args) + .current_dir(&repo) + .env("GIT_AUTHOR_NAME", "test") + .env("GIT_AUTHOR_EMAIL", "test@test") + .env("GIT_COMMITTER_NAME", "test") + .env("GIT_COMMITTER_EMAIL", "test@test") + .output() + .expect("git command failed") + }; + r(&["init", "-b", "main"]); + std::fs::write(repo.join("file.txt"), "x").unwrap(); + r(&["add", "."]); + r(&["-c", "commit.gpgsign=false", "commit", "-m", "init"]); + (tmp, repo) + } + + #[test] + fn get_repo_root_returns_toplevel_for_subdirectory() { + let (_tmp, repo) = init_temp_repo(); + let sub = repo.join("packages").join("app"); + std::fs::create_dir_all(&sub).unwrap(); + + let root = get_repo_root(&sub).expect("should resolve repo root"); + assert_eq!(root, repo.canonicalize().unwrap()); + } + + #[test] + fn get_repo_root_resolves_worktree_root_not_subdir() { + let (_tmp, repo) = init_temp_repo(); + // Create a worktree on a new branch + let wt_path = repo.parent().unwrap().join("my-worktree"); + let status = std::process::Command::new("git") + .args([ + "-C", + repo.to_str().unwrap(), + "worktree", + "add", + wt_path.to_str().unwrap(), + "-b", + "wt-branch", + ]) + .output() + .expect("git worktree add"); + assert!(status.status.success(), "worktree add failed"); + + // Create a nested subdirectory inside the worktree (monorepo subproject) + let nested = wt_path.join("packages").join("app"); + std::fs::create_dir_all(&nested).unwrap(); + + // get_repo_root from the nested subdir should return the worktree root, + // NOT the main repo — this is the path `git worktree remove` needs. + let root = get_repo_root(&nested).expect("should resolve worktree root"); + assert_eq!(root, wt_path.canonicalize().unwrap()); + } + // ─── CI check parsing tests ──────────────────────────────────────── #[test]