From acc4b047c568fcad156dd1e84ed5f26a55b81346 Mon Sep 17 00:00:00 2001 From: EncodePanda Date: Mon, 2 Jun 2025 16:56:30 +0200 Subject: [PATCH 1/7] Add artifact_type_name and recognized_files functions --- src/crufty/artifact_type.rs | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/crufty/artifact_type.rs b/src/crufty/artifact_type.rs index 78ed9cb..6a632a8 100644 --- a/src/crufty/artifact_type.rs +++ b/src/crufty/artifact_type.rs @@ -1,9 +1,13 @@ -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] #[allow(dead_code)] pub enum ArtifactType { Rust, Scala, - Custom { pattern: &'static str }, + Custom { + pattern: &'static str, + name: &'static str, + files: &'static [&'static str], + }, } pub fn builtin() -> [ArtifactType; 2] { @@ -15,7 +19,25 @@ impl ArtifactType { match self { ArtifactType::Rust => "**/target", ArtifactType::Scala => "**/target", - ArtifactType::Custom { pattern } => pattern, + ArtifactType::Custom { pattern, .. } => pattern, + } + } + + pub fn artifact_type_name(&self) -> &'static str { + match self { + ArtifactType::Rust => "Rust", + ArtifactType::Scala => "Scala", + ArtifactType::Custom { name, .. } => name, + } + } + + pub fn recognized_files(&self) -> Vec { + match self { + ArtifactType::Rust => vec!["Cargo.toml".to_string()], + ArtifactType::Scala => vec!["build.sbt".to_string()], + ArtifactType::Custom { files, .. } => { + files.iter().map(|s| s.to_string()).collect() + } } } } From 09851a77c39f3b5e3a8da05d68b5802d81356984 Mon Sep 17 00:00:00 2001 From: EncodePanda Date: Mon, 2 Jun 2025 17:50:23 +0200 Subject: [PATCH 2/7] Add function that detects ArtifacType based on the files of artifact parent --- src/crufty/fetcher.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/crufty/fetcher.rs b/src/crufty/fetcher.rs index b7b9ecb..a2b3b93 100644 --- a/src/crufty/fetcher.rs +++ b/src/crufty/fetcher.rs @@ -16,6 +16,27 @@ fn mk_global_set( builder.build() } +fn detect_artifact_type( + parent_path: &PathBuf, + artifact_types: &[ArtifactType], +) -> Option { + match artifact_types { + [] => None, + [head, tail @ ..] => { + let recognized_files = head.recognized_files(); + let all_files_present = recognized_files.iter().all(|file| { + let file_path = parent_path.join(file); + file_path.exists() + }); + + match all_files_present { + true => Some(head.clone()), + false => detect_artifact_type(parent_path, tail), + } + } + } +} + pub fn fetch_artifacts( root_path: &PathBuf, artifact_types: Vec, From 501ef88ea9b03a993d2cc9e2a86faa0b64173fd2 Mon Sep 17 00:00:00 2001 From: EncodePanda Date: Mon, 2 Jun 2025 18:05:36 +0200 Subject: [PATCH 3/7] Add ArtifactCandidate builder, use type detection in the fetcher --- src/crufty/estimator.rs | 4 +- src/crufty/fetcher.rs | 83 +++++++++++++++++++++++++++++++++++------ src/crufty/types.rs | 34 +++++++++++++++-- src/main.rs | 6 ++- 4 files changed, 108 insertions(+), 19 deletions(-) diff --git a/src/crufty/estimator.rs b/src/crufty/estimator.rs index 1f741dd..10491a2 100644 --- a/src/crufty/estimator.rs +++ b/src/crufty/estimator.rs @@ -80,7 +80,7 @@ mod tests { // given let temp = TempDir::new().unwrap(); let path = temp.path().to_path_buf(); - let mut artifact = ArtifactCandidate::new(path); + let mut artifact = ArtifactCandidate::builder(path).build(); // when let artifact = estimate(&mut artifact); // then @@ -96,7 +96,7 @@ mod tests { let file = temp.child("test.txt"); file.write_str("Hello, world!").unwrap(); let path = temp.path().to_path_buf(); - let mut artifact = ArtifactCandidate::new(path); + let mut artifact = ArtifactCandidate::builder(path).build(); // when let artifact = estimate(&mut artifact); // then diff --git a/src/crufty/fetcher.rs b/src/crufty/fetcher.rs index a2b3b93..7496f6d 100644 --- a/src/crufty/fetcher.rs +++ b/src/crufty/fetcher.rs @@ -7,7 +7,7 @@ use walkdir::WalkDir; use super::types::ArtifactCandidate; fn mk_global_set( - artifact_types: Vec, + artifact_types: &Vec, ) -> Result { let mut builder = GlobSetBuilder::new(); for art_type in artifact_types { @@ -39,7 +39,7 @@ fn detect_artifact_type( pub fn fetch_artifacts( root_path: &PathBuf, - artifact_types: Vec, + artifact_types: &Vec, ) -> Vec { match mk_global_set(artifact_types) { Err(_) => vec![], @@ -52,7 +52,16 @@ pub fn fetch_artifacts( let path = entry.into_path(); let rel_path = path.strip_prefix(root_path).ok()?; match globset.is_match(&rel_path) { - true => Some(ArtifactCandidate::new(path)), + true => { + let parent_path = path.parent()?; + let art_type = detect_artifact_type( + &parent_path.to_path_buf(), + &artifact_types, + ); + let candidate = + ArtifactCandidate::builder(path).art_type(art_type).build(); + Some(candidate) + } false => None, } } @@ -85,7 +94,11 @@ mod tests { } fn custom_project(pattern: &'static str) -> Vec { - vec![ArtifactType::Custom { pattern }] + vec![ArtifactType::Custom { + pattern, + name: "Custom", + files: &[], + }] } fn mk_rust_project(base: &P) { @@ -100,18 +113,20 @@ mod tests { mk_rust_project(&temp); // when we search for Rust artifacts - let results = fetch_artifacts(&temp.to_path_buf(), only_rust_projects()); + let results = fetch_artifacts(&temp.to_path_buf(), &only_rust_projects()); // then assert_eq!(results.len(), 1, "Expected exactly one artifact"); let expected_path = temp.child("target").path().to_path_buf(); - let expected = ArtifactCandidate::new(expected_path); + let expected = ArtifactCandidate::builder(expected_path) + .art_type(Some(ArtifactType::Rust)) + .build(); assert_eq!(&results[0], &expected); // when we search for projects other than Rust let results = - fetch_artifacts(&temp.to_path_buf(), custom_project("**/bla")); + fetch_artifacts(&temp.to_path_buf(), &custom_project("**/bla")); // then assert_eq!(results.len(), 0, "Expected zero artifacts"); @@ -131,7 +146,7 @@ mod tests { // when we search for Rust artifacts let mut results = - fetch_artifacts(&temp.to_path_buf(), only_rust_projects()); + fetch_artifacts(&temp.to_path_buf(), &only_rust_projects()); // then assert_eq!(results.len(), 3, "Expected exactly three artifacts"); @@ -139,12 +154,16 @@ mod tests { let expected_path_1 = temp.child("project1").child("target").path().to_path_buf(); - let expected_1 = ArtifactCandidate::new(expected_path_1); + let expected_1 = ArtifactCandidate::builder(expected_path_1) + .art_type(Some(ArtifactType::Rust)) + .build(); assert_eq!(&results[0], &expected_1); let expected_path_2 = temp.child("project2").child("target").path().to_path_buf(); - let expected_2 = ArtifactCandidate::new(expected_path_2); + let expected_2 = ArtifactCandidate::builder(expected_path_2) + .art_type(Some(ArtifactType::Rust)) + .build(); assert_eq!(&results[1], &expected_2); let expected_path_3 = temp @@ -153,15 +172,55 @@ mod tests { .child("target") .path() .to_path_buf(); - let expected_3 = ArtifactCandidate::new(expected_path_3); + let expected_3 = ArtifactCandidate::builder(expected_path_3) + .art_type(Some(ArtifactType::Rust)) + .build(); assert_eq!(&results[2], &expected_3); // when we search for projects other than Rust let results = - fetch_artifacts(&temp.to_path_buf(), custom_project("**/bla")); + fetch_artifacts(&temp.to_path_buf(), &custom_project("**/bla")); // then assert_eq!(results.len(), 0, "Expected zero artifacts"); temp.close().unwrap(); } + + #[test] + fn test_custom_artifact_type_equivalent_to_rust() { + // given there is a single rust project in the folder + let temp = TempDir::new().unwrap(); + mk_rust_project(&temp); + + // when we search for Rust artifacts using built-in type + let rust_results = + fetch_artifacts(&temp.to_path_buf(), &only_rust_projects()); + + // when we search using Custom type with same pattern and files as Rust + let custom_rust_type = ArtifactType::Custom { + pattern: "**/target", + name: "CustomRust", + files: &["Cargo.toml"], + }; + let custom_results = + fetch_artifacts(&temp.to_path_buf(), &vec![custom_rust_type]); + + // then both should find the same artifact + assert_eq!(rust_results.len(), 1); + assert_eq!(custom_results.len(), 1); + assert_eq!(rust_results[0].path, custom_results[0].path); + + // but the artifact types should be different + assert_eq!(rust_results[0].art_type, Some(ArtifactType::Rust)); + assert_eq!( + custom_results[0] + .art_type + .as_ref() + .unwrap() + .artifact_type_name(), + "CustomRust" + ); + + temp.close().unwrap(); + } } diff --git a/src/crufty/types.rs b/src/crufty/types.rs index 76da3df..b1728e7 100644 --- a/src/crufty/types.rs +++ b/src/crufty/types.rs @@ -1,3 +1,4 @@ +use crate::crufty::artifact_type::ArtifactType; use std::fmt; use std::path::PathBuf; @@ -26,19 +27,46 @@ impl fmt::Display for Size { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct ArtifactCandidate { pub path: PathBuf, pub size: Size, + pub art_type: Option, } -impl ArtifactCandidate { +pub struct ArtifactCandidateBuilder { + path: PathBuf, + size: Size, + art_type: Option, +} + +impl ArtifactCandidateBuilder { pub fn new(path: PathBuf) -> Self { - ArtifactCandidate { + ArtifactCandidateBuilder { path, size: Size::UnknownSize, + art_type: None, } } + + pub fn art_type(mut self, art_type: Option) -> Self { + self.art_type = art_type; + self + } + + pub fn build(self) -> ArtifactCandidate { + ArtifactCandidate { + path: self.path, + size: self.size, + art_type: self.art_type, + } + } +} + +impl ArtifactCandidate { + pub fn builder(path: PathBuf) -> ArtifactCandidateBuilder { + ArtifactCandidateBuilder::new(path) + } } impl Ord for ArtifactCandidate { diff --git a/src/main.rs b/src/main.rs index 3770293..37df56e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,8 @@ fn scan() -> io::Result<()> { .write_line(&format!("[+] Scanning: {}", style(path.display()).bold()))?; let spinner = ui::create_spinner("collecting artifacts"); - let mut artifacts = fetch_artifacts(&path, artifact_type::builtin().to_vec()); + let mut artifacts = + fetch_artifacts(&path, &artifact_type::builtin().to_vec()); spinner.finish_and_clear(); term.write_line("")?; @@ -83,7 +84,8 @@ fn clean() -> io::Result<()> { let path = env::current_dir()?; let spinner = ui::create_spinner("collecting artifacts"); - let mut artifacts = fetch_artifacts(&path, artifact_type::builtin().to_vec()); + let mut artifacts = + fetch_artifacts(&path, &artifact_type::builtin().to_vec()); spinner.finish_and_clear(); if artifacts.is_empty() { From 1ac19754807579981a53c320caef914ce7087bfd Mon Sep 17 00:00:00 2001 From: EncodePanda Date: Mon, 2 Jun 2025 18:06:30 +0200 Subject: [PATCH 4/7] Add types information when displaying artifacts in main --- src/crufty/ui.rs | 3 +-- src/main.rs | 8 +++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/crufty/ui.rs b/src/crufty/ui.rs index 5b418f4..ed4c6e8 100644 --- a/src/crufty/ui.rs +++ b/src/crufty/ui.rs @@ -6,8 +6,7 @@ use super::types::Size; /// Creates and returns a configured progress bar for use in the application. pub fn create_progress_bar(total: u64) -> ProgressBar { let pb = ProgressBar::new(total); - let template = - "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}"; + let template = "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}"; let bar_style = ProgressStyle::default_bar() .template(template) .unwrap() diff --git a/src/main.rs b/src/main.rs index 37df56e..489676a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,10 +56,16 @@ fn scan() -> io::Result<()> { let rel_path = artifact.path.strip_prefix(&path).unwrap_or(&artifact.path); + let artifact_type_name = match &artifact.art_type { + Some(art_type) => art_type.artifact_type_name(), + None => "Unknown", + }; + term.write_line(&format!( - "[{}] {:<36} {}", + "[{}] {:<36} {:<8} {}", i + 1, style(format!("./{}", rel_path.display())).bold(), + style(format!("({})", artifact_type_name)).dim(), style_size(&artifact.size) ))?; } From 3c6a6c5c359d020036387706ad3d3d7b4709501c Mon Sep 17 00:00:00 2001 From: EncodePanda Date: Mon, 2 Jun 2025 18:07:04 +0200 Subject: [PATCH 5/7] Missing formatter --- src/crufty/estimator.rs | 2 +- src/crufty/ui.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crufty/estimator.rs b/src/crufty/estimator.rs index 10491a2..a86a315 100644 --- a/src/crufty/estimator.rs +++ b/src/crufty/estimator.rs @@ -72,8 +72,8 @@ fn calculate_dir_size(path: &PathBuf) -> std::io::Result { #[cfg(test)] mod tests { use super::*; - use assert_fs::prelude::*; use assert_fs::TempDir; + use assert_fs::prelude::*; #[test] fn test_estimate_path_empty_dir() { diff --git a/src/crufty/ui.rs b/src/crufty/ui.rs index ed4c6e8..18f3952 100644 --- a/src/crufty/ui.rs +++ b/src/crufty/ui.rs @@ -1,4 +1,4 @@ -use console::{style, StyledObject}; +use console::{StyledObject, style}; use indicatif::{ProgressBar, ProgressStyle}; use super::types::Size; From 11360e911768ab1cd155f0be70be97f0512aa54d Mon Sep 17 00:00:00 2001 From: EncodePanda Date: Mon, 2 Jun 2025 18:07:18 +0200 Subject: [PATCH 6/7] Upgrade version to 0.2.0-rc1 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c086f0e..73780fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -190,7 +190,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crufty" -version = "0.1.0" +version = "0.2.0-rc1" dependencies = [ "assert_fs", "clap", diff --git a/Cargo.toml b/Cargo.toml index da46370..e261c76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "crufty" -version = "0.1.0" +version = "0.2.0-rc1" edition = "2024" license = "MIT" description = "A command-line tool that scans projects for large build artifacts and cleans them up safely" From b58f6e15620b47dc222ffa5ab60ea851773c747d Mon Sep 17 00:00:00 2001 From: EncodePanda Date: Mon, 2 Jun 2025 19:07:54 +0200 Subject: [PATCH 7/7] Remove Clone impl that were not used --- src/crufty/types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crufty/types.rs b/src/crufty/types.rs index b1728e7..541815f 100644 --- a/src/crufty/types.rs +++ b/src/crufty/types.rs @@ -2,7 +2,7 @@ use crate::crufty::artifact_type::ArtifactType; use std::fmt; use std::path::PathBuf; -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq)] pub enum Size { UnknownSize, KnownSize(u64), @@ -27,7 +27,7 @@ impl fmt::Display for Size { } } -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq)] pub struct ArtifactCandidate { pub path: PathBuf, pub size: Size,