use git::context::facade::GitContext; use git::service::diffs::{ChangedPathDiffDto, DiffOptionsDto}; use git::service::merge::{MergeCommitDto, MergePlanDto}; use models::pull_request::pull_requests; use sea_orm::ActiveValue::Set; use sea_orm::{ActiveModelTrait, IntoActiveModel}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use crate::errors::PullError; use crate::services::events::{EVENT_HEAD_UPDATED, write_event}; use crate::services::service::PullService; #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct PullSummaryDto { pub head_sha: String, pub base_sha: String, pub merge_base: Option, pub mergeable: bool, pub mergeable_state: String, pub conflicts: Vec, pub commits_ahead: i64, pub commits_behind: i64, pub additions: i32, pub deletions: i32, pub files_changed: i32, } impl PullService { pub(crate) fn revisions(pull: &pull_requests::Model) -> (String, String) { ( format!("refs/heads/{}", pull.base_branch), pull.head_sha.clone(), ) } pub(crate) async fn plan_for( &self, ctx: &GitContext, pull: &pull_requests::Model, ) -> Result { let (base_ref, head) = Self::revisions(pull); Ok(self.git.merge_plan(ctx, &base_ref, &head).await?) } pub async fn pull_commits( &self, space_name: String, repo_name: String, pull: &pull_requests::Model, ) -> Result, PullError> { let (base_ref, head) = Self::revisions(pull); Ok(self .git .merge_commits(space_name, repo_name, &base_ref, &head) .await?) } pub async fn pull_files( &self, ctx: &GitContext, space_name: String, repo_name: String, pull: &pull_requests::Model, ) -> Result, PullError> { let plan = self.plan_for(ctx, pull).await?; let from = plan .merge_base .clone() .unwrap_or_else(|| plan.base_sha.clone()); Ok(self .git .diff_files( space_name, repo_name, from, plan.head_sha.clone(), DiffOptionsDto::default(), ) .await?) } pub async fn pull_patch( &self, ctx: &GitContext, space_name: String, repo_name: String, pull: &pull_requests::Model, ) -> Result { let plan = self.plan_for(ctx, pull).await?; let from = plan .merge_base .clone() .unwrap_or_else(|| plan.base_sha.clone()); Ok(self .git .diff_raw(space_name, repo_name, from, plan.head_sha.clone()) .await?) } /// 重新对齐 head/base 快照并重算行数与冲突状态。 pub async fn refresh_pull( &self, space_name: String, repo_name: String, pull: &pull_requests::Model, actor: Option, ) -> Result<(pull_requests::Model, PullSummaryDto), PullError> { let head_ref = format!("refs/heads/{}", pull.head_branch); let head_now = match self .git .rev_parse(space_name.clone(), repo_name.clone(), head_ref) .await { Ok(sha) => Some(sha), Err(git::share::errors::GitError::NotFound) => None, Err(other) => return Err(PullError::from(other)), }; let moved = head_now.as_deref().is_some_and(|sha| sha != pull.head_sha); let head = head_now.clone().unwrap_or_else(|| pull.head_sha.clone()); let plan = self .git .merge_plan( &self .git .context(space_name.clone(), repo_name.clone()) .await?, &format!("refs/heads/{}", pull.base_branch), &head, ) .await?; let stats = self .diff_stats_for(&space_name, &repo_name, &plan, &plan.head_sha) .await?; let mut model = pull.clone().into_active_model(); model.head_sha = Set(head.clone()); model.base_sha = Set(plan.base_sha.clone()); model.additions = Set(stats.0); model.deletions = Set(stats.1); model.files_changed = Set(stats.2); if moved { model.updated_at = Set(Some(OffsetDateTime::now_utc())); } let updated = model.update(self.db_write()).await?; if moved { write_event( &self.state, self.db_write(), &updated, actor, EVENT_HEAD_UPDATED, Some(head.clone()), None, ) .await?; } Ok(( updated, PullSummaryDto { head_sha: plan.head_sha.clone(), base_sha: plan.base_sha.clone(), merge_base: plan.merge_base.clone(), mergeable: plan.clean, mergeable_state: if plan.clean { "clean".to_string() } else { "dirty".to_string() }, conflicts: plan.conflicts.clone(), commits_ahead: plan.head_only, commits_behind: plan.base_only, additions: stats.0, deletions: stats.1, files_changed: stats.2, }, )) } } impl PullService { /// 详情视图:附带实时可合并状态与领先/落后提交数。 pub async fn pull_detail( &self, space_name: String, repo_name: String, ctx: &GitContext, pull: &pull_requests::Model, ) -> Result { let users = self.pull_users(std::slice::from_ref(pull)).await?; let mut dto = PullService::dto(pull, &users); if pull.has_merged || pull.closed_at.is_some() { return Ok(dto); } match self.plan_for(ctx, pull).await { Ok(plan) => { dto.mergeable = Some(plan.clean); dto.mergeable_state = Some(if plan.clean { "clean".to_string() } else { "dirty".to_string() }); dto.conflicts = Some(plan.conflicts.clone()); dto.commits_ahead = Some(plan.head_only); dto.commits_behind = Some(plan.base_only); } Err(error) => { tracing::debug!(error = %error, "pull merge plan unavailable"); dto.mergeable = None; dto.mergeable_state = Some("unstable".to_string()); } } let _ = (space_name, repo_name); Ok(dto) } }