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