Repository: tbillington/kondo (language: rust)

Issue:
Add clean subcommand



Discussion / hints:

Fix patch:
diff --git a/kondo-lib/src/lib.rs b/kondo-lib/src/lib.rs
index 0602347..1affba3 100644
--- a/kondo-lib/src/lib.rs
+++ b/kondo-lib/src/lib.rs
@@ -155,6 +155,19 @@ impl Project {
             ProjectType::Unreal => PROJECT_UNREAL_NAME,
         }
     }
+
+    /// Deletes the project's artifact directories and their contents
+    pub fn clean(&self) {
+        for artifact_dir in self
+            .artifact_dirs()
+            .map(|ad| self.path.join(ad))
+            .filter(|ad| ad.exists())
+        {
+            if let Err(e) = fs::remove_dir_all(&artifact_dir) {
+                eprintln!("error removing directory {:?}: {:?}", artifact_dir, e);
+            }
+        }
+    }
 }
 
 fn is_hidden(entry: &walkdir::DirEntry) -> bool {
@@ -227,7 +240,7 @@ pub fn scan<P: AsRef<path::Path>>(p: &P) -> impl Iterator<Item = Project> {
     }
 }
 
-fn dir_size(path: &path::Path) -> u64 {
+pub fn dir_size(path: &path::Path) -> u64 {
     walkdir::WalkDir::new(path)
         .follow_links(SYMLINK_FOLLOW)
         .into_iter()
@@ -296,3 +309,11 @@ pub fn clean(project_path: &str) -> Result<(), Box<dyn error::Error>> {
 
     Ok(())
 }
+
+pub fn path_canonicalise(base: &path::PathBuf, tail: path::PathBuf) -> path::PathBuf {
+    if tail.is_absolute() {
+        tail
+    } else {
+        base.join(tail).canonicalize().expect("Unable to canonicalize!")
+    }
+}
\ No newline at end of file
diff --git a/kondo/src/main.rs b/kondo/src/main.rs
index 165d95d..559df21 100644
--- a/kondo/src/main.rs
+++ b/kondo/src/main.rs
@@ -2,7 +2,7 @@ use structopt::StructOpt;
 
 use std::{env, io, path, process};
 
-use kondo_lib::{pretty_size, scan, Project};
+use kondo_lib::{dir_size, path_canonicalise, pretty_size, scan, Project};
 
 #[derive(StructOpt, Debug)]
 #[structopt(name = "kondo")]
@@ -19,11 +19,29 @@ struct Opt {
     #[structopt(short, long)]
     command: Option<String>,
 
+    /// Run subcommand
+    #[structopt(subcommand)]
+    subcommand: Option<Command>,
+
     /// The directories to examine
     #[structopt(name = "DIRS", parse(from_os_str))]
     dirs: Vec<std::path::PathBuf>,
 }
 
+#[derive(StructOpt, Debug)]
+enum Command {
+    /// Clean projects in specified paths
+    Clean {
+        /// Show projects that will be cleaned without actually cleaning them
+        #[structopt(long)]
+        dry_run: bool,
+
+        /// The directories to examine
+        #[structopt(name = "DIRS", parse(from_os_str))]
+        dirs: Vec<std::path::PathBuf>,
+    },
+}
+
 fn main() -> Result<(), Box<dyn std::error::Error>> {
     use io::Write;
     let opt = Opt::from_args();
@@ -45,6 +63,53 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
         }
     };
 
+    let stdout = io::stdout();
+    let mut write_handle = stdout.lock();
+
+    match opt.subcommand {
+        Some(Command::Clean { dry_run, dirs }) => {
+            let cd = env::current_dir()?;
+            let dirs = if dirs.is_empty() {
+                vec![cd]
+            } else {
+                dirs.into_iter()
+                    .map(|d| path_canonicalise(&cd, d))
+                    .collect()
+            };
+            let project_dirs = dirs.iter().flat_map(scan);
+            let mut total = 0;
+            let mut artifact_dirs = Vec::with_capacity(10); // pre-allocated vec to reduce allocations
+            for project in project_dirs {
+                writeln!(&mut write_handle, "{}", project.name())?;
+                artifact_dirs.extend(
+                    project
+                        .artifact_dirs()
+                        .map(|d| (d.to_string(), project.path.join(d))),
+                );
+                for (i, (name, path)) in artifact_dirs.iter().enumerate() {
+                    write!(
+                        &mut write_handle,
+                        "  {}─ {}",
+                        if i == dirs.len() - 1 { "└" } else { "├" },
+                        name,
+                    )?;
+                    let size = dir_size(path);
+                    total += size;
+                    write!(&mut write_handle, " ({})", pretty_size(size))?;
+                    if !dry_run {
+                        project.clean();
+                        write!(&mut write_handle, " ✔")?;
+                    }
+                    writeln!(&mut write_handle)?;
+                }
+                artifact_dirs.clear();
+            }
+            writeln!(&mut write_handle, "Disk saving: {}", pretty_size(total))?;
+            return Ok(());
+        }
+        None => {}
+    }
+
     let project_dirs: Vec<Project> = dirs.iter().flat_map(scan).collect();
 
     if let Some(command) = opt.command {
@@ -60,9 +125,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
         return Ok(());
     };
 
-    let stdout = io::stdout();
-    let mut write_handle = stdout.lock();
-
     if opt.artifact_dirs {
         for dir in project_dirs.iter() {
             let dir_base = &dir.path;


Test patch: