350 lines
12 KiB
Rust
350 lines
12 KiB
Rust
use crate::AppService;
|
|
use crate::error::AppError;
|
|
use models::pull_request::{self as pr_module, ReviewState, pull_request_review};
|
|
use models::repos::repo_branch_protect;
|
|
use sea_orm::*;
|
|
use serde::{Deserialize, Serialize};
|
|
use session::Session;
|
|
use uuid::Uuid;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
|
|
pub struct BranchProtectionResponse {
|
|
pub id: i64,
|
|
pub repo: Uuid,
|
|
pub branch: String,
|
|
pub forbid_push: bool,
|
|
pub forbid_pull: bool,
|
|
pub forbid_merge: bool,
|
|
pub forbid_deletion: bool,
|
|
pub forbid_force_push: bool,
|
|
pub forbid_tag_push: bool,
|
|
pub required_approvals: i32,
|
|
pub dismiss_stale_reviews: bool,
|
|
pub require_linear_history: bool,
|
|
pub allow_fork_syncing: bool,
|
|
}
|
|
|
|
impl From<repo_branch_protect::Model> for BranchProtectionResponse {
|
|
fn from(m: repo_branch_protect::Model) -> Self {
|
|
Self {
|
|
id: m.id,
|
|
repo: m.repo,
|
|
branch: m.branch,
|
|
forbid_push: m.forbid_push,
|
|
forbid_pull: m.forbid_pull,
|
|
forbid_merge: m.forbid_merge,
|
|
forbid_deletion: m.forbid_deletion,
|
|
forbid_force_push: m.forbid_force_push,
|
|
forbid_tag_push: m.forbid_tag_push,
|
|
required_approvals: m.required_approvals,
|
|
dismiss_stale_reviews: m.dismiss_stale_reviews,
|
|
require_linear_history: m.require_linear_history,
|
|
allow_fork_syncing: m.allow_fork_syncing,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct BranchProtectionCreateRequest {
|
|
pub branch: String,
|
|
#[serde(default)]
|
|
pub forbid_push: bool,
|
|
#[serde(default)]
|
|
pub forbid_pull: bool,
|
|
#[serde(default)]
|
|
pub forbid_merge: bool,
|
|
#[serde(default)]
|
|
pub forbid_deletion: bool,
|
|
#[serde(default)]
|
|
pub forbid_force_push: bool,
|
|
#[serde(default)]
|
|
pub forbid_tag_push: bool,
|
|
#[serde(default)]
|
|
pub required_approvals: i32,
|
|
#[serde(default)]
|
|
pub dismiss_stale_reviews: bool,
|
|
#[serde(default)]
|
|
pub require_linear_history: bool,
|
|
#[serde(default = "default_allow_fork_syncing")]
|
|
pub allow_fork_syncing: bool,
|
|
}
|
|
|
|
fn default_allow_fork_syncing() -> bool {
|
|
true
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
|
|
pub struct BranchProtectionUpdateRequest {
|
|
pub branch: Option<String>,
|
|
pub forbid_push: Option<bool>,
|
|
pub forbid_pull: Option<bool>,
|
|
pub forbid_merge: Option<bool>,
|
|
pub forbid_deletion: Option<bool>,
|
|
pub forbid_force_push: Option<bool>,
|
|
pub forbid_tag_push: Option<bool>,
|
|
pub required_approvals: Option<i32>,
|
|
pub dismiss_stale_reviews: Option<bool>,
|
|
pub require_linear_history: Option<bool>,
|
|
pub allow_fork_syncing: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct ApprovalCheckResult {
|
|
pub enough_approvals: bool,
|
|
pub approvals: i32,
|
|
pub required: i32,
|
|
pub reviewers: Vec<ReviewerInfo>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
|
|
pub struct ReviewerInfo {
|
|
pub reviewer: Uuid,
|
|
pub state: String,
|
|
pub submitted_at: Option<chrono::DateTime<chrono::Utc>>,
|
|
}
|
|
|
|
impl AppService {
|
|
/// List all branch protection rules for a repository.
|
|
pub async fn branch_protection_list(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
ctx: &Session,
|
|
) -> Result<Vec<BranchProtectionResponse>, AppError> {
|
|
let _ = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
let rules = repo_branch_protect::Entity::find()
|
|
.filter(repo_branch_protect::Column::Repo.eq(repo.id))
|
|
.all(&self.db)
|
|
.await?;
|
|
Ok(rules
|
|
.into_iter()
|
|
.map(BranchProtectionResponse::from)
|
|
.collect())
|
|
}
|
|
|
|
/// Get a single branch protection rule by id.
|
|
pub async fn branch_protection_get(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
rule_id: i64,
|
|
ctx: &Session,
|
|
) -> Result<BranchProtectionResponse, AppError> {
|
|
let _ = ctx.user().ok_or(AppError::Unauthorized)?;
|
|
let repo = self.utils_find_repo(namespace, repo_name, ctx).await?;
|
|
let rule = repo_branch_protect::Entity::find_by_id(rule_id)
|
|
.filter(repo_branch_protect::Column::Repo.eq(repo.id))
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or_else(|| AppError::NotFound("Branch protection rule not found".to_string()))?;
|
|
Ok(BranchProtectionResponse::from(rule))
|
|
}
|
|
|
|
/// Create a branch protection rule.
|
|
pub async fn branch_protection_create(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
request: BranchProtectionCreateRequest,
|
|
ctx: &Session,
|
|
) -> Result<BranchProtectionResponse, AppError> {
|
|
let repo = self
|
|
.utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
|
|
.await?;
|
|
let active = repo_branch_protect::ActiveModel {
|
|
id: Default::default(),
|
|
repo: Set(repo.id),
|
|
branch: Set(request.branch.clone()),
|
|
forbid_push: Set(request.forbid_push),
|
|
forbid_pull: Set(request.forbid_pull),
|
|
forbid_merge: Set(request.forbid_merge),
|
|
forbid_deletion: Set(request.forbid_deletion),
|
|
forbid_force_push: Set(request.forbid_force_push),
|
|
forbid_tag_push: Set(request.forbid_tag_push),
|
|
required_approvals: Set(request.required_approvals),
|
|
dismiss_stale_reviews: Set(request.dismiss_stale_reviews),
|
|
require_linear_history: Set(request.require_linear_history),
|
|
allow_fork_syncing: Set(request.allow_fork_syncing),
|
|
};
|
|
let rule = active.insert(&self.db).await?;
|
|
let _ = self
|
|
.project_log_activity(
|
|
repo.project,
|
|
Some(repo.id),
|
|
ctx.user().unwrap_or(Uuid::nil()),
|
|
super::super::project::activity::ActivityLogParams {
|
|
event_type: "branch_protection_create".to_string(),
|
|
title: format!(
|
|
"Branch protection created for '{}' on branch '{}'",
|
|
repo_name, request.branch
|
|
),
|
|
repo_id: Some(repo.id),
|
|
content: None,
|
|
event_id: None,
|
|
event_sub_id: None,
|
|
metadata: Some(serde_json::json!({ "branch": request.branch })),
|
|
is_private: false,
|
|
},
|
|
)
|
|
.await;
|
|
Ok(BranchProtectionResponse::from(rule))
|
|
}
|
|
|
|
/// Update a branch protection rule.
|
|
pub async fn branch_protection_update(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
rule_id: i64,
|
|
request: BranchProtectionUpdateRequest,
|
|
ctx: &Session,
|
|
) -> Result<BranchProtectionResponse, AppError> {
|
|
let repo = self
|
|
.utils_check_repo_admin(namespace.clone(), repo_name.clone(), ctx)
|
|
.await?;
|
|
let rule = repo_branch_protect::Entity::find_by_id(rule_id)
|
|
.filter(repo_branch_protect::Column::Repo.eq(repo.id))
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or_else(|| AppError::NotFound("Branch protection rule not found".to_string()))?;
|
|
let mut active: repo_branch_protect::ActiveModel = rule.into();
|
|
if let Some(v) = request.branch {
|
|
active.branch = Set(v);
|
|
}
|
|
if let Some(v) = request.forbid_push {
|
|
active.forbid_push = Set(v);
|
|
}
|
|
if let Some(v) = request.forbid_pull {
|
|
active.forbid_pull = Set(v);
|
|
}
|
|
if let Some(v) = request.forbid_merge {
|
|
active.forbid_merge = Set(v);
|
|
}
|
|
if let Some(v) = request.forbid_deletion {
|
|
active.forbid_deletion = Set(v);
|
|
}
|
|
if let Some(v) = request.forbid_force_push {
|
|
active.forbid_force_push = Set(v);
|
|
}
|
|
if let Some(v) = request.forbid_tag_push {
|
|
active.forbid_tag_push = Set(v);
|
|
}
|
|
if let Some(v) = request.required_approvals {
|
|
active.required_approvals = Set(v);
|
|
}
|
|
if let Some(v) = request.dismiss_stale_reviews {
|
|
active.dismiss_stale_reviews = Set(v);
|
|
}
|
|
if let Some(v) = request.require_linear_history {
|
|
active.require_linear_history = Set(v);
|
|
}
|
|
if let Some(v) = request.allow_fork_syncing {
|
|
active.allow_fork_syncing = Set(v);
|
|
}
|
|
let updated = active.update(&self.db).await?;
|
|
Ok(BranchProtectionResponse::from(updated))
|
|
}
|
|
|
|
/// Delete a branch protection rule.
|
|
pub async fn branch_protection_delete(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
rule_id: i64,
|
|
ctx: &Session,
|
|
) -> Result<(), AppError> {
|
|
let repo = self
|
|
.utils_check_repo_admin(namespace, repo_name, ctx)
|
|
.await?;
|
|
let deleted = repo_branch_protect::Entity::delete_many()
|
|
.filter(repo_branch_protect::Column::Id.eq(rule_id))
|
|
.filter(repo_branch_protect::Column::Repo.eq(repo.id))
|
|
.exec(&self.db)
|
|
.await?;
|
|
if deleted.rows_affected == 0 {
|
|
return Err(AppError::NotFound(
|
|
"Branch protection rule not found".to_string(),
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Check approval count for a PR against branch protection required_approvals.
|
|
pub async fn branch_protection_check_approvals(
|
|
&self,
|
|
namespace: String,
|
|
repo_name: String,
|
|
pr_number: i64,
|
|
_ctx: &Session,
|
|
) -> Result<ApprovalCheckResult, AppError> {
|
|
let repo = self.utils_find_repo(namespace, repo_name, _ctx).await?;
|
|
let pr = pr_module::PullRequest::find()
|
|
.filter(pr_module::pull_request::Column::Repo.eq(repo.id))
|
|
.filter(pr_module::pull_request::Column::Number.eq(pr_number))
|
|
.one(&self.db)
|
|
.await?
|
|
.ok_or_else(|| AppError::NotFound("Pull request not found".to_string()))?;
|
|
|
|
// Find branch protection for the base branch
|
|
let protection = repo_branch_protect::Entity::find()
|
|
.filter(repo_branch_protect::Column::Repo.eq(repo.id))
|
|
.filter(repo_branch_protect::Column::Branch.eq(&pr.base))
|
|
.one(&self.db)
|
|
.await?;
|
|
|
|
let required = protection
|
|
.as_ref()
|
|
.map(|p| p.required_approvals)
|
|
.unwrap_or(0);
|
|
|
|
if required <= 0 {
|
|
return Ok(ApprovalCheckResult {
|
|
enough_approvals: true,
|
|
approvals: 0,
|
|
required,
|
|
reviewers: vec![],
|
|
});
|
|
}
|
|
|
|
// Count approvals from pull_request_review table
|
|
let reviews = pull_request_review::Entity::find()
|
|
.filter(pull_request_review::Column::Repo.eq(repo.id))
|
|
.filter(pull_request_review::Column::Number.eq(pr_number))
|
|
.filter(pull_request_review::Column::State.eq(ReviewState::Approved.to_string()))
|
|
.all(&self.db)
|
|
.await?;
|
|
|
|
let approvals = reviews.len() as i32;
|
|
let reviewers: Vec<ReviewerInfo> = reviews
|
|
.into_iter()
|
|
.map(|r| ReviewerInfo {
|
|
reviewer: r.reviewer,
|
|
state: r.state,
|
|
submitted_at: r.submitted_at,
|
|
})
|
|
.collect();
|
|
|
|
Ok(ApprovalCheckResult {
|
|
enough_approvals: approvals >= required,
|
|
approvals,
|
|
required,
|
|
reviewers,
|
|
})
|
|
}
|
|
|
|
/// Find the branch protection rule for a given repo+branch.
|
|
pub async fn branch_protection_find(
|
|
&self,
|
|
repo_id: Uuid,
|
|
branch: &str,
|
|
) -> Result<Option<repo_branch_protect::Model>, AppError> {
|
|
let rule = repo_branch_protect::Entity::find()
|
|
.filter(repo_branch_protect::Column::Repo.eq(repo_id))
|
|
.filter(repo_branch_protect::Column::Branch.eq(branch))
|
|
.one(&self.db)
|
|
.await?;
|
|
Ok(rule)
|
|
}
|
|
}
|