use models::issue::issue_label; use models::pull_request::{pull_request_label, pull_requests}; use sea_orm::{ ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, }; use serde::Deserialize; use utoipa::ToSchema; use uuid::Uuid; use crate::errors::PullError; use crate::services::events::{EVENT_LABELED, EVENT_UNLABELED}; use crate::services::service::PullService; use crate::share::authz::require_unlocked; #[derive(Debug, Clone, Deserialize, ToSchema)] pub struct AttachLabelCmd { /// 标签 UUID 或名称(同一仓库目录内的标签)。 pub label: String, } impl PullService { async fn resolve_label( &self, dir_id: Uuid, reference: &str, ) -> Result { let reference = reference.trim(); let select = issue_label::Entity::find() .filter(issue_label::Column::DirId.eq(dir_id)) .filter(issue_label::Column::IsArchived.eq(false)); let row = if let Ok(id) = Uuid::parse_str(reference) { select.filter(issue_label::Column::Id.eq(id)) } else { select.filter(issue_label::Column::Name.eq(reference)) } .one(self.db_read()) .await?; row.ok_or(PullError::BadRequest(format!( "label `{reference}` not found in this repository directory" ))) } /// 可附加到本仓库合并请求的标签候选(同目录、未归档),供侧栏搜索下拉使用。 pub async fn list_label_options( &self, dir_id: Uuid, ) -> Result, PullError> { let rows = issue_label::Entity::find() .filter(issue_label::Column::DirId.eq(dir_id)) .filter(issue_label::Column::IsArchived.eq(false)) .order_by_asc(issue_label::Column::Name) .all(self.db_read()) .await?; Ok(rows .into_iter() .map(issues::services::labels::LabelDto::from) .collect()) } pub async fn list_labels( &self, pull_id: Uuid, ) -> Result, PullError> { let links = pull_request_label::Entity::find() .filter(pull_request_label::Column::PullRequestId.eq(pull_id)) .order_by_asc(pull_request_label::Column::CreatedAt) .all(self.db_read()) .await?; if links.is_empty() { return Ok(Vec::new()); } let ids: Vec = links.iter().map(|link| link.label_id).collect(); let rows = issue_label::Entity::find() .filter(issue_label::Column::Id.is_in(ids)) .all(self.db_read()) .await?; Ok(rows .into_iter() .map(issues::services::labels::LabelDto::from) .collect()) } pub async fn attach_label( &self, pull: &pull_requests::Model, dir_id: Uuid, caller: i64, cmd: AttachLabelCmd, ) -> Result, PullError> { require_unlocked(pull)?; let label = self.resolve_label(dir_id, &cmd.label).await?; let existing = pull_request_label::Entity::find() .filter(pull_request_label::Column::PullRequestId.eq(pull.id)) .filter(pull_request_label::Column::LabelId.eq(label.id)) .one(self.db_read()) .await?; if existing.is_none() { pull_request_label::ActiveModel { id: Set(Uuid::now_v7()), pull_request_id: Set(pull.id), label_id: Set(label.id), created_at: Set(OffsetDateTime::now_utc()), } .insert(self.db_write()) .await?; self.record_event( pull, Some(caller), EVENT_LABELED, None, Some(label.name.clone()), ) .await?; } self.list_labels(pull.id).await } pub async fn detach_label( &self, pull: &pull_requests::Model, dir_id: Uuid, caller: i64, label_ref: &str, ) -> Result, PullError> { require_unlocked(pull)?; let label = self.resolve_label(dir_id, label_ref).await?; let deleted = pull_request_label::Entity::delete_many() .filter(pull_request_label::Column::PullRequestId.eq(pull.id)) .filter(pull_request_label::Column::LabelId.eq(label.id)) .exec(self.db_write()) .await?; if deleted.rows_affected > 0 { self.record_event( pull, Some(caller), EVENT_UNLABELED, None, Some(label.name.clone()), ) .await?; } self.list_labels(pull.id).await } } use time::OffsetDateTime;