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            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}