use crate::git::{display_name, repos_or_exit};
use crate::history::{self, CommitMatch};
use colored::Colorize;
use std::path::PathBuf;
#[derive(clap::Args)]
pub struct Args {
#[arg(default_value = ".")]
pub path: PathBuf,
#[arg(
short,
long,
default_value = "password,secret,token,api_key,passwd,credentials"
)]
pub terms: String,
#[arg(short, long, default_value_t = 3)]
pub depth: usize,
}
pub fn run(args: Args) {
let terms: Vec<String> = args
.terms
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
println!("\n{}", "REPO SCANNER".bold());
println!("{}", format!("base : {}", args.path.display()).dimmed());
println!("{}", format!("terms : {}", terms.join(", ")).dimmed());
println!("{}\n", "━".repeat(56));
let repos = repos_or_exit(&args.path, args.depth);
let mut total_repos = 0usize;
let mut repos_with_hits = 0usize;
let mut total_hits = 0usize;
for repo in &repos {
total_repos += 1;
let name = display_name(repo);
println!(
"{} {}",
format!("📁 {}", name).bold(),
repo.display().to_string().dimmed()
);
let repo_str = match repo.to_str() {
Some(s) => s,
None => {
println!(" {}\n", "✓ no matches".green());
continue;
}
};
let commits = history::pickaxe(repo_str, &terms, true);
if commits.is_empty() {
println!(" {}\n", "✓ no matches".green());
continue;
}
let hits = count_hits(&commits);
repos_with_hits += 1;
total_hits += hits;
println!(" {}\n", format!("⚠ {} hit(s)", hits).red().bold());
for commit in &commits {
let branches = history::branches_for(repo_str, &commit.full);
println!(
" {} {} {}",
commit.date.dimmed(),
commit.short.cyan().bold(),
commit.subject
);
if !branches.is_empty() {
println!(" {} {}", "branch │".magenta(), branches.join(" "));
}
for file in &commit.files {
println!(" {} {}", "file │".dimmed(), file.path);
if file.lines.is_empty() {
println!(
" {} {}",
"hit │".yellow(),
"(binary or no visible diff)".dimmed()
);
}
for (_is_addition, line) in &file.lines {
println!(" {} {}", "hit │".yellow(), line.trim());
}
}
println!();
}
}
print_summary(total_repos, repos_with_hits, total_hits);
}
fn count_hits(commits: &[CommitMatch]) -> usize {
commits
.iter()
.flat_map(|c| c.files.iter())
.map(|f| f.lines.len().max(1))
.sum()
}
fn print_summary(total_repos: usize, repos_with_hits: usize, total_hits: usize) {
println!("{}", "━".repeat(56));
println!("{}", "SUMMARY".bold());
println!(" repos scanned : {}", total_repos.to_string().bold());
if repos_with_hits > 0 {
println!(
" repos with hits : {}",
repos_with_hits.to_string().red().bold()
);
println!(
" total hits : {}",
total_hits.to_string().red().bold()
);
println!("\n {}", "to remove a secret from history:".dimmed());
println!(
" {}",
" recent, top of one branch : git reset --hard <hash>^ && git push --force".dimmed()
);
println!(
" {}",
" deep or on many branches : git filter-repo --replace-text (rewrites all history)"
.dimmed()
);
} else {
println!(" {}", "✓ all clean".green().bold());
}
println!();
}