Skip to main content

apm/cmd/
show.rs

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            // Fallback: search tickets/ on default branch, then archive_dir if set.
42            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}