use std::io::prelude::*;
use anyhow::Result;
use clap::Parser;
use rayon::prelude::*;
use yansi::Paint;
use crate::{cmd, errors, model, model::IndexSet, path};
#[derive(Parser, Clone, Debug)]
#[command(author, about, long_about)]
pub struct PruneOptions {
#[arg(
long = "jobs",
short = 'j',
num_args = 0..=1,
default_value_t = 0,
default_missing_value = "0",
value_name = "JOBS",
)]
num_jobs: usize,
#[arg(long, short = 'd', default_value_t = -1)]
max_depth: isize,
#[arg(long, default_value_t = -1)]
min_depth: isize,
#[arg(long, default_value_t = -1)]
exact_depth: isize,
#[arg(long)]
no_prompt: bool,
#[arg(long = "rm")]
remove: bool,
paths: Vec<String>,
}
pub fn main(app_context: &model::ApplicationContext, options: &mut PruneOptions) -> Result<()> {
let config = app_context.get_root_config_mut();
if options.num_jobs < 3 {
options.num_jobs = 3;
}
if options.max_depth >= 0 && options.max_depth < options.min_depth {
println!("error: --max-depth cannot be less than --min-depth");
std::process::exit(errors::EX_USAGE);
}
if options.exact_depth >= 0 {
if options.min_depth >= 0 || options.max_depth >= 0 {
println!("error: --exact-depth cannot be used with --min-depth and --max-depth");
std::process::exit(errors::EX_USAGE);
}
options.min_depth = options.exact_depth;
options.max_depth = options.exact_depth;
}
let exit_status = prune(config, options, &options.paths)?;
errors::exit_status_into_result(exit_status)
}
enum PathBufMessage {
Path(std::path::PathBuf),
Finished,
}
struct TraverseFilesystem<'a> {
min_depth: isize,
max_depth: isize,
send_repo_path: crossbeam::channel::Sender<PathBufMessage>,
root_path: std::path::PathBuf,
path_filters: &'a Vec<std::path::PathBuf>,
configured_tree_paths: &'a IndexSet<std::path::PathBuf>,
}
impl TraverseFilesystem<'_> {
fn traverse(&self) {
self.traverse_toplevel(&self.root_path).unwrap_or(());
self.send_repo_path
.send(PathBufMessage::Finished)
.unwrap_or(());
}
fn traverse_toplevel(&self, pathbuf: &std::path::PathBuf) -> std::io::Result<()> {
let current_depth: isize = 0;
let entries: Vec<_> = std::fs::read_dir(pathbuf)?.collect();
entries.par_iter().for_each(|entry_result| {
if let Ok(entry) = entry_result {
let path = entry.path();
if let Some(path_canon) = self.validate_entry_for_traversal(&path) {
self.traverse_subdir(&path_canon, current_depth)
.unwrap_or(());
}
}
});
Ok(())
}
fn traverse_subdir(
&self,
pathbuf: &std::path::PathBuf,
current_depth: isize,
) -> std::io::Result<()> {
let mut git_dir = pathbuf.to_path_buf();
git_dir.push(".git");
if git_dir.exists() {
if is_within_bounds(current_depth, self.min_depth, self.max_depth) {
self.send_repo_path
.send(PathBufMessage::Path(pathbuf.to_path_buf()))
.unwrap_or(());
}
return Ok(());
}
if let Some(extension) = pathbuf.extension() {
if extension == "git" {
if is_within_bounds(current_depth, self.min_depth, self.max_depth) {
self.send_repo_path
.send(PathBufMessage::Path(pathbuf.to_path_buf()))
.unwrap_or(());
}
return Ok(());
}
}
let entries: Vec<_> = std::fs::read_dir(pathbuf)?.collect();
entries.par_iter().for_each(|entry_result| {
if let Ok(entry) = entry_result {
let path = entry.path();
if let Some(path_canon) = self.validate_entry_for_traversal(&path) {
if is_within_max_bounds(current_depth, self.max_depth) {
self.traverse_subdir(&path_canon, current_depth + 1)
.unwrap_or(());
}
}
}
});
Ok(())
}
fn validate_entry_for_traversal(&self, path: &std::path::Path) -> Option<std::path::PathBuf> {
if path.is_dir()
&& !path.is_symlink()
&& match path.file_name() {
Some(basename) => basename != ".git",
None => false,
}
{
if let Ok(path_canon) = path::canonicalize(path) {
if !self.configured_tree_paths.contains(&path_canon) && !self.is_filtered_path(path)
{
return Some(path_canon);
}
}
}
None
}
fn is_filtered_path(&self, path: &std::path::Path) -> bool {
if self.path_filters.is_empty() {
return false;
}
for path_filter in self.path_filters {
if path.starts_with(path_filter) || path_filter.starts_with(path) {
return false;
}
}
true
}
}
fn is_within_bounds(value: isize, min_depth: isize, max_depth: isize) -> bool {
value >= min_depth && is_within_max_bounds(value, max_depth)
}
fn is_within_max_bounds(value: isize, max_depth: isize) -> bool {
max_depth == -1 || value <= max_depth
}
struct RemovePaths {
recv_remove_path: crossbeam::channel::Receiver<PathBufMessage>,
send_finished_path: crossbeam::channel::Sender<PathBufMessage>,
dry_run: bool,
}
impl RemovePaths {
fn remove_paths(&self, remove_scope: &rayon::ScopeFifo<'_>) {
loop {
match self.recv_remove_path.recv() {
Ok(PathBufMessage::Path(pathbuf)) => {
if !self.dry_run {
let pathbuf = pathbuf.to_path_buf();
remove_scope.spawn_fifo(move |_| {
rm_rf::ensure_removed(&pathbuf).unwrap_or(());
let mut parent_option = pathbuf.parent();
while let Some(parent_pathbuf) = parent_option {
if !parent_pathbuf.exists() {
break;
}
if std::fs::remove_dir(parent_pathbuf).is_err() {
break;
}
parent_option = parent_pathbuf.parent();
}
});
}
self.send_finished_path
.send(PathBufMessage::Path(pathbuf))
.unwrap_or(());
}
Ok(PathBufMessage::Finished) | Err(_) => {
self.send_finished_path
.send(PathBufMessage::Finished)
.unwrap_or(());
return;
}
}
}
}
}
enum PromptResponse {
All, Delete, Skip, Quit, }
fn prompt_for_deletion(pathbuf: &dyn AsRef<std::path::Path>) -> PromptResponse {
let stdin = std::io::stdin();
let mut stdout = std::io::stdout();
let answer;
loop {
let path_string = pathbuf.as_ref().to_string_lossy();
let path_basename = match pathbuf.as_ref().file_name() {
Some(stem) => stem.to_string_lossy(),
None => continue,
};
println!();
println!("{} {}", "#".cyan(), path_string.blue().bold());
println!(
"{}",
format!("Delete the \"{path_basename}\" repository?").yellow()
);
println!(
"{}: \"{}\" deletes \"{}\" and {} subsequent repositories!",
"WARNING".red().bold(),
"all".yellow(),
path_basename,
"ALL".red().bold(),
);
print!(
"Choices: {}, {}, {}, {} [{},{},{},{}]? ",
"yes".blue(),
"no".blue(),
"all".yellow(),
"quit".green(),
"y".blue(),
"n".blue(),
"all".yellow(),
"q".green(),
);
stdout.flush().unwrap_or(());
let mut buffer = String::new();
if stdin.read_line(&mut buffer).is_ok() {
match buffer.trim().to_lowercase().as_str() {
"all" => {
answer = PromptResponse::All;
println!();
break;
}
"y" | "yes" => {
answer = PromptResponse::Delete;
break;
}
"n" | "no" | "s" | "skip" => {
answer = PromptResponse::Skip;
break;
}
"q" | "quit" => {
answer = PromptResponse::Quit;
println!();
break;
}
_ => {
println!();
}
}
}
}
answer
}
struct PromptUser {
recv_repo_path: crossbeam::channel::Receiver<PathBufMessage>,
send_remove_path: crossbeam::channel::Sender<PathBufMessage>,
recv_finished_path: crossbeam::channel::Receiver<PathBufMessage>,
no_prompt: bool,
quit: bool,
}
impl PromptUser {
fn prompt_for_deletion(&mut self) {
loop {
match self.recv_repo_path.recv() {
Ok(PathBufMessage::Path(pathbuf)) => {
if !self.quit {
self.prompt_pathbuf_for_deletion(&pathbuf);
}
}
Ok(PathBufMessage::Finished) | Err(_) => {
self.send_remove_path
.send(PathBufMessage::Finished)
.unwrap_or(());
break;
}
}
if !self.no_prompt {
self.display_finished_nonblocking();
}
}
self.display_finished_blocking();
}
fn prompt_pathbuf_for_deletion(&mut self, path: &dyn AsRef<std::path::Path>) {
if self.no_prompt {
self.send_remove_path
.send(PathBufMessage::Path(path.as_ref().to_path_buf()))
.unwrap_or(());
return;
}
match prompt_for_deletion(&path) {
PromptResponse::All => {
self.no_prompt = true;
self.send_remove_path
.send(PathBufMessage::Path(path.as_ref().to_path_buf()))
.unwrap_or(());
}
PromptResponse::Delete => {
self.send_remove_path
.send(PathBufMessage::Path(path.as_ref().to_path_buf()))
.unwrap_or(());
}
PromptResponse::Skip => (),
PromptResponse::Quit => {
self.quit = true;
self.send_remove_path
.send(PathBufMessage::Finished)
.unwrap_or(());
}
}
}
fn display_finished_nonblocking(&self) {
let mut printed = false;
while let Ok(PathBufMessage::Path(pathbuf)) = self.recv_finished_path.try_recv() {
if !printed {
printed = true;
println!();
}
print_deleted_pathbuf(&pathbuf);
}
}
fn display_finished_blocking(&self) {
while let Ok(PathBufMessage::Path(pathbuf)) = self.recv_finished_path.recv() {
print_deleted_pathbuf(&pathbuf);
}
}
}
fn print_deleted_pathbuf(pathbuf: &std::path::Path) {
println!(
"{} {}: {}",
"#".cyan(),
"Deleted".green(),
pathbuf.to_string_lossy().blue().bold(),
);
}
pub fn prune(
config: &model::Configuration,
options: &PruneOptions,
paths: &[String],
) -> Result<i32> {
let exit_status: i32 = 0;
if !options.remove {
let msg = "NOTE: Safe mode enabled. Repositories will not be deleted.";
println!("{}", msg.green());
let msg = "Use '--rm' to enable deletion.";
println!("{}", msg.green());
}
cmd::initialize_threads(options.num_jobs)?;
let (send_repo_path, recv_repo_path) = crossbeam::channel::unbounded();
let (send_remove_path, recv_remove_path) = crossbeam::channel::unbounded();
let (send_finished_path, recv_finished_path) = crossbeam::channel::unbounded();
let mut configured_tree_paths = IndexSet::new();
{
for tree in config.trees.values() {
if let Some(pathbuf) = tree.canonical_pathbuf() {
configured_tree_paths.insert(pathbuf);
}
}
}
let root_path = config.root_path.to_path_buf();
let path_filters: Vec<std::path::PathBuf> = paths
.iter()
.map(|value| config.relative_pathbuf(value))
.collect();
rayon::scope_fifo(|scope| {
scope.spawn_fifo(|remove_scope| {
let remove_paths = RemovePaths {
recv_remove_path,
send_finished_path,
dry_run: !options.remove,
};
remove_paths.remove_paths(remove_scope);
});
scope.spawn_fifo(|_| {
let quit = false;
let mut prompt_user = PromptUser {
recv_repo_path,
send_remove_path,
recv_finished_path,
no_prompt: options.no_prompt,
quit,
};
prompt_user.prompt_for_deletion();
});
scope.spawn_fifo(|_| {
let traverse_filesystem = TraverseFilesystem {
min_depth: options.min_depth,
max_depth: options.max_depth,
send_repo_path,
root_path,
path_filters: &path_filters,
configured_tree_paths: &configured_tree_paths,
};
traverse_filesystem.traverse();
});
});
Ok(exit_status)
}