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