Skip to content
Clone
27fffe3
250 lines 8.7 KB Rust
use models::pull_request::{pull_request_review, pull_request_reviewer, pull_requests};
use sea_orm::{
ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter, QueryOrder,
};
use serde::{Deserialize, Serialize};
use space::services::spaces::UserDto;
use time::OffsetDateTime;
use utoipa::ToSchema;
use uuid::Uuid;
use crate::errors::PullError;
use crate::services::events::{
EVENT_REVIEW_REQUEST_CANCELLED, EVENT_REVIEW_REQUESTED, write_event,
};
use crate::services::reviews::unique_ids;
use crate::services::service::PullService;
use crate::share::authz::require_open;
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<String>,
/// 是否计入必需审批人数。
#[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<UserDto>,
pub state: ReviewerState,
pub required: bool,
pub review_id: Option<Uuid>,
pub requested_by: Option<UserDto>,
pub created_at: OffsetDateTime,
pub updated_at: Option<OffsetDateTime>,
}
impl PullService {
pub async fn request_reviewers(
&self,
pull: &pull_requests::Model,
caller: i64,
cmd: RequestReviewersCmd,
) -> Result<Vec<PullReviewerDto>, PullError> {
require_open(pull)?;
if cmd.reviewers.is_empty() {
return Err(PullError::BadRequest(
"reviewers must not be empty".to_string(),
));
}
let mut added: Vec<i64> = Vec::new();
for reference in &cmd.reviewers {
let uid = self.resolve_user_id(reference).await?;
if uid == pull.author_id {
return Err(PullError::BadRequest(
"the pull request author cannot be a requested reviewer".to_string(),
));
}
let existing = pull_request_reviewer::Entity::find()
.filter(pull_request_reviewer::Column::PullRequestId.eq(pull.id))
.filter(pull_request_reviewer::Column::UserId.eq(uid))
.one(self.db_read())
.await?;
match existing {
Some(row) => {
let mut model: pull_request_reviewer::ActiveModel = row.into();
model.state = Set(ReviewerState::Requested);
model.required = Set(cmd.required);
model.requested_by_id = Set(caller);
model.review_id = Set(None);
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(uid),
state: Set(ReviewerState::Requested),
required: Set(cmd.required),
review_id: Set(None),
requested_by_id: Set(caller),
created_at: Set(OffsetDateTime::now_utc()),
updated_at: Set(None),
}
.insert(self.db_write())
.await?;
}
}
added.push(uid);
}
write_event(
self.db_write(),
self.state.events_enabled,
pull,
Some(caller),
EVENT_REVIEW_REQUESTED,
None,
Some(
added
.iter()
.map(|id| id.to_string())
.collect::<Vec<_>>()
.join(","),
),
)
.await?;
self.list_reviewers(pull.id).await
}
pub async fn cancel_reviewer_request(
&self,
pull: &pull_requests::Model,
caller: i64,
reviewer_id: i64,
) -> Result<Vec<PullReviewerDto>, 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)?;
pull_request_reviewer::Entity::delete_by_id(row.id)
.exec(self.db_write())
.await?;
write_event(
self.db_write(),
self.state.events_enabled,
pull,
Some(caller),
EVENT_REVIEW_REQUEST_CANCELLED,
None,
Some(reviewer_id.to_string()),
)
.await?;
self.list_reviewers(pull.id).await
}
pub async fn list_reviewers(&self, pull_id: Uuid) -> Result<Vec<PullReviewerDto>, 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
.user_briefs(&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(())
}
}