diff --git a/src/main.rs b/src/main.rs index 220d7f5..cc0d196 100644 --- a/src/main.rs +++ b/src/main.rs @@ -193,17 +193,30 @@ fn activate_venv(venv_path: &PathBuf) { } } -fn anchor(shell_override: Option<&str>) { - let venv_path = get_venv_path(); - let cfg_path = venv_path.join("pyvenv.cfg"); - - if !venv_path.is_dir() || !cfg_path.exists() { - eprintln!( - "uv-shell anchor: no .venv found in {}", - venv_path.parent().unwrap_or(&venv_path).display() - ); - return; +/// Walk from `start` up to the filesystem root looking for `.venv/pyvenv.cfg`. +fn find_venv_upward(start: &std::path::Path) -> Option { + let mut dir = start; + loop { + let candidate = dir.join(".venv"); + if candidate.is_dir() && candidate.join("pyvenv.cfg").exists() { + return Some(candidate); + } + match dir.parent() { + Some(parent) => dir = parent, + None => return None, + } } +} + +fn anchor(shell_override: Option<&str>) { + let cwd = env::current_dir().expect("failed to get current directory"); + let venv_path = match find_venv_upward(&cwd) { + Some(p) => p, + None => { + eprintln!("uv-shell anchor: no .venv found in {} or any parent directory", cwd.display()); + return; + } + }; let bin_dir = get_bin_dir(); let venv_bin = venv_path.join(bin_dir); diff --git a/tests/integration.rs b/tests/integration.rs index 762bde2..a5cbdfc 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -76,6 +76,10 @@ fn anchor_no_venv_reports_on_stderr() { stderr.contains("no .venv found"), "anchor should explain why it produced no output, got: {stderr}" ); + assert!( + stderr.contains("or any parent directory"), + "anchor should mention traversal in message, got: {stderr}" + ); let _ = fs::remove_dir_all(&dir); } @@ -97,6 +101,33 @@ fn anchor_prints_exports_bash() { let _ = fs::remove_dir_all(&dir); } +#[test] +fn anchor_finds_venv_in_parent_directory() { + // project root has .venv; anchor is run from a subdirectory + let dir = tmpdir_with_venv("anchor-traversal"); + let subdir = dir.join("src/utils"); + fs::create_dir_all(&subdir).unwrap(); + + let out = Command::new(bin()) + .args(["anchor", "--shell", "bash"]) + .current_dir(&subdir) + .output() + .unwrap(); + assert!(out.status.success()); + + let stdout = String::from_utf8_lossy(&out.stdout); + assert!( + stdout.contains("export VIRTUAL_ENV=\""), + "anchor should find .venv two levels up, got:\n{stdout}" + ); + // The VIRTUAL_ENV path should point to the root .venv, not a subdir .venv + assert!( + stdout.contains(dir.join(".venv").to_str().unwrap()), + "VIRTUAL_ENV should point to root .venv" + ); + let _ = fs::remove_dir_all(&dir); +} + #[test] fn anchor_creates_activated_marker() { let dir = tmpdir_with_venv("anchor-marker");