mirror of
https://github.com/kristoferssolo/project-finder.git
synced 2025-10-21 19:50:35 +00:00
docs: add docstrings
This commit is contained in:
parent
859bd1135e
commit
5647d63978
60
Cargo.lock
generated
60
Cargo.lock
generated
@ -314,16 +314,6 @@ version = "0.4.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lock_api"
|
|
||||||
version = "0.4.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"scopeguard",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.26"
|
version = "0.4.26"
|
||||||
@ -387,29 +377,6 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot"
|
|
||||||
version = "0.12.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
|
|
||||||
dependencies = [
|
|
||||||
"lock_api",
|
|
||||||
"parking_lot_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "parking_lot_core"
|
|
||||||
version = "0.9.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"libc",
|
|
||||||
"redox_syscall",
|
|
||||||
"smallvec",
|
|
||||||
"windows-targets",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@ -455,15 +422,6 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_syscall"
|
|
||||||
version = "0.5.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@ -512,12 +470,6 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scopeguard"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@ -551,16 +503,6 @@ version = "1.14.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
|
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "socket2"
|
|
||||||
version = "0.5.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"windows-sys 0.52.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@ -618,10 +560,8 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|||||||
10
Cargo.toml
10
Cargo.toml
@ -19,7 +19,15 @@ clap = { version = "4.5", features = ["derive"] }
|
|||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
regex = "1.11"
|
regex = "1.11"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1.44", features = [
|
||||||
|
"fs",
|
||||||
|
"io-util",
|
||||||
|
"macros",
|
||||||
|
"process",
|
||||||
|
"rt",
|
||||||
|
"rt-multi-thread",
|
||||||
|
"sync",
|
||||||
|
] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
which = "7.0"
|
which = "7.0"
|
||||||
|
|||||||
@ -5,6 +5,7 @@ use crate::{
|
|||||||
use regex::{Regex, escape};
|
use regex::{Regex, escape};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
fmt::Display,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Stdio,
|
process::Stdio,
|
||||||
};
|
};
|
||||||
@ -15,13 +16,35 @@ use tokio::{
|
|||||||
};
|
};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
/// Run fd command to find files and directories
|
/// Helper to wrap command errors in a uniform way.
|
||||||
|
fn wrap_command_error<E: Display>(action: &str, err: E) -> ProjectFinderError {
|
||||||
|
ProjectFinderError::CommandExecutionFailed(format!("{action}: {err}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the `fd` command to find files matching one or more literal patterns.
|
||||||
|
///
|
||||||
|
/// The function builds a combined regex pattern from the list of patterns, runs the
|
||||||
|
/// command asynchronously, and collects matching file paths in a map keyed by the literal
|
||||||
|
/// file name.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// - `deps`: Dependencies hold the path to the `fd` binary.
|
||||||
|
/// - `dir`: The directory in which to search.
|
||||||
|
/// - `patterns`: A list of file name patterns (literals) to match.
|
||||||
|
/// - `max_depth`: The maximum directory depth for the search.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A map where each key is one of the patterns and the value is the list of matching
|
||||||
|
/// file paths.
|
||||||
pub async fn find_files(
|
pub async fn find_files(
|
||||||
deps: &Dependencies,
|
deps: &Dependencies,
|
||||||
dir: &Path,
|
dir: &Path,
|
||||||
patterns: &[&str],
|
patterns: &[&str],
|
||||||
max_depth: usize,
|
max_depth: usize,
|
||||||
) -> Result<HashMap<String, Vec<PathBuf>>> {
|
) -> Result<HashMap<String, Vec<PathBuf>>> {
|
||||||
|
// Build a regex pattern that matches any of the provided (literal) patterns.
|
||||||
let combined_patterns = format!(
|
let combined_patterns = format!(
|
||||||
"({})",
|
"({})",
|
||||||
patterns
|
patterns
|
||||||
@ -32,7 +55,6 @@ pub async fn find_files(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut cmd = Command::new(&deps.fd_path);
|
let mut cmd = Command::new(&deps.fd_path);
|
||||||
|
|
||||||
cmd.arg("--hidden")
|
cmd.arg("--hidden")
|
||||||
.arg("--no-ignore-vcs")
|
.arg("--no-ignore-vcs")
|
||||||
.arg("--type")
|
.arg("--type")
|
||||||
@ -49,23 +71,28 @@ pub async fn find_files(
|
|||||||
ProjectFinderError::CommandExecutionFailed(format!("Failed to spawn fd: {e}"))
|
ProjectFinderError::CommandExecutionFailed(format!("Failed to spawn fd: {e}"))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Take the stdout and wrap it with a buffered reader.
|
// Capture stdout and wrap it with a buffered reader.
|
||||||
let stdout = child.stdout.take().ok_or_else(|| {
|
let stdout = child.stdout.take().ok_or_else(|| {
|
||||||
ProjectFinderError::CommandExecutionFailed("Failed to capture stdout".into())
|
ProjectFinderError::CommandExecutionFailed("Failed to capture stdout".into())
|
||||||
})?;
|
})?;
|
||||||
let reader = BufReader::new(stdout);
|
let reader = BufReader::new(stdout);
|
||||||
let mut lines = reader.lines();
|
let mut lines = reader.lines();
|
||||||
|
|
||||||
|
// Prepare the results map with an empty vector for each pattern.
|
||||||
let mut results = patterns
|
let mut results = patterns
|
||||||
.iter()
|
.iter()
|
||||||
.map(|pattern| ((*pattern).to_string(), Vec::new()))
|
.map(|pattern| ((*pattern).to_string(), Vec::new()))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
// Process output as lines arrive.
|
// Stream and process output as lines arrive.
|
||||||
while let Some(line) = lines.next_line().await.map_err(|e| {
|
while let Some(line) = lines
|
||||||
ProjectFinderError::CommandExecutionFailed(format!("Failed to read stdout: {e}"))
|
.next_line()
|
||||||
})? {
|
.await
|
||||||
|
.map_err(|e| wrap_command_error("Failed to read stdout", e))?
|
||||||
|
{
|
||||||
let path = PathBuf::from(line);
|
let path = PathBuf::from(line);
|
||||||
|
// For each found file, only add it if its file name exactly matches one
|
||||||
|
// of the provided patterns.
|
||||||
if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) {
|
if let Some(file_name) = path.file_name().and_then(|f| f.to_str()) {
|
||||||
if let Some(entries) = results.get_mut(file_name) {
|
if let Some(entries) = results.get_mut(file_name) {
|
||||||
entries.push(path);
|
entries.push(path);
|
||||||
@ -73,10 +100,11 @@ pub async fn find_files(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the process has finished.
|
// Wait for the command to finish.
|
||||||
let status = child.wait().await.map_err(|e| {
|
let status = child
|
||||||
ProjectFinderError::CommandExecutionFailed(format!("Failed to wait process: {e}"))
|
.wait()
|
||||||
})?;
|
.await
|
||||||
|
.map_err(|e| wrap_command_error("Failed to wait process", e))?;
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
warn!("fd command exited with non-zero status: {status}");
|
warn!("fd command exited with non-zero status: {status}");
|
||||||
}
|
}
|
||||||
@ -84,14 +112,26 @@ pub async fn find_files(
|
|||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find Git repositories
|
/// Find Git repositories by searching for '.git' directories.
|
||||||
|
///
|
||||||
|
/// This function invokes the `fd` command with the pattern '^.git$'. For each
|
||||||
|
/// found directory, it returns the parent path (the Git repository root).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// - `deps`: Dependencies containing the path to the `fd` binary.
|
||||||
|
/// - `dir`: The directory to search for Git repositories.
|
||||||
|
/// - `max_depth`: The maximum directory depth to search.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A vector of paths representing the roots of Git repositories.
|
||||||
pub async fn find_git_repos(
|
pub async fn find_git_repos(
|
||||||
deps: &Dependencies,
|
deps: &Dependencies,
|
||||||
dir: &Path,
|
dir: &Path,
|
||||||
max_depth: usize,
|
max_depth: usize,
|
||||||
) -> Result<Vec<PathBuf>> {
|
) -> Result<Vec<PathBuf>> {
|
||||||
let mut cmd = Command::new(&deps.fd_path);
|
let mut cmd = Command::new(&deps.fd_path);
|
||||||
|
|
||||||
cmd.arg("--hidden")
|
cmd.arg("--hidden")
|
||||||
.arg("--type")
|
.arg("--type")
|
||||||
.arg("d")
|
.arg("d")
|
||||||
@ -103,21 +143,22 @@ pub async fn find_git_repos(
|
|||||||
|
|
||||||
debug!("Finding git repos in {}", dir.display());
|
debug!("Finding git repos in {}", dir.display());
|
||||||
|
|
||||||
let output = cmd.output().await.map_err(|e| {
|
let output = cmd
|
||||||
ProjectFinderError::CommandExecutionFailed(format!("Failed to find git repositories: {e}"))
|
.output()
|
||||||
})?;
|
.await
|
||||||
|
.map_err(|e| wrap_command_error("Failed to find git repositories", e))?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
warn!("fd command failed: {}", stderr);
|
warn!("fd command failed: {stderr}");
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
let stdout = String::from_utf8(output.stdout).map_err(ProjectFinderError::Utf8Error)?;
|
let stdout = String::from_utf8(output.stdout).map_err(ProjectFinderError::Utf8Error)?;
|
||||||
|
|
||||||
|
// For each found '.git' directory, return its parent directory.
|
||||||
let paths = stdout
|
let paths = stdout
|
||||||
.lines()
|
.lines()
|
||||||
// Convert .git directories to their parent (the actual repo root)
|
|
||||||
.filter_map(|line| {
|
.filter_map(|line| {
|
||||||
let path = PathBuf::from(line);
|
let path = PathBuf::from(line);
|
||||||
path.parent().map(std::path::Path::to_path_buf)
|
path.parent().map(std::path::Path::to_path_buf)
|
||||||
@ -127,6 +168,16 @@ pub async fn find_git_repos(
|
|||||||
Ok(paths)
|
Ok(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read a file into memory and check if it contains any match of the provided regex.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// - `file`: The file to read.
|
||||||
|
/// - `pattern`: The regex pattern to search for.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// `true` if the regex matches the file’s contents, `false` otherwise.
|
||||||
pub async fn grep_file_in_memory(file: &Path, pattern: &str) -> Result<bool> {
|
pub async fn grep_file_in_memory(file: &Path, pattern: &str) -> Result<bool> {
|
||||||
let contents = read_to_string(file).await.map_err(|e| {
|
let contents = read_to_string(file).await.map_err(|e| {
|
||||||
ProjectFinderError::CommandExecutionFailed(format!(
|
ProjectFinderError::CommandExecutionFailed(format!(
|
||||||
@ -135,9 +186,8 @@ pub async fn grep_file_in_memory(file: &Path, pattern: &str) -> Result<bool> {
|
|||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let re = Regex::new(pattern).map_err(|e| {
|
let re = Regex::new(pattern)
|
||||||
ProjectFinderError::CommandExecutionFailed(format!("Invalid regex patter {pattern}: {e}"))
|
.map_err(|e| wrap_command_error(&format!("Invalid regex patter {pattern}"), e))?;
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(re.is_match(&contents))
|
Ok(re.is_match(&contents))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Debug, Parser, Clone)]
|
#[derive(Debug, Parser, Clone)]
|
||||||
#[clap(
|
#[clap(
|
||||||
@ -9,7 +10,7 @@ use clap::Parser;
|
|||||||
pub struct Config {
|
pub struct Config {
|
||||||
/// Directories to search for projects
|
/// Directories to search for projects
|
||||||
#[clap(default_value = ".")]
|
#[clap(default_value = ".")]
|
||||||
pub paths: Vec<String>,
|
pub paths: Vec<PathBuf>,
|
||||||
|
|
||||||
/// Maximum search depth
|
/// Maximum search depth
|
||||||
#[clap(short, long, default_value = "5")]
|
#[clap(short, long, default_value = "5")]
|
||||||
|
|||||||
@ -2,29 +2,41 @@ use crate::errors::{ProjectFinderError, Result};
|
|||||||
use tracing::info;
|
use tracing::info;
|
||||||
use which::which;
|
use which::which;
|
||||||
|
|
||||||
|
/// Represents external dependencies required by the application.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Dependencies {
|
pub struct Dependencies {
|
||||||
pub fd_path: String,
|
pub fd_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Dependencies {
|
impl Dependencies {
|
||||||
|
/// Creates a new instance of `Dependencies` from the given `fd` binary path.
|
||||||
pub fn new(fd_path: impl Into<String>) -> Self {
|
pub fn new(fd_path: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
fd_path: fd_path.into(),
|
fd_path: fd_path.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if all required dependencies are available, returning an instance of
|
||||||
|
/// `Dependencies` with the paths set appropriately.
|
||||||
|
///
|
||||||
|
/// At the moment, this only verifies that the `fd` binary is available.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns a `ProjectFinderError::DependencyNotFound` error if `fd` is not found.
|
||||||
pub fn check() -> Result<Self> {
|
pub fn check() -> Result<Self> {
|
||||||
info!("Checking dependencies...");
|
info!("Checking dependencies...");
|
||||||
|
|
||||||
let fd_path = which("fd").map_err(|_| {
|
let fd_path = which("fd")
|
||||||
|
.map(|path| path.to_string_lossy().into_owned())
|
||||||
|
.map_err(|_| {
|
||||||
ProjectFinderError::DependencyNotFound(
|
ProjectFinderError::DependencyNotFound(
|
||||||
"fd - install from https://github.com/sharkdp/fd".into(),
|
"fd - install from https://github.com/sharkdp/fd".into(),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
info!("Found fd at: {}", fd_path.display());
|
info!("Found fd at: {fd_path}");
|
||||||
|
|
||||||
Ok(Self::new(fd_path.to_string_lossy()))
|
Ok(Self::new(fd_path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,10 +38,12 @@ const MARKER_PATTERNS: [&str; 13] = [
|
|||||||
"bunfig.toml",
|
"bunfig.toml",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Check whether a given path exists.
|
||||||
async fn path_exists(path: &Path) -> bool {
|
async fn path_exists(path: &Path) -> bool {
|
||||||
metadata(path).await.is_ok()
|
metadata(path).await.is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Struct responsible for scanning directories and detecting projects.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ProjectFinder {
|
pub struct ProjectFinder {
|
||||||
config: Config,
|
config: Config,
|
||||||
@ -52,6 +54,7 @@ pub struct ProjectFinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectFinder {
|
impl ProjectFinder {
|
||||||
|
/// Create a new `ProjectFinder` instance.
|
||||||
pub fn new(config: Config, deps: Dependencies) -> Self {
|
pub fn new(config: Config, deps: Dependencies) -> Self {
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
@ -62,25 +65,24 @@ impl ProjectFinder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find projects in the configured paths.
|
||||||
pub async fn find_projects(&self) -> Result<Vec<PathBuf>> {
|
pub async fn find_projects(&self) -> Result<Vec<PathBuf>> {
|
||||||
let semaphore = Arc::new(Semaphore::new(8)); // Limit to 8 concurrent tasks
|
let semaphore = Arc::new(Semaphore::new(8)); // Limit to 8 concurrent tasks
|
||||||
let mut handles = vec![];
|
let mut handles = Vec::new();
|
||||||
|
|
||||||
for path in &self.config.paths {
|
for path in &self.config.paths {
|
||||||
let path_buf = PathBuf::from(path);
|
if !path.is_dir() {
|
||||||
if !path_buf.is_dir() {
|
return Err(ProjectFinderError::PathNotFound(path.clone()));
|
||||||
return Err(ProjectFinderError::PathNotFound(path_buf));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.config.verbose {
|
if self.config.verbose {
|
||||||
info!("Searching in: {}", path);
|
info!("Searching in: {}", path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
let finder_clone = self.clone();
|
let finder_clone = self.clone();
|
||||||
let path_clone = path_buf.clone();
|
let path_clone = path.clone();
|
||||||
let semaphore_clone = Arc::clone(&semaphore);
|
let semaphore_clone = Arc::clone(&semaphore);
|
||||||
|
|
||||||
// Spawn a task for each directory with semaphore permit
|
|
||||||
let handle = spawn(async move {
|
let handle = spawn(async move {
|
||||||
let _permit = semaphore_clone.acquire().await.map_err(|e| {
|
let _permit = semaphore_clone.acquire().await.map_err(|e| {
|
||||||
ProjectFinderError::CommandExecutionFailed(format!(
|
ProjectFinderError::CommandExecutionFailed(format!(
|
||||||
@ -92,6 +94,7 @@ impl ProjectFinder {
|
|||||||
handles.push(handle);
|
handles.push(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Await all tasks and collect errors.
|
||||||
let handle_results = join_all(handles).await;
|
let handle_results = join_all(handles).await;
|
||||||
let mut errors = handle_results
|
let mut errors = handle_results
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@ -109,13 +112,12 @@ impl ProjectFinder {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Return first error if any occurred
|
// If all tasks failed, return one of the errors.
|
||||||
// Only fail if all tasks failed
|
|
||||||
if !errors.is_empty() && errors.len() == self.config.paths.len() {
|
if !errors.is_empty() && errors.len() == self.config.paths.len() {
|
||||||
return Err(errors.remove(0));
|
return Err(errors.remove(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return sorted results
|
// Gather discovered projects, sort and apply max_results limit, if set.
|
||||||
let mut projects = self
|
let mut projects = self
|
||||||
.discovered_projects
|
.discovered_projects
|
||||||
.read()
|
.read()
|
||||||
@ -123,10 +125,7 @@ impl ProjectFinder {
|
|||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<PathBuf>>();
|
.collect::<Vec<PathBuf>>();
|
||||||
|
|
||||||
projects.sort();
|
projects.sort();
|
||||||
|
|
||||||
// Apply max_results if set
|
|
||||||
if self.config.max_results > 0 && projects.len() > self.config.max_results {
|
if self.config.max_results > 0 && projects.len() > self.config.max_results {
|
||||||
projects.truncate(self.config.max_results);
|
projects.truncate(self.config.max_results);
|
||||||
}
|
}
|
||||||
@ -134,16 +133,18 @@ impl ProjectFinder {
|
|||||||
Ok(projects)
|
Ok(projects)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Process a single directory by scanning for git repositories and marker files.
|
||||||
async fn process_directory(&self, dir: &Path) -> Result<()> {
|
async fn process_directory(&self, dir: &Path) -> Result<()> {
|
||||||
// First find all git repositories (usually the most reliable project indicators)
|
// Look for git repositories first.
|
||||||
let git_repos = find_git_repos(&self.deps, dir, self.config.depth).await?;
|
let git_repos = find_git_repos(&self.deps, dir, self.config.depth).await?;
|
||||||
|
|
||||||
{
|
{
|
||||||
self.discovered_projects.write().await.extend(git_repos);
|
let mut projects = self.discovered_projects.write().await;
|
||||||
|
projects.extend(git_repos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Look for marker files.
|
||||||
let marker_map = find_files(&self.deps, dir, &MARKER_PATTERNS, self.config.depth).await?;
|
let marker_map = find_files(&self.deps, dir, &MARKER_PATTERNS, self.config.depth).await?;
|
||||||
|
|
||||||
for (pattern, paths) in marker_map {
|
for (pattern, paths) in marker_map {
|
||||||
for path in paths {
|
for path in paths {
|
||||||
if let Some(parent_dir) = path.parent() {
|
if let Some(parent_dir) = path.parent() {
|
||||||
@ -155,6 +156,7 @@ impl ProjectFinder {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Process a marker file found in a directory.
|
||||||
async fn process_marker(&self, dir: &Path, marker_name: &str) -> Result<()> {
|
async fn process_marker(&self, dir: &Path, marker_name: &str) -> Result<()> {
|
||||||
// Determine marker type
|
// Determine marker type
|
||||||
let marker_type = marker_name.parse().expect("How did we get here?");
|
let marker_type = marker_name.parse().expect("How did we get here?");
|
||||||
|
|||||||
@ -34,14 +34,6 @@ async fn main() {
|
|||||||
Ok(deps) => deps,
|
Ok(deps) => deps,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{e}");
|
error!("{e}");
|
||||||
eprintln!("Error: {e}");
|
|
||||||
eprintln!(
|
|
||||||
"This tool requires both 'fd' and 'ripgrep' (rg) to be installed and available in your PATH."
|
|
||||||
);
|
|
||||||
eprintln!("Please install the missing dependencies and try again.");
|
|
||||||
eprintln!("\nInstallation instructions:");
|
|
||||||
eprintln!(" fd: https://github.com/sharkdp/fd#installation");
|
|
||||||
eprintln!(" ripgrep: https://github.com/BurntSushi/ripgrep#installation");
|
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -51,7 +43,6 @@ async fn main() {
|
|||||||
|
|
||||||
match finder.find_projects().await {
|
match finder.find_projects().await {
|
||||||
Ok(projects) => {
|
Ok(projects) => {
|
||||||
// Output results
|
|
||||||
for project in projects {
|
for project in projects {
|
||||||
println!("{}", project.display());
|
println!("{}", project.display());
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user