ef0b83a
use git::context::facade::GitContext;
use models::issue::issue_event_line::{self, IssueEventType};
use models::issue::issue_relation_pull;
use models::pull_request::pull_requests;
use sea_orm::ActiveValue::Set;
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use time::OffsetDateTime;
use utoipa::ToSchema;
use uuid::Uuid;
use crate::errors::PullError;
use crate::services::pulls::PullDto;
use crate::services::service::PullService;
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct LinkIssueCmd {
/// PR 编号或 UUID。
pub pull_ref: String,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct IssuePullLinkDto {
/// 关联行主键(issue_relation_pull.id),解除关联时作为路径参数回传。
pub relation_id: Uuid,
/// 建立关联的时间。
pub linked_at: OffsetDateTime,
pub pull: PullDto,
}
impl PullService {
/// 关联/解绑工单时必须对目标 PR 所在仓库有写权限,且工单与仓库同属一个空间。
pub async fn link_issue(
&self,
ctx: &GitContext,
issue_id: Uuid,
caller: i64,
pull_ref: &str,
) -> Result<pull_requests::Model, PullError> {
let pull = crate::share::lookup::find_pull(self, ctx.model.id, pull_ref).await?;
let existing = issue_relation_pull::Entity::find()
.filter(issue_relation_pull::Column::IssueId.eq(issue_id))
.filter(issue_relation_pull::Column::PullRequestId.eq(pull.id))
.one(self.db_read())
.await?;
if existing.is_none() {
let now = OffsetDateTime::now_utc();
issue_relation_pull::ActiveModel {
id: Set(Uuid::now_v7()),
issue_id: Set(issue_id),
pull_request_id: Set(pull.id),
created_at: Set(now),
}
.insert(self.db_write())
.await?;
// 关联也是一条活动记录:与「添加了标签」同级的 issue 事件。
self.write_pull_event(issue_id, caller, IssueEventType::PullLinked, &pull)
.await?;
}
Ok(pull)
}
pub async fn unlink_issue(
&self,
ctx: &GitContext,
issue_id: Uuid,
relation_id: Uuid,
caller: i64,
) -> Result<(), PullError> {
let relation = issue_relation_pull::Entity::find_by_id(relation_id)
.filter(issue_relation_pull::Column::IssueId.eq(issue_id))
.one(self.db_read())
.await?;
let deleted = issue_relation_pull::Entity::delete_many()
.filter(issue_relation_pull::Column::Id.eq(relation_id))
.filter(issue_relation_pull::Column::IssueId.eq(issue_id))
.exec(self.db_write())
.await?;
if deleted.rows_affected == 0 {
return Err(PullError::NotFound);
}
if let Some(relation) = relation
&& let Some(pull) = pull_requests::Entity::find_by_id(relation.pull_request_id)
.one(self.db_read())
.await?
{
self.write_pull_event(issue_id, caller, IssueEventType::PullUnlinked, &pull)
.await?;
}
let _ = ctx;
Ok(())
}
/// 写一条「关联/解除关联合并请求」的 issue 事件;事件文本为 `#<编号> <标题>`。
async fn write_pull_event(
&self,
issue_id: Uuid,
caller: i64,
event_type: IssueEventType,
pull: &pull_requests::Model,
) -> Result<(), PullError> {
let title = pull.title.trim();
let clipped: String = title.chars().take(80).collect();
let label = format!(
"#{} {}{}",
pull.autoincrement_id,
clipped,
if title.chars().count() > 80 {
"…"
} else {
""
}
);
let linked = matches!(event_type, IssueEventType::PullLinked);
issue_event_line::ActiveModel {
id: Set(Uuid::now_v7()),
issue_id: Set(Some(issue_id)),
project_id: Set(None),
user_id: Set(caller),
event_type: Set(event_type),
comment_id: Set(None),
commit_sha: Set(None),
old_title: Set(None),
new_title: Set(None),
old_ref: Set((!linked).then(|| label.clone())),
new_ref: Set(linked.then(|| label.clone())),
content: Set(Some(label)),
created_at: Set(OffsetDateTime::now_utc()),
}
.insert(self.db_write())
.await?;
Ok(())
}
pub async fn list_linked_pulls(
&self,
issue_id: Uuid,
space_id: Uuid,
) -> Result<Vec<IssuePullLinkDto>, PullError> {
let links = issue_relation_pull::Entity::find()
.filter(issue_relation_pull::Column::IssueId.eq(issue_id))
.order_by_asc(issue_relation_pull::Column::CreatedAt)
.all(self.db_read())
.await?;
if links.is_empty() {
return Ok(Vec::new());
}
let ids: Vec<Uuid> = links.iter().map(|link| link.pull_request_id).collect();
let rows = pull_requests::Entity::find()
.filter(pull_requests::Column::Id.is_in(ids))
.all(self.db_read())
.await?;
let mut visible: Vec<pull_requests::Model> = Vec::with_capacity(rows.len());
for row in rows {
let repo = models::repo::repos::Entity::find_by_id(row.repo_id)
.one(self.db_read())
.await?;
let show = match repo {
None => false,
Some(repo) if !repo.is_private => true,
Some(repo) => git::share::access::space_for_dir(&self.state, repo.dir_id)
.await?
.is_some_and(|space| space.id == space_id),
};
if show {
visible.push(row);
}
}
let users = self.pull_users(&visible).await?;
let by_id: HashMap<Uuid, &pull_requests::Model> =
visible.iter().map(|row| (row.id, row)).collect();
Ok(links
.iter()
.filter_map(|link| {
by_id
.get(&link.pull_request_id)
.map(|row| IssuePullLinkDto {
relation_id: link.id,
linked_at: link.created_at,
pull: PullService::dto(row, &users),
})
})
.collect())
}
}