1use anyhow::{bail, Result};
2use apm_core::{config::Config, git, ticket, ticket_fmt};
3use std::path::Path;
4
5pub fn run(root: &Path, id_arg: &str, no_aggressive: bool, edit: bool) -> Result<()> {
6 let config = Config::load(root)?;
7
8 let branches = git::ticket_branches(root)?;
9 let branch_result = ticket_fmt::resolve_ticket_branch(&branches, id_arg);
10
11 match branch_result {
12 Ok(branch) => {
13 let aggressive = config.sync.aggressive && !no_aggressive;
14 crate::util::fetch_branch_if_aggressive(root, &branch, aggressive);
15
16 let suffix = branch.trim_start_matches("ticket/");
17 let filename = format!("{suffix}.md");
18 let rel_path = format!("{}/{}", config.tickets.dir.to_string_lossy(), filename);
19 let dummy_path = root.join(&rel_path);
20
21 let content = git::read_from_branch(root, &branch, &rel_path)?;
22 let t = ticket::Ticket::parse(&dummy_path, &content)?;
23
24 show_ticket(&t, &content, root, &branch, &rel_path, edit)
25 }
26 Err(_) => {
27 let default_branch = &config.project.default_branch;
29 let prefixes = ticket::id_arg_prefixes(id_arg)?;
30
31 if let Some((rel_path, content)) = find_in_dir(
32 root,
33 default_branch,
34 &config.tickets.dir.to_string_lossy(),
35 &prefixes,
36 ) {
37 let dummy_path = root.join(&rel_path);
38 let t = ticket::Ticket::parse(&dummy_path, &content)?;
39 return show_ticket_readonly(&t, &content, edit);
40 }
41
42 if let Some(archive_dir) = &config.tickets.archive_dir {
43 if let Some((rel_path, content)) = find_in_dir(
44 root,
45 default_branch,
46 &archive_dir.to_string_lossy(),
47 &prefixes,
48 ) {
49 let dummy_path = root.join(&rel_path);
50 let t = ticket::Ticket::parse(&dummy_path, &content)?;
51 return show_ticket_readonly(&t, &content, edit);
52 }
53 }
54
55 bail!("no ticket matches '{id_arg}'")
56 }
57 }
58}
59
60fn find_in_dir(
61 root: &Path,
62 branch: &str,
63 dir: &str,
64 prefixes: &[String],
65) -> Option<(String, String)> {
66 let files = git::list_files_on_branch(root, branch, dir).ok()?;
67 for rel_path in files {
68 let filename = rel_path.split('/').last().unwrap_or("");
69 let file_id = filename.split('-').next().unwrap_or("");
70 if prefixes.iter().any(|p| file_id.starts_with(p.as_str())) {
71 if let Ok(content) = git::read_from_branch(root, branch, &rel_path) {
72 return Some((rel_path, content));
73 }
74 }
75 }
76 None
77}
78
79fn show_ticket(
80 t: &ticket::Ticket,
81 content: &str,
82 root: &Path,
83 branch: &str,
84 rel_path: &str,
85 edit: bool,
86) -> Result<()> {
87 if !edit {
88 print_ticket(t);
89 return Ok(());
90 }
91
92 let id = &t.frontmatter.id;
93 let tmp_path = std::env::temp_dir().join(format!("apm-{id}.md"));
94 std::fs::write(&tmp_path, content)?;
95
96 if let Err(e) = crate::editor::open(&tmp_path) {
97 let _ = std::fs::remove_file(&tmp_path);
98 return Err(e);
99 }
100
101 let edited = std::fs::read_to_string(&tmp_path)?;
102 let _ = std::fs::remove_file(&tmp_path);
103
104 if edited != content {
105 git::commit_to_branch(root, branch, rel_path, &edited, &format!("ticket({id}): edit"))?;
106 }
107
108 Ok(())
109}
110
111fn show_ticket_readonly(t: &ticket::Ticket, _content: &str, edit: bool) -> Result<()> {
112 if edit {
113 bail!("--edit is not supported for archived tickets (no active branch)");
114 }
115 print_ticket(t);
116 Ok(())
117}
118
119fn print_ticket(t: &ticket::Ticket) {
120 let fm = &t.frontmatter;
121 println!("{} — {}", fm.id, fm.title);
122 println!("state: {}", fm.state);
123 println!("priority: {} effort: {} risk: {}", fm.priority, fm.effort, fm.risk);
124 if let Some(b) = &fm.branch { println!("branch: {b}"); }
125 if let Some(e) = &fm.epic { println!("epic: {e}"); }
126 if let Some(tb) = &fm.target_branch { println!("target_branch: {tb}"); }
127 if let Some(deps) = &fm.depends_on {
128 if !deps.is_empty() {
129 println!("depends_on: {}", deps.join(", "));
130 }
131 }
132 if let Some(o) = &fm.owner {
133 println!("owner: {o}");
134 }
135 if let Some(a) = &fm.agent {
136 println!("agent: {a}");
137 }
138 if !fm.agent_overrides.is_empty() {
139 let mut keys: Vec<&String> = fm.agent_overrides.keys().collect();
140 keys.sort();
141 let parts: Vec<String> = keys.iter()
142 .map(|k| format!("{}={}", k, fm.agent_overrides[*k]))
143 .collect();
144 println!("agent_overrides: {}", parts.join(", "));
145 }
146 println!();
147 print!("{}", t.body);
148}