use anyhow::{Context, Result};
use clap::Args;
use stkd_core::Repository;
use stkd_provider_api::MergeMethod;
use crate::output;
use crate::provider_context::ProviderContext;
#[derive(Args)]
pub struct LandArgs {
#[arg(long, default_value = "squash")]
method: String,
#[arg(long, short)]
stack: bool,
#[arg(long, default_value = "true")]
delete_local: bool,
#[arg(long)]
no_sync: bool,
#[arg(long, short = 'y')]
yes: bool,
#[arg(long)]
dry_run: bool,
}
pub async fn execute(args: LandArgs) -> Result<()> {
let repo = Repository::open(".")?;
let current = repo.current_branch()?.ok_or_else(|| {
anyhow::anyhow!("Not on a branch")
})?;
if !repo.storage().is_tracked(¤t) {
anyhow::bail!(
"Branch '{}' is not tracked. Run 'gt track' first.",
current
);
}
let graph = repo.load_graph()?;
let info = repo.storage()
.load_branch(¤t)?
.context("Branch info not found")?;
let mr_number = info.merge_request_id.ok_or_else(|| {
anyhow::anyhow!("No MR found for branch '{}'. Run 'gt submit' first.", current)
})?;
let merge_method = match args.method.to_lowercase().as_str() {
"merge" => MergeMethod::Merge,
"squash" => MergeMethod::Squash,
"rebase" => MergeMethod::Rebase,
"ff" | "fast-forward" => MergeMethod::FastForward,
_ => anyhow::bail!("Invalid merge method: {}. Use 'merge', 'squash', 'rebase', or 'ff'.", args.method),
};
let ctx = ProviderContext::from_repo(&repo).await?;
let branches_to_land: Vec<(String, u64)> = if args.stack {
let mut to_land = Vec::new();
let ancestors = graph.ancestors(¤t);
for ancestor in ancestors.into_iter().rev() {
if let Some(info) = repo.storage().load_branch(ancestor)? {
if let Some(mr_num) = info.merge_request_id {
to_land.push((ancestor.to_string(), mr_num));
}
}
}
to_land.push((current.clone(), mr_number));
to_land
} else {
vec![(current.clone(), mr_number)]
};
if args.dry_run {
output::info("Dry run - showing what would be done:");
output::info("");
output::info("Branches to land:");
for (branch, mr_num) in &branches_to_land {
output::info(&format!(" {} Merge MR #{} for {}", output::ARROW, mr_num, branch));
}
output::info(&format!("\nMerge method: {}", args.method));
output::info(&format!("Provider: {}", ctx.provider_type));
if args.delete_local {
output::info("Will delete local branches after landing");
}
if !args.no_sync {
output::info(&format!("Will sync and switch to {}", repo.trunk()));
}
output::info("");
output::hint("Run without --dry-run to execute");
return Ok(());
}
if !args.yes {
output::info("Branches to land:");
for (branch, mr_num) in &branches_to_land {
output::info(&format!(" {} {} (MR #{})", output::ARROW, branch, mr_num));
}
output::info(&format!("\nMerge method: {}", args.method));
output::info(&format!("Provider: {}", ctx.provider_type));
if !output::confirm("Proceed with landing?") {
output::info("Aborted.");
return Ok(());
}
}
for (branch, mr_num) in &branches_to_land {
output::info(&format!("Landing {} (MR #{})...", branch, mr_num));
match ctx.provider().merge_mr(&ctx.repo_id, (*mr_num).into(), merge_method).await {
Ok(result) => {
if result.merged {
output::success(&format!("Merged MR #{} for {}", mr_num, branch));
} else {
output::warn(&format!("MR #{} was not merged: {}", mr_num, result.message));
continue;
}
}
Err(e) => {
output::error(&format!("Failed to merge MR #{}: {}", mr_num, e));
anyhow::bail!("Landing failed at branch '{}'", branch);
}
}
if args.delete_local && *branch != current {
output::info(&format!(" {} Deleting local branch {}...", output::ARROW, branch));
if let Err(e) = repo.delete_branch(branch, true) {
output::warn(&format!("Failed to delete local branch {}: {}", branch, e));
}
}
}
if !args.no_sync {
output::info("Syncing with remote...");
let status = std::process::Command::new("git")
.args(["fetch", "origin"])
.status()
.context("Failed to run git fetch")?;
if !status.success() {
output::warn("Failed to fetch from remote");
}
if branches_to_land.iter().any(|(b, _)| b == ¤t) {
output::info(&format!("Switching to {}...", repo.trunk()));
repo.checkout(repo.trunk())?;
let status = std::process::Command::new("git")
.args(["pull", "--ff-only"])
.status()
.context("Failed to run git pull")?;
if !status.success() {
output::warn("Failed to pull trunk");
}
if args.delete_local {
output::info(&format!("Deleting local branch {}...", current));
let _ = std::process::Command::new("git")
.args(["branch", "-D", ¤t])
.status();
let _ = repo.untrack_branch(¤t);
}
}
}
output::success("Landing complete!");
let children = graph.children(¤t);
if !children.is_empty() {
output::hint(&format!(
"Child branches ({}) may need restacking. Run 'gt sync'",
children.join(", ")
));
}
Ok(())
}