Repository: tbillington/kondo (language: rust)

Issue:
Only delete old project (skip active project)
Hi.

When I use `kondo` in my directory project, it deletes all the build artifacts in all directories. Including the active one.
[woubuc/sweep: Reduce the disk usage of your projects by removing dependencies & builds](https://github.com/woubuc/sweep) on the other hand, only delete inactive directory: old projects that haven't been changed in more than a month.

> Sweep (swp) finds old projects that haven't been changed in more than a month.

I thought I was missing some options. But I don't find any clue with `kondo --help`.




Discussion / hints:

> When I use kondo in my directory project, it deletes all the build artifacts in all directories. Including the active one.

Hey @azzamsa , just to be sure, kondo should only delete from projects that you choose to "clean", if it's deleting from projects without your express consent please file an issue for that!

In regards to "active" projects, it's hard to say what is active, that will depend on the user. I was actually working on a way to surface the projects last modified date [yesterday](https://github.com/tbillington/kondo/pull/63/files#diff-c795841a6b51dae267159004071767f20f46ae229d6591acac29578849c7d007R119). It's my hope that will provide the user enough information to base their decision.

As for automatically cleaning projects older than a certain date, I honestly hadn't thought of that, but it's something I may consider adding. Thank you for letting me know about that!
> As for automatically cleaning projects older than a certain date, I honestly hadn't thought of that, but it's something I may consider adding. Thank you for letting me know about that!

Yes, I think it doesn't need to be configurable as args. `last modified file > one month ` is enough to say the project is inactive.
Or we could create a poll in discussion what will be the default (hardcoded): one month or one week.
I've added a printout of the last date a file in the project was modified, which was the first step towards something like this

```
/Users/choc/code/sp-liveview Cargo project (2 days ago)
  └─ target (615.6MiB)
  delete above artifact directories? ([y]es, [n]o, [a]ll, [q]uit): n

/Users/choc/code/rust-au Cargo project (3 months ago)
  └─ target (2.2GiB)
  delete above artifact directories? ([y]es, [n]o, [a]ll, [q]uit): n

/Users/choc/code/rust-prod-api Cargo project (2 months ago)
  └─ target (1.7GiB)
  delete above artifact directories? ([y]es, [n]o, [a]ll, [q]uit): q
```
What about some kind of minimal filter syntax for passing to kondo.

For example `kondo projects-dir -f 1m` which would only show projects modified more than 1 month ago. Then you could do things like `kondo -f 2d` or `kondo -f 1y` etc.

You could combine that with the `--all` or `-a` flag to automatically clean the matching projects.
> You could combine that with the `--all` or `-a` flag to automatically clean the matching projects.

I think it should be the default, to avoid breaking current workflow.

Fix patch:
diff --git a/kondo/src/main.rs b/kondo/src/main.rs
index f6cd1d0..bb3b08f 100644
--- a/kondo/src/main.rs
+++ b/kondo/src/main.rs
@@ -3,7 +3,9 @@ use structopt::StructOpt;
 use std::{
     env::current_dir,
     error::Error,
+    fmt,
     io::{stdin, stdout, BufRead, Write},
+    num::ParseIntError,
     path::PathBuf,
 };
 
@@ -34,6 +36,10 @@ struct Opt {
     /// Restrict directory traversal to the root filesystem
     #[structopt(short, long)]
     same_filesystem: bool,
+
+    /// Only directories with a file last modified n units of time ago will be looked at. Ex: 20d. Units are m: minutes, h: hours, d: days, w: weeks, M: months and y: years.
+    #[structopt(short, long, parse(try_from_str = parse_age_filter), default_value = "0d")]
+    older: u64,
 }
 
 fn prepare_directories(dirs: Vec<PathBuf>) -> Result<Vec<PathBuf>, Box<dyn Error>> {
@@ -68,6 +74,57 @@ fn prepare_directories(dirs: Vec<PathBuf>) -> Result<Vec<PathBuf>, Box<dyn Error
     Ok(dirs)
 }
 
+#[derive(Debug)]
+pub enum ParseAgeFilterError {
+    ParseIntError(ParseIntError),
+    InvalidUnit,
+}
+
+impl fmt::Display for ParseAgeFilterError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            ParseAgeFilterError::ParseIntError(e) => e.fmt(f),
+            ParseAgeFilterError::InvalidUnit => {
+                "invalid age unit, must be one of m, h, d, w, M, y".fmt(f)
+            }
+        }
+    }
+}
+
+impl From<ParseIntError> for ParseAgeFilterError {
+    fn from(e: ParseIntError) -> Self {
+        Self::ParseIntError(e)
+    }
+}
+
+pub fn parse_age_filter(age_filter: &str) -> Result<u64, ParseAgeFilterError> {
+    const MINUTE: u64 = 60;
+    const HOUR: u64 = MINUTE * 60;
+    const DAY: u64 = HOUR * 24;
+    const WEEK: u64 = DAY * 7;
+    const MONTH: u64 = WEEK * 4;
+    const YEAR: u64 = MONTH * 12;
+
+    let (digit_end, unit) = age_filter
+        .char_indices()
+        .last()
+        .ok_or(ParseAgeFilterError::InvalidUnit)?;
+
+    let multiplier = match unit {
+        'm' => MINUTE,
+        'h' => HOUR,
+        'd' => DAY,
+        'w' => WEEK,
+        'M' => MONTH,
+        'y' => YEAR,
+        _ => return Err(ParseAgeFilterError::InvalidUnit),
+    };
+
+    let count = age_filter[..digit_end].parse::<u64>()?;
+    let seconds = count * multiplier;
+    Ok(seconds)
+}
+
 fn main() -> Result<(), Box<dyn Error>> {
     let opt = Opt::from_args();
 
@@ -125,16 +182,22 @@ fn main() -> Result<(), Box<dyn Error>> {
             continue;
         }
 
-        if opt.quiet == 0 {
-            let mut last_modified_str = String::new();
+        let mut last_modified_str = String::new();
+        let mut last_modified_int: u64 = 0;
 
-            if let Ok(last_modified) = project.last_modified(&scan_options) {
-                if let Ok(elapsed) = last_modified.elapsed() {
-                    let elapsed = print_elapsed(elapsed.as_secs());
-                    last_modified_str = format!("({elapsed})");
-                }
+        if let Ok(last_modified) = project.last_modified(&scan_options) {
+            if let Ok(elapsed) = last_modified.elapsed() {
+                last_modified_int = elapsed.as_secs();
+                let elapsed = print_elapsed(last_modified_int);
+                last_modified_str = format!("({elapsed})");
             }
+        }
+
+        if last_modified_int < opt.older {
+            continue;
+        }
 
+        if opt.quiet == 0 {
             writeln!(
                 &mut write_handle,
                 "{} {} project {last_modified_str}{}",


Test patch: