1use anyhow::{bail, Result};
2use apm_core::{config::{Config, LocalConfig}, git, ticket, ticket_fmt};
3use chrono::Utc;
4use std::path::Path;
5
6pub fn run(root: &Path, id_arg: &str, username: &str, no_aggressive: bool, force: bool) -> Result<()> {
7 run_inner(root, id_arg, username, no_aggressive, force, None)
8}
9
10pub fn run_inner(root: &Path, id_arg: &str, username: &str, no_aggressive: bool, force: bool, confirm_override: Option<bool>) -> Result<()> {
11 let config = Config::load(root)?;
12 let local = LocalConfig::load(root);
13 apm_core::validate::validate_owner(&config, &local, username)?;
14 let aggressive = config.sync.aggressive && !no_aggressive;
15 let mut tickets = ticket::load_all_from_git(root, &config.tickets.dir)?;
16 let id = ticket::resolve_id_in_slice(&tickets, id_arg)?;
17
18 if aggressive {
19 let branches = git::ticket_branches(root).unwrap_or_default();
20 if let Some(b) = branches.iter().find(|b| {
21 b.strip_prefix("ticket/")
22 .and_then(|s| s.split('-').next())
23 .map(|bid| bid == id.as_str())
24 .unwrap_or(false)
25 }) {
26 crate::util::fetch_branch_if_aggressive(root, b, aggressive);
27 }
28 }
29
30 let Some(t) = tickets.iter_mut().find(|t| t.frontmatter.id == id) else {
31 bail!("ticket {id:?} not found");
32 };
33
34 if force {
35 let is_terminal = config.workflow.states.iter()
36 .find(|s| s.id == t.frontmatter.state)
37 .map(|s| s.terminal)
38 .unwrap_or(false);
39 if is_terminal {
40 bail!("cannot change owner of a closed ticket");
41 }
42 if let Some(current_owner) = &t.frontmatter.owner.clone() {
43 let confirmed = match confirm_override {
44 Some(b) => b,
45 None => crate::util::prompt_yes_no(&format!(
46 "Ticket {id} is currently owned by {current_owner}. Reassign to {username}? [y/N] "
47 ))?
48 };
49 if !confirmed {
50 println!("aborted");
51 return Ok(());
52 }
53 }
54 } else {
55 ticket::check_owner(root, t)?;
56 }
57
58 ticket::set_field(&mut t.frontmatter, "owner", username)?;
59 t.frontmatter.updated_at = Some(Utc::now());
60
61 let content = t.serialize()?;
62 let rel_path = format!(
63 "{}/{}",
64 config.tickets.dir.to_string_lossy(),
65 t.path.file_name().unwrap().to_string_lossy()
66 );
67 let branch = t
68 .frontmatter
69 .branch
70 .clone()
71 .or_else(|| ticket_fmt::branch_name_from_path(&t.path))
72 .unwrap_or_else(|| format!("ticket/{id}"));
73
74 let commit_msg = if username == "-" {
75 format!("ticket({id}): assign owner = -")
76 } else {
77 format!("ticket({id}): assign owner = {username}")
78 };
79
80 git::commit_to_branch(root, &branch, &rel_path, &content, &commit_msg)?;
81
82 if aggressive {
83 if let Err(e) = git::push_branch(root, &branch) {
84 eprintln!("warning: push failed: {e:#}");
85 }
86 }
87
88 if username == "-" {
89 println!("{id}: owner cleared");
90 } else {
91 println!("{id}: owner = {username}");
92 }
93 Ok(())
94}