1use anyhow::Result;
2use apm_core::{config::Config, git, sync};
3use std::io::IsTerminal;
4use std::path::Path;
5
6pub fn run(root: &Path, offline: bool, quiet: bool, no_aggressive: bool, auto_close: bool, push_default: bool, push_refs: bool) -> Result<()> {
7 if git::detect_mid_merge_state(root).is_some() {
11 eprintln!("{}", apm_core::sync_guidance::MID_MERGE_IN_PROGRESS);
12 return Ok(());
13 }
14
15 let config = Config::load(root)?;
16 let aggressive = config.sync.aggressive && !no_aggressive;
17 let is_tty = std::io::stdin().is_terminal();
18
19 if !offline {
20 let mut sync_warnings: Vec<String> = Vec::new();
21 crate::util::fetch_if_aggressive(root, true);
22
23 let ahead_refs = git::sync_non_checked_out_refs(root, &mut sync_warnings);
24 let default_is_ahead = git::sync_default_branch(root, &config.project.default_branch, &mut sync_warnings);
25
26 if default_is_ahead {
28 let should_push = push_default || (is_tty && !quiet && {
29 let prompt = format!("push {} to origin now? [y/N] ", config.project.default_branch);
32 crate::util::prompt_yes_no(&prompt)?
33 });
34 if should_push {
35 sync_warnings.retain(|w| !w.contains(&config.project.default_branch) || !w.contains("ahead"));
37 if let Err(e) = git::push_branch(root, &config.project.default_branch) {
38 eprintln!("warning: push failed: {e:#}");
39 } else if !quiet {
40 println!("pushed {} to origin", config.project.default_branch);
41 }
42 }
43 }
44
45 if !ahead_refs.is_empty() {
47 let n = ahead_refs.len();
48 let should_push = push_refs || (is_tty && !quiet && {
49 let prompt = format!("push {n} ahead branch{} to origin now? [y/N] ", if n == 1 { "" } else { "es" });
50 crate::util::prompt_yes_no(&prompt)?
51 });
52 if should_push {
53 for branch in &ahead_refs {
54 if let Err(e) = git::push_branch(root, branch) {
55 eprintln!("warning: push {branch} failed: {e:#}");
56 }
57 }
58 if !quiet {
59 println!("pushed {n} ahead branch{} to origin", if n == 1 { "" } else { "es" });
60 }
61 }
62 }
63
64 for w in &sync_warnings {
65 eprintln!("{w}");
66 }
67 }
68
69 let candidates = sync::detect(root, &config)?;
70
71 let branches = git::ticket_branches(root)?;
72 if !quiet {
73 println!(
74 "sync: {} ticket branch{} visible",
75 branches.len(),
76 if branches.len() == 1 { "" } else { "es" },
77 );
78 }
79
80 if !candidates.close.is_empty() {
81 let confirmed = auto_close || (!quiet && prompt_close(&candidates.close)?);
82 if confirmed {
83 let caller = apm_core::config::resolve_caller_name();
84 let actor = format!("{}(apm-sync)", caller);
85 let apply_out = sync::apply(root, &config, &candidates, &actor, aggressive)?;
86 for (id, err) in &apply_out.failed {
87 eprintln!("warning: could not close {id:?}: {err}");
88 }
89 for msg in &apply_out.messages {
90 println!("{msg}");
91 }
92 }
93 }
94
95 Ok(())
96}
97
98fn prompt_close(candidates: &[sync::CloseCandidate]) -> Result<bool> {
99 println!("\nTickets ready to close:");
100 for c in candidates {
101 println!(" #{} {} ({})", c.ticket.frontmatter.id, c.ticket.frontmatter.title, c.reason);
102 }
103 Ok(crate::util::prompt_yes_no("\nClose all? [y/N] ")?)
104}