use api::users::UserDto; use models::pull_request::{pull_request_review, pull_request_reviewer, pull_requests}; use sea_orm::ActiveValue::Set; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, TransactionTrait, }; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use utoipa::ToSchema; use uuid::Uuid; use crate::errors::PullError; use crate::services::events::{EVENT_REVIEW_REQUEST_CANCELLED, EVENT_REVIEW_REQUESTED}; use crate::services::reviews; use crate::services::service::PullService; use crate::share::authz; use models::pull_request::pull_request_review::ReviewState; use models::pull_request::pull_request_reviewer::ReviewerState; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ReviewerOutcome { Approved, ChangesRequested, Commented, Dismissed, Requested, } impl ReviewerOutcome { pub fn from_state(state: ReviewState) -> Self { match state { ReviewState::Approved => Self::Approved, ReviewState::ChangesRequested => Self::ChangesRequested, ReviewState::Commented => Self::Commented, ReviewState::Dismissed => Self::Dismissed, ReviewState::Pending => Self::Requested, } } fn to_reviewer_state(self) -> ReviewerState { match self { Self::Approved => ReviewerState::Approved, Self::ChangesRequested => ReviewerState::ChangesRequested, Self::Commented => ReviewerState::Commented, Self::Dismissed => ReviewerState::Dismissed, Self::Requested => ReviewerState::Requested, } } } #[derive(Debug, Clone, Deserialize, ToSchema)] pub struct RequestReviewersCmd { /// 用户名或 UUID。 pub reviewers: Vec, /// 是否计入必需审批人数。 #[serde(default = "default_required")] pub required: bool, } fn default_required() -> bool { true } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct PullReviewerDto { pub id: Uuid, pub user: Option, pub state: ReviewerState, pub required: bool, pub review_id: Option, pub requested_by: Option, pub created_at: OffsetDateTime, pub updated_at: Option, } impl PullService { pub async fn request_reviewers( &self, pull: &pull_requests::Model, caller: i64, cmd: RequestReviewersCmd, ) -> Result, PullError> { authz::require_open(pull)?; if cmd.reviewers.is_empty() { return Err(PullError::BadRequest( "reviewers must not be empty".to_string(), )); } let mut added: Vec = Vec::new(); for reference in &cmd.reviewers { let uid = self.actors.resolve(reference).await?; if uid == pull.author_id { return Err(PullError::BadRequest( "the pull request author cannot be a requested reviewer".to_string(), )); } added.push(uid); } let required = cmd.required; let event_pull = pull.clone(); let this = self.clone(); self.db_write() .transaction::<_, (), PullError>(move |tx| { Box::pin(async move { for uid in &added { let existing = pull_request_reviewer::Entity::find() .filter(pull_request_reviewer::Column::PullRequestId.eq(event_pull.id)) .filter(pull_request_reviewer::Column::UserId.eq(*uid)) .one(tx) .await?; match existing { Some(row) => { let mut model: pull_request_reviewer::ActiveModel = row.into(); model.state = Set(ReviewerState::Requested); model.required = Set(required); model.requested_by_id = Set(caller); model.review_id = Set(None); model.updated_at = Set(Some(OffsetDateTime::now_utc())); model.update(tx).await?; } None => { pull_request_reviewer::ActiveModel { id: Set(Uuid::now_v7()), pull_request_id: Set(event_pull.id), user_id: Set(*uid), state: Set(ReviewerState::Requested), required: Set(required), review_id: Set(None), requested_by_id: Set(caller), created_at: Set(OffsetDateTime::now_utc()), updated_at: Set(None), } .insert(tx) .await?; } } } this.record_event_with( tx, &event_pull, Some(caller), EVENT_REVIEW_REQUESTED, None, Some( added .iter() .map(|id| id.to_string()) .collect::>() .join(","), ), ) .await?; Ok(()) }) }) .await?; self.list_reviewers(pull.id).await } pub async fn cancel_reviewer_request( &self, pull: &pull_requests::Model, caller: i64, reviewer_id: i64, ) -> Result, PullError> { let row = pull_request_reviewer::Entity::find() .filter(pull_request_reviewer::Column::PullRequestId.eq(pull.id)) .filter(pull_request_reviewer::Column::UserId.eq(reviewer_id)) .one(self.db_read()) .await? .ok_or(PullError::NotFound)?; let event_pull = pull.clone(); let this = self.clone(); self.db_write() .transaction::<_, (), PullError>(move |tx| { Box::pin(async move { pull_request_reviewer::Entity::delete_by_id(row.id) .exec(tx) .await?; this.record_event_with( tx, &event_pull, Some(caller), EVENT_REVIEW_REQUEST_CANCELLED, None, Some(reviewer_id.to_string()), ) .await?; Ok(()) }) }) .await?; self.list_reviewers(pull.id).await } pub async fn list_reviewers(&self, pull_id: Uuid) -> Result, PullError> { let rows = pull_request_reviewer::Entity::find() .filter(pull_request_reviewer::Column::PullRequestId.eq(pull_id)) .order_by_asc(pull_request_reviewer::Column::CreatedAt) .all(self.db_read()) .await?; let users = self .actors .briefs(&reviews::unique_ids( rows.iter() .flat_map(|row| [row.user_id, row.requested_by_id]), )) .await?; Ok(rows .iter() .map(|row| PullReviewerDto { id: row.id, user: users.get(&row.user_id).cloned(), state: row.state, required: row.required, review_id: row.review_id, requested_by: users.get(&row.requested_by_id).cloned(), created_at: row.created_at, updated_at: row.updated_at, }) .collect()) } /// 评审结论落库后同步审阅请求状态。 pub async fn sync_reviewer( &self, pull_id: Uuid, user_id: i64, outcome: ReviewerOutcome, ) -> Result<(), PullError> { let latest = pull_request_review::Entity::find() .filter(pull_request_review::Column::PullRequestId.eq(pull_id)) .filter(pull_request_review::Column::UserId.eq(user_id)) .order_by_desc(pull_request_review::Column::CreatedAt) .one(self.db_read()) .await?; let row = pull_request_reviewer::Entity::find() .filter(pull_request_reviewer::Column::PullRequestId.eq(pull_id)) .filter(pull_request_reviewer::Column::UserId.eq(user_id)) .one(self.db_read()) .await?; let state = outcome.to_reviewer_state(); match row { Some(existing) => { let mut model: pull_request_reviewer::ActiveModel = existing.into(); model.state = Set(state); model.review_id = Set(latest.map(|review| review.id)); model.updated_at = Set(Some(OffsetDateTime::now_utc())); model.update(self.db_write()).await?; } None => { pull_request_reviewer::ActiveModel { id: Set(Uuid::now_v7()), pull_request_id: Set(pull_id), user_id: Set(user_id), state: Set(state), required: Set(false), review_id: Set(latest.map(|review| review.id)), requested_by_id: Set(user_id), created_at: Set(OffsetDateTime::now_utc()), updated_at: Set(None), } .insert(self.db_write()) .await?; } } Ok(()) } }