From fd2482e3e6d2b2363698e09dd5cc62b9dae44ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabor=20K=C3=B6rber?= Date: Fri, 8 May 2026 23:30:30 +0200 Subject: [PATCH] fix(fermata): .botignore no longer stops project-root walk-up Only .git, botignore.toml, and .botignore.toml define project boundaries. A bare .botignore is a policy file, not a root marker. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/core/project.rs | 2 +- tests/core_project.rs | 43 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/core/project.rs b/src/core/project.rs index bceba07..c9f3e15 100644 --- a/src/core/project.rs +++ b/src/core/project.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; /// Markers checked in priority order when walking up from a target path. -const MARKERS: &[&str] = &["botignore.toml", ".botignore", ".git"]; +const MARKERS: &[&str] = &["botignore.toml", ".botignore.toml", ".git"]; /// Walk upward from `target` (or its parent if `target` is a file) looking /// for the nearest project root. Roots are identified by the presence of diff --git a/tests/core_project.rs b/tests/core_project.rs index b4a3dc3..78d1d49 100644 --- a/tests/core_project.rs +++ b/tests/core_project.rs @@ -8,7 +8,7 @@ fn finds_botignore_toml_first() { let root = tmp.path(); fs::create_dir_all(root.join("sub/deep")).unwrap(); fs::write(root.join("botignore.toml"), "").unwrap(); - fs::write(root.join(".botignore"), "").unwrap(); + fs::write(root.join(".botignore.toml"), "").unwrap(); fs::create_dir_all(root.join(".git")).unwrap(); let target = root.join("sub/deep/file.rs"); @@ -19,7 +19,40 @@ fn finds_botignore_toml_first() { } #[test] -fn falls_back_to_botignore() { +fn finds_dot_botignore_toml() { + let tmp = TempDir::new().unwrap(); + let root = tmp.path(); + fs::create_dir_all(root.join("sub")).unwrap(); + fs::write(root.join(".botignore.toml"), "").unwrap(); + + let target = root.join("sub/file.rs"); + fs::write(&target, "").unwrap(); + + let found = find_project_root(&target).unwrap(); + assert_eq!(found, root); +} + +#[test] +fn botignore_alone_does_not_stop_walk() { + // A bare .botignore is a policy file, not a project boundary. + // The walk should continue past it to find a real root marker. + let tmp = TempDir::new().unwrap(); + let root = tmp.path(); + fs::create_dir_all(root.join("a/b")).unwrap(); + fs::create_dir_all(root.join(".git")).unwrap(); + fs::write(root.join("a/.botignore"), "*.secret").unwrap(); + + let target = root.join("a/b/file.rs"); + fs::write(&target, "").unwrap(); + + // Should find root (with .git), NOT root/a (with .botignore) + let found = find_project_root(&target).unwrap(); + assert_eq!(found, root); +} + +#[test] +fn botignore_only_returns_none() { + // If only .botignore exists with no real root marker, no project root is found. let tmp = TempDir::new().unwrap(); let root = tmp.path(); fs::create_dir_all(root.join("sub")).unwrap(); @@ -28,8 +61,8 @@ fn falls_back_to_botignore() { let target = root.join("sub/file.rs"); fs::write(&target, "").unwrap(); - let found = find_project_root(&target).unwrap(); - assert_eq!(found, root); + // .botignore alone should NOT define a project root + assert!(find_project_root(&target).is_none()); } #[test] @@ -59,7 +92,7 @@ fn walks_up_from_file_path_not_cwd() { let tmp = TempDir::new().unwrap(); let root = tmp.path(); fs::create_dir_all(root.join("a/b/c")).unwrap(); - fs::write(root.join("a/.botignore"), "").unwrap(); + fs::write(root.join("a/botignore.toml"), "").unwrap(); let target = root.join("a/b/c/file.rs"); fs::write(&target, "").unwrap();