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() -> anyhow::Result<()> {
721 let matcher = compile_name_matcher(None)?;
722 assert!(matcher.is_match("any-name"));
723 assert!(matcher.is_match(""));
724 assert!(matcher.is_match("something-else"));
725 Ok(())
726 }
727
728 #[test]
729 fn test_name_matcher_glob_star() -> anyhow::Result<()> {
730 let matcher = compile_name_matcher(Some("my-app*"))?;
731 assert!(matcher.is_match("my-app"));
732 assert!(matcher.is_match("my-app-v2"));
733 assert!(matcher.is_match("my-appXYZ"));
734 assert!(!matcher.is_match("other-app"));
735 assert!(!matcher.is_match(""));
736 Ok(())
737 }
738
739 #[test]
740 fn test_name_matcher_glob_question_mark() -> anyhow::Result<()> {
741 let matcher = compile_name_matcher(Some("app-?"))?;
742 assert!(matcher.is_match("app-1"));
743 assert!(matcher.is_match("app-a"));
744 assert!(!matcher.is_match("app-12"));
745 assert!(!matcher.is_match("app-"));
746 Ok(())
747 }
748
749 #[test]
750 fn test_name_matcher_regex_prefix() -> anyhow::Result<()> {
751 let matcher = compile_name_matcher(Some("regex:^client-.*"))?;
752 assert!(matcher.is_match("client-api"));
753 assert!(matcher.is_match("client-web"));
754 assert!(!matcher.is_match("server-api"));
755 assert!(!matcher.is_match(""));
756 Ok(())
757 }
758
759 #[test]
760 fn test_name_matcher_regex_invalid_returns_error() {
761 let result = compile_name_matcher(Some("regex:[invalid"));
762 assert!(result.is_err());
763 }
764
765 #[test]
766 fn test_name_matcher_glob_invalid_returns_error() {
767 let result = compile_name_matcher(Some("["));
770 assert!(result.is_err());
771 }
772
773 #[test]
774 fn test_filter_projects_by_name_glob() -> anyhow::Result<()> {
775 let projects = vec![
776 create_test_project(
777 ProjectType::Rust,
778 "/a",
779 "/a/target",
780 1000,
781 Some("my-app".into()),
782 ),
783 create_test_project(
784 ProjectType::Rust,
785 "/b",
786 "/b/target",
787 1000,
788 Some("my-app-v2".into()),
789 ),
790 create_test_project(
791 ProjectType::Rust,
792 "/c",
793 "/c/target",
794 1000,
795 Some("other-project".into()),
796 ),
797 ];
798
799 let filter_opts = FilterOptions {
800 keep_size: "0".to_string(),
801 keep_days: 0,
802 name_pattern: Some("my-app*".to_string()),
803 };
804
805 let filtered = filter_projects(projects, &filter_opts)?;
806 assert_eq!(filtered.len(), 2);
807 assert!(
808 filtered
809 .iter()
810 .all(|p| p.name.as_deref().unwrap_or("").starts_with("my-app"))
811 );
812 Ok(())
813 }
814
815 #[test]
816 fn test_filter_projects_by_name_regex() -> anyhow::Result<()> {
817 let projects = vec![
818 create_test_project(
819 ProjectType::Rust,
820 "/a",
821 "/a/target",
822 1000,
823 Some("client-api".into()),
824 ),
825 create_test_project(
826 ProjectType::Rust,
827 "/b",
828 "/b/target",
829 1000,
830 Some("client-web".into()),
831 ),
832 create_test_project(
833 ProjectType::Rust,
834 "/c",
835 "/c/target",
836 1000,
837 Some("server-api".into()),
838 ),
839 ];
840
841 let filter_opts = FilterOptions {
842 keep_size: "0".to_string(),
843 keep_days: 0,
844 name_pattern: Some("regex:^client-.*".to_string()),
845 };
846
847 let filtered = filter_projects(projects, &filter_opts)?;
848 assert_eq!(filtered.len(), 2);
849 assert!(
850 filtered
851 .iter()
852 .all(|p| p.name.as_deref().unwrap_or("").starts_with("client-"))
853 );
854 Ok(())
855 }
856
857 #[test]
858 fn test_filter_projects_name_none_no_match() -> anyhow::Result<()> {
859 let projects = vec![
860 create_test_project(ProjectType::Rust, "/a", "/a/target", 1000, None),
861 create_test_project(
862 ProjectType::Rust,
863 "/b",
864 "/b/target",
865 1000,
866 Some("named".into()),
867 ),
868 ];
869
870 let filter_opts = FilterOptions {
871 keep_size: "0".to_string(),
872 keep_days: 0,
873 name_pattern: Some("named*".to_string()),
874 };
875
876 let filtered = filter_projects(projects, &filter_opts)?;
877 assert_eq!(filtered.len(), 1);
879 assert_eq!(filtered[0].name.as_deref(), Some("named"));
880 Ok(())
881 }
882}