1use anyhow::Result;
7use chrono::{DateTime, Local};
8use glob::Pattern as GlobPattern;
9use rayon::prelude::*;
10use regex::Regex;
11use std::cmp::Reverse;
12use std::fs;
13use std::time::SystemTime;
14
15use crate::config::filter::SortCriteria;
16use crate::config::{FilterOptions, SortOptions};
17use crate::project::{Project, ProjectType};
18use crate::utils::parse_size;
19
20enum NameMatcher {
25 None,
26 Glob(GlobPattern),
27 Regex(Regex),
28}
29
30impl NameMatcher {
31 fn is_match(&self, name: &str) -> bool {
32 match self {
33 Self::None => true,
34 Self::Glob(pat) => pat.matches(name),
35 Self::Regex(re) => re.is_match(name),
36 }
37 }
38}
39
40fn compile_name_matcher(pattern: Option<&str>) -> Result<NameMatcher> {
46 let Some(pat) = pattern else {
47 return Ok(NameMatcher::None);
48 };
49 if pat.is_empty() {
50 return Ok(NameMatcher::None);
51 }
52 if let Some(regex_pat) = pat.strip_prefix("regex:") {
53 Ok(NameMatcher::Regex(Regex::new(regex_pat)?))
54 } else {
55 Ok(NameMatcher::Glob(GlobPattern::new(pat)?))
56 }
57}
58
59pub fn filter_projects(
98 projects: Vec<Project>,
99 filter_opts: &FilterOptions,
100) -> Result<Vec<Project>> {
101 let keep_size_bytes = parse_size(&filter_opts.keep_size)?;
102 let keep_days = filter_opts.keep_days;
103 let name_matcher = compile_name_matcher(filter_opts.name_pattern.as_deref())?;
104
105 Ok(projects
106 .into_par_iter()
107 .filter(|project| meets_size_criteria(project, keep_size_bytes))
108 .filter(|project| meets_time_criteria(project, keep_days))
109 .filter(|project| {
110 let name = project.name.as_deref().unwrap_or("");
111 name_matcher.is_match(name)
112 })
113 .collect())
114}
115
116fn meets_size_criteria(project: &Project, min_size: u64) -> bool {
118 project.total_size() >= min_size
119}
120
121fn meets_time_criteria(project: &Project, keep_days: u32) -> bool {
123 if keep_days == 0 {
124 return true;
125 }
126
127 is_project_old_enough(project, keep_days)
128}
129
130fn is_project_old_enough(project: &Project, keep_days: u32) -> bool {
132 let Some(primary) = project.build_arts.first() else {
133 return true;
134 };
135 let Result::Ok(metadata) = fs::metadata(&primary.path) else {
136 return true; };
138
139 let Result::Ok(modified) = metadata.modified() else {
140 return true; };
142
143 let modified_time: DateTime<Local> = modified.into();
144 let cutoff_time = Local::now() - chrono::Duration::days(i64::from(keep_days));
145
146 modified_time <= cutoff_time
147}
148
149pub fn sort_projects(projects: &mut Vec<Project>, sort_opts: &SortOptions) {
182 let Some(criteria) = sort_opts.criteria else {
183 return;
184 };
185
186 match criteria {
187 SortCriteria::Size => {
188 projects.sort_by_key(|p| Reverse(p.total_size()));
189 }
190 SortCriteria::Age => {
191 sort_by_age(projects);
192 }
193 SortCriteria::Name => {
194 projects.sort_by(|a, b| {
195 let name_a = a.name.as_deref().unwrap_or("");
196 let name_b = b.name.as_deref().unwrap_or("");
197 name_a.to_lowercase().cmp(&name_b.to_lowercase())
198 });
199 }
200 SortCriteria::Type => {
201 projects.sort_by(|a, b| type_order(&a.kind).cmp(&type_order(&b.kind)));
202 }
203 }
204
205 if sort_opts.reverse {
206 projects.reverse();
207 }
208}
209
210fn sort_by_age(projects: &mut Vec<Project>) {
215 let mut decorated: Vec<(Project, SystemTime)> = projects
216 .drain(..)
217 .map(|p| {
218 let mtime = p
219 .build_arts
220 .first()
221 .and_then(|a| fs::metadata(&a.path).ok())
222 .and_then(|m| m.modified().ok())
223 .unwrap_or(SystemTime::UNIX_EPOCH);
224 (p, mtime)
225 })
226 .collect();
227
228 decorated.sort_by(|a, b| a.1.cmp(&b.1));
229
230 projects.extend(decorated.into_iter().map(|(p, _)| p));
231}
232
233const fn type_order(kind: &ProjectType) -> u8 {
238 match kind {
239 ProjectType::Cpp => 0,
240 ProjectType::Dart => 1,
241 ProjectType::Deno => 2,
242 ProjectType::DotNet => 3,
243 ProjectType::Elixir => 4,
244 ProjectType::Go => 5,
245 ProjectType::Haskell => 6,
246 ProjectType::Java => 7,
247 ProjectType::Node => 8,
248 ProjectType::Php => 9,
249 ProjectType::Python => 10,
250 ProjectType::Ruby => 11,
251 ProjectType::Rust => 12,
252 ProjectType::Scala => 13,
253 ProjectType::Swift => 14,
254 ProjectType::Zig => 15,
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use crate::project::{BuildArtifacts, Project, ProjectType};
262 use std::path::PathBuf;
263
264 fn create_test_project(
266 kind: ProjectType,
267 root_path: &str,
268 build_path: &str,
269 size: u64,
270 name: Option<String>,
271 ) -> Project {
272 Project::new(
273 kind,
274 PathBuf::from(root_path),
275 vec![BuildArtifacts {
276 path: PathBuf::from(build_path),
277 size,
278 }],
279 name,
280 )
281 }
282
283 #[test]
284 fn test_meets_size_criteria() {
285 let project = create_test_project(
286 ProjectType::Rust,
287 "/test",
288 "/test/target",
289 1_000_000, Some("test".to_string()),
291 );
292
293 assert!(meets_size_criteria(&project, 500_000)); assert!(meets_size_criteria(&project, 1_000_000)); assert!(!meets_size_criteria(&project, 2_000_000)); }
297
298 #[test]
299 fn test_meets_time_criteria_disabled() {
300 let project = create_test_project(
301 ProjectType::Rust,
302 "/test",
303 "/test/target",
304 1_000_000,
305 Some("test".to_string()),
306 );
307
308 assert!(meets_time_criteria(&project, 0));
310 }
311
312 #[test]
315 fn test_sort_by_size_descending() {
316 let mut projects = vec![
317 create_test_project(
318 ProjectType::Rust,
319 "/a",
320 "/a/target",
321 100,
322 Some("small".into()),
323 ),
324 create_test_project(
325 ProjectType::Rust,
326 "/b",
327 "/b/target",
328 300,
329 Some("large".into()),
330 ),
331 create_test_project(
332 ProjectType::Rust,
333 "/c",
334 "/c/target",
335 200,
336 Some("medium".into()),
337 ),
338 ];
339
340 let sort_opts = SortOptions {
341 criteria: Some(SortCriteria::Size),
342 reverse: false,
343 };
344 sort_projects(&mut projects, &sort_opts);
345
346 assert_eq!(projects[0].total_size(), 300);
347 assert_eq!(projects[1].total_size(), 200);
348 assert_eq!(projects[2].total_size(), 100);
349 }
350
351 #[test]
352 fn test_sort_by_size_reversed() {
353 let mut projects = vec![
354 create_test_project(
355 ProjectType::Rust,
356 "/a",
357 "/a/target",
358 100,
359 Some("small".into()),
360 ),
361 create_test_project(
362 ProjectType::Rust,
363 "/b",
364 "/b/target",
365 300,
366 Some("large".into()),
367 ),
368 create_test_project(
369 ProjectType::Rust,
370 "/c",
371 "/c/target",
372 200,
373 Some("medium".into()),
374 ),
375 ];
376
377 let sort_opts = SortOptions {
378 criteria: Some(SortCriteria::Size),
379 reverse: true,
380 };
381 sort_projects(&mut projects, &sort_opts);
382
383 assert_eq!(projects[0].total_size(), 100);
384 assert_eq!(projects[1].total_size(), 200);
385 assert_eq!(projects[2].total_size(), 300);
386 }
387
388 #[test]
389 fn test_sort_by_name_alphabetical() {
390 let mut projects = vec![
391 create_test_project(
392 ProjectType::Rust,
393 "/c",
394 "/c/target",
395 100,
396 Some("charlie".into()),
397 ),
398 create_test_project(
399 ProjectType::Rust,
400 "/a",
401 "/a/target",
402 100,
403 Some("alpha".into()),
404 ),
405 create_test_project(
406 ProjectType::Rust,
407 "/b",
408 "/b/target",
409 100,
410 Some("bravo".into()),
411 ),
412 ];
413
414 let sort_opts = SortOptions {
415 criteria: Some(SortCriteria::Name),
416 reverse: false,
417 };
418 sort_projects(&mut projects, &sort_opts);
419
420 assert_eq!(projects[0].name.as_deref(), Some("alpha"));
421 assert_eq!(projects[1].name.as_deref(), Some("bravo"));
422 assert_eq!(projects[2].name.as_deref(), Some("charlie"));
423 }
424
425 #[test]
426 fn test_sort_by_name_case_insensitive() {
427 let mut projects = vec![
428 create_test_project(
429 ProjectType::Rust,
430 "/c",
431 "/c/target",
432 100,
433 Some("Charlie".into()),
434 ),
435 create_test_project(
436 ProjectType::Rust,
437 "/a",
438 "/a/target",
439 100,
440 Some("alpha".into()),
441 ),
442 create_test_project(
443 ProjectType::Rust,
444 "/b",
445 "/b/target",
446 100,
447 Some("Bravo".into()),
448 ),
449 ];
450
451 let sort_opts = SortOptions {
452 criteria: Some(SortCriteria::Name),
453 reverse: false,
454 };
455 sort_projects(&mut projects, &sort_opts);
456
457 assert_eq!(projects[0].name.as_deref(), Some("alpha"));
458 assert_eq!(projects[1].name.as_deref(), Some("Bravo"));
459 assert_eq!(projects[2].name.as_deref(), Some("Charlie"));
460 }
461
462 #[test]
463 fn test_sort_by_name_none_names_first() {
464 let mut projects = vec![
465 create_test_project(
466 ProjectType::Rust,
467 "/c",
468 "/c/target",
469 100,
470 Some("charlie".into()),
471 ),
472 create_test_project(ProjectType::Rust, "/a", "/a/target", 100, None),
473 create_test_project(
474 ProjectType::Rust,
475 "/b",
476 "/b/target",
477 100,
478 Some("alpha".into()),
479 ),
480 ];
481
482 let sort_opts = SortOptions {
483 criteria: Some(SortCriteria::Name),
484 reverse: false,
485 };
486 sort_projects(&mut projects, &sort_opts);
487
488 assert_eq!(projects[0].name.as_deref(), None);
490 assert_eq!(projects[1].name.as_deref(), Some("alpha"));
491 assert_eq!(projects[2].name.as_deref(), Some("charlie"));
492 }
493
494 #[test]
495 fn test_sort_by_type() {
496 let mut projects = vec![
497 create_test_project(
498 ProjectType::Rust,
499 "/r",
500 "/r/target",
501 100,
502 Some("rust-proj".into()),
503 ),
504 create_test_project(
505 ProjectType::Go,
506 "/g",
507 "/g/vendor",
508 100,
509 Some("go-proj".into()),
510 ),
511 create_test_project(
512 ProjectType::Python,
513 "/p",
514 "/p/__pycache__",
515 100,
516 Some("py-proj".into()),
517 ),
518 create_test_project(
519 ProjectType::Node,
520 "/n",
521 "/n/node_modules",
522 100,
523 Some("node-proj".into()),
524 ),
525 create_test_project(
526 ProjectType::Java,
527 "/j",
528 "/j/target",
529 100,
530 Some("java-proj".into()),
531 ),
532 create_test_project(
533 ProjectType::Cpp,
534 "/c",
535 "/c/build",
536 100,
537 Some("cpp-proj".into()),
538 ),
539 create_test_project(
540 ProjectType::Swift,
541 "/s",
542 "/s/.build",
543 100,
544 Some("swift-proj".into()),
545 ),
546 create_test_project(
547 ProjectType::DotNet,
548 "/d",
549 "/d/obj",
550 100,
551 Some("dotnet-proj".into()),
552 ),
553 create_test_project(
554 ProjectType::Ruby,
555 "/rb",
556 "/rb/vendor/bundle",
557 100,
558 Some("ruby-proj".into()),
559 ),
560 create_test_project(
561 ProjectType::Elixir,
562 "/ex",
563 "/ex/_build",
564 100,
565 Some("elixir-proj".into()),
566 ),
567 create_test_project(
568 ProjectType::Deno,
569 "/dn",
570 "/dn/vendor",
571 100,
572 Some("deno-proj".into()),
573 ),
574 ];
575
576 let sort_opts = SortOptions {
577 criteria: Some(SortCriteria::Type),
578 reverse: false,
579 };
580 sort_projects(&mut projects, &sort_opts);
581
582 assert_eq!(projects[0].kind, ProjectType::Cpp);
583 assert_eq!(projects[1].kind, ProjectType::Deno);
584 assert_eq!(projects[2].kind, ProjectType::DotNet);
585 assert_eq!(projects[3].kind, ProjectType::Elixir);
586 assert_eq!(projects[4].kind, ProjectType::Go);
587 assert_eq!(projects[5].kind, ProjectType::Java);
588 assert_eq!(projects[6].kind, ProjectType::Node);
589 assert_eq!(projects[7].kind, ProjectType::Python);
590 assert_eq!(projects[8].kind, ProjectType::Ruby);
591 assert_eq!(projects[9].kind, ProjectType::Rust);
592 assert_eq!(projects[10].kind, ProjectType::Swift);
593 }
594
595 #[test]
596 fn test_sort_by_type_reversed() {
597 let mut projects = vec![
598 create_test_project(
599 ProjectType::Go,
600 "/g",
601 "/g/vendor",
602 100,
603 Some("go-proj".into()),
604 ),
605 create_test_project(
606 ProjectType::Rust,
607 "/r",
608 "/r/target",
609 100,
610 Some("rust-proj".into()),
611 ),
612 create_test_project(
613 ProjectType::Node,
614 "/n",
615 "/n/node_modules",
616 100,
617 Some("node-proj".into()),
618 ),
619 ];
620
621 let sort_opts = SortOptions {
622 criteria: Some(SortCriteria::Type),
623 reverse: true,
624 };
625 sort_projects(&mut projects, &sort_opts);
626
627 assert_eq!(projects[0].kind, ProjectType::Rust);
628 assert_eq!(projects[1].kind, ProjectType::Node);
629 assert_eq!(projects[2].kind, ProjectType::Go);
630 }
631
632 #[test]
633 fn test_sort_none_criteria_preserves_order() {
634 let mut projects = vec![
635 create_test_project(
636 ProjectType::Rust,
637 "/c",
638 "/c/target",
639 100,
640 Some("charlie".into()),
641 ),
642 create_test_project(
643 ProjectType::Rust,
644 "/a",
645 "/a/target",
646 300,
647 Some("alpha".into()),
648 ),
649 create_test_project(
650 ProjectType::Rust,
651 "/b",
652 "/b/target",
653 200,
654 Some("bravo".into()),
655 ),
656 ];
657
658 let sort_opts = SortOptions {
659 criteria: None,
660 reverse: false,
661 };
662 sort_projects(&mut projects, &sort_opts);
663
664 assert_eq!(projects[0].name.as_deref(), Some("charlie"));
666 assert_eq!(projects[1].name.as_deref(), Some("alpha"));
667 assert_eq!(projects[2].name.as_deref(), Some("bravo"));
668 }
669
670 #[test]
671 fn test_sort_empty_list() {
672 let mut projects: Vec<Project> = vec![];
673
674 let sort_opts = SortOptions {
675 criteria: Some(SortCriteria::Size),
676 reverse: false,
677 };
678 sort_projects(&mut projects, &sort_opts);
679
680 assert!(projects.is_empty());
681 }
682
683 #[test]
684 fn test_sort_single_element() {
685 let mut projects = vec![create_test_project(
686 ProjectType::Rust,
687 "/a",
688 "/a/target",
689 100,
690 Some("only".into()),
691 )];
692
693 let sort_opts = SortOptions {
694 criteria: Some(SortCriteria::Name),
695 reverse: false,
696 };
697 sort_projects(&mut projects, &sort_opts);
698
699 assert_eq!(projects.len(), 1);
700 assert_eq!(projects[0].name.as_deref(), Some("only"));
701 }
702
703 #[test]
704 fn test_type_order_values() {
705 assert!(type_order(&ProjectType::Cpp) < type_order(&ProjectType::Deno));
706 assert!(type_order(&ProjectType::Deno) < type_order(&ProjectType::DotNet));
707 assert!(type_order(&ProjectType::DotNet) < type_order(&ProjectType::Elixir));
708 assert!(type_order(&ProjectType::Elixir) < type_order(&ProjectType::Go));
709 assert!(type_order(&ProjectType::Go) < type_order(&ProjectType::Java));
710 assert!(type_order(&ProjectType::Java) < type_order(&ProjectType::Node));
711 assert!(type_order(&ProjectType::Node) < type_order(&ProjectType::Python));
712 assert!(type_order(&ProjectType::Python) < type_order(&ProjectType::Ruby));
713 assert!(type_order(&ProjectType::Ruby) < type_order(&ProjectType::Rust));
714 assert!(type_order(&ProjectType::Rust) < type_order(&ProjectType::Swift));
715 }
716
717 #[test]
720 fn test_name_matcher_none_passes_all() {
721 let matcher = compile_name_matcher(None).unwrap();
722 assert!(matcher.is_match("any-name"));
723 assert!(matcher.is_match(""));
724 assert!(matcher.is_match("something-else"));
725 }
726
727 #[test]
728 fn test_name_matcher_glob_star() {
729 let matcher = compile_name_matcher(Some("my-app*")).unwrap();
730 assert!(matcher.is_match("my-app"));
731 assert!(matcher.is_match("my-app-v2"));
732 assert!(matcher.is_match("my-appXYZ"));
733 assert!(!matcher.is_match("other-app"));
734 assert!(!matcher.is_match(""));
735 }
736
737 #[test]
738 fn test_name_matcher_glob_question_mark() {
739 let matcher = compile_name_matcher(Some("app-?")).unwrap();
740 assert!(matcher.is_match("app-1"));
741 assert!(matcher.is_match("app-a"));
742 assert!(!matcher.is_match("app-12"));
743 assert!(!matcher.is_match("app-"));
744 }
745
746 #[test]
747 fn test_name_matcher_regex_prefix() {
748 let matcher = compile_name_matcher(Some("regex:^client-.*")).unwrap();
749 assert!(matcher.is_match("client-api"));
750 assert!(matcher.is_match("client-web"));
751 assert!(!matcher.is_match("server-api"));
752 assert!(!matcher.is_match(""));
753 }
754
755 #[test]
756 fn test_name_matcher_regex_invalid_returns_error() {
757 let result = compile_name_matcher(Some("regex:[invalid"));
758 assert!(result.is_err());
759 }
760
761 #[test]
762 fn test_name_matcher_glob_invalid_returns_error() {
763 let result = compile_name_matcher(Some("["));
766 assert!(result.is_err());
767 }
768
769 #[test]
770 fn test_filter_projects_by_name_glob() {
771 let projects = vec![
772 create_test_project(
773 ProjectType::Rust,
774 "/a",
775 "/a/target",
776 1000,
777 Some("my-app".into()),
778 ),
779 create_test_project(
780 ProjectType::Rust,
781 "/b",
782 "/b/target",
783 1000,
784 Some("my-app-v2".into()),
785 ),
786 create_test_project(
787 ProjectType::Rust,
788 "/c",
789 "/c/target",
790 1000,
791 Some("other-project".into()),
792 ),
793 ];
794
795 let filter_opts = FilterOptions {
796 keep_size: "0".to_string(),
797 keep_days: 0,
798 name_pattern: Some("my-app*".to_string()),
799 };
800
801 let filtered = filter_projects(projects, &filter_opts).unwrap();
802 assert_eq!(filtered.len(), 2);
803 assert!(
804 filtered
805 .iter()
806 .all(|p| p.name.as_deref().unwrap_or("").starts_with("my-app"))
807 );
808 }
809
810 #[test]
811 fn test_filter_projects_by_name_regex() {
812 let projects = vec![
813 create_test_project(
814 ProjectType::Rust,
815 "/a",
816 "/a/target",
817 1000,
818 Some("client-api".into()),
819 ),
820 create_test_project(
821 ProjectType::Rust,
822 "/b",
823 "/b/target",
824 1000,
825 Some("client-web".into()),
826 ),
827 create_test_project(
828 ProjectType::Rust,
829 "/c",
830 "/c/target",
831 1000,
832 Some("server-api".into()),
833 ),
834 ];
835
836 let filter_opts = FilterOptions {
837 keep_size: "0".to_string(),
838 keep_days: 0,
839 name_pattern: Some("regex:^client-.*".to_string()),
840 };
841
842 let filtered = filter_projects(projects, &filter_opts).unwrap();
843 assert_eq!(filtered.len(), 2);
844 assert!(
845 filtered
846 .iter()
847 .all(|p| p.name.as_deref().unwrap_or("").starts_with("client-"))
848 );
849 }
850
851 #[test]
852 fn test_filter_projects_name_none_no_match() {
853 let projects = vec![
854 create_test_project(ProjectType::Rust, "/a", "/a/target", 1000, None),
855 create_test_project(
856 ProjectType::Rust,
857 "/b",
858 "/b/target",
859 1000,
860 Some("named".into()),
861 ),
862 ];
863
864 let filter_opts = FilterOptions {
865 keep_size: "0".to_string(),
866 keep_days: 0,
867 name_pattern: Some("named*".to_string()),
868 };
869
870 let filtered = filter_projects(projects, &filter_opts).unwrap();
871 assert_eq!(filtered.len(), 1);
873 assert_eq!(filtered[0].name.as_deref(), Some("named"));
874 }
875}