gitdataai/libs/service/git/branch_protection.rs
2026-04-14 19:02:01 +08:00

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)
}
}