gitdataai/libs/service/issue/pull_request.rs
2026-04-15 09:08:09 +08:00

209 lines
6.9 KiB
Rust

use crate::AppService;
use crate::error::AppError;
use crate::project::activity::ActivityLogParams;
use chrono::Utc;
use models::issues::{issue, issue_pull_request};
use models::projects::project_members;
use sea_orm::*;
use serde::{Deserialize, Serialize};
use session::Session;
use utoipa::ToSchema;
use uuid::Uuid;
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct IssueLinkPullRequestRequest {
pub repo: Uuid,
pub number: i64,
}
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct IssuePullRequestResponse {
pub issue: Uuid,
pub repo: Uuid,
pub number: i64,
pub relation_at: chrono::DateTime<Utc>,
}
impl AppService {
/// List pull requests linked to an issue.
pub async fn issue_pull_request_list(
&self,
project_name: String,
issue_number: i64,
ctx: &Session,
) -> Result<Vec<IssuePullRequestResponse>, AppError> {
let project = self.utils_find_project_by_name(project_name).await?;
if let Some(uid) = ctx.user() {
self.check_project_access(project.id, uid).await?;
}
let issue = issue::Entity::find()
.filter(issue::Column::Project.eq(project.id))
.filter(issue::Column::Number.eq(issue_number))
.one(&self.db)
.await?
.ok_or(AppError::NotFound("Issue not found".to_string()))?;
let prs = issue_pull_request::Entity::find()
.filter(issue_pull_request::Column::Issue.eq(issue.id))
.all(&self.db)
.await?;
let responses: Vec<IssuePullRequestResponse> = prs
.into_iter()
.map(|pr| IssuePullRequestResponse {
issue: pr.issue,
repo: pr.repo,
number: pr.number,
relation_at: pr.relation_at,
})
.collect();
Ok(responses)
}
/// Link a pull request to an issue.
pub async fn issue_pull_request_link(
&self,
project_name: String,
issue_number: i64,
request: IssueLinkPullRequestRequest,
ctx: &Session,
) -> Result<IssuePullRequestResponse, AppError> {
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
let project = self.utils_find_project_by_name(project_name).await?;
let _member = project_members::Entity::find()
.filter(project_members::Column::Project.eq(project.id))
.filter(project_members::Column::User.eq(user_uid))
.one(&self.db)
.await?
.ok_or(AppError::NoPower)?;
let issue = issue::Entity::find()
.filter(issue::Column::Project.eq(project.id))
.filter(issue::Column::Number.eq(issue_number))
.one(&self.db)
.await?
.ok_or(AppError::NotFound("Issue not found".to_string()))?;
let existing = issue_pull_request::Entity::find()
.filter(issue_pull_request::Column::Issue.eq(issue.id))
.filter(issue_pull_request::Column::Repo.eq(request.repo))
.filter(issue_pull_request::Column::Number.eq(request.number))
.one(&self.db)
.await?;
if existing.is_some() {
return Err(AppError::BadRequest(
"PR already linked to this issue".to_string(),
));
}
let now = Utc::now();
let active = issue_pull_request::ActiveModel {
issue: Set(issue.id),
repo: Set(request.repo),
number: Set(request.number),
relation_at: Set(now),
..Default::default()
};
let model = active.insert(&self.db).await?;
self.invalidate_issue_cache(project.id, issue_number).await;
let response = Ok(IssuePullRequestResponse {
issue: model.issue,
repo: model.repo,
number: model.number,
relation_at: model.relation_at,
});
let _ = self
.project_log_activity(
project.id,
Some(model.repo),
user_uid,
ActivityLogParams {
event_type: "pr_issue_link".to_string(),
title: format!(
"{} linked PR #{} to issue #{}",
user_uid, request.number, issue_number
),
repo_id: Some(model.repo),
content: None,
event_id: Some(model.issue),
event_sub_id: Some(issue_number),
metadata: Some(serde_json::json!({
"pr_number": request.number,
"pr_repo": request.repo,
})),
is_private: false,
},
)
.await;
response
}
/// Unlink a pull request from an issue.
pub async fn issue_pull_request_unlink(
&self,
project_name: String,
issue_number: i64,
repo_id: Uuid,
pr_number: i64,
ctx: &Session,
) -> Result<(), AppError> {
let user_uid = ctx.user().ok_or(AppError::Unauthorized)?;
let project = self.utils_find_project_by_name(project_name).await?;
let _member = project_members::Entity::find()
.filter(project_members::Column::Project.eq(project.id))
.filter(project_members::Column::User.eq(user_uid))
.one(&self.db)
.await?
.ok_or(AppError::NoPower)?;
let issue = issue::Entity::find()
.filter(issue::Column::Project.eq(project.id))
.filter(issue::Column::Number.eq(issue_number))
.one(&self.db)
.await?
.ok_or(AppError::NotFound("Issue not found".to_string()))?;
issue_pull_request::Entity::delete_many()
.filter(issue_pull_request::Column::Issue.eq(issue.id))
.filter(issue_pull_request::Column::Repo.eq(repo_id))
.filter(issue_pull_request::Column::Number.eq(pr_number))
.exec(&self.db)
.await?;
self.invalidate_issue_cache(project.id, issue_number).await;
let _ = self
.project_log_activity(
project.id,
Some(repo_id),
user_uid,
ActivityLogParams {
event_type: "pr_issue_unlink".to_string(),
title: format!(
"{} unlinked PR #{} from issue #{}",
user_uid, pr_number, issue_number
),
repo_id: Some(repo_id),
content: None,
event_id: Some(issue.id),
event_sub_id: Some(issue_number),
metadata: Some(serde_json::json!({
"pr_number": pr_number,
"pr_repo": repo_id,
})),
is_private: false,
},
)
.await;
Ok(())
}
}