use anyhow::{Context, Result};
use clap::Args;
use stkd_core::{rebase, Repository};
use std::time::Duration;
use crate::output;
use crate::provider_context::ProviderContext;
#[derive(Args)]
pub struct SyncArgs {
#[arg(long)]
no_delete: bool,
#[arg(long)]
no_restack: bool,
#[arg(long)]
no_pull: bool,
#[arg(long)]
force: bool,
#[arg(long)]
dry_run: bool,
#[arg(long, value_name = "SECONDS")]
watch: Option<Option<u64>>,
}
pub async fn execute(args: SyncArgs) -> Result<()> {
if let Some(interval) = args.watch {
let interval_secs = interval.unwrap_or(60);
output::info(&format!(
"Watch mode enabled - syncing every {} seconds",
interval_secs
));
output::hint("Press Ctrl+C to stop");
output::info("");
loop {
if let Err(e) = sync_once(&args).await {
output::error(&format!("Sync failed: {}", e));
}
output::info("");
output::info(&format!(
"Next sync in {} seconds...",
interval_secs
));
tokio::time::sleep(Duration::from_secs(interval_secs)).await;
output::info("");
}
}
sync_once(&args).await
}
async fn sync_once(args: &SyncArgs) -> Result<()> {
let repo = Repository::open(".")?;
let trunk = repo.trunk().to_string();
if args.dry_run {
output::info("Dry run - showing what would be done:");
output::info("");
if !args.no_pull {
output::info(&format!(" {} Fetch from remote and update {}", output::ARROW, trunk));
} else {
output::info(&format!(" {} Fetch from remote", output::ARROW));
}
output::info(&format!(" {} Check MR status for tracked branches", output::ARROW));
if !args.no_delete {
output::info(&format!(" {} Delete branches with merged MRs", output::ARROW));
}
if !args.no_restack {
let graph = repo.load_graph()?;
let needs_restack = graph.needs_restack(repo.git())?;
if !needs_restack.is_empty() || args.force {
output::info(&format!(" {} Restack branches that need updating:", output::ARROW));
for branch in &needs_restack {
output::info(&format!(" - {}", branch));
}
if needs_restack.is_empty() && args.force {
output::info(" (force flag set, will restack all)");
}
} else {
output::info(&format!(" {} No branches need restacking", output::ARROW));
}
}
output::info("");
output::hint("Run without --dry-run to execute");
return Ok(());
}
let current_branch = repo.current_branch()?;
let spinner = output::spinner("Fetching from remote...");
let result = std::process::Command::new("git")
.args(["fetch", "origin", "--prune"])
.output()
.context("Failed to run git fetch")?;
if !result.status.success() {
output::finish_progress_error(&spinner, "Failed to fetch from remote");
anyhow::bail!("Failed to fetch from remote");
}
output::finish_progress(&spinner, "Fetched from remote");
if !args.no_pull {
output::info(&format!("Updating {}...", trunk));
repo.checkout(&trunk)?;
let status = std::process::Command::new("git")
.args(["pull", "--ff-only", "origin", &trunk])
.status()
.context("Failed to run git pull")?;
if !status.success() {
output::warn(&format!("Could not fast-forward {}. You may need to resolve conflicts manually.", trunk));
} else {
output::success(&format!("Updated {}", trunk));
}
if let Some(ref branch) = current_branch {
if branch != &trunk {
if repo.git().find_branch(branch, git2::BranchType::Local).is_ok() {
repo.checkout(branch)?;
}
}
}
}
let mr_spinner = output::spinner("Checking MR status...");
let mut merged_branches = Vec::new();
let mut closed_branches = Vec::new();
let mut provider_connected = false;
match ProviderContext::from_repo(&repo).await {
Ok(ctx) => {
provider_connected = true;
let branches = repo.storage().list_branches()?;
for branch_info in branches {
if let Some(mr_number) = branch_info.merge_request_id {
mr_spinner.set_message(format!("Checking MR #{}...", mr_number));
match ctx.provider().get_mr(&ctx.repo_id, mr_number.into()).await {
Ok(mr) => {
match mr.state {
stkd_provider_api::MergeRequestState::Merged => {
merged_branches.push(branch_info.name.clone());
}
stkd_provider_api::MergeRequestState::Closed => {
let remote_ref = format!("refs/remotes/origin/{}", branch_info.name);
if repo.git().find_reference(&remote_ref).is_err() {
merged_branches.push(branch_info.name.clone());
} else {
closed_branches.push(branch_info.name.clone());
}
}
_ => {
}
}
}
Err(_) => {
}
}
}
}
}
Err(e) => {
output::finish_progress_error(&mr_spinner, "Could not connect to provider");
output::hint(&format!("Error: {}. Continuing without MR status update", e));
}
}
if provider_connected {
if !merged_branches.is_empty() || !closed_branches.is_empty() {
output::finish_progress(
&mr_spinner,
&format!(
"Found {} merged, {} closed MR(s)",
merged_branches.len(),
closed_branches.len()
),
);
} else {
output::finish_progress(&mr_spinner, "All MRs up to date");
}
}
if !args.no_delete && !merged_branches.is_empty() {
output::info(&format!("\nCleaning up {} merged branch(es)...", merged_branches.len()));
for branch in &merged_branches {
output::info(&format!(" {} Deleting {}...", output::ARROW, branch));
if let Err(e) = std::process::Command::new("git")
.args(["branch", "-D", branch])
.status()
{
output::warn(&format!("Failed to delete {}: {}", branch, e));
continue;
}
if let Err(e) = repo.untrack_branch(branch) {
output::warn(&format!("Failed to untrack {}: {}", branch, e));
}
output::success(&format!("Deleted {}", branch));
}
}
if !args.no_restack {
let graph = repo.load_graph()?;
let needs_restack = graph.needs_restack(repo.git())?;
if !needs_restack.is_empty() || args.force {
output::info("\nRestacking branches...");
match rebase::restack_all(repo.git(), repo.storage(), &graph) {
Ok(results) => {
for result in results {
match result {
rebase::RebaseResult::Success { branch, .. } => {
output::success(&format!("Restacked {}", branch));
}
rebase::RebaseResult::UpToDate { branch } => {
output::info(&format!(" {} {} is up to date", output::ARROW, branch));
}
rebase::RebaseResult::Conflict { branch, onto } => {
output::warn(&format!("Conflict restacking {} onto {}", branch, onto));
output::hint("Resolve conflicts and run 'gt continue'");
return Ok(());
}
}
}
}
Err(e) => {
output::error(&format!("Restack failed: {}", e));
output::hint("Run 'gt abort' to cancel or resolve manually");
return Err(e.into());
}
}
} else {
output::info("All branches are up to date");
}
}
if let Some(ref branch) = current_branch {
if !merged_branches.contains(branch) {
if repo.git().find_branch(branch, git2::BranchType::Local).is_ok() {
repo.checkout(branch)?;
}
} else {
output::info(&format!("Branch {} was merged, switching to {}", branch, trunk));
repo.checkout(&trunk)?;
}
}
output::success("Sync complete");
Ok(())
}