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::Deno => 1,
194 ProjectType::DotNet => 2,
195 ProjectType::Elixir => 3,
196 ProjectType::Go => 4,
197 ProjectType::Java => 5,
198 ProjectType::Node => 6,
199 ProjectType::Python => 7,
200 ProjectType::Ruby => 8,
201 ProjectType::Rust => 9,
202 ProjectType::Swift => 10,
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use crate::project::{BuildArtifacts, Project, ProjectType};
210 use std::path::PathBuf;
211
212 fn create_test_project(
214 kind: ProjectType,
215 root_path: &str,
216 build_path: &str,
217 size: u64,
218 name: Option<String>,
219 ) -> Project {
220 Project::new(
221 kind,
222 PathBuf::from(root_path),
223 vec![BuildArtifacts {
224 path: PathBuf::from(build_path),
225 size,
226 }],
227 name,
228 )
229 }
230
231 #[test]
232 fn test_meets_size_criteria() {
233 let project = create_test_project(
234 ProjectType::Rust,
235 "/test",
236 "/test/target",
237 1_000_000, Some("test".to_string()),
239 );
240
241 assert!(meets_size_criteria(&project, 500_000)); assert!(meets_size_criteria(&project, 1_000_000)); assert!(!meets_size_criteria(&project, 2_000_000)); }
245
246 #[test]
247 fn test_meets_time_criteria_disabled() {
248 let project = create_test_project(
249 ProjectType::Rust,
250 "/test",
251 "/test/target",
252 1_000_000,
253 Some("test".to_string()),
254 );
255
256 assert!(meets_time_criteria(&project, 0));
258 }
259
260 #[test]
263 fn test_sort_by_size_descending() {
264 let mut projects = vec![
265 create_test_project(
266 ProjectType::Rust,
267 "/a",
268 "/a/target",
269 100,
270 Some("small".into()),
271 ),
272 create_test_project(
273 ProjectType::Rust,
274 "/b",
275 "/b/target",
276 300,
277 Some("large".into()),
278 ),
279 create_test_project(
280 ProjectType::Rust,
281 "/c",
282 "/c/target",
283 200,
284 Some("medium".into()),
285 ),
286 ];
287
288 let sort_opts = SortOptions {
289 criteria: Some(SortCriteria::Size),
290 reverse: false,
291 };
292 sort_projects(&mut projects, &sort_opts);
293
294 assert_eq!(projects[0].total_size(), 300);
295 assert_eq!(projects[1].total_size(), 200);
296 assert_eq!(projects[2].total_size(), 100);
297 }
298
299 #[test]
300 fn test_sort_by_size_reversed() {
301 let mut projects = vec![
302 create_test_project(
303 ProjectType::Rust,
304 "/a",
305 "/a/target",
306 100,
307 Some("small".into()),
308 ),
309 create_test_project(
310 ProjectType::Rust,
311 "/b",
312 "/b/target",
313 300,
314 Some("large".into()),
315 ),
316 create_test_project(
317 ProjectType::Rust,
318 "/c",
319 "/c/target",
320 200,
321 Some("medium".into()),
322 ),
323 ];
324
325 let sort_opts = SortOptions {
326 criteria: Some(SortCriteria::Size),
327 reverse: true,
328 };
329 sort_projects(&mut projects, &sort_opts);
330
331 assert_eq!(projects[0].total_size(), 100);
332 assert_eq!(projects[1].total_size(), 200);
333 assert_eq!(projects[2].total_size(), 300);
334 }
335
336 #[test]
337 fn test_sort_by_name_alphabetical() {
338 let mut projects = vec![
339 create_test_project(
340 ProjectType::Rust,
341 "/c",
342 "/c/target",
343 100,
344 Some("charlie".into()),
345 ),
346 create_test_project(
347 ProjectType::Rust,
348 "/a",
349 "/a/target",
350 100,
351 Some("alpha".into()),
352 ),
353 create_test_project(
354 ProjectType::Rust,
355 "/b",
356 "/b/target",
357 100,
358 Some("bravo".into()),
359 ),
360 ];
361
362 let sort_opts = SortOptions {
363 criteria: Some(SortCriteria::Name),
364 reverse: false,
365 };
366 sort_projects(&mut projects, &sort_opts);
367
368 assert_eq!(projects[0].name.as_deref(), Some("alpha"));
369 assert_eq!(projects[1].name.as_deref(), Some("bravo"));
370 assert_eq!(projects[2].name.as_deref(), Some("charlie"));
371 }
372
373 #[test]
374 fn test_sort_by_name_case_insensitive() {
375 let mut projects = vec![
376 create_test_project(
377 ProjectType::Rust,
378 "/c",
379 "/c/target",
380 100,
381 Some("Charlie".into()),
382 ),
383 create_test_project(
384 ProjectType::Rust,
385 "/a",
386 "/a/target",
387 100,
388 Some("alpha".into()),
389 ),
390 create_test_project(
391 ProjectType::Rust,
392 "/b",
393 "/b/target",
394 100,
395 Some("Bravo".into()),
396 ),
397 ];
398
399 let sort_opts = SortOptions {
400 criteria: Some(SortCriteria::Name),
401 reverse: false,
402 };
403 sort_projects(&mut projects, &sort_opts);
404
405 assert_eq!(projects[0].name.as_deref(), Some("alpha"));
406 assert_eq!(projects[1].name.as_deref(), Some("Bravo"));
407 assert_eq!(projects[2].name.as_deref(), Some("Charlie"));
408 }
409
410 #[test]
411 fn test_sort_by_name_none_names_first() {
412 let mut projects = vec![
413 create_test_project(
414 ProjectType::Rust,
415 "/c",
416 "/c/target",
417 100,
418 Some("charlie".into()),
419 ),
420 create_test_project(ProjectType::Rust, "/a", "/a/target", 100, None),
421 create_test_project(
422 ProjectType::Rust,
423 "/b",
424 "/b/target",
425 100,
426 Some("alpha".into()),
427 ),
428 ];
429
430 let sort_opts = SortOptions {
431 criteria: Some(SortCriteria::Name),
432 reverse: false,
433 };
434 sort_projects(&mut projects, &sort_opts);
435
436 assert_eq!(projects[0].name.as_deref(), None);
438 assert_eq!(projects[1].name.as_deref(), Some("alpha"));
439 assert_eq!(projects[2].name.as_deref(), Some("charlie"));
440 }
441
442 #[test]
443 fn test_sort_by_type() {
444 let mut projects = vec![
445 create_test_project(
446 ProjectType::Rust,
447 "/r",
448 "/r/target",
449 100,
450 Some("rust-proj".into()),
451 ),
452 create_test_project(
453 ProjectType::Go,
454 "/g",
455 "/g/vendor",
456 100,
457 Some("go-proj".into()),
458 ),
459 create_test_project(
460 ProjectType::Python,
461 "/p",
462 "/p/__pycache__",
463 100,
464 Some("py-proj".into()),
465 ),
466 create_test_project(
467 ProjectType::Node,
468 "/n",
469 "/n/node_modules",
470 100,
471 Some("node-proj".into()),
472 ),
473 create_test_project(
474 ProjectType::Java,
475 "/j",
476 "/j/target",
477 100,
478 Some("java-proj".into()),
479 ),
480 create_test_project(
481 ProjectType::Cpp,
482 "/c",
483 "/c/build",
484 100,
485 Some("cpp-proj".into()),
486 ),
487 create_test_project(
488 ProjectType::Swift,
489 "/s",
490 "/s/.build",
491 100,
492 Some("swift-proj".into()),
493 ),
494 create_test_project(
495 ProjectType::DotNet,
496 "/d",
497 "/d/obj",
498 100,
499 Some("dotnet-proj".into()),
500 ),
501 create_test_project(
502 ProjectType::Ruby,
503 "/rb",
504 "/rb/vendor/bundle",
505 100,
506 Some("ruby-proj".into()),
507 ),
508 create_test_project(
509 ProjectType::Elixir,
510 "/ex",
511 "/ex/_build",
512 100,
513 Some("elixir-proj".into()),
514 ),
515 create_test_project(
516 ProjectType::Deno,
517 "/dn",
518 "/dn/vendor",
519 100,
520 Some("deno-proj".into()),
521 ),
522 ];
523
524 let sort_opts = SortOptions {
525 criteria: Some(SortCriteria::Type),
526 reverse: false,
527 };
528 sort_projects(&mut projects, &sort_opts);
529
530 assert_eq!(projects[0].kind, ProjectType::Cpp);
531 assert_eq!(projects[1].kind, ProjectType::Deno);
532 assert_eq!(projects[2].kind, ProjectType::DotNet);
533 assert_eq!(projects[3].kind, ProjectType::Elixir);
534 assert_eq!(projects[4].kind, ProjectType::Go);
535 assert_eq!(projects[5].kind, ProjectType::Java);
536 assert_eq!(projects[6].kind, ProjectType::Node);
537 assert_eq!(projects[7].kind, ProjectType::Python);
538 assert_eq!(projects[8].kind, ProjectType::Ruby);
539 assert_eq!(projects[9].kind, ProjectType::Rust);
540 assert_eq!(projects[10].kind, ProjectType::Swift);
541 }
542
543 #[test]
544 fn test_sort_by_type_reversed() {
545 let mut projects = vec![
546 create_test_project(
547 ProjectType::Go,
548 "/g",
549 "/g/vendor",
550 100,
551 Some("go-proj".into()),
552 ),
553 create_test_project(
554 ProjectType::Rust,
555 "/r",
556 "/r/target",
557 100,
558 Some("rust-proj".into()),
559 ),
560 create_test_project(
561 ProjectType::Node,
562 "/n",
563 "/n/node_modules",
564 100,
565 Some("node-proj".into()),
566 ),
567 ];
568
569 let sort_opts = SortOptions {
570 criteria: Some(SortCriteria::Type),
571 reverse: true,
572 };
573 sort_projects(&mut projects, &sort_opts);
574
575 assert_eq!(projects[0].kind, ProjectType::Rust);
576 assert_eq!(projects[1].kind, ProjectType::Node);
577 assert_eq!(projects[2].kind, ProjectType::Go);
578 }
579
580 #[test]
581 fn test_sort_none_criteria_preserves_order() {
582 let mut projects = vec![
583 create_test_project(
584 ProjectType::Rust,
585 "/c",
586 "/c/target",
587 100,
588 Some("charlie".into()),
589 ),
590 create_test_project(
591 ProjectType::Rust,
592 "/a",
593 "/a/target",
594 300,
595 Some("alpha".into()),
596 ),
597 create_test_project(
598 ProjectType::Rust,
599 "/b",
600 "/b/target",
601 200,
602 Some("bravo".into()),
603 ),
604 ];
605
606 let sort_opts = SortOptions {
607 criteria: None,
608 reverse: false,
609 };
610 sort_projects(&mut projects, &sort_opts);
611
612 assert_eq!(projects[0].name.as_deref(), Some("charlie"));
614 assert_eq!(projects[1].name.as_deref(), Some("alpha"));
615 assert_eq!(projects[2].name.as_deref(), Some("bravo"));
616 }
617
618 #[test]
619 fn test_sort_empty_list() {
620 let mut projects: Vec<Project> = vec![];
621
622 let sort_opts = SortOptions {
623 criteria: Some(SortCriteria::Size),
624 reverse: false,
625 };
626 sort_projects(&mut projects, &sort_opts);
627
628 assert!(projects.is_empty());
629 }
630
631 #[test]
632 fn test_sort_single_element() {
633 let mut projects = vec![create_test_project(
634 ProjectType::Rust,
635 "/a",
636 "/a/target",
637 100,
638 Some("only".into()),
639 )];
640
641 let sort_opts = SortOptions {
642 criteria: Some(SortCriteria::Name),
643 reverse: false,
644 };
645 sort_projects(&mut projects, &sort_opts);
646
647 assert_eq!(projects.len(), 1);
648 assert_eq!(projects[0].name.as_deref(), Some("only"));
649 }
650
651 #[test]
652 fn test_type_order_values() {
653 assert!(type_order(&ProjectType::Cpp) < type_order(&ProjectType::Deno));
654 assert!(type_order(&ProjectType::Deno) < type_order(&ProjectType::DotNet));
655 assert!(type_order(&ProjectType::DotNet) < type_order(&ProjectType::Elixir));
656 assert!(type_order(&ProjectType::Elixir) < type_order(&ProjectType::Go));
657 assert!(type_order(&ProjectType::Go) < type_order(&ProjectType::Java));
658 assert!(type_order(&ProjectType::Java) < type_order(&ProjectType::Node));
659 assert!(type_order(&ProjectType::Node) < type_order(&ProjectType::Python));
660 assert!(type_order(&ProjectType::Python) < type_order(&ProjectType::Ruby));
661 assert!(type_order(&ProjectType::Ruby) < type_order(&ProjectType::Rust));
662 assert!(type_order(&ProjectType::Rust) < type_order(&ProjectType::Swift));
663 }
664}