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 if aggressive {
22 let (content, class) = git::read_from_branch_with_class(root, &branch, &rel_path)?;
23 match class {
24 git::BranchClass::Behind => {
25 eprintln!("note: local ref is behind origin — showing origin content (run `apm sync` to fast-forward)");
26 }
27 git::BranchClass::Diverged => {
28 eprintln!("warning: local ref has diverged from origin — showing local content");
29 }
30 _ => {}
31 }
32 let t = ticket::Ticket::parse(&dummy_path, &content)?;
33 show_ticket(&t, &content, root, &branch, &rel_path, edit)
34 } else {
35 let content = git::read_from_branch(root, &branch, &rel_path)?;
36 let t = ticket::Ticket::parse(&dummy_path, &content)?;
37 show_ticket(&t, &content, root, &branch, &rel_path, edit)
38 }
39 }
40 Err(_) => {
41 let default_branch = &config.project.default_branch;
43 let prefixes = ticket::id_arg_prefixes(id_arg)?;
44
45 if let Some((rel_path, content)) = find_in_dir(
46 root,
47 default_branch,
48 &config.tickets.dir.to_string_lossy(),
49 &prefixes,
50 ) {
51 let dummy_path = root.join(&rel_path);
52 let t = ticket::Ticket::parse(&dummy_path, &content)?;
53 return show_ticket_readonly(&t, &content, edit);
54 }
55
56 if let Some(archive_dir) = &config.tickets.archive_dir {
57 if let Some((rel_path, content)) = find_in_dir(
58 root,
59 default_branch,
60 &archive_dir.to_string_lossy(),
61 &prefixes,
62 ) {
63 let dummy_path = root.join(&rel_path);
64 let t = ticket::Ticket::parse(&dummy_path, &content)?;
65 return show_ticket_readonly(&t, &content, edit);
66 }
67 }
68
69 bail!("no ticket matches '{id_arg}'")
70 }
71 }
72}
73
74fn find_in_dir(
75 root: &Path,
76 branch: &str,
77 dir: &str,
78 prefixes: &[String],
79) -> Option<(String, String)> {
80 let files = git::list_files_on_branch(root, branch, dir).ok()?;
81 for rel_path in files {
82 let filename = rel_path.split('/').last().unwrap_or("");
83 let file_id = filename.split('-').next().unwrap_or("");
84 if prefixes.iter().any(|p| file_id.starts_with(p.as_str())) {
85 if let Ok(content) = git::read_from_branch(root, branch, &rel_path) {
86 return Some((rel_path, content));
87 }
88 }
89 }
90 None
91}
92
93fn show_ticket(
94 t: &ticket::Ticket,
95 content: &str,
96 root: &Path,
97 branch: &str,
98 rel_path: &str,
99 edit: bool,
100) -> Result<()> {
101 if !edit {
102 print_ticket(t);
103 return Ok(());
104 }
105
106 let id = &t.frontmatter.id;
107 let tmp_path = std::env::temp_dir().join(format!("apm-{id}.md"));
108 std::fs::write(&tmp_path, content)?;
109
110 if let Err(e) = crate::editor::open(&tmp_path) {
111 let _ = std::fs::remove_file(&tmp_path);
112 return Err(e);
113 }
114
115 let edited = std::fs::read_to_string(&tmp_path)?;
116 let _ = std::fs::remove_file(&tmp_path);
117
118 if edited != content {
119 git::commit_to_branch(root, branch, rel_path, &edited, &format!("ticket({id}): edit"))?;
120 }
121
122 Ok(())
123}
124
125fn show_ticket_readonly(t: &ticket::Ticket, _content: &str, edit: bool) -> Result<()> {
126 if edit {
127 bail!("--edit is not supported for archived tickets (no active branch)");
128 }
129 print_ticket(t);
130 Ok(())
131}
132
133fn print_ticket(t: &ticket::Ticket) {
134 let fm = &t.frontmatter;
135 println!("{} — {}", fm.id, fm.title);
136 println!("state: {}", fm.state);
137 println!("priority: {} effort: {} risk: {}", fm.priority, fm.effort, fm.risk);
138 if let Some(b) = &fm.branch { println!("branch: {b}"); }
139 if let Some(e) = &fm.epic { println!("epic: {e}"); }
140 if let Some(tb) = &fm.target_branch { println!("target_branch: {tb}"); }
141 if let Some(deps) = &fm.depends_on {
142 if !deps.is_empty() {
143 println!("depends_on: {}", deps.join(", "));
144 }
145 }
146 if let Some(o) = &fm.owner {
147 println!("owner: {o}");
148 }
149 if let Some(a) = &fm.agent {
150 println!("agent: {a}");
151 }
152 if !fm.agent_overrides.is_empty() {
153 let mut keys: Vec<&String> = fm.agent_overrides.keys().collect();
154 keys.sort();
155 let parts: Vec<String> = keys.iter()
156 .map(|k| format!("{}={}", k, fm.agent_overrides[*k]))
157 .collect();
158 println!("agent_overrides: {}", parts.join(", "));
159 }
160 println!();
161 print!("{}", t.body);
162}