ralph/queue/prune/
core.rs1use super::super::{load_queue_or_default, save_queue};
22use super::types::{PruneOptions, PruneReport};
23use crate::contracts::Task;
24use crate::timeutil;
25use anyhow::Result;
26use std::cmp::Ordering;
27use std::collections::HashSet;
28use std::path::Path;
29use time::{Duration, OffsetDateTime};
30
31pub fn prune_done_tasks(done_path: &Path, options: PruneOptions) -> Result<PruneReport> {
43 let mut done = load_queue_or_default(done_path)?;
44 let report = prune_done_queue(&mut done.tasks, &options)?;
45
46 if !options.dry_run && !report.pruned_ids.is_empty() {
47 save_queue(done_path, &done)?;
48 }
49
50 Ok(report)
51}
52
53pub(crate) fn prune_done_queue(
59 tasks: &mut Vec<Task>,
60 options: &PruneOptions,
61) -> Result<PruneReport> {
62 let now_dt = OffsetDateTime::now_utc();
63 prune_done_queue_at(tasks, options, now_dt)
64}
65
66pub(crate) fn prune_done_queue_at(
67 tasks: &mut Vec<Task>,
68 options: &PruneOptions,
69 now_dt: OffsetDateTime,
70) -> Result<PruneReport> {
71 let age_duration = options.age_days.map(|days| Duration::days(days as i64));
72
73 let mut indices: Vec<usize> = (0..tasks.len()).collect();
74 indices.sort_by(|&idx_a, &idx_b| compare_completed_desc(&tasks[idx_a], &idx_b, tasks));
75
76 let mut keep_set: HashSet<usize> = HashSet::new();
77 if let Some(keep_n) = options.keep_last {
78 for &idx in indices.iter().take(keep_n as usize) {
79 keep_set.insert(idx);
80 }
81 }
82
83 let mut pruned_ids = Vec::new();
84 let mut kept_ids = Vec::new();
85 let mut keep_mask = vec![false; tasks.len()];
86
87 for (idx, task) in tasks.iter().enumerate() {
88 if keep_set.contains(&idx) {
89 keep_mask[idx] = true;
90 kept_ids.push(task.id.clone());
91 continue;
92 }
93
94 if !options.statuses.is_empty() && !options.statuses.contains(&task.status) {
95 keep_mask[idx] = true;
96 kept_ids.push(task.id.clone());
97 continue;
98 }
99
100 if let Some(ref completed_at) = task.completed_at {
101 if let Some(task_dt) = parse_completed_at(completed_at) {
102 if let Some(age_dur) = age_duration {
103 let age = if now_dt >= task_dt {
104 now_dt - task_dt
105 } else {
106 Duration::ZERO
107 };
108 if age < age_dur {
109 keep_mask[idx] = true;
110 kept_ids.push(task.id.clone());
111 continue;
112 }
113 }
114 } else {
115 keep_mask[idx] = true;
116 kept_ids.push(task.id.clone());
117 continue;
118 }
119 } else {
120 keep_mask[idx] = true;
121 kept_ids.push(task.id.clone());
122 continue;
123 }
124
125 pruned_ids.push(task.id.clone());
126 }
127
128 let mut new_tasks = Vec::new();
129 for (idx, task) in tasks.drain(..).enumerate() {
130 if keep_mask[idx] {
131 new_tasks.push(task);
132 }
133 }
134 *tasks = new_tasks;
135
136 Ok(PruneReport {
137 pruned_ids,
138 kept_ids,
139 })
140}
141
142#[cfg(test)]
143pub(crate) fn prune_done_tasks_at(
144 done_path: &Path,
145 options: PruneOptions,
146 now_dt: OffsetDateTime,
147) -> Result<PruneReport> {
148 let mut done = load_queue_or_default(done_path)?;
149 let report = prune_done_queue_at(&mut done.tasks, &options, now_dt)?;
150
151 if !options.dry_run && !report.pruned_ids.is_empty() {
152 save_queue(done_path, &done)?;
153 }
154
155 Ok(report)
156}
157
158fn parse_completed_at(ts: &str) -> Option<OffsetDateTime> {
161 timeutil::parse_rfc3339_opt(ts)
162}
163
164fn compare_completed_desc(a: &Task, idx_b: &usize, tasks: &[Task]) -> Ordering {
168 let b = &tasks[*idx_b];
169 let a_ts = parse_completed_at;
170 let b_ts = parse_completed_at;
171
172 match (a.completed_at.as_deref(), b.completed_at.as_deref()) {
173 (Some(ts_a), Some(ts_b)) => match (a_ts(ts_a), b_ts(ts_b)) {
174 (Some(dt_a), Some(dt_b)) => dt_a.cmp(&dt_b).reverse(),
175 (Some(_), None) => Ordering::Less,
176 (None, Some(_)) => Ordering::Greater,
177 (None, None) => Ordering::Equal,
178 },
179 (Some(_), None) => Ordering::Less,
180 (None, Some(_)) => Ordering::Greater,
181 (None, None) => Ordering::Equal,
182 }
183}