Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 9 additions & 16 deletions src/audit/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,29 +190,22 @@ pub async fn run_truffle_scan(
json: bool,
target_repos: Option<Vec<String>>,
) -> Result<(TruffleStatistics, HygieneStatistics)> {
let (start_time, repos) = init_command(SCANNING_MESSAGE).await;
// Pass target_repos to init_command for optimized filtering during discovery
let (start_time, repos) = init_command(SCANNING_MESSAGE, target_repos.clone()).await;

if repos.is_empty() {
if target_repos.is_some() {
println!("\r❌ No matching repositories found");
set_terminal_title_and_flush("✅ repos");
return Ok((TruffleStatistics::new(), HygieneStatistics::new()));
}

println!("\r{NO_REPOS_MESSAGE}");
set_terminal_title_and_flush("✅ repos");
return Ok((TruffleStatistics::new(), HygieneStatistics::new()));
}

// Filter repositories if specific targets are specified
let repos_to_scan = if let Some(targets) = target_repos {
repos
.into_iter()
.filter(|(name, _)| targets.contains(name))
.collect()
} else {
repos
};

if repos_to_scan.is_empty() {
println!("\r❌ No matching repositories found");
set_terminal_title_and_flush("✅ repos");
return Ok((TruffleStatistics::new(), HygieneStatistics::new()));
}
let repos_to_scan = repos;

let total_repos = repos_to_scan.len();
let repo_word = if total_repos == 1 {
Expand Down
2 changes: 1 addition & 1 deletion src/commands/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ pub async fn handle_config_command(args: ConfigArgs) -> Result<()> {
args
};

let (start_time, repos) = init_command(SCANNING_MESSAGE).await;
let (start_time, repos) = init_command(SCANNING_MESSAGE, None).await;

if repos.is_empty() {
println!("\r{NO_REPOS_MESSAGE}");
Expand Down
2 changes: 1 addition & 1 deletion src/commands/publish/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub async fn handle_publish_command(
) -> Result<()> {
set_terminal_title("📦 repos");

let (start_time, repos) = init_command(SCANNING_MESSAGE).await;
let (start_time, repos) = init_command(SCANNING_MESSAGE, None).await;

if repos.is_empty() {
println!("\r{NO_REPOS_MESSAGE}");
Expand Down
8 changes: 4 additions & 4 deletions src/commands/staging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub async fn handle_stage_command(pattern: String) -> Result<()> {
// Set terminal title to indicate repos is running
set_terminal_title("🚀 repos stage");

let (start_time, repos) = init_command(SCANNING_MESSAGE).await;
let (start_time, repos) = init_command(SCANNING_MESSAGE, None).await;

if repos.is_empty() {
println!("\r{NO_REPOS_MESSAGE}");
Expand Down Expand Up @@ -71,7 +71,7 @@ pub async fn handle_unstage_command(pattern: String) -> Result<()> {
// Set terminal title to indicate repos is running
set_terminal_title("🚀 repos unstage");

let (start_time, repos) = init_command(SCANNING_MESSAGE).await;
let (start_time, repos) = init_command(SCANNING_MESSAGE, None).await;

if repos.is_empty() {
println!("\r{NO_REPOS_MESSAGE}");
Expand Down Expand Up @@ -115,7 +115,7 @@ pub async fn handle_staging_status_command() -> Result<()> {
// Set terminal title to indicate repos is running
set_terminal_title("🚀 repos status");

let (start_time, repos) = init_command(SCANNING_MESSAGE).await;
let (start_time, repos) = init_command(SCANNING_MESSAGE, None).await;

if repos.is_empty() {
println!("\r{NO_REPOS_MESSAGE}");
Expand Down Expand Up @@ -364,7 +364,7 @@ pub async fn handle_commit_command(message: String, include_empty: bool) -> Resu
// Set terminal title to indicate repos is running
set_terminal_title("🚀 repos commit");

let (start_time, repos) = init_command(SCANNING_MESSAGE).await;
let (start_time, repos) = init_command(SCANNING_MESSAGE, None).await;

if repos.is_empty() {
println!("\r{NO_REPOS_MESSAGE}");
Expand Down
12 changes: 6 additions & 6 deletions src/commands/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub async fn handle_push_command(
// Set terminal title to indicate repos is running
set_terminal_title("🚀 repos");

let (start_time, repos) = init_command(SCANNING_MESSAGE).await;
let (start_time, repos) = init_command(SCANNING_MESSAGE, None).await;

if repos.is_empty() {
println!("\r{NO_REPOS_MESSAGE}");
Expand Down Expand Up @@ -70,7 +70,7 @@ pub async fn handle_push_command(

// Check for subrepo drift unless explicitly skipped
if !no_drift_check {
check_and_display_drift();
check_and_display_drift().await;
}

// Set terminal title to green checkbox to indicate completion
Expand Down Expand Up @@ -342,9 +342,9 @@ async fn process_push_repositories(
}

/// Check for subrepo drift and display concise summary
fn check_and_display_drift() {
async fn check_and_display_drift() {
// Try to analyze subrepos - if it fails (e.g., no subrepos), silently skip
if let Ok(statuses) = crate::subrepo::status::analyze_subrepos() {
if let Ok(statuses) = crate::subrepo::status::analyze_subrepos().await {
// Only display if there's drift to report
if statuses.iter().any(|s| s.has_drift) {
crate::subrepo::status::display_drift_summary(&statuses);
Expand All @@ -366,7 +366,7 @@ pub async fn handle_pull_command(
// Set terminal title to indicate repos is running
set_terminal_title("🔽 repos");

let (start_time, repos) = init_command(SCANNING_MESSAGE).await;
let (start_time, repos) = init_command(SCANNING_MESSAGE, None).await;

if repos.is_empty() {
println!("\r{NO_REPOS_MESSAGE}");
Expand Down Expand Up @@ -410,7 +410,7 @@ pub async fn handle_pull_command(

// Check for subrepo drift unless explicitly skipped
if !no_drift_check {
check_and_display_drift();
check_and_display_drift().await;
}

// Set terminal title to green checkbox to indicate completion
Expand Down
2 changes: 1 addition & 1 deletion src/core/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
//!
//! ```rust,no_run
//! use goobits_repos::core::find_repos_from_path;
//! let repos = find_repos_from_path("/path/to/search");
//! let repos = find_repos_from_path("/path/to/search", None);
//! ```

// Core types
Expand Down
28 changes: 22 additions & 6 deletions src/core/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ fn is_git_file(path: &Path) -> bool {
/// This function uses parallel directory walking for significantly better performance
/// with large directory trees (5-10x faster than sequential walking).
/// Uses `DashMap` for lock-free concurrent access, eliminating mutex contention.
pub fn find_repos_from_path(search_path: impl AsRef<Path>) -> Vec<(String, PathBuf)> {
pub fn find_repos_from_path(
search_path: impl AsRef<Path>,
filter: Option<&[String]>,
) -> Vec<(String, PathBuf)> {
let search_path = search_path.as_ref();

// Use DashMap for lock-free concurrent access (20-40% faster than Mutex<HashMap>)
Expand All @@ -47,6 +50,9 @@ pub fn find_repos_from_path(search_path: impl AsRef<Path>) -> Vec<(String, PathB
let name_counts = Arc::new(DashMap::with_capacity(ESTIMATED_REPO_COUNT));
let search_path_buf = search_path.to_path_buf();

// Convert filter to Arc for sharing across threads
let filter = filter.map(|f| Arc::new(f.to_vec()));

// Build parallel walker with optimizations
let walker = WalkBuilder::new(search_path)
.follow_links(true) // Follow symlinks to find symlinked repos
Expand Down Expand Up @@ -75,6 +81,7 @@ pub fn find_repos_from_path(search_path: impl AsRef<Path>) -> Vec<(String, PathB
let repos_map = Arc::clone(&repos_map);
let name_counts = Arc::clone(&name_counts);
let search_path_buf = search_path_buf.clone();
let filter = filter.clone();

Box::new(move |result| {
use ignore::WalkState;
Expand Down Expand Up @@ -141,6 +148,12 @@ pub fn find_repos_from_path(search_path: impl AsRef<Path>) -> Vec<(String, PathB
}
};

if let Some(ref f) = filter {
if !f.contains(&repo_name) {
return WalkState::Continue;
}
}

entry.insert(repo_name);
}
}
Expand Down Expand Up @@ -172,20 +185,23 @@ pub fn find_repos_from_path(search_path: impl AsRef<Path>) -> Vec<(String, PathB
///
/// This is a convenience wrapper around `find_repos_from_path()` that searches
/// from the current working directory.
pub fn find_repos() -> Vec<(String, PathBuf)> {
find_repos_from_path(".")
pub fn find_repos(filter: Option<&[String]>) -> Vec<(String, PathBuf)> {
find_repos_from_path(".", filter)
}

/// Common initialization for commands that scan repositories
#[must_use]
pub async fn init_command(scanning_msg: &str) -> (std::time::Instant, Vec<(String, PathBuf)>) {
pub async fn init_command(
scanning_msg: &str,
filter: Option<Vec<String>>,
) -> (std::time::Instant, Vec<(String, PathBuf)>) {
println!();
print!("{scanning_msg}");
// Flush stdout - ignore errors as this is non-critical
let _ = std::io::stdout().flush();

let start_time = std::time::Instant::now();
let repos = tokio::task::spawn_blocking(find_repos)
let repos = tokio::task::spawn_blocking(move || find_repos(filter.as_deref()))
.await
.unwrap_or_else(|e| {
eprintln!("Error in repository discovery: {e}");
Expand Down Expand Up @@ -308,7 +324,7 @@ mod tests {
}

// Run discovery
let repos = find_repos_from_path(root);
let repos = find_repos_from_path(root, None);

assert_eq!(repos.len(), 3);

Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
//!
//! #[tokio::main]
//! async fn main() {
//! let repos = find_repos_from_path(".");
//! let repos = find_repos_from_path(".", None);
//! for (name, path) in repos {
//! println!("{}: {}", name, path.display());
//! }
Expand Down
16 changes: 10 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,15 +224,15 @@ struct Cli {
}

/// Handles subrepo subcommands
fn handle_subrepo_command(subcommand: SubrepoCommand) -> Result<()> {
async fn handle_subrepo_command(subcommand: SubrepoCommand) -> Result<()> {
match subcommand {
SubrepoCommand::Validate => {
let report = subrepo::validation::validate_subrepos()?;
let report = subrepo::validation::validate_subrepos().await?;
subrepo::validation::display_report(&report);
Ok(())
}
SubrepoCommand::Status { all } => {
let statuses = subrepo::status::analyze_subrepos()?;
let statuses = subrepo::status::analyze_subrepos().await?;
subrepo::status::display_status(&statuses, all);
Ok(())
}
Expand All @@ -241,8 +241,10 @@ fn handle_subrepo_command(subcommand: SubrepoCommand) -> Result<()> {
to,
stash,
force,
} => subrepo::sync::sync_subrepo(&name, &to, stash, force),
SubrepoCommand::Update { name, force } => subrepo::sync::update_subrepo(&name, force),
} => subrepo::sync::sync_subrepo(&name, &to, stash, force).await,
SubrepoCommand::Update { name, force } => {
subrepo::sync::update_subrepo(&name, force).await
}
}
}

Expand Down Expand Up @@ -362,7 +364,9 @@ async fn main() -> Result<()> {
)
.await
}
Some(Commands::Subrepo { subcommand }) => handle_subrepo_command(subcommand.clone()),
Some(Commands::Subrepo { subcommand }) => {
handle_subrepo_command(subcommand.clone()).await
}
None => {
// Default behavior - show help
use clap::CommandFactory;
Expand Down
4 changes: 2 additions & 2 deletions src/subrepo/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ impl SubrepoStatus {
}

/// Analyze all subrepos and return status for shared ones
pub fn analyze_subrepos() -> Result<Vec<SubrepoStatus>> {
let report = super::validation::validate_subrepos()?;
pub async fn analyze_subrepos() -> Result<Vec<SubrepoStatus>> {
let report = super::validation::validate_subrepos().await?;

let mut statuses = Vec::new();
for (remote_url, instances) in report.by_remote {
Expand Down
8 changes: 4 additions & 4 deletions src/subrepo/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ fn fetch_latest_commit(path: &Path) -> Result<String> {
}

/// Sync a subrepo to a specific commit across all parent repositories
pub fn sync_subrepo(name: &str, target_commit: &str, stash: bool, force: bool) -> Result<()> {
let report = super::validation::validate_subrepos()?;
pub async fn sync_subrepo(name: &str, target_commit: &str, stash: bool, force: bool) -> Result<()> {
let report = super::validation::validate_subrepos().await?;
sync_subrepo_with_report(name, target_commit, stash, force, &report)
}

Expand Down Expand Up @@ -223,8 +223,8 @@ pub fn sync_subrepo_with_report(
}

/// Update a subrepo to the latest commit from remote
pub fn update_subrepo(name: &str, force: bool) -> Result<()> {
let report = super::validation::validate_subrepos()?;
pub async fn update_subrepo(name: &str, force: bool) -> Result<()> {
let report = super::validation::validate_subrepos().await?;
update_subrepo_with_report(name, force, &report)
}

Expand Down
54 changes: 29 additions & 25 deletions src/subrepo/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,43 @@ use std::collections::HashMap;
use std::path::Path;

/// Discover all nested repositories and generate a validation report
pub fn validate_subrepos() -> Result<ValidationReport> {
let parent_repos = crate::core::discovery::find_repos();
let mut all_nested = Vec::new();
pub async fn validate_subrepos() -> Result<ValidationReport> {
tokio::task::spawn_blocking(move || {
let parent_repos = crate::core::discovery::find_repos(None);
let mut all_nested = Vec::new();

println!(
"🔍 Scanning {} parent repositories for nested repos...\n",
parent_repos.len()
);
println!(
"🔍 Scanning {} parent repositories for nested repos...\n",
parent_repos.len()
);

for (parent_name, parent_path) in parent_repos {
let nested = find_nested_in_parent(&parent_name, &parent_path)?;
all_nested.extend(nested);
}
for (parent_name, parent_path) in parent_repos {
let nested = find_nested_in_parent(&parent_name, &parent_path)?;
all_nested.extend(nested);
}

// Group by remote URL
let mut by_remote: HashMap<String, Vec<SubrepoInstance>> = HashMap::new();
let mut no_remote = Vec::new();
// Group by remote URL
let mut by_remote: HashMap<String, Vec<SubrepoInstance>> = HashMap::new();
let mut no_remote = Vec::new();

for instance in all_nested {
if let Some(ref remote) = instance.remote_url {
by_remote.entry(remote.clone()).or_default().push(instance);
} else {
no_remote.push(instance);
for instance in all_nested {
if let Some(ref remote) = instance.remote_url {
by_remote.entry(remote.clone()).or_default().push(instance);
} else {
no_remote.push(instance);
}
}
}

let total_nested = by_remote.values().map(std::vec::Vec::len).sum::<usize>() + no_remote.len();
let total_nested =
by_remote.values().map(std::vec::Vec::len).sum::<usize>() + no_remote.len();

Ok(ValidationReport {
total_nested,
by_remote,
no_remote,
Ok(ValidationReport {
total_nested,
by_remote,
no_remote,
})
})
.await?
}

/// Find nested repositories within a parent repository
Expand Down
Loading