use crate::AppService; use crate::error::AppError; use chrono::Utc; use models::issues::{issue, issue_repo}; 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 IssueLinkRepoRequest { pub repo: Uuid, } #[derive(Debug, Clone, Serialize, ToSchema)] pub struct IssueRepoResponse { pub issue: Uuid, pub repo: Uuid, pub relation_at: chrono::DateTime, } impl AppService { /// List repos linked to an issue. pub async fn issue_repo_list( &self, project_name: String, issue_number: i64, ctx: &Session, ) -> Result, 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 repos = issue_repo::Entity::find() .filter(issue_repo::Column::Issue.eq(issue.id)) .all(&self.db) .await?; let responses: Vec = repos .into_iter() .map(|r| IssueRepoResponse { issue: r.issue, repo: r.repo, relation_at: r.relation_at, }) .collect(); Ok(responses) } /// Link a repo to an issue. pub async fn issue_repo_link( &self, project_name: String, issue_number: i64, request: IssueLinkRepoRequest, ctx: &Session, ) -> Result { 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_repo::Entity::find() .filter(issue_repo::Column::Issue.eq(issue.id)) .filter(issue_repo::Column::Repo.eq(request.repo)) .one(&self.db) .await?; if existing.is_some() { return Err(AppError::BadRequest( "Repo already linked to this issue".to_string(), )); } let now = Utc::now(); let active = issue_repo::ActiveModel { issue: Set(issue.id), repo: Set(request.repo), relation_at: Set(now), ..Default::default() }; let model = active.insert(&self.db).await?; self.invalidate_issue_cache(project.id, issue_number).await; Ok(IssueRepoResponse { issue: model.issue, repo: model.repo, relation_at: model.relation_at, }) } /// Unlink a repo from an issue. pub async fn issue_repo_unlink( &self, project_name: String, issue_number: i64, repo_id: Uuid, 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_repo::Entity::delete_many() .filter(issue_repo::Column::Issue.eq(issue.id)) .filter(issue_repo::Column::Repo.eq(repo_id)) .exec(&self.db) .await?; self.invalidate_issue_cache(project.id, issue_number).await; Ok(()) } }