use db::sqlx; use model::repos::RepoProtectModel; use serde::{Deserialize, Serialize}; use session::Session; use crate::{AppService, Pagination, error::AppError, session_user}; #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct ProtectResponse { #[schema(value_type = String)] pub id: uuid::Uuid, #[schema(value_type = String)] pub repo: uuid::Uuid, pub pattern: String, pub require_pull_request: bool, pub required_approvals: i32, pub require_status_checks: bool, pub required_status_contexts: Vec, pub enforce_admins: bool, pub allow_force_pushes: bool, pub allow_deletions: bool, #[schema(value_type = String)] pub created_at: chrono::DateTime, #[schema(value_type = String)] pub updated_at: chrono::DateTime, } pub fn protect_response(p: RepoProtectModel) -> ProtectResponse { ProtectResponse { id: p.id, repo: p.repo, pattern: p.pattern, require_pull_request: p.require_pull_request, required_approvals: p.required_approvals, require_status_checks: p.require_status_checks, required_status_contexts: p .required_status_contexts .split('.') .filter(|s| !s.is_empty()) .map(|s| s.to_string()) .collect(), enforce_admins: p.enforce_admins, allow_force_pushes: p.allow_force_pushes, allow_deletions: p.allow_deletions, created_at: p.created_at, updated_at: p.updated_at, } } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct CreateProtect { pub pattern: String, pub require_pull_request: Option, pub required_approvals: Option, pub require_status_checks: Option, pub required_status_contexts: Option>, pub enforce_admins: Option, pub allow_force_pushes: Option, pub allow_deletions: Option, } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct UpdateProtect { pub pattern: Option, pub require_pull_request: Option, pub required_approvals: Option, pub require_status_checks: Option, pub required_status_contexts: Option>, pub enforce_admins: Option, pub allow_force_pushes: Option, pub allow_deletions: Option, } impl AppService { pub async fn repo_protect_list( &self, ctx: &Session, wk_name: &str, repo_name: &str, pagination: Pagination, ) -> Result, AppError> { let repo = self.git_require_member(ctx, wk_name, repo_name).await?; let rows = sqlx::query_as::<_, RepoProtectModel>( "SELECT id, repo, pattern, require_pull_request, required_approvals, \ require_status_checks, required_status_contexts, enforce_admins, \ allow_force_pushes, allow_deletions, created_at, updated_at \ FROM repo_protect WHERE repo = $1 \ ORDER BY pattern ASC OFFSET $2 LIMIT $3", ) .bind(repo.id) .bind(pagination.offset() as i64) .bind(pagination.limit() as i64) .fetch_all(self.db.reader()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; Ok(rows.into_iter().map(protect_response).collect()) } pub async fn repo_protect_create( &self, ctx: &Session, wk_name: &str, repo_name: &str, params: CreateProtect, ) -> Result { let user_uid = session_user(ctx)?; let wk = self.workspace_resolve(wk_name).await?; self.workspace_require_admin(wk.id, user_uid).await?; let repo = self.repo_resolve(wk.id, repo_name).await?; let pattern = params.pattern.trim(); if pattern.is_empty() { return Err(AppError::BadRequest( "pattern is required".to_string(), )); } let contexts = params .required_status_contexts .unwrap_or_default() .join("."); let id = uuid::Uuid::now_v7(); let now = chrono::Utc::now(); let row = sqlx::query_as::<_, RepoProtectModel>( "INSERT INTO repo_protect \ (id, repo, pattern, require_pull_request, required_approvals, \ require_status_checks, required_status_contexts, enforce_admins, \ allow_force_pushes, allow_deletions, created_at, updated_at) \ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $11) \ RETURNING id, repo, pattern, require_pull_request, required_approvals, \ require_status_checks, required_status_contexts, enforce_admins, \ allow_force_pushes, allow_deletions, created_at, updated_at", ) .bind(id) .bind(repo.id) .bind(pattern) .bind(params.require_pull_request.unwrap_or(true)) .bind(params.required_approvals.unwrap_or(1)) .bind(params.require_status_checks.unwrap_or(false)) .bind(&contexts) .bind(params.enforce_admins.unwrap_or(false)) .bind(params.allow_force_pushes.unwrap_or(false)) .bind(params.allow_deletions.unwrap_or(false)) .bind(now) .fetch_one(self.db.writer()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; Ok(protect_response(row)) } pub async fn repo_protect_update( &self, ctx: &Session, wk_name: &str, repo_name: &str, protect_id: uuid::Uuid, params: UpdateProtect, ) -> Result { let user_uid = session_user(ctx)?; let wk = self.workspace_resolve(wk_name).await?; self.workspace_require_admin(wk.id, user_uid).await?; let repo = self.repo_resolve(wk.id, repo_name).await?; let existing = sqlx::query_as::<_, RepoProtectModel>( "SELECT id, repo, pattern, require_pull_request, required_approvals, \ require_status_checks, required_status_contexts, enforce_admins, \ allow_force_pushes, allow_deletions, created_at, updated_at \ FROM repo_protect WHERE id = $1 AND repo = $2", ) .bind(protect_id) .bind(repo.id) .fetch_optional(self.db.reader()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))? .ok_or(AppError::NotFound("protected branch rule not found".to_string()))?; let pattern = params.pattern.unwrap_or(existing.pattern); let require_pull_request = params .require_pull_request .unwrap_or(existing.require_pull_request); let required_approvals = params .required_approvals .unwrap_or(existing.required_approvals); let require_status_checks = params .require_status_checks .unwrap_or(existing.require_status_checks); let contexts = params .required_status_contexts .map(|c| c.join(".")) .unwrap_or(existing.required_status_contexts); let enforce_admins = params.enforce_admins.unwrap_or(existing.enforce_admins); let allow_force_pushes = params .allow_force_pushes .unwrap_or(existing.allow_force_pushes); let allow_deletions = params.allow_deletions.unwrap_or(existing.allow_deletions); let row = sqlx::query_as::<_, RepoProtectModel>( "UPDATE repo_protect SET \ pattern = $1, require_pull_request = $2, required_approvals = $3, \ require_status_checks = $4, required_status_contexts = $5, enforce_admins = $6, \ allow_force_pushes = $7, allow_deletions = $8, updated_at = $9 \ WHERE id = $10 \ RETURNING id, repo, pattern, require_pull_request, required_approvals, \ require_status_checks, required_status_contexts, enforce_admins, \ allow_force_pushes, allow_deletions, created_at, updated_at", ) .bind(&pattern) .bind(require_pull_request) .bind(required_approvals) .bind(require_status_checks) .bind(&contexts) .bind(enforce_admins) .bind(allow_force_pushes) .bind(allow_deletions) .bind(chrono::Utc::now()) .bind(protect_id) .fetch_one(self.db.writer()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; Ok(protect_response(row)) } pub async fn repo_protect_delete( &self, ctx: &Session, wk_name: &str, repo_name: &str, protect_id: uuid::Uuid, ) -> Result<(), AppError> { let user_uid = session_user(ctx)?; let wk = self.workspace_resolve(wk_name).await?; self.workspace_require_admin(wk.id, user_uid).await?; let repo = self.repo_resolve(wk.id, repo_name).await?; let result = sqlx::query("DELETE FROM repo_protect WHERE id = $1 AND repo = $2") .bind(protect_id) .bind(repo.id) .execute(self.db.writer()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; if result.rows_affected() == 0 { return Err(AppError::NotFound( "protected branch rule not found".to_string(), )); } Ok(()) } }