use anyhow::{Context, Error};
use crate::git::repository::core::GitRepo;
impl GitRepo {
pub fn merge(&self, branch_name: &str, message: Option<&str>) -> Result<String, Error> {
let signature = self
.create_signature()
.context("Failed to create signature")?;
let branch_ref = format!("refs/heads/{branch_name}");
let target_obj = self
.repo()
.revparse_single(&branch_ref)
.context(format!("Failed to find branch '{branch_name}'"))?;
let target_commit = target_obj
.peel_to_commit()
.context("Failed to get target commit")?;
let head_ref = self.repo().head().context("Failed to get HEAD")?;
let head_commit = head_ref
.peel_to_commit()
.context("Failed to get current commit")?;
if head_commit.id() == target_commit.id() {
return Ok("Already up-to-date".to_string());
}
let merge_base = self
.repo()
.merge_base(head_commit.id(), target_commit.id())
.context("Failed to find merge base")?;
if merge_base == head_commit.id() {
let current_branch_name = self
.get_current_branch()
.context("Failed to get current branch")?;
let branch_ref_name = format!("refs/heads/{current_branch_name}");
self.repo()
.reference(
&branch_ref_name,
target_commit.id(),
true,
"Fast-forward merge",
)
.context("Failed to update branch reference")?;
if !self.is_bare() {
let target_tree = target_commit.tree().context("Failed to get target tree")?;
let mut checkout_opts = git2::build::CheckoutBuilder::new();
checkout_opts.force();
self.repo()
.checkout_tree(target_tree.as_object(), Some(&mut checkout_opts))
.context("Failed to checkout target tree")?;
}
Ok(format!(
"Fast-forward merge: {target_commit_id}",
target_commit_id = target_commit.id()
))
} else if merge_base == target_commit.id() {
Ok("Already up-to-date".to_string())
} else {
let head_tree = head_commit.tree().context("Failed to get HEAD tree")?;
let mut index = self.repo().index().context("Failed to get index")?;
index
.read_tree(&head_tree)
.context("Failed to read head tree")?;
let mut merge_options = git2::MergeOptions::new();
let mut checkout_opts = git2::build::CheckoutBuilder::new();
checkout_opts.conflict_style_merge(true);
let annotated_commit = self
.repo()
.find_annotated_commit(target_commit.id())
.context("Failed to create annotated commit")?;
let (analysis, _) = self
.repo()
.merge_analysis(&[&annotated_commit])
.context("Failed to analyze merge")?;
if analysis.is_up_to_date() {
Ok("Already up-to-date".to_string())
} else if analysis.is_fast_forward() {
self.repo()
.set_head_detached(target_commit.id())
.context("Failed to fast-forward merge")?;
Ok(format!(
"Fast-forward merge: {target_commit_id}",
target_commit_id = target_commit.id()
))
} else if analysis.is_normal() {
self.repo()
.merge(
&[&annotated_commit],
Some(&mut merge_options),
Some(&mut checkout_opts),
)
.context("Failed to perform merge")?;
let mut index = self
.repo()
.index()
.context("Failed to get index after merge")?;
if index.has_conflicts() {
return Err(anyhow::anyhow!(
"Merge conflicts detected. Please resolve conflicts and commit manually."
));
}
let tree_id = index.write_tree().context("Failed to write merge tree")?;
let tree = self
.repo()
.find_tree(tree_id)
.context("Failed to find merge tree")?;
let default_message = format!("Merge branch '{branch_name}'");
let commit_message = message.unwrap_or(&default_message);
let merge_commit_id = self
.repo()
.commit(
Some("HEAD"),
&signature,
&signature,
commit_message,
&tree,
&[&head_commit, &target_commit],
)
.context("Failed to create merge commit")?;
self.repo()
.cleanup_state()
.context("Failed to cleanup merge state")?;
Ok(format!("Merge commit created: {merge_commit_id}"))
} else {
Err(anyhow::anyhow!("Unsupported merge analysis result"))
}
}
}
}
#[cfg(test)]
mod tests {
use crate::test_utils::{create_test_repo, RepoAssertions, RepoTestOperations};
#[test]
fn merge_works() -> Result<(), Box<dyn std::error::Error>> {
let (_temp_dir, repo) = create_test_repo();
repo.add_file_and_commit("README.md", "initial", "Initial commit")?
.create_and_checkout_branch("feature")?
.add_file_and_commit("feature.txt", "feature content", "Add feature")?
.checkout_branch("master")?;
let result = repo.merge("feature", None).unwrap();
assert!(result.contains("Fast-forward merge") || result.contains("Merge commit created"));
repo.assert_file_exists("feature.txt");
let result = repo.merge("feature", None).unwrap();
assert_eq!(result, "Already up-to-date");
let result = repo.merge("nonexistent", None);
assert!(result.is_err());
Ok(())
}
}