1use anyhow::Result;
2use apm_core::config::resolve_identity;
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 let mut stale_tickets: Vec<(&str, &str)> = Vec::new();
29 let mut diverged_tickets: Vec<(&str, &str)> = Vec::new();
30
31 for t in &filtered {
32 let fm = &t.frontmatter;
33 let owner = fm.owner.as_deref().unwrap_or("-");
34 let base = match fm.target_branch.as_deref() {
35 Some(branch) => apm_core::epic::epic_id_from_branch(branch).to_owned(),
36 None => ctx.config.project.default_branch.clone(),
37 };
38 let id_display = if t.local_stale {
39 format!("*{}", fm.id)
40 } else {
41 fm.id.clone()
42 };
43 println!("{:<9} [{:<12}] {:<16} {:<12} {}", id_display, fm.state, owner, base, fm.title);
44
45 if t.local_stale {
46 stale_tickets.push((&fm.id, &fm.title));
47 }
48 if t.local_diverged {
49 diverged_tickets.push((&fm.id, &fm.title));
50 }
51 }
52
53 if !diverged_tickets.is_empty() {
54 eprintln!();
55 eprintln!("warning: local ref has diverged from origin on {} ticket(s) — showing local content:", diverged_tickets.len());
56 for (id, title) in &diverged_tickets {
57 eprintln!(" {} {}", id, title);
58 }
59 }
60
61 if !stale_tickets.is_empty() {
62 println!();
63 println!(" * local ref behind origin — run `apm sync` to fast-forward:");
64 for (id, title) in &stale_tickets {
65 println!(" *{} {}", id, title);
66 }
67 }
68
69 let default_branch = &ctx.config.project.default_branch;
71 let mut epic_map: std::collections::BTreeMap<String, String> = std::collections::BTreeMap::new();
72 for t in &filtered {
73 if let Some(tb) = t.frontmatter.target_branch.as_deref() {
74 let id = apm_core::epic::epic_id_from_branch(tb).to_owned();
75 epic_map.entry(id).or_insert_with(|| tb.to_owned());
76 }
77 }
78 if !epic_map.is_empty() {
79 let mut stale_epics: Vec<(String, String)> = Vec::new();
80 for (id, branch) in &epic_map {
81 let s = apm_core::epic::merge_tree_status(root, default_branch, branch)
82 .unwrap_or(apm_core::epic::MergeStatus { ahead: 0, clean: true });
83 if s.ahead > 0 {
84 let label = if s.clean {
85 format!("↓{} clean", s.ahead)
86 } else {
87 format!("↓{} CONFLICTS", s.ahead)
88 };
89 stale_epics.push((id.clone(), label));
90 }
91 }
92 if !stale_epics.is_empty() {
93 println!();
94 println!(" epics:");
95 for (id, label) in &stale_epics {
96 println!(" {id:<8} {label}");
97 }
98 }
99 }
100
101 Ok(())
102}