use anyhow::{Context, Result, bail};
use git2::{BranchType, Repository};
use serde::{Deserialize, Serialize};
use crate::core::msg;
use crate::core::repo;
use crate::core::transaction::{self, LoomState, Rollback};
use crate::git::{self, MergeOutcome};
#[derive(Serialize, Deserialize)]
struct MergeContext {
branch_name: String,
}
pub fn run(branch: Option<String>, all: bool) -> Result<()> {
let repo = repo::open_repo()?;
let workdir = repo::require_workdir(&repo, "merge")?;
let git_dir = repo.path().to_path_buf();
let info = repo::gather_repo_info(&repo, false, 1)?;
let branch_name = match branch {
Some(name) => resolve_non_woven_branch(&repo, &info, &name)?,
None => pick_branch(&repo, &info, all)?,
};
let local_name = if branch_name.contains('/') {
let local = repo::upstream_local_branch(&branch_name);
git::branch_create(workdir, &local, &branch_name)?;
let mut local_branch = repo.find_branch(&local, BranchType::Local)?;
local_branch.set_upstream(Some(&branch_name))?;
local
} else {
branch_name.clone()
};
match crate::git::merge_no_ff(workdir, &git_dir, &local_name)? {
MergeOutcome::Completed => {
msg::success(&format!("Woven `{}` into integration branch", local_name));
}
MergeOutcome::Conflicted => {
let state = LoomState {
command: "merge".to_string(),
rollback: Rollback::default(),
context: serde_json::to_value(MergeContext {
branch_name: local_name,
})?,
};
transaction::save(&git_dir, &state)?;
transaction::warn_conflict_paused("merge");
}
}
Ok(())
}
pub fn after_continue(context: &serde_json::Value) -> anyhow::Result<()> {
let ctx: MergeContext =
serde_json::from_value(context.clone()).context("Failed to parse merge resume context")?;
msg::success(&format!(
"Woven `{}` into integration branch",
ctx.branch_name
));
Ok(())
}
fn resolve_non_woven_branch(
repo: &Repository,
info: &repo::RepoInfo,
branch_arg: &str,
) -> Result<String> {
if info.branches.iter().any(|b| b.name == branch_arg) {
bail!(
"Branch '{}' is already woven into the integration branch",
branch_arg
);
}
if repo.find_branch(branch_arg, BranchType::Local).is_ok() {
return Ok(branch_arg.to_string());
}
if repo.find_branch(branch_arg, BranchType::Remote).is_ok() {
return Ok(branch_arg.to_string());
}
bail!("Branch '{}' not found", branch_arg)
}
fn pick_branch(repo: &Repository, info: &repo::RepoInfo, include_remote: bool) -> Result<String> {
let woven_names: Vec<&str> = info.branches.iter().map(|b| b.name.as_str()).collect();
let current_branch = &info.branch_name;
let mut items: Vec<String> = Vec::new();
for branch_result in repo.branches(Some(BranchType::Local))? {
let (branch, _) = branch_result?;
if let Some(name) = branch.name()?
&& name != current_branch
&& !woven_names.contains(&name)
{
items.push(name.to_string());
}
}
if include_remote {
let local_names: std::collections::HashSet<String> = repo
.branches(Some(BranchType::Local))?
.filter_map(|b| b.ok())
.filter_map(|(b, _)| b.name().ok().flatten().map(|n| n.to_string()))
.collect();
let upstream_label = &info.upstream.label;
for branch_result in repo.branches(Some(BranchType::Remote))? {
let (branch, _) = branch_result?;
if let Some(name) = branch.name()? {
if name == upstream_label {
continue;
}
if name.ends_with("/HEAD") {
continue;
}
if !local_names.contains(&repo::upstream_local_branch(name)) {
items.push(name.to_string());
}
}
}
}
if items.is_empty() {
bail!("No branches available to merge");
}
msg::select("Select branch to weave", items)
}