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
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
110 changes: 110 additions & 0 deletions benches/fixes_benchmark.rs
Original file line number Diff line number Diff line change
@@ -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);
18 changes: 9 additions & 9 deletions src/audit/fixes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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('#'))
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand All @@ -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()
Expand All @@ -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);
Expand Down
7 changes: 3 additions & 4 deletions src/audit/scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down