Skip to main content

apm/cmd/
sync.rs

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    // Bail early if the repo is mid-merge, mid-rebase, or mid-cherry-pick.
8    // Any sync work done in this state would compound the incomplete operation.
9    // Let the user resolve the pending operation first.
10    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        // Handle default branch push.
27        if default_is_ahead {
28            let should_push = push_default || (is_tty && !quiet && {
29                // Remove the MAIN_AHEAD warning we just pushed — if user says yes, we push instead.
30                // The warning was already added; we'll print it only if user says no.
31                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                // Remove the MAIN_AHEAD warning from sync_warnings since we're pushing.
36                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        // Handle ticket/epic branch push.
46        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}