diff --git a/Cargo.toml b/Cargo.toml index 6d2b009..ddbf9de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,10 @@ harness = false name = "visibility_benchmark" harness = false +[[bench]] +name = "fixes_benchmark" +harness = false + [lib] name = "goobits_repos" path = "src/lib.rs" diff --git a/benches/fixes_benchmark.rs b/benches/fixes_benchmark.rs new file mode 100644 index 0000000..1156f13 --- /dev/null +++ b/benches/fixes_benchmark.rs @@ -0,0 +1,110 @@ +use criterion::{criterion_group, criterion_main, Criterion}; +use goobits_repos::audit::fixes::{apply_fixes, FixOptions}; +use goobits_repos::audit::hygiene::{HygieneStatistics, HygieneViolation, ViolationType}; +use goobits_repos::audit::hygiene::report::HygieneStatus; +use std::fs; +use std::process::Command; +use tempfile::TempDir; + +fn setup_repo() -> TempDir { + let temp_dir = TempDir::new().unwrap(); + let root = temp_dir.path(); + + // Init git repo + Command::new("git") + .arg("init") + .arg("-q") // Quiet + .current_dir(root) + .output() + .expect("git init failed"); + + // Configure user for commit + Command::new("git") + .args(["config", "user.email", "you@example.com"]) + .current_dir(root) + .output() + .expect("git config email failed"); + Command::new("git") + .args(["config", "user.name", "Your Name"]) + .current_dir(root) + .output() + .expect("git config name failed"); + + // Create a file that should be ignored + let ignored_file = root.join("node_modules").join("foo.js"); + fs::create_dir_all(ignored_file.parent().unwrap()).unwrap(); + fs::write(&ignored_file, "console.log('hello');").unwrap(); + + // Track it (so it is a violation) + Command::new("git") + .args(["add", "."]) + .current_dir(root) + .output() + .expect("git add failed"); + + Command::new("git") + .args(["commit", "-m", "initial", "--quiet"]) + .current_dir(root) + .output() + .expect("git commit failed"); + + temp_dir +} + +async fn run_apply_fixes(path: &std::path::Path) { + let mut stats = HygieneStatistics::new(); + let violation = HygieneViolation { + file_path: "node_modules/foo.js".to_string(), + violation_type: ViolationType::GitignoreViolation, + size_bytes: None, + }; + + // Use update to populate stats + stats.update( + "test-repo", + path.to_str().unwrap(), + &HygieneStatus::Violations, + "found violations", + vec![violation] + ); + + let options = FixOptions { + interactive: false, + fix_gitignore: true, + fix_large: false, + fix_secrets: false, + untrack_files: true, + dry_run: false, // We want to execute IO + skip_confirm: true, + target_repos: None, + }; + + let _ = apply_fixes(&stats, options).await; +} + +fn bench_fixes(c: &mut Criterion) { + let runtime = tokio::runtime::Runtime::new().unwrap(); + + let mut group = c.benchmark_group("fixes"); + group.sample_size(10); // Reduce sample size to save time + + group.bench_function("apply_fixes_gitignore", |b| { + b.to_async(&runtime).iter_custom(|iters| async move { + let mut total_duration = std::time::Duration::new(0, 0); + for _ in 0..iters { + let temp_dir = setup_repo(); + let path = temp_dir.path().to_path_buf(); + + let start = std::time::Instant::now(); + run_apply_fixes(&path).await; + let elapsed = start.elapsed(); + total_duration += elapsed; + } + total_duration + }) + }); + group.finish(); +} + +criterion_group!(benches, bench_fixes); +criterion_main!(benches); diff --git a/src/audit/fixes.rs b/src/audit/fixes.rs index 2aa1c06..584294a 100644 --- a/src/audit/fixes.rs +++ b/src/audit/fixes.rs @@ -8,9 +8,9 @@ use anyhow::{anyhow, Result}; use serde_json; use std::collections::{HashMap, HashSet}; -use std::fs; use std::io::{self, Write}; use std::path::Path; +use tokio::fs; use tokio::process::Command; use super::hygiene::{HygieneStatistics, HygieneViolation, ViolationType}; @@ -310,7 +310,7 @@ async fn fix_gitignore_violations( // Read existing .gitignore let gitignore_path = Path::new(repo_path).join(".gitignore"); - let existing_content = fs::read_to_string(&gitignore_path).unwrap_or_default(); + let existing_content = fs::read_to_string(&gitignore_path).await.unwrap_or_default(); let existing_patterns: HashSet<_> = existing_content .lines() .filter(|l| !l.trim().is_empty() && !l.starts_with('#')) @@ -340,7 +340,7 @@ async fn fix_gitignore_violations( gitignore_content.push('\n'); } - fs::write(&gitignore_path, gitignore_content)?; + fs::write(&gitignore_path, gitignore_content).await?; // Untrack files if requested let mut untracked_count = 0; @@ -508,7 +508,7 @@ async fn fix_large_files( } // Write paths to temporary file - fs::write(&paths_file, paths_content)?; + fs::write(&paths_file, paths_content).await?; // Run git filter-repo to remove the files let paths_file_str = paths_file @@ -528,7 +528,7 @@ async fn fix_large_files( .await?; // Clean up temp file - let _ = fs::remove_file(paths_file); + let _ = fs::remove_file(paths_file).await; if !result.status.success() { let stderr = String::from_utf8_lossy(&result.stderr); @@ -636,7 +636,7 @@ async fn fix_secrets_in_history(repo_path: &str, options: &FixOptions) -> Result } if !replacements_content.is_empty() { - fs::write(&replacements_file, replacements_content)?; + fs::write(&replacements_file, replacements_content).await?; // Run git filter-repo to replace secrets with REDACTED let replacements_file_str = replacements_file @@ -654,7 +654,7 @@ async fn fix_secrets_in_history(repo_path: &str, options: &FixOptions) -> Result .output() .await?; - let _ = fs::remove_file(replacements_file); + let _ = fs::remove_file(replacements_file).await; if !result.status.success() { let stderr = String::from_utf8_lossy(&result.stderr); @@ -673,7 +673,7 @@ async fn fix_secrets_in_history(repo_path: &str, options: &FixOptions) -> Result .map(|f| format!("literal:{f}\n")) .collect(); - fs::write(&paths_file, paths_content)?; + fs::write(&paths_file, paths_content).await?; let paths_file_str = paths_file .to_str() @@ -691,7 +691,7 @@ async fn fix_secrets_in_history(repo_path: &str, options: &FixOptions) -> Result .output() .await?; - let _ = fs::remove_file(paths_file); + let _ = fs::remove_file(paths_file).await; if !result.status.success() { let stderr = String::from_utf8_lossy(&result.stderr); diff --git a/src/audit/scanner.rs b/src/audit/scanner.rs index c9ab18d..b51ff7f 100644 --- a/src/audit/scanner.rs +++ b/src/audit/scanner.rs @@ -549,13 +549,12 @@ async fn install_trufflehog_direct() -> Result<()> { let install_dir = "/usr/local/bin"; // Check if we have write access to /usr/local/bin + let test_file = std::path::Path::new(install_dir).join("test_write"); let install_path = if tokio::fs::metadata(install_dir).await.is_ok() - && tokio::fs::File::create(format!("{install_dir}/test_write")) - .await - .is_ok() + && tokio::fs::File::create(&test_file).await.is_ok() { // Clean up test file - let _ = tokio::fs::remove_file(format!("{install_dir}/test_write")).await; + let _ = tokio::fs::remove_file(&test_file).await; install_dir.to_string() } else { // Fallback to user's local bin