1use std::path::Path;
2
3use anyhow::Result;
4
5use crate::index::{Index, IndexEntry};
6use crate::unit::Status;
7use crate::util::parse_status;
8
9#[derive(Default)]
11pub struct ListParams {
12 pub status: Option<String>,
13 pub priority: Option<u8>,
14 pub parent: Option<String>,
15 pub label: Option<String>,
16 pub assignee: Option<String>,
17 pub current_user: Option<String>,
18 pub include_closed: bool,
19}
20
21pub fn list(mana_dir: &Path, params: &ListParams) -> Result<Vec<IndexEntry>> {
23 let index = Index::load_or_rebuild(mana_dir)?;
24 let status_filter = params.status.as_deref().and_then(parse_status);
25
26 let mut entries = index.units.clone();
27
28 if status_filter == Some(Status::Closed) || params.include_closed {
29 if let Ok(archived) = Index::collect_archived(mana_dir) {
30 entries.extend(archived);
31 }
32 }
33
34 entries.retain(|entry| {
35 if !params.include_closed
36 && status_filter != Some(Status::Closed)
37 && entry.status == Status::Closed
38 {
39 return false;
40 }
41 if let Some(s) = status_filter {
42 if entry.status != s {
43 return false;
44 }
45 }
46 if let Some(p) = params.priority {
47 if entry.priority != p {
48 return false;
49 }
50 }
51 if let Some(ref parent) = params.parent {
52 if entry.parent.as_deref() != Some(parent.as_str()) {
53 return false;
54 }
55 }
56 if let Some(ref label) = params.label {
57 if !entry.labels.contains(label) {
58 return false;
59 }
60 }
61 if let Some(ref assignee) = params.assignee {
62 if entry.assignee.as_deref() != Some(assignee.as_str()) {
63 return false;
64 }
65 }
66 if let Some(ref user) = params.current_user {
67 let claimed_match = entry
68 .claimed_by
69 .as_ref()
70 .is_some_and(|c| c == user || c.starts_with(&format!("{}/", user)));
71 let assignee_match = entry.assignee.as_deref() == Some(user.as_str());
72 if !claimed_match && !assignee_match {
73 return false;
74 }
75 }
76 true
77 });
78
79 Ok(entries)
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::ops::create::{self, tests::minimal_params};
86 use crate::ops::update;
87 use std::fs;
88 use tempfile::TempDir;
89
90 fn setup() -> (TempDir, std::path::PathBuf) {
91 let dir = TempDir::new().unwrap();
92 let bd = dir.path().join(".mana");
93 fs::create_dir(&bd).unwrap();
94 crate::config::Config {
95 project: "test".to_string(),
96 next_id: 1,
97 auto_close_parent: true,
98 run: None,
99 plan: None,
100 max_loops: 10,
101 max_concurrent: 4,
102 poll_interval: 30,
103 extends: vec![],
104 rules_file: None,
105 file_locking: false,
106 worktree: false,
107 on_close: None,
108 on_fail: None,
109 verify_timeout: None,
110 review: None,
111 user: None,
112 user_email: None,
113 auto_commit: false,
114 commit_template: None,
115 research: None,
116 run_model: None,
117 plan_model: None,
118 review_model: None,
119 research_model: None,
120 batch_verify: false,
121 memory_reserve_mb: 0,
122 notify: None,
123 }
124 .save(&bd)
125 .unwrap();
126 (dir, bd)
127 }
128
129 #[test]
130 fn list_all() {
131 let (_dir, bd) = setup();
132 create::create(&bd, minimal_params("A")).unwrap();
133 create::create(&bd, minimal_params("B")).unwrap();
134 assert_eq!(list(&bd, &ListParams::default()).unwrap().len(), 2);
135 }
136
137 #[test]
138 fn list_excludes_closed() {
139 let (_dir, bd) = setup();
140 create::create(&bd, minimal_params("Open")).unwrap();
141 create::create(&bd, minimal_params("Closed")).unwrap();
142 update::update(
143 &bd,
144 "2",
145 update::UpdateParams {
146 title: None,
147 description: None,
148 acceptance: None,
149 notes: None,
150 design: None,
151 status: Some("closed".into()),
152 priority: None,
153 assignee: None,
154 add_label: None,
155 remove_label: None,
156 decisions: vec![],
157 resolve_decisions: vec![],
158 },
159 )
160 .unwrap();
161 let entries = list(&bd, &ListParams::default()).unwrap();
162 assert_eq!(entries.len(), 1);
163 assert_eq!(entries[0].id, "1");
164 }
165
166 #[test]
167 fn list_filter_priority() {
168 let (_dir, bd) = setup();
169 let mut p0 = minimal_params("Urgent");
170 p0.priority = Some(0);
171 create::create(&bd, p0).unwrap();
172 create::create(&bd, minimal_params("Normal")).unwrap();
173 let entries = list(
174 &bd,
175 &ListParams {
176 priority: Some(0),
177 ..Default::default()
178 },
179 )
180 .unwrap();
181 assert_eq!(entries.len(), 1);
182 assert_eq!(entries[0].title, "Urgent");
183 }
184
185 #[test]
186 fn list_filter_parent() {
187 let (_dir, bd) = setup();
188 create::create(&bd, minimal_params("Parent")).unwrap();
189 let mut child = minimal_params("Child");
190 child.parent = Some("1".to_string());
191 create::create(&bd, child).unwrap();
192 let entries = list(
193 &bd,
194 &ListParams {
195 parent: Some("1".into()),
196 ..Default::default()
197 },
198 )
199 .unwrap();
200 assert_eq!(entries.len(), 1);
201 assert_eq!(entries[0].id, "1.1");
202 }
203
204 #[test]
205 fn list_filter_assignee() {
206 let (_dir, bd) = setup();
207 let mut alice = minimal_params("Alice");
208 alice.assignee = Some("alice".to_string());
209 create::create(&bd, alice).unwrap();
210 let mut bob = minimal_params("Bob");
211 bob.assignee = Some("bob".to_string());
212 create::create(&bd, bob).unwrap();
213
214 let entries = list(
215 &bd,
216 &ListParams {
217 assignee: Some("alice".into()),
218 ..Default::default()
219 },
220 )
221 .unwrap();
222
223 assert_eq!(entries.len(), 1);
224 assert_eq!(entries[0].title, "Alice");
225 }
226
227 #[test]
228 fn list_filter_current_user_matches_claimed_or_assigned() {
229 let (_dir, bd) = setup();
230 let mut claimed = minimal_params("Claimed");
231 claimed.assignee = Some("other".to_string());
232 create::create(&bd, claimed).unwrap();
233 let mut assigned = minimal_params("Assigned");
234 assigned.assignee = Some("alice".to_string());
235 create::create(&bd, assigned).unwrap();
236
237 let first_path = crate::discovery::find_unit_file(&bd, "1").unwrap();
238 let mut first_unit = crate::unit::Unit::from_file(&first_path).unwrap();
239 first_unit.claimed_by = Some("alice/session".to_string());
240 first_unit.to_file(&first_path).unwrap();
241 let index = Index::build(&bd).unwrap();
242 index.save(&bd).unwrap();
243
244 let entries = list(
245 &bd,
246 &ListParams {
247 current_user: Some("alice".into()),
248 ..Default::default()
249 },
250 )
251 .unwrap();
252
253 assert_eq!(entries.len(), 2);
254 }
255}