1use anyhow::Result;
7use chrono::{DateTime, Local};
8use rayon::prelude::*;
9use std::fs;
10use std::time::SystemTime;
11
12use crate::config::filter::SortCriteria;
13use crate::config::{FilterOptions, SortOptions};
14use crate::project::{Project, ProjectType};
15use crate::utils::parse_size;
16
17pub fn filter_projects(
55 projects: Vec<Project>,
56 filter_opts: &FilterOptions,
57) -> Result<Vec<Project>> {
58 let keep_size_bytes = parse_size(&filter_opts.keep_size)?;
59 let keep_days = filter_opts.keep_days;
60
61 Ok(projects
62 .into_par_iter()
63 .filter(|project| meets_size_criteria(project, keep_size_bytes))
64 .filter(|project| meets_time_criteria(project, keep_days))
65 .collect())
66}
67
68const fn meets_size_criteria(project: &Project, min_size: u64) -> bool {
70 project.build_arts.size >= min_size
71}
72
73fn meets_time_criteria(project: &Project, keep_days: u32) -> bool {
75 if keep_days == 0 {
76 return true;
77 }
78
79 is_project_old_enough(project, keep_days)
80}
81
82fn is_project_old_enough(project: &Project, keep_days: u32) -> bool {
84 let Result::Ok(metadata) = fs::metadata(&project.build_arts.path) else {
85 return true; };
87
88 let Result::Ok(modified) = metadata.modified() else {
89 return true; };
91
92 let modified_time: DateTime<Local> = modified.into();
93 let cutoff_time = Local::now() - chrono::Duration::days(i64::from(keep_days));
94
95 modified_time <= cutoff_time
96}
97
98pub fn sort_projects(projects: &mut Vec<Project>, sort_opts: &SortOptions) {
131 let Some(criteria) = sort_opts.criteria else {
132 return;
133 };
134
135 match criteria {
136 SortCriteria::Size => {
137 projects.sort_by(|a, b| b.build_arts.size.cmp(&a.build_arts.size));
138 }
139 SortCriteria::Age => {
140 sort_by_age(projects);
141 }
142 SortCriteria::Name => {
143 projects.sort_by(|a, b| {
144 let name_a = a.name.as_deref().unwrap_or("");
145 let name_b = b.name.as_deref().unwrap_or("");
146 name_a.to_lowercase().cmp(&name_b.to_lowercase())
147 });
148 }
149 SortCriteria::Type => {
150 projects.sort_by(|a, b| type_order(&a.kind).cmp(&type_order(&b.kind)));
151 }
152 }
153
154 if sort_opts.reverse {
155 projects.reverse();
156 }
157}
158
159fn sort_by_age(projects: &mut Vec<Project>) {
164 let mut decorated: Vec<(Project, SystemTime)> = projects
165 .drain(..)
166 .map(|p| {
167 let mtime = fs::metadata(&p.build_arts.path)
168 .and_then(|m| m.modified())
169 .unwrap_or(SystemTime::UNIX_EPOCH);
170 (p, mtime)
171 })
172 .collect();
173
174 decorated.sort_by(|a, b| a.1.cmp(&b.1));
175
176 projects.extend(decorated.into_iter().map(|(p, _)| p));
177}
178
179const fn type_order(kind: &ProjectType) -> u8 {
184 match kind {
185 ProjectType::Cpp => 0,
186 ProjectType::Deno => 1,
187 ProjectType::DotNet => 2,
188 ProjectType::Elixir => 3,
189 ProjectType::Go => 4,
190 ProjectType::Java => 5,
191 ProjectType::Node => 6,
192 ProjectType::Python => 7,
193 ProjectType::Ruby => 8,
194 ProjectType::Rust => 9,
195 ProjectType::Swift => 10,
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::project::{BuildArtifacts, Project, ProjectType};
203 use std::path::PathBuf;
204
205 fn create_test_project(
207 kind: ProjectType,
208 root_path: &str,
209 build_path: &str,
210 size: u64,
211 name: Option<String>,
212 ) -> Project {
213 Project::new(
214 kind,
215 PathBuf::from(root_path),
216 BuildArtifacts {
217 path: PathBuf::from(build_path),
218 size,
219 },
220 name,
221 )
222 }
223
224 #[test]
225 fn test_meets_size_criteria() {
226 let project = create_test_project(
227 ProjectType::Rust,
228 "/test",
229 "/test/target",
230 1_000_000, Some("test".to_string()),
232 );
233
234 assert!(meets_size_criteria(&project, 500_000)); assert!(meets_size_criteria(&project, 1_000_000)); assert!(!meets_size_criteria(&project, 2_000_000)); }
238
239 #[test]
240 fn test_meets_time_criteria_disabled() {
241 let project = create_test_project(
242 ProjectType::Rust,
243 "/test",
244 "/test/target",
245 1_000_000,
246 Some("test".to_string()),
247 );
248
249 assert!(meets_time_criteria(&project, 0));
251 }
252
253 #[test]
256 fn test_sort_by_size_descending() {
257 let mut projects = vec![
258 create_test_project(
259 ProjectType::Rust,
260 "/a",
261 "/a/target",
262 100,
263 Some("small".into()),
264 ),
265 create_test_project(
266 ProjectType::Rust,
267 "/b",
268 "/b/target",
269 300,
270 Some("large".into()),
271 ),
272 create_test_project(
273 ProjectType::Rust,
274 "/c",
275 "/c/target",
276 200,
277 Some("medium".into()),
278 ),
279 ];
280
281 let sort_opts = SortOptions {
282 criteria: Some(SortCriteria::Size),
283 reverse: false,
284 };
285 sort_projects(&mut projects, &sort_opts);
286
287 assert_eq!(projects[0].build_arts.size, 300);
288 assert_eq!(projects[1].build_arts.size, 200);
289 assert_eq!(projects[2].build_arts.size, 100);
290 }
291
292 #[test]
293 fn test_sort_by_size_reversed() {
294 let mut projects = vec![
295 create_test_project(
296 ProjectType::Rust,
297 "/a",
298 "/a/target",
299 100,
300 Some("small".into()),
301 ),
302 create_test_project(
303 ProjectType::Rust,
304 "/b",
305 "/b/target",
306 300,
307 Some("large".into()),
308 ),
309 create_test_project(
310 ProjectType::Rust,
311 "/c",
312 "/c/target",
313 200,
314 Some("medium".into()),
315 ),
316 ];
317
318 let sort_opts = SortOptions {
319 criteria: Some(SortCriteria::Size),
320 reverse: true,
321 };
322 sort_projects(&mut projects, &sort_opts);
323
324 assert_eq!(projects[0].build_arts.size, 100);
325 assert_eq!(projects[1].build_arts.size, 200);
326 assert_eq!(projects[2].build_arts.size, 300);
327 }
328
329 #[test]
330 fn test_sort_by_name_alphabetical() {
331 let mut projects = vec![
332 create_test_project(
333 ProjectType::Rust,
334 "/c",
335 "/c/target",
336 100,
337 Some("charlie".into()),
338 ),
339 create_test_project(
340 ProjectType::Rust,
341 "/a",
342 "/a/target",
343 100,
344 Some("alpha".into()),
345 ),
346 create_test_project(
347 ProjectType::Rust,
348 "/b",
349 "/b/target",
350 100,
351 Some("bravo".into()),
352 ),
353 ];
354
355 let sort_opts = SortOptions {
356 criteria: Some(SortCriteria::Name),
357 reverse: false,
358 };
359 sort_projects(&mut projects, &sort_opts);
360
361 assert_eq!(projects[0].name.as_deref(), Some("alpha"));
362 assert_eq!(projects[1].name.as_deref(), Some("bravo"));
363 assert_eq!(projects[2].name.as_deref(), Some("charlie"));
364 }
365
366 #[test]
367 fn test_sort_by_name_case_insensitive() {
368 let mut projects = vec![
369 create_test_project(
370 ProjectType::Rust,
371 "/c",
372 "/c/target",
373 100,
374 Some("Charlie".into()),
375 ),
376 create_test_project(
377 ProjectType::Rust,
378 "/a",
379 "/a/target",
380 100,
381 Some("alpha".into()),
382 ),
383 create_test_project(
384 ProjectType::Rust,
385 "/b",
386 "/b/target",
387 100,
388 Some("Bravo".into()),
389 ),
390 ];
391
392 let sort_opts = SortOptions {
393 criteria: Some(SortCriteria::Name),
394 reverse: false,
395 };
396 sort_projects(&mut projects, &sort_opts);
397
398 assert_eq!(projects[0].name.as_deref(), Some("alpha"));
399 assert_eq!(projects[1].name.as_deref(), Some("Bravo"));
400 assert_eq!(projects[2].name.as_deref(), Some("Charlie"));
401 }
402
403 #[test]
404 fn test_sort_by_name_none_names_first() {
405 let mut projects = vec![
406 create_test_project(
407 ProjectType::Rust,
408 "/c",
409 "/c/target",
410 100,
411 Some("charlie".into()),
412 ),
413 create_test_project(ProjectType::Rust, "/a", "/a/target", 100, None),
414 create_test_project(
415 ProjectType::Rust,
416 "/b",
417 "/b/target",
418 100,
419 Some("alpha".into()),
420 ),
421 ];
422
423 let sort_opts = SortOptions {
424 criteria: Some(SortCriteria::Name),
425 reverse: false,
426 };
427 sort_projects(&mut projects, &sort_opts);
428
429 assert_eq!(projects[0].name.as_deref(), None);
431 assert_eq!(projects[1].name.as_deref(), Some("alpha"));
432 assert_eq!(projects[2].name.as_deref(), Some("charlie"));
433 }
434
435 #[test]
436 fn test_sort_by_type() {
437 let mut projects = vec![
438 create_test_project(
439 ProjectType::Rust,
440 "/r",
441 "/r/target",
442 100,
443 Some("rust-proj".into()),
444 ),
445 create_test_project(
446 ProjectType::Go,
447 "/g",
448 "/g/vendor",
449 100,
450 Some("go-proj".into()),
451 ),
452 create_test_project(
453 ProjectType::Python,
454 "/p",
455 "/p/__pycache__",
456 100,
457 Some("py-proj".into()),
458 ),
459 create_test_project(
460 ProjectType::Node,
461 "/n",
462 "/n/node_modules",
463 100,
464 Some("node-proj".into()),
465 ),
466 create_test_project(
467 ProjectType::Java,
468 "/j",
469 "/j/target",
470 100,
471 Some("java-proj".into()),
472 ),
473 create_test_project(
474 ProjectType::Cpp,
475 "/c",
476 "/c/build",
477 100,
478 Some("cpp-proj".into()),
479 ),
480 create_test_project(
481 ProjectType::Swift,
482 "/s",
483 "/s/.build",
484 100,
485 Some("swift-proj".into()),
486 ),
487 create_test_project(
488 ProjectType::DotNet,
489 "/d",
490 "/d/obj",
491 100,
492 Some("dotnet-proj".into()),
493 ),
494 create_test_project(
495 ProjectType::Ruby,
496 "/rb",
497 "/rb/vendor/bundle",
498 100,
499 Some("ruby-proj".into()),
500 ),
501 create_test_project(
502 ProjectType::Elixir,
503 "/ex",
504 "/ex/_build",
505 100,
506 Some("elixir-proj".into()),
507 ),
508 create_test_project(
509 ProjectType::Deno,
510 "/dn",
511 "/dn/vendor",
512 100,
513 Some("deno-proj".into()),
514 ),
515 ];
516
517 let sort_opts = SortOptions {
518 criteria: Some(SortCriteria::Type),
519 reverse: false,
520 };
521 sort_projects(&mut projects, &sort_opts);
522
523 assert_eq!(projects[0].kind, ProjectType::Cpp);
524 assert_eq!(projects[1].kind, ProjectType::Deno);
525 assert_eq!(projects[2].kind, ProjectType::DotNet);
526 assert_eq!(projects[3].kind, ProjectType::Elixir);
527 assert_eq!(projects[4].kind, ProjectType::Go);
528 assert_eq!(projects[5].kind, ProjectType::Java);
529 assert_eq!(projects[6].kind, ProjectType::Node);
530 assert_eq!(projects[7].kind, ProjectType::Python);
531 assert_eq!(projects[8].kind, ProjectType::Ruby);
532 assert_eq!(projects[9].kind, ProjectType::Rust);
533 assert_eq!(projects[10].kind, ProjectType::Swift);
534 }
535
536 #[test]
537 fn test_sort_by_type_reversed() {
538 let mut projects = vec![
539 create_test_project(
540 ProjectType::Go,
541 "/g",
542 "/g/vendor",
543 100,
544 Some("go-proj".into()),
545 ),
546 create_test_project(
547 ProjectType::Rust,
548 "/r",
549 "/r/target",
550 100,
551 Some("rust-proj".into()),
552 ),
553 create_test_project(
554 ProjectType::Node,
555 "/n",
556 "/n/node_modules",
557 100,
558 Some("node-proj".into()),
559 ),
560 ];
561
562 let sort_opts = SortOptions {
563 criteria: Some(SortCriteria::Type),
564 reverse: true,
565 };
566 sort_projects(&mut projects, &sort_opts);
567
568 assert_eq!(projects[0].kind, ProjectType::Rust);
569 assert_eq!(projects[1].kind, ProjectType::Node);
570 assert_eq!(projects[2].kind, ProjectType::Go);
571 }
572
573 #[test]
574 fn test_sort_none_criteria_preserves_order() {
575 let mut projects = vec![
576 create_test_project(
577 ProjectType::Rust,
578 "/c",
579 "/c/target",
580 100,
581 Some("charlie".into()),
582 ),
583 create_test_project(
584 ProjectType::Rust,
585 "/a",
586 "/a/target",
587 300,
588 Some("alpha".into()),
589 ),
590 create_test_project(
591 ProjectType::Rust,
592 "/b",
593 "/b/target",
594 200,
595 Some("bravo".into()),
596 ),
597 ];
598
599 let sort_opts = SortOptions {
600 criteria: None,
601 reverse: false,
602 };
603 sort_projects(&mut projects, &sort_opts);
604
605 assert_eq!(projects[0].name.as_deref(), Some("charlie"));
607 assert_eq!(projects[1].name.as_deref(), Some("alpha"));
608 assert_eq!(projects[2].name.as_deref(), Some("bravo"));
609 }
610
611 #[test]
612 fn test_sort_empty_list() {
613 let mut projects: Vec<Project> = vec![];
614
615 let sort_opts = SortOptions {
616 criteria: Some(SortCriteria::Size),
617 reverse: false,
618 };
619 sort_projects(&mut projects, &sort_opts);
620
621 assert!(projects.is_empty());
622 }
623
624 #[test]
625 fn test_sort_single_element() {
626 let mut projects = vec![create_test_project(
627 ProjectType::Rust,
628 "/a",
629 "/a/target",
630 100,
631 Some("only".into()),
632 )];
633
634 let sort_opts = SortOptions {
635 criteria: Some(SortCriteria::Name),
636 reverse: false,
637 };
638 sort_projects(&mut projects, &sort_opts);
639
640 assert_eq!(projects.len(), 1);
641 assert_eq!(projects[0].name.as_deref(), Some("only"));
642 }
643
644 #[test]
645 fn test_type_order_values() {
646 assert!(type_order(&ProjectType::Cpp) < type_order(&ProjectType::Deno));
647 assert!(type_order(&ProjectType::Deno) < type_order(&ProjectType::DotNet));
648 assert!(type_order(&ProjectType::DotNet) < type_order(&ProjectType::Elixir));
649 assert!(type_order(&ProjectType::Elixir) < type_order(&ProjectType::Go));
650 assert!(type_order(&ProjectType::Go) < type_order(&ProjectType::Java));
651 assert!(type_order(&ProjectType::Java) < type_order(&ProjectType::Node));
652 assert!(type_order(&ProjectType::Node) < type_order(&ProjectType::Python));
653 assert!(type_order(&ProjectType::Python) < type_order(&ProjectType::Ruby));
654 assert!(type_order(&ProjectType::Ruby) < type_order(&ProjectType::Rust));
655 assert!(type_order(&ProjectType::Rust) < type_order(&ProjectType::Swift));
656 }
657}