use crate::core::merge::{find_merge_base, is_ancestor, merge_trees, MergeStrategy};
use crate::core::{
find_repo_root, get_current_branch, read_head, read_ref, write_ref, Commit, Object, ObjectHash,
};
use crate::response::MergeResponse;
use crate::storage::ObjectStore;
use std::str::FromStr;
pub fn execute(
branch: String,
strategy: Option<String>,
) -> Result<MergeResponse, crate::errors::LitError> {
let repo_root = find_repo_root()?;
let store = ObjectStore::new(&repo_root);
let strategy = match &strategy {
Some(s) => MergeStrategy::from_str(s)?,
None => MergeStrategy::Recursive,
};
let head_hash_str = read_head(&repo_root)?;
let head_hash = ObjectHash::from_hex(head_hash_str);
let target_hash_str = read_ref(&repo_root, &format!("heads/{}", branch))
.or_else(|_| read_ref(&repo_root, &format!("remotes/{}", branch)))?;
let target_hash = ObjectHash::from_hex(target_hash_str);
if head_hash.to_string() == target_hash.to_string() {
return Ok(MergeResponse {
merged: true,
fast_forward: false,
commit_hash: None,
message: "Already up to date.".to_string(),
has_conflicts: false,
file_results: vec![],
strategy: format!("{:?}", strategy).to_lowercase(),
});
}
if is_ancestor(&store, &head_hash, &target_hash)? {
let current_branch = get_current_branch(&repo_root).unwrap_or_else(|_| "main".to_string());
write_ref(
&repo_root,
&format!("heads/{}", current_branch),
target_hash.as_str(),
)?;
return Ok(MergeResponse {
merged: true,
fast_forward: true,
commit_hash: Some(target_hash.to_string()),
message: format!("Fast-forward merge to {}", target_hash.short()),
has_conflicts: false,
file_results: vec![],
strategy: format!("{:?}", strategy).to_lowercase(),
});
}
if is_ancestor(&store, &target_hash, &head_hash)? {
return Ok(MergeResponse {
merged: true,
fast_forward: false,
commit_hash: None,
message: "Already up to date.".to_string(),
has_conflicts: false,
file_results: vec![],
strategy: format!("{:?}", strategy).to_lowercase(),
});
}
let merge_base = find_merge_base(&store, &head_hash, &target_hash)?;
let ours_tree = get_commit_tree(&store, &head_hash)?;
let theirs_tree = get_commit_tree(&store, &target_hash)?;
let base_tree = match &merge_base {
Some(base_hash) => Some(get_commit_tree(&store, base_hash)?),
None => None,
};
let merge_result = merge_trees(
&store,
base_tree.as_ref(),
&ours_tree,
&theirs_tree,
strategy,
)?;
let file_results: Vec<crate::response::FileMergeInfo> = merge_result
.file_results
.iter()
.map(|r| crate::response::FileMergeInfo {
path: r.path.clone(),
status: format!("{:?}", r.status).to_lowercase(),
conflict_count: r.conflicts.len(),
})
.collect();
if merge_result.has_conflicts && strategy == MergeStrategy::Recursive {
let conflict_dir = repo_root.join(".lit").join("merge");
std::fs::create_dir_all(&conflict_dir)
.map_err(|e| format!("Failed to create merge state dir: {}", e))?;
let conflict_data = serde_json::to_string_pretty(&merge_result.file_results)
.map_err(|e| format!("Failed to serialize conflicts: {}", e))?;
std::fs::write(conflict_dir.join("conflicts.json"), conflict_data)
.map_err(|e| format!("Failed to write conflict state: {}", e))?;
std::fs::write(conflict_dir.join("MERGE_HEAD"), target_hash.to_string())
.map_err(|e| format!("Failed to write MERGE_HEAD: {}", e))?;
let conflict_count: usize = merge_result
.file_results
.iter()
.filter(|r| r.status == crate::core::merge::FileMergeStatus::Conflict)
.count();
return Ok(MergeResponse {
merged: false,
fast_forward: false,
commit_hash: None,
message: format!(
"CONFLICT: Merge of '{}' has {} conflicting file(s). Use 'lit resolve' to resolve.",
branch, conflict_count
),
has_conflicts: true,
file_results,
strategy: format!("{:?}", strategy).to_lowercase(),
});
}
let tree_hash = merge_result.tree.ok_or("Merge failed to produce a tree")?;
let current_branch = get_current_branch(&repo_root).unwrap_or_else(|_| "main".to_string());
let author = std::env::var("USER")
.or_else(|_| std::env::var("USERNAME"))
.unwrap_or_else(|_| "Unknown".to_string());
let commit = Commit::new(
tree_hash,
vec![head_hash, target_hash],
author,
format!("Merge branch '{}'", branch),
);
let commit_obj = Object::Commit(commit);
let commit_hash = store.write(&commit_obj)?;
write_ref(
&repo_root,
&format!("heads/{}", current_branch),
commit_hash.as_str(),
)?;
let merge_dir = repo_root.join(".lit").join("merge");
if merge_dir.exists() {
let _ = std::fs::remove_dir_all(&merge_dir);
}
Ok(MergeResponse {
merged: true,
fast_forward: false,
commit_hash: Some(commit_hash.to_string()),
message: format!("Merge branch '{}' ({})", branch, commit_hash.short()),
has_conflicts: false,
file_results,
strategy: format!("{:?}", strategy).to_lowercase(),
})
}
fn get_commit_tree(
store: &ObjectStore,
commit_hash: &ObjectHash,
) -> Result<crate::core::Tree, crate::errors::LitError> {
let commit = match store.read(commit_hash)? {
Object::Commit(c) => c,
_ => return Err(format!("Expected commit object for {}", commit_hash).into()),
};
match store.read(&commit.tree)? {
Object::Tree(t) => Ok(t),
_ => Err(format!("Expected tree object for {}", commit.tree).into()),
}
}