use crate::cli::CliProgress;
use crate::cli::style::{CHECK, Stylize, arrow, check, spinner_style};
use anstream::println;
use dialoguer::Confirm;
use indicatif::ProgressBar;
use jj_ryu::error::{Error, Result};
use jj_ryu::graph::build_change_graph;
use jj_ryu::platform::{create_platform_service, parse_repo_info};
use jj_ryu::repo::{JjWorkspace, select_remote};
use jj_ryu::submit::{
SubmissionPlan, analyze_submission, create_submission_plan, execute_submission,
};
use jj_ryu::tracking::load_tracking;
use std::path::Path;
use std::time::Duration;
#[derive(Debug, Clone, Default)]
pub struct SyncOptions {
pub dry_run: bool,
pub confirm: bool,
pub all: bool,
}
#[allow(clippy::too_many_lines)]
pub async fn run_sync(path: &Path, remote: Option<&str>, options: SyncOptions) -> Result<()> {
let mut workspace = JjWorkspace::open(path)?;
let workspace_root = workspace.workspace_root().to_path_buf();
let tracking = load_tracking(&workspace_root)?;
let tracked_names: Vec<&str> = tracking.tracked_names().into_iter().collect();
if tracked_names.is_empty() && !options.all {
return Err(Error::Tracking(
"No bookmarks tracked. Run 'ryu track' first, or use 'ryu sync --all' to sync all bookmarks.".to_string()
));
}
let remotes = workspace.git_remotes()?;
let remote_name = select_remote(&remotes, remote)?;
let remote_info = remotes
.iter()
.find(|r| r.name == remote_name)
.ok_or_else(|| Error::RemoteNotFound(remote_name.clone()))?;
let platform_config = parse_repo_info(&remote_info.url)?;
let platform = create_platform_service(&platform_config).await?;
if !options.dry_run {
let spinner = ProgressBar::new_spinner();
spinner.set_style(spinner_style());
spinner.set_message(format!("Fetching from {}...", remote_name.emphasis()));
spinner.enable_steady_tick(Duration::from_millis(80));
workspace.git_fetch(&remote_name)?;
spinner.finish_with_message(format!(
"{} Fetched from {}",
check(),
remote_name.emphasis()
));
}
let graph = build_change_graph(&workspace)?;
if graph.stack.is_none() {
println!("{}", "No stack to sync".muted());
println!(
"{}",
"Create bookmarks between trunk and working copy first.".muted()
);
return Ok(());
}
let default_branch = workspace.default_branch()?;
let progress = CliProgress::compact();
let mut analysis = analyze_submission(&graph, None)?;
if !options.all && !tracked_names.is_empty() {
analysis
.segments
.retain(|s| tracked_names.contains(&s.bookmark.name.as_str()));
if analysis.segments.is_empty() {
return Err(Error::Tracking(
"No tracked bookmarks in stack. Use 'ryu track' to track bookmarks, or 'ryu sync --all'.".to_string()
));
}
}
let plan =
create_submission_plan(&analysis, platform.as_ref(), &remote_name, &default_branch).await?;
if options.confirm && !options.dry_run {
print_sync_preview(&plan);
if !Confirm::new()
.with_prompt("Proceed with sync?")
.default(true)
.interact()
.map_err(|e| Error::Internal(format!("Failed to read confirmation: {e}")))?
{
println!("{}", "Aborted".muted());
return Ok(());
}
println!();
}
println!(
"{} {}",
"Syncing stack:".emphasis(),
analysis.target_bookmark.accent()
);
let result = execute_submission(
&plan,
&mut workspace,
platform.as_ref(),
&progress,
options.dry_run,
)
.await?;
println!();
if options.dry_run {
println!("{}", "Dry run complete".muted());
} else {
println!(
"{} {} pushed, {} created, {} updated",
format!("{CHECK} Sync complete:").success(),
result.pushed_bookmarks.len().accent(),
result.created_prs.len().accent(),
result.updated_prs.len().accent()
);
}
Ok(())
}
fn print_sync_preview(plan: &SubmissionPlan) {
println!("{}:", "Sync plan".emphasis());
println!();
if plan.execution_steps.is_empty() {
println!(" {}", "Already in sync".muted());
println!();
return;
}
println!(" {}:", "Steps".emphasis());
for step in &plan.execution_steps {
println!(" {} {}", arrow(), step);
}
println!();
}