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            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            // Fallback: search tickets/ on default branch, then archive_dir if set.
28            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    println!();
136    print!("{}", t.body);
137}