use anyhow::Result;
use apm_core::{config::Config, git, sync};
use std::io::IsTerminal;
use std::path::Path;
pub fn run(root: &Path, offline: bool, quiet: bool, no_aggressive: bool, auto_close: bool, push_default: bool, push_refs: bool) -> Result<()> {
if git::detect_mid_merge_state(root).is_some() {
eprintln!("{}", apm_core::sync_guidance::MID_MERGE_IN_PROGRESS);
return Ok(());
}
let config = Config::load(root)?;
let aggressive = config.sync.aggressive && !no_aggressive;
let is_tty = std::io::stdin().is_terminal();
if !offline {
let mut sync_warnings: Vec<String> = Vec::new();
crate::util::fetch_if_aggressive(root, true);
let ahead_refs = git::sync_non_checked_out_refs(root, &mut sync_warnings);
let default_is_ahead = git::sync_default_branch(root, &config.project.default_branch, &mut sync_warnings);
let wt_result = git::sync_checked_out_worktrees(root, &mut sync_warnings);
if !quiet {
for (wt_path, _branch) in &wt_result.fast_forwarded {
println!("fast-forwarded worktree: {}", wt_path.display());
}
}
if !quiet {
for (wt_path, branch, dirty_files) in &wt_result.skipped_dirty {
let files_list = dirty_files
.iter()
.map(|f| format!(" {f}"))
.collect::<Vec<_>>()
.join("\n");
sync_warnings.push(
apm_core::sync_guidance::WORKTREE_DIRTY_SKIP
.replace("<path>", &wt_path.display().to_string())
.replace("<branch>", branch)
.replace("<files>", &files_list),
);
}
for (wt_path, branch) in &wt_result.skipped_ahead {
sync_warnings.push(
apm_core::sync_guidance::WORKTREE_AHEAD
.replace("<path>", &wt_path.display().to_string())
.replace("<branch>", branch),
);
}
for (wt_path, branch) in &wt_result.skipped_diverged {
sync_warnings.push(
apm_core::sync_guidance::WORKTREE_DIVERGED
.replace("<path>", &wt_path.display().to_string())
.replace("<branch>", branch),
);
}
}
if default_is_ahead {
let should_push = push_default || (is_tty && !quiet && {
let prompt = format!("push {} to origin now? [y/N] ", config.project.default_branch);
crate::util::prompt_yes_no(&prompt)?
});
if should_push {
sync_warnings.retain(|w| !w.contains(&config.project.default_branch) || !w.contains("ahead"));
if let Err(e) = git::push_branch(root, &config.project.default_branch) {
eprintln!("warning: push failed: {e:#}");
} else if !quiet {
println!("pushed {} to origin", config.project.default_branch);
}
}
}
if !ahead_refs.is_empty() {
let n = ahead_refs.len();
let should_push = push_refs || (is_tty && !quiet && {
let prompt = format!("push {n} ahead branch{} to origin now? [y/N] ", if n == 1 { "" } else { "es" });
crate::util::prompt_yes_no(&prompt)?
});
if should_push {
for branch in &ahead_refs {
if let Err(e) = git::push_branch(root, branch) {
eprintln!("warning: push {branch} failed: {e:#}");
}
}
if !quiet {
println!("pushed {n} ahead branch{} to origin", if n == 1 { "" } else { "es" });
}
}
}
for w in &sync_warnings {
eprintln!("{w}");
}
let total_wt = wt_result.fast_forwarded.len()
+ wt_result.skipped_dirty.len()
+ wt_result.skipped_ahead.len()
+ wt_result.skipped_diverged.len();
if !quiet && total_wt > 0 {
let mut parts: Vec<String> = Vec::new();
let ff = wt_result.fast_forwarded.len();
if ff > 0 {
parts.push(format!(
"{ff} worktree{} fast-forwarded",
if ff == 1 { "" } else { "s" }
));
}
let dirty = wt_result.skipped_dirty.len();
if dirty > 0 {
parts.push(format!("{dirty} skipped (local changes)"));
}
let ad = wt_result.skipped_ahead.len() + wt_result.skipped_diverged.len();
if ad > 0 {
parts.push(format!("{ad} skipped (ahead/diverged)"));
}
println!("worktrees: {}", parts.join(", "));
}
}
let candidates = sync::detect(root, &config)?;
let branches = git::ticket_branches(root)?;
if !quiet {
println!(
"sync: {} ticket branch{} visible",
branches.len(),
if branches.len() == 1 { "" } else { "es" },
);
}
for hint in &candidates.hints {
eprintln!("{hint}");
}
if !candidates.close.is_empty() {
let confirmed = auto_close || (!quiet && prompt_close(&candidates.close)?);
if confirmed {
let caller = apm_core::config::resolve_caller_name();
let actor = format!("{}(apm-sync)", caller);
let apply_out = sync::apply(root, &config, &candidates, &actor, aggressive)?;
for (id, err) in &apply_out.failed {
eprintln!("warning: could not close {id:?}: {err}");
}
for msg in &apply_out.messages {
println!("{msg}");
}
}
}
Ok(())
}
fn prompt_close(candidates: &[sync::CloseCandidate]) -> Result<bool> {
println!("\nTickets ready to close:");
for c in candidates {
println!(" #{} {} ({})", c.ticket.frontmatter.id, c.ticket.frontmatter.title, c.reason);
}
Ok(crate::util::prompt_yes_no("\nClose all? [y/N] ")?)
}