Skip to content
Clone
27fffe3
216 lines 6.9 KB Rust
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::{ActiveModelTrait, ActiveValue::Set, IntoActiveModel};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::errors::PullError;
use crate::services::events::EVENT_HEAD_UPDATED;
use crate::services::events::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<String>,
pub mergeable: bool,
pub mergeable_state: String,
pub conflicts: Vec<String>,
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<MergePlanDto, PullError> {
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<Vec<MergeCommitDto>, 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<Vec<ChangedPathDiffDto>, 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<String, 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_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<i64>,
) -> 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.db_write(),
self.state.events_enabled,
&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<crate::services::pulls::PullDto, PullError> {
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)
}
}