260 lines
9.3 KiB
Rust
260 lines
9.3 KiB
Rust
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<String>,
|
|
pub enforce_admins: bool,
|
|
pub allow_force_pushes: bool,
|
|
pub allow_deletions: bool,
|
|
#[schema(value_type = String)]
|
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
|
#[schema(value_type = String)]
|
|
pub updated_at: chrono::DateTime<chrono::Utc>,
|
|
}
|
|
|
|
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<bool>,
|
|
pub required_approvals: Option<i32>,
|
|
pub require_status_checks: Option<bool>,
|
|
pub required_status_contexts: Option<Vec<String>>,
|
|
pub enforce_admins: Option<bool>,
|
|
pub allow_force_pushes: Option<bool>,
|
|
pub allow_deletions: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct UpdateProtect {
|
|
pub pattern: Option<String>,
|
|
pub require_pull_request: Option<bool>,
|
|
pub required_approvals: Option<i32>,
|
|
pub require_status_checks: Option<bool>,
|
|
pub required_status_contexts: Option<Vec<String>>,
|
|
pub enforce_admins: Option<bool>,
|
|
pub allow_force_pushes: Option<bool>,
|
|
pub allow_deletions: Option<bool>,
|
|
}
|
|
|
|
impl AppService {
|
|
pub async fn repo_protect_list(
|
|
&self,
|
|
ctx: &Session,
|
|
wk_name: &str,
|
|
repo_name: &str,
|
|
pagination: Pagination,
|
|
) -> Result<Vec<ProtectResponse>, 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<ProtectResponse, 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 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<ProtectResponse, 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 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(())
|
|
}
|
|
}
|