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 post_plan: None,
110 verify_timeout: None,
111 review: None,
112 user: None,
113 user_email: None,
114 auto_commit: false,
115 commit_template: None,
116 research: None,
117 run_model: None,
118 plan_model: None,
119 review_model: None,
120 research_model: None,
121 batch_verify: false,
122 memory_reserve_mb: 0,
123 notify: None,
124 }
125 .save(&bd)
126 .unwrap();
127 (dir, bd)
128 }
129
130 #[test]
131 fn list_all() {
132 let (_dir, bd) = setup();
133 create::create(&bd, minimal_params("A")).unwrap();
134 create::create(&bd, minimal_params("B")).unwrap();
135 assert_eq!(list(&bd, &ListParams::default()).unwrap().len(), 2);
136 }
137
138 #[test]
139 fn list_excludes_closed() {
140 let (_dir, bd) = setup();
141 create::create(&bd, minimal_params("Open")).unwrap();
142 create::create(&bd, minimal_params("Closed")).unwrap();
143 update::update(
144 &bd,
145 "2",
146 update::UpdateParams {
147 title: None,
148 description: None,
149 acceptance: None,
150 notes: None,
151 design: None,
152 status: Some("closed".into()),
153 priority: None,
154 assignee: None,
155 add_label: None,
156 remove_label: None,
157 decisions: vec![],
158 resolve_decisions: vec![],
159 },
160 )
161 .unwrap();
162 let entries = list(&bd, &ListParams::default()).unwrap();
163 assert_eq!(entries.len(), 1);
164 assert_eq!(entries[0].id, "1");
165 }
166
167 #[test]
168 fn list_filter_priority() {
169 let (_dir, bd) = setup();
170 let mut p0 = minimal_params("Urgent");
171 p0.priority = Some(0);
172 create::create(&bd, p0).unwrap();
173 create::create(&bd, minimal_params("Normal")).unwrap();
174 let entries = list(
175 &bd,
176 &ListParams {
177 priority: Some(0),
178 ..Default::default()
179 },
180 )
181 .unwrap();
182 assert_eq!(entries.len(), 1);
183 assert_eq!(entries[0].title, "Urgent");
184 }
185
186 #[test]
187 fn list_filter_parent() {
188 let (_dir, bd) = setup();
189 create::create(&bd, minimal_params("Parent")).unwrap();
190 let mut child = minimal_params("Child");
191 child.parent = Some("1".to_string());
192 create::create(&bd, child).unwrap();
193 let entries = list(
194 &bd,
195 &ListParams {
196 parent: Some("1".into()),
197 ..Default::default()
198 },
199 )
200 .unwrap();
201 assert_eq!(entries.len(), 1);
202 assert_eq!(entries[0].id, "1.1");
203 }
204
205 #[test]
206 fn list_filter_assignee() {
207 let (_dir, bd) = setup();
208 let mut alice = minimal_params("Alice");
209 alice.assignee = Some("alice".to_string());
210 create::create(&bd, alice).unwrap();
211 let mut bob = minimal_params("Bob");
212 bob.assignee = Some("bob".to_string());
213 create::create(&bd, bob).unwrap();
214
215 let entries = list(
216 &bd,
217 &ListParams {
218 assignee: Some("alice".into()),
219 ..Default::default()
220 },
221 )
222 .unwrap();
223
224 assert_eq!(entries.len(), 1);
225 assert_eq!(entries[0].title, "Alice");
226 }
227
228 #[test]
229 fn list_filter_current_user_matches_claimed_or_assigned() {
230 let (_dir, bd) = setup();
231 let mut claimed = minimal_params("Claimed");
232 claimed.assignee = Some("other".to_string());
233 create::create(&bd, claimed).unwrap();
234 let mut assigned = minimal_params("Assigned");
235 assigned.assignee = Some("alice".to_string());
236 create::create(&bd, assigned).unwrap();
237
238 let first_path = crate::discovery::find_unit_file(&bd, "1").unwrap();
239 let mut first_unit = crate::unit::Unit::from_file(&first_path).unwrap();
240 first_unit.claimed_by = Some("alice/session".to_string());
241 first_unit.to_file(&first_path).unwrap();
242 let index = Index::build(&bd).unwrap();
243 index.save(&bd).unwrap();
244
245 let entries = list(
246 &bd,
247 &ListParams {
248 current_user: Some("alice".into()),
249 ..Default::default()
250 },
251 )
252 .unwrap();
253
254 assert_eq!(entries.len(), 2);
255 }
256}