From 724e0fffd3a910f252d68b43b6cf941f119a5b49 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Thu, 20 Mar 2025 14:03:50 +0200 Subject: [PATCH] refactor: mitigate some issues --- Cargo.lock | 105 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/commands.rs | 1 + src/config.rs | 4 ++ src/dependencies.rs | 2 +- src/finder.rs | 84 ++++++++++++++++++++++++++--------- 6 files changed, 176 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3060e7..831d0fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,6 +180,95 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "gimli" version = "0.31.1" @@ -318,6 +407,12 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "proc-macro2" version = "1.0.94" @@ -333,6 +428,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "futures", "thiserror", "tokio", "tracing", @@ -401,6 +497,15 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.14.0" diff --git a/Cargo.toml b/Cargo.toml index 376ade6..afab3a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ readme = "README.md" [dependencies] anyhow = "1" clap = { version = "4.5", features = ["derive"] } +futures = "0.3" thiserror = "2.0" tokio = { version = "1", features = ["full"] } tracing = "0.1" diff --git a/src/commands.rs b/src/commands.rs index bed6359..d911a28 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -93,6 +93,7 @@ pub async fn grep_file(deps: &Dependencies, file: &Path, pattern: &str) -> Resul let mut cmd = Command::new(&deps.rg_path); cmd.arg("-q") // quiet mode, just return exit code + .arg("-e") // explicitly specify pattern .arg(pattern) .arg(file); diff --git a/src/config.rs b/src/config.rs index fde869a..6e0d8b5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -18,4 +18,8 @@ pub struct Config { /// Show verbose output #[clap(short, long)] pub verbose: bool, + + /// Maximum number of results to return + #[clap(short = 'n', long, default_value = "0")] + pub max_results: usize, } diff --git a/src/dependencies.rs b/src/dependencies.rs index 2742f93..850fdc6 100644 --- a/src/dependencies.rs +++ b/src/dependencies.rs @@ -31,7 +31,7 @@ impl Dependencies { ) })?; info!("Found fd at: {}", fd_path.display()); - info!("Found rupgrep at: {}", rg_path.display()); + info!("Found ripgrep at: {}", rg_path.display()); Ok(Self::new( fd_path.to_string_lossy(), diff --git a/src/finder.rs b/src/finder.rs index 0577576..cd4506e 100644 --- a/src/finder.rs +++ b/src/finder.rs @@ -4,6 +4,7 @@ use crate::{ dependencies::Dependencies, errors::{ProjectFinderError, Result}, }; +use futures::future::join_all; use std::{ collections::{HashMap, HashSet}, path::{Path, PathBuf}, @@ -46,7 +47,7 @@ impl ProjectFinder { } pub async fn find_projects(&self) -> Result> { - // Process each search directory + let semaphore = Arc::new(tokio::sync::Semaphore::new(8)); // Limit to 8 concurrent tasks let mut handles = vec![]; for path in &self.config.paths { @@ -61,27 +62,43 @@ impl ProjectFinder { let finder_clone = self.clone(); let path_clone = path_buf.clone(); + let semaphore_clone = Arc::clone(&semaphore); - // Spawn a task for each directory - let handle = - tokio::spawn(async move { finder_clone.process_directory(&path_clone).await }); + // Spawn a task for each directory with semaphore permit + let handle = tokio::spawn(async move { + let _permit = semaphore_clone.acquire().await.map_err(|e| { + ProjectFinderError::CommandExecutionFailed(format!( + "Failed to aquire semaphore: {e}" + )) + })?; + finder_clone.process_directory(&path_clone).await + }); handles.push(handle); } - // Wait for all tasks to complete - for handle in handles { - match handle.await { - Ok(result) => { - // Propagate internal errors - if let Err(e) = result { - debug!("Task failed: {}", e); - } - } + let handle_results = join_all(handles).await; + + let mut errors = handle_results + .into_iter() + .filter_map(|handle_result| match handle_result { + Ok(task_result) => task_result.err().map(|e| { + debug!("Task failed: {}", e); + e + }), Err(e) => { debug!("Task join error: {}", e); + Some(ProjectFinderError::CommandExecutionFailed(format!( + "Task panicked: {e}", + ))) } - } + }) + .collect::>(); + + // Return first error if any occurred + if !errors.is_empty() && errors.len() == self.config.paths.len() { + // Only fail if all tasks failed + return Err(errors.remove(0)); } // Return sorted results @@ -91,6 +108,12 @@ impl ProjectFinder { }; projects.sort(); + + // Apply max_results if set + if self.config.max_results > 0 && projects.len() > self.config.max_results { + projects.truncate(self.config.max_results); + } + Ok(projects) } @@ -148,12 +171,23 @@ impl ProjectFinder { // Find project root let project_root = self.find_project_root(dir, &marker_type).await?; - // Check if it's a subdirectory of an already discovered project + // Improved nested project detection + // Only ignore if it's a subproject of the same type (prevents ignoring + // valid nested projects of different types) let mut should_add = true; { let projects = self.discovered_projects.lock().await; for known_project in projects.iter() { - if project_root.starts_with(known_project) && project_root != *known_project { + // Check if this is a direct parent (not just any ancestor) + let is_direct_parent = project_root + .parent() + .is_some_and(|parent| parent == known_project); + + // Only exclude if it's a subdirectory and has the same marker type + // or if it's exactly the same directory + if project_root == *known_project + || project_root.starts_with(known_project) && !is_direct_parent + { should_add = false; break; } @@ -293,14 +327,24 @@ impl ProjectFinder { // Define workspace patterns to check let workspace_patterns = [ - (dir.join("package.json"), r#""workspaces""#), - (dir.join("deno.json"), r#""workspaces""#), - (dir.join("deno.jsonc"), r#""workspaces""#), + (dir.join("package.json"), r#"("workspaces"|"workspace")"#), + (dir.join("deno.json"), r#"("workspaces"|"imports")"#), + (dir.join("deno.jsonc"), r#"("workspaces"|"imports")"#), (dir.join("bunfig.toml"), r"workspaces"), + (dir.join("Cargo.toml"), r"^\[workspace\]"), + (dir.join("rush.json"), r"."), + (dir.join("nx.json"), r"."), + (dir.join("turbo.json"), r"."), ]; // Files that indicate workspaces just by existing - let workspace_files = [dir.join("pnpm-workspace.yaml"), dir.join("lerna.json")]; + let workspace_files = [ + dir.join("pnpm-workspace.yaml"), + dir.join("lerna.json"), + dir.join("yarn.lock"), // Common in yarn workspaces + dir.join(".yarnrc.yml"), // Yarn 2+ workspaces + dir.join("workspace.json"), // Generic workspace file + ]; // Check for workspace by pattern matching for (file, pattern) in &workspace_patterns {