Skip to main content

apm/cmd/
spec.rs

1use anyhow::{bail, Result};
2use apm_core::{config::{Config, SectionType}, git, spec, ticket, ticket_fmt};
3use std::{io::Read, path::Path};
4
5#[allow(clippy::too_many_arguments)]
6// Each argument maps to a distinct CLI flag.
7pub fn run(root: &Path, id_arg: &str, section: Option<String>, set: Option<String>, set_file: Option<String>, check: bool, mark: Option<String>, append: Option<String>, append_file: Option<String>, add_task: Option<String>, no_aggressive: bool) -> Result<()> {
8    if set.is_some() && section.is_none() { bail!("--set requires --section"); }
9    if set_file.is_some() && section.is_none() { bail!("--set-file requires --section"); }
10    if mark.is_some() && section.is_none() { bail!("--mark requires --section"); }
11    if append.is_some() && section.is_none() { bail!("--append requires --section"); }
12    if append_file.is_some() && section.is_none() { bail!("--append-file requires --section"); }
13    if add_task.is_some() && section.is_none() { bail!("--add-task requires --section"); }
14    let config = Config::load(root)?;
15    let aggressive = config.sync.aggressive && !no_aggressive;
16    let branches = git::ticket_branches(root)?;
17    let branch = ticket_fmt::resolve_ticket_branch(&branches, id_arg)?;
18    let id = branch.strip_prefix("ticket/").and_then(|s| s.split('-').next()).unwrap_or(id_arg).to_string();
19    let rel_path = format!("{}/{}.md", config.tickets.dir.to_string_lossy(), branch.trim_start_matches("ticket/"));
20
21    crate::util::fetch_branch_if_aggressive(root, &branch, aggressive);
22
23    let content = git::read_from_branch(root, &branch, &rel_path)?;
24    if let (Some(ref name), Some(ref item)) = (&section, &mark) {
25        let new = spec::mark_item(&content, name, item)?;
26        git::commit_to_branch(root, &branch, &rel_path, &new, &format!("ticket({id}): mark \"{item}\" in {name}"))?;
27        if aggressive {
28            if let Err(e) = git::push_branch(root, &branch) {
29                eprintln!("warning: push failed: {e:#}");
30            }
31        }
32        println!("ticket #{id}: marked \"{item}\" in {name:?}"); return Ok(());
33    }
34    let mut t = ticket::Ticket::parse(&root.join(&rel_path), &content)?;
35    let mut doc = t.document()?;
36    if check {
37        let errors = doc.validate(&config.ticket.sections);
38        if errors.is_empty() { println!("all required sections present"); return Ok(()); }
39        errors.iter().for_each(|e| eprintln!("{e}")); std::process::exit(1);
40    }
41    let config_active = !config.ticket.sections.is_empty();
42    let Some(ref name) = section else {
43        for (section_name, value) in &doc.sections {
44            println!("### {section_name}\n\n{value}\n");
45        }
46        return Ok(());
47    };
48    if config_active && !config.has_section(name) {
49        bail!("unknown section {:?}; not defined in [ticket.sections]", name);
50    }
51    if let Some(ref task_text) = add_task {
52        if config_active {
53            match config.find_section(name) {
54                Some(sc) if sc.type_ != SectionType::Tasks =>
55                    bail!("--add-task requires a tasks section; {:?} has type {:?}", name, sc.type_),
56                None => bail!("unknown section {:?}; not defined in [ticket.sections]", name),
57                _ => {}
58            }
59        }
60        let item = format!("- [ ] {}", task_text.trim());
61        spec::append_section(&mut doc, name, item);
62        t.body = doc.serialize();
63        git::commit_to_branch(root, &branch, &rel_path, &t.serialize()?,
64            &format!("ticket({id}): add task to {name}"))?;
65        if aggressive {
66            if let Err(e) = git::push_branch(root, &branch) {
67                eprintln!("warning: push failed: {e:#}");
68            }
69        }
70        println!("ticket #{id}: task added to {name:?}");
71        return Ok(());
72    }
73    let append_resolved = match (append, append_file) {
74        (Some(v), _) => Some(v),
75        (None, Some(path)) => Some(std::fs::read_to_string(&path)
76            .map_err(|e| anyhow::anyhow!("--append-file: {}: {e}", path))?),
77        (None, None) => None,
78    };
79    if let Some(value) = append_resolved {
80        let trimmed = value.trim().to_string();
81        let formatted = if config_active {
82            let sc = config.find_section(name).unwrap();
83            spec::apply_section_type(&sc.type_, trimmed)
84        } else {
85            trimmed
86        };
87        spec::append_section(&mut doc, name, formatted);
88        t.body = doc.serialize();
89        git::commit_to_branch(root, &branch, &rel_path, &t.serialize()?,
90            &format!("ticket({id}): append to section {name}"))?;
91        if aggressive {
92            if let Err(e) = git::push_branch(root, &branch) {
93                eprintln!("warning: push failed: {e:#}");
94            }
95        }
96        println!("ticket #{id}: section {name:?} updated");
97        return Ok(());
98    }
99    let set_resolved = match (set, set_file) {
100        (Some(v), _) => Some(v),
101        (None, Some(path)) => Some(std::fs::read_to_string(&path).map_err(|e| anyhow::anyhow!("--set-file: {}: {e}", path))?),
102        (None, None) => None,
103    };
104    if let Some(value) = set_resolved {
105        let text = if value == "-" { let mut b = String::new(); std::io::stdin().read_to_string(&mut b)?; b } else { value };
106        let trimmed = text.trim().to_string();
107        let formatted = if config_active {
108            let section_config = config.find_section(name).unwrap();
109            spec::apply_section_type(&section_config.type_, trimmed)
110        } else {
111            trimmed
112        };
113        spec::set_section(&mut doc, name, formatted);
114        t.body = doc.serialize();
115        git::commit_to_branch(root, &branch, &rel_path, &t.serialize()?, &format!("ticket({id}): set section {name}"))?;
116        if aggressive {
117            if let Err(e) = git::push_branch(root, &branch) {
118                eprintln!("warning: push failed: {e:#}");
119            }
120        }
121        println!("ticket #{id}: section {name:?} updated");
122    } else {
123        if let Some(text) = spec::get_section(&doc, name) { println!("{text}"); }
124    }
125    Ok(())
126}