Skip to main content

mana_core/ops/
list.rs

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/// Parameters for listing/filtering units.
10#[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
21/// Load the index, apply filters, and return matching entries.
22pub 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}