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