Skip to main content

apm/cmd/
list.rs

1use anyhow::Result;
2use apm_core::{classify_recovery_options, config::resolve_identity, is_merge_failure_state, RecoveryKind, RecoveryOption};
3use std::path::Path;
4use crate::ctx::CmdContext;
5
6pub fn run(root: &Path, state_filter: Option<String>, unassigned: bool, all: bool, actionable_filter: Option<String>, no_aggressive: bool, mine: bool, author: Option<String>, owner: Option<String>) -> Result<()> {
7    let ctx = CmdContext::load(root, no_aggressive)?;
8
9    let mine_user: Option<String> = if mine {
10        Some(resolve_identity(root))
11    } else {
12        None
13    };
14    let author_filter = if mine { None } else { author };
15
16    let filtered = apm_core::ticket::list_filtered(
17        &ctx.tickets,
18        &ctx.config,
19        state_filter.as_deref(),
20        unassigned,
21        all,
22        actionable_filter.as_deref(),
23        author_filter.as_deref(),
24        owner.as_deref(),
25        mine_user.as_deref(),
26    );
27
28    // Pre-compute stale epic IDs before printing rows.
29    let default_branch = &ctx.config.project.default_branch;
30    let mut epic_map: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
31    for t in &filtered {
32        if let Some(tb) = t.frontmatter.target_branch.as_deref() {
33            if tb.starts_with("epic/") {
34                let id = apm_core::epic::epic_id_from_branch(tb).to_owned();
35                epic_map.entry(id).or_insert_with(|| tb.to_owned());
36            }
37        }
38    }
39    let mut stale_epic_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
40    for (id, branch) in &epic_map {
41        let s = apm_core::epic::merge_tree_status(root, default_branch, branch)
42            .unwrap_or(apm_core::epic::MergeStatus { ahead: 0, clean: true });
43        if s.ahead > 0 {
44            stale_epic_ids.insert(id.clone());
45        }
46    }
47
48    let mut stale_tickets: Vec<(&str, &str)> = Vec::new();
49    let mut diverged_tickets: Vec<(&str, &str)> = Vec::new();
50
51    for t in &filtered {
52        let fm = &t.frontmatter;
53        let owner = fm.owner.as_deref().unwrap_or("-");
54        let base = match fm.target_branch.as_deref() {
55            Some(branch) if branch.starts_with("epic/") => {
56                let id = apm_core::epic::epic_id_from_branch(branch);
57                if stale_epic_ids.contains(id) {
58                    format!("{}↓", id)
59                } else {
60                    id.to_owned()
61                }
62            }
63            Some(branch) => apm_core::epic::epic_id_from_branch(branch).to_owned(),
64            None => ctx.config.project.default_branch.clone(),
65        };
66        let id_display = if t.local_stale {
67            format!("*{}", fm.id)
68        } else {
69            fm.id.clone()
70        };
71        println!("{:<9} [{:<12}] {:<16} {:<12} {}", id_display, fm.state, owner, base, fm.title);
72
73        if t.local_stale {
74            stale_tickets.push((&fm.id, &fm.title));
75        }
76        if t.local_diverged {
77            diverged_tickets.push((&fm.id, &fm.title));
78        }
79    }
80
81    if !diverged_tickets.is_empty() {
82        eprintln!();
83        eprintln!("warning: local ref has diverged from origin on {} ticket(s) — showing local content:", diverged_tickets.len());
84        for (id, title) in &diverged_tickets {
85            eprintln!("    {}  {}", id, title);
86        }
87    }
88
89    if !stale_tickets.is_empty() {
90        println!();
91        println!("  * local ref behind origin — run `apm sync` to fast-forward:");
92        for (id, title) in &stale_tickets {
93            println!("      *{}  {}", id, title);
94        }
95    }
96
97    if let Some(state) = &state_filter {
98        if is_merge_failure_state(state, &ctx.config.workflow) {
99            let opts = classify_recovery_options(state, &ctx.config.workflow);
100            let relevant: Vec<&RecoveryOption> = opts.iter().filter(|o| matches!(
101                o.kind,
102                RecoveryKind::RetryMerge | RecoveryKind::ReturnToWorker
103            )).collect();
104            if !relevant.is_empty() {
105                let parts: Vec<String> = relevant.iter()
106                    .map(|o| format!("{} → apm state <id> {}", o.label, o.to))
107                    .collect();
108                println!("\nRecovery: {}", parts.join("  "));
109            }
110        }
111    }
112
113    Ok(())
114}