use crate::error::{Result, ToriiError};
use git2::{Repository, Status, StatusOptions};
use std::path::Path;
#[derive(Debug, Default)]
pub struct Opts {
pub force: bool,
pub dirs: bool,
pub include_ignored: bool,
pub only_ignored: bool,
}
pub fn clean(repo_path: &Path, opts: &Opts) -> Result<()> {
let repo = Repository::open(repo_path).map_err(ToriiError::Git)?;
let workdir = repo
.workdir()
.ok_or_else(|| ToriiError::InvalidConfig("bare repo".into()))?
.to_path_buf();
let mut so = StatusOptions::new();
so.include_untracked(true);
so.recurse_untracked_dirs(opts.dirs);
so.include_ignored(opts.include_ignored || opts.only_ignored);
let statuses = repo.statuses(Some(&mut so)).map_err(ToriiError::Git)?;
let mut targets: Vec<(String, bool)> = Vec::new(); for entry in statuses.iter() {
let st = entry.status();
let is_untracked = st.contains(Status::WT_NEW);
let is_ignored = st.contains(Status::IGNORED);
let keep = match (opts.only_ignored, opts.include_ignored) {
(true, _) => is_ignored,
(false, true) => is_untracked || is_ignored,
(false, false) => is_untracked,
};
if keep {
if let Some(path) = entry.path() {
targets.push((path.to_string(), is_ignored));
}
}
}
if targets.is_empty() {
println!("โจ Nothing to clean.");
return Ok(());
}
let action = if opts.force { "Removing" } else { "Would remove" };
println!("๐งน {action}:");
for (path, ignored) in &targets {
let tag = if *ignored { " (ignored)" } else { "" };
println!(" - {path}{tag}");
}
if !opts.force {
println!(
"\n(dry-run โ pass -f to actually delete. {} entr{} matched.)",
targets.len(),
if targets.len() == 1 { "y" } else { "ies" }
);
return Ok(());
}
for (path, _) in &targets {
let abs = workdir.join(path);
let meta = std::fs::symlink_metadata(&abs).ok();
match meta {
Some(m) if m.is_dir() => {
if !opts.dirs {
continue; }
let _ = std::fs::remove_dir_all(&abs);
}
Some(_) => {
let _ = std::fs::remove_file(&abs);
}
None => {}
}
}
println!("\nโ
Cleaned {} entr{}.", targets.len(),
if targets.len() == 1 { "y" } else { "ies" });
Ok(())
}