Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
28 changes: 25 additions & 3 deletions src/crufty/artifact_type.rs
Original file line number Diff line number Diff line change
@@ -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] {
Expand All @@ -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<String> {
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()
}
}
}
}
6 changes: 3 additions & 3 deletions src/crufty/estimator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ fn calculate_dir_size(path: &PathBuf) -> std::io::Result<u64> {
#[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() {
// 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
Expand All @@ -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
Expand Down
104 changes: 92 additions & 12 deletions src/crufty/fetcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use walkdir::WalkDir;
use super::types::ArtifactCandidate;

fn mk_global_set(
artifact_types: Vec<ArtifactType>,
artifact_types: &Vec<ArtifactType>,
) -> Result<GlobSet, globset::Error> {
let mut builder = GlobSetBuilder::new();
for art_type in artifact_types {
Expand All @@ -16,9 +16,30 @@ fn mk_global_set(
builder.build()
}

fn detect_artifact_type(
parent_path: &PathBuf,
artifact_types: &[ArtifactType],
) -> Option<ArtifactType> {
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<ArtifactType>,
artifact_types: &Vec<ArtifactType>,
) -> Vec<ArtifactCandidate> {
match mk_global_set(artifact_types) {
Err(_) => vec![],
Expand All @@ -31,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,
}
}
Expand Down Expand Up @@ -64,7 +94,11 @@ mod tests {
}

fn custom_project(pattern: &'static str) -> Vec<ArtifactType> {
vec![ArtifactType::Custom { pattern }]
vec![ArtifactType::Custom {
pattern,
name: "Custom",
files: &[],
}]
}

fn mk_rust_project<P: PathChild>(base: &P) {
Expand All @@ -79,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");

Expand All @@ -110,20 +146,24 @@ 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");
results.sort();

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
Expand All @@ -132,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();
}
}
34 changes: 31 additions & 3 deletions src/crufty/types.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
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),
Expand Down Expand Up @@ -30,15 +31,42 @@ impl fmt::Display for Size {
pub struct ArtifactCandidate {
pub path: PathBuf,
pub size: Size,
pub art_type: Option<ArtifactType>,
}

impl ArtifactCandidate {
pub struct ArtifactCandidateBuilder {
path: PathBuf,
size: Size,
art_type: Option<ArtifactType>,
}

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<ArtifactType>) -> 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 {
Expand Down
5 changes: 2 additions & 3 deletions src/crufty/ui.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use console::{style, StyledObject};
use console::{StyledObject, style};
use indicatif::{ProgressBar, ProgressStyle};

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()
Expand Down
14 changes: 11 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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("")?;
Expand All @@ -55,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)
))?;
}
Expand All @@ -83,7 +90,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() {
Expand Down