use crate::bare::GitBare; use crate::error::{GitError, GitResult}; use crate::pb::{MergeRequest, MergeResult, merge_result}; impl GitBare { pub fn merge(&self, request: MergeRequest) -> GitResult { let target_branch = request.target_branch.clone(); crate::sanitize::validate_ref_name(&target_branch)?; let source_revision = match request.source.and_then(|s| s.selector) { Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex.clone(), Some(crate::pb::object_selector::Selector::Revision(name)) => { crate::sanitize::validate_revision(&name.revision)?; name.revision.clone() } None => return Err(GitError::InvalidArgument("source is required".into())), }; tracing::info!( repo = %self.bare_dir.display(), target = %target_branch, source = %source_revision, "merging" ); let repo = self.gix_repo()?; let branch_ref = format!("refs/heads/{}", target_branch); let target_id = repo .find_reference(branch_ref.as_str()) .ok() .and_then(|mut r| r.peel_to_id().ok()) .map(|id| id.to_string()) .ok_or_else(|| GitError::RefNotFound(target_branch.clone()))?; let source_id = repo.rev_parse_single(source_revision.as_str())?.to_string(); if target_id == source_id { return Ok(MergeResult { status: merge_result::Status::MergeResultStatusAlreadyUpToDate as i32, commit: None, merge_base: Some(self.oid_to_pb(target_id)), conflicts: vec![], stats: None, message: String::from("Already up to date"), }); } let target_oid = gix::hash::ObjectId::from_hex(target_id.as_bytes()) .map_err(|e| GitError::InvalidOid(e.to_string()))?; let source_oid = gix::hash::ObjectId::from_hex(source_id.as_bytes()) .map_err(|e| GitError::InvalidOid(e.to_string()))?; let merge_base = repo .merge_base(target_oid, source_oid) .ok() .map(|id| id.to_string()); let ff_mode = request .options .as_ref() .map(|o| o.fast_forward) .unwrap_or(0); let ff_only = ff_mode == crate::pb::merge_options::FastForwardMode::MergeFastForwardModeOnly as i32; if let Some(ref base) = merge_base { if *base == target_id { let no_ff = ff_mode == crate::pb::merge_options::FastForwardMode::MergeFastForwardModeNoFf as i32; if !no_ff { self.update_branch_ref(&target_branch, &source_id, Some(&target_id), false)?; return Ok(MergeResult { status: merge_result::Status::MergeResultStatusFastForward as i32, commit: None, merge_base: Some(self.oid_to_pb(base.clone())), conflicts: vec![], stats: None, message: format!("Fast-forward to {}", source_id), }); } } if *base == source_id { return Ok(MergeResult { status: merge_result::Status::MergeResultStatusAlreadyUpToDate as i32, commit: None, merge_base: Some(self.oid_to_pb(base.clone())), conflicts: vec![], stats: None, message: String::from("Already up to date"), }); } } if ff_only { return Ok(MergeResult { status: merge_result::Status::MergeResultStatusAborted as i32, commit: None, merge_base: merge_base.map(|b| self.oid_to_pb(b)), conflicts: vec![], stats: None, message: String::from("Not possible to fast-forward"), }); } let bare = self.bare_dir.to_string_lossy().into_owned(); let result = duct::cmd( "git", [ "--git-dir", bare.as_str(), "merge-tree", "--write-tree", "--no-messages", "-z", target_id.as_str(), source_id.as_str(), ], ) .stdout_capture() .stderr_capture() .unchecked() .run()?; let stdout = String::from_utf8_lossy(&result.stdout).into_owned(); if result.status.success() { let merged_tree = stdout.trim().to_string(); let message = if !request.message.is_empty() { request.message.clone() } else { format!("Merge '{}' into {}", source_revision, target_branch) }; let parents = vec![target_id.clone(), source_id.clone()]; let commit_id = self.commit_tree( &merged_tree, &parents, &message, request.committer.as_ref(), request.committer.as_ref(), )?; self.update_branch_ref(&target_branch, &commit_id, Some(&target_id), false)?; Ok(MergeResult { status: merge_result::Status::MergeResultStatusMerged as i32, commit: Some(self.get_commit(crate::pb::GetCommitRequest { repository: request.repository, revision: Some(crate::pb::ObjectSelector { selector: Some(crate::pb::object_selector::Selector::Revision( crate::pb::ObjectName { revision: commit_id, }, )), }), include_stats: false, include_raw: false, })?), merge_base: merge_base.map(|b| self.oid_to_pb(b)), conflicts: vec![], stats: None, message, }) } else { let conflicts = stdout .split('\0') .filter(|s| !s.is_empty()) .map(|path| crate::pb::MergeConflict { path: path.to_string(), ..Default::default() }) .collect(); Ok(MergeResult { status: merge_result::Status::MergeResultStatusConflicts as i32, commit: None, merge_base: merge_base.map(|b| self.oid_to_pb(b)), conflicts, stats: None, message: String::from("Merge conflicts detected"), }) } } }