use crate::rpcgit::conflict::types::ListConflictOptions; use serde::{Deserialize, Serialize}; use crate::context::facade::GitContext; use crate::service::GitService; use crate::service::cache::{self, IMMUTABLE_TTL}; use crate::share::errors::{GitError, map_baby}; #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct MergeStatusDto { pub mergeable: bool, pub conflicts: Vec, pub fast_forwardable: bool, } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct ConflictDetailDto { pub path: String, pub content: Option, } impl GitService { async fn resolve_pair( &self, ctx: &crate::context::facade::GitContext, ours: &str, theirs: &str, ) -> Result<(String, String), GitError> { let baby = self.baby(ctx); let our_oid = baby.rev_parse(ours).await.map_err(map_baby)?; let their_oid = baby.rev_parse(theirs).await.map_err(map_baby)?; Ok((our_oid.to_string(), their_oid.to_string())) } pub async fn conflict_check( &self, ctx: &GitContext, base: String, head: String, ) -> Result { let (our_oid, their_oid) = self.resolve_pair(&ctx, &base, &head).await?; let key = format!( "repo.{}.conflict_check.{}.{}", ctx.model.id, our_oid, their_oid ); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { let baby = self.baby(&ctx); let fast_forwardable = baby.is_ancestor(&base, &head).await.map_err(map_baby)?; let files = baby .list_conflict_files( &our_oid, &their_oid, &ListConflictOptions { allow_tree_conflicts: false, skip_content: true, }, ) .await .map_err(map_baby)?; let conflicts: Vec = files .iter() .filter_map(|f| { f.header .our_path .clone() .map(|p| p.to_string_lossy().into_owned()) .or_else(|| { f.header .their_path .clone() .map(|p| p.to_string_lossy().into_owned()) }) }) .collect(); Ok(MergeStatusDto { mergeable: conflicts.is_empty(), fast_forwardable, conflicts, }) }) .await } pub async fn conflict_detail( &self, ctx: &GitContext, base: String, head: String, path: String, ) -> Result { let (our_oid, their_oid) = self.resolve_pair(&ctx, &base, &head).await?; let key = format!( "repo.{}.conflict_detail.{}.{}.{}", ctx.model.id, our_oid, their_oid, path ); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { let baby = self.baby(&ctx); let files = baby .list_conflict_files( &our_oid, &their_oid, &ListConflictOptions { allow_tree_conflicts: false, skip_content: false, }, ) .await .map_err(map_baby)?; let wanted = std::path::Path::new(&path); let file = files.into_iter().find(|f| { f.header.our_path.as_deref() == Some(wanted) || f.header.their_path.as_deref() == Some(wanted) }); match file { Some(f) => Ok(ConflictDetailDto { path, content: f.content, }), None => Err(GitError::NotFound), } }) .await } pub async fn conflict_check_threeway( &self, ctx: &GitContext, ours: String, theirs: String, ancestor: String, ) -> Result { let (our_oid, their_oid) = self.resolve_pair(&ctx, &ours, &theirs).await?; let ancestor_oid = self .baby(&ctx) .rev_parse(&ancestor) .await .map_err(map_baby)? .to_string(); let key = format!( "repo.{}.conflict_threeway.{}.{}.{}", ctx.model.id, our_oid, their_oid, ancestor_oid ); cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async { let baby = self.baby(&ctx); let merged = baby .merge_tree(&our_oid, &their_oid, Some(&ancestor_oid), false) .await .map_err(map_baby)?; let fast_forwardable = baby.is_ancestor(&ours, &theirs).await.map_err(map_baby)?; let conflicts: Vec = merged .conflict_files .iter() .map(|p| p.to_string_lossy().into_owned()) .collect(); Ok(MergeStatusDto { mergeable: !merged.has_conflicts, conflicts, fast_forwardable, }) }) .await } }