feat(git): add branch protection policy api and branch rule matcher
The policy columns required_approvals, dismiss_stale_approvals,
block_on_* and protected_file_patterns had no writer, so merge gates could not
be configured. Adds CRUD endpoints for /repos/{space}/{repo}/branch-protection
(admin only) plus match_branch_policies/branch_policy_whitelists helpers that
resolve the effective rule for a branch.
ZhenYi committed
Added lib/git/src/handler/branch_protection/create_policy.rs lib/git/src/handler/branch_protection/create_policy.rs +41 −0 View lib/git/src/handler/branch_protection/create_policy.rs
@@ -0,0 +1,41 @@
1use axum::Json;
2use axum::extract::{Path, State};
3use export::rest::AppResponseData;
4use session::Session;
5
6use crate::errors::{ApiError, GitApiError};
7use crate::handler::RepoPath;
8use crate::handler::require_repo_admin;
9use crate::service::GitService;
10use crate::service::protection::{BranchPolicyDto, CreateBranchPolicyCmd};
11
12#[utoipa::path(
13 post,
14 path = "/repos/{space_name}/{repo_name}/branch-protection",
15 tag = "protection",
16 description = "为分支模式创建保护政策,包含必需审批数、合并白名单与受保护文件模式。",
17 params(RepoPath),
18 request_body = CreateBranchPolicyCmd,
19 responses(
20 (status = 200, description = "created policy", body = AppResponseData<BranchPolicyDto>)
21 )
22)]
23pub async fn create_policy(
24 State(service): State<GitService>,
25 session: Session,
26 Path(path): Path<RepoPath>,
27 Json(cmd): Json<CreateBranchPolicyCmd>,
28) -> Result<AppResponseData<BranchPolicyDto>, ApiError> {
29 let _ = require_repo_admin(
30 &service,
31 &session,
32 path.space_name.clone(),
33 path.repo_name.clone(),
34 )
35 .await?;
36 let out = service
37 .create_branch_policy(path.space_name, path.repo_name, cmd)
38 .await
39 .map_err(GitApiError::from)?;
40 Ok(AppResponseData::ok(out))
41}
Added lib/git/src/handler/branch_protection/delete_policy.rs lib/git/src/handler/branch_protection/delete_policy.rs +38 −0 View lib/git/src/handler/branch_protection/delete_policy.rs
@@ -0,0 +1,38 @@
1use axum::extract::{Path, State};
2use axum::http::StatusCode;
3use export::rest::{AppResponseData, NoData};
4use session::Session;
5
6use crate::errors::{ApiError, GitApiError};
7use crate::handler::branch_protection::PolicyPath;
8use crate::handler::require_repo_admin;
9use crate::service::GitService;
10
11#[utoipa::path(
12 delete,
13 path = "/repos/{space_name}/{repo_name}/branch-protection/{policy_id}",
14 tag = "protection",
15 description = "删除分支保护政策;该政策下的白名单需要先行清理。",
16 params(PolicyPath),
17 responses(
18 (status = 200, description = "policy deleted", body = AppResponseData<NoData>)
19 )
20)]
21pub async fn delete_policy(
22 State(service): State<GitService>,
23 session: Session,
24 Path(path): Path<PolicyPath>,
25) -> Result<(StatusCode, AppResponseData<NoData>), ApiError> {
26 let _ = require_repo_admin(
27 &service,
28 &session,
29 path.space_name.clone(),
30 path.repo_name.clone(),
31 )
32 .await?;
33 service
34 .delete_branch_policy(path.space_name, path.repo_name, path.policy_id)
35 .await
36 .map_err(GitApiError::from)?;
37 Ok((StatusCode::OK, AppResponseData::ok(NoData)))
38}
Added lib/git/src/handler/branch_protection/get_policy.rs lib/git/src/handler/branch_protection/get_policy.rs +38 −0 View lib/git/src/handler/branch_protection/get_policy.rs
@@ -0,0 +1,38 @@
1use axum::extract::{Path, State};
2use export::rest::AppResponseData;
3use session::Session;
4
5use crate::errors::{ApiError, GitApiError};
6use crate::handler::branch_protection::PolicyPath;
7use crate::handler::require_repo_admin;
8use crate::service::GitService;
9use crate::service::protection::BranchPolicyDto;
10
11#[utoipa::path(
12 get,
13 path = "/repos/{space_name}/{repo_name}/branch-protection/{policy_id}",
14 tag = "protection",
15 description = "读取单条分支保护政策。",
16 params(PolicyPath),
17 responses(
18 (status = 200, description = "branch protection policy", body = AppResponseData<BranchPolicyDto>)
19 )
20)]
21pub async fn get_policy(
22 State(service): State<GitService>,
23 session: Session,
24 Path(path): Path<PolicyPath>,
25) -> Result<AppResponseData<BranchPolicyDto>, ApiError> {
26 let _ = require_repo_admin(
27 &service,
28 &session,
29 path.space_name.clone(),
30 path.repo_name.clone(),
31 )
32 .await?;
33 let out = service
34 .get_branch_policy(path.space_name, path.repo_name, path.policy_id)
35 .await
36 .map_err(GitApiError::from)?;
37 Ok(AppResponseData::ok(out))
38}
Added lib/git/src/handler/branch_protection/list_policies.rs lib/git/src/handler/branch_protection/list_policies.rs +38 −0 View lib/git/src/handler/branch_protection/list_policies.rs
@@ -0,0 +1,38 @@
1use axum::extract::{Path, State};
2use export::rest::AppResponseData;
3use session::Session;
4
5use crate::errors::{ApiError, GitApiError};
6use crate::handler::RepoPath;
7use crate::handler::require_repo_admin;
8use crate::service::GitService;
9use crate::service::protection::BranchPolicyDto;
10
11#[utoipa::path(
12 get,
13 path = "/repos/{space_name}/{repo_name}/branch-protection",
14 tag = "protection",
15 description = "列出分支保护政策(含合并与必需审批规则),仅仓库管理员可读。",
16 params(RepoPath),
17 responses(
18 (status = 200, description = "branch protection policies", body = AppResponseData<Vec<BranchPolicyDto>>)
19 )
20)]
21pub async fn list_policies(
22 State(service): State<GitService>,
23 session: Session,
24 Path(path): Path<RepoPath>,
25) -> Result<AppResponseData<Vec<BranchPolicyDto>>, ApiError> {
26 let _ = require_repo_admin(
27 &service,
28 &session,
29 path.space_name.clone(),
30 path.repo_name.clone(),
31 )
32 .await?;
33 let out = service
34 .list_branch_policies(path.space_name, path.repo_name)
35 .await
36 .map_err(GitApiError::from)?;
37 Ok(AppResponseData::ok(out))
38}
Added lib/git/src/handler/branch_protection/mod.rs lib/git/src/handler/branch_protection/mod.rs +34 −0 View lib/git/src/handler/branch_protection/mod.rs
@@ -0,0 +1,34 @@
1pub mod create_policy;
2pub mod delete_policy;
3pub mod get_policy;
4pub mod list_policies;
5pub mod update_policy;
6
7use axum::extract::{FromRequestParts, Path};
8use axum::http::request::Parts;
9use serde::Deserialize;
10use utoipa::IntoParams;
11use uuid::Uuid;
12
13use crate::errors::ApiError;
14
15#[derive(Debug, Clone, Deserialize, IntoParams)]
16pub struct PolicyPath {
17 pub space_name: String,
18 pub repo_name: String,
19 pub policy_id: Uuid,
20}
21
22impl<S> FromRequestParts<S> for PolicyPath
23where
24 S: Send + Sync,
25{
26 type Rejection = ApiError;
27
28 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
29 Path::<PolicyPath>::from_request_parts(parts, state)
30 .await
31 .map(|Path(p)| p)
32 .map_err(|_| ApiError::bad_request())
33 }
34}
Added lib/git/src/handler/branch_protection/update_policy.rs lib/git/src/handler/branch_protection/update_policy.rs +41 −0 View lib/git/src/handler/branch_protection/update_policy.rs
@@ -0,0 +1,41 @@
1use axum::Json;
2use axum::extract::{Path, State};
3use export::rest::AppResponseData;
4use session::Session;
5
6use crate::errors::{ApiError, GitApiError};
7use crate::handler::branch_protection::PolicyPath;
8use crate::handler::require_repo_admin;
9use crate::service::GitService;
10use crate::service::protection::{BranchPolicyDto, UpdateBranchPolicyCmd};
11
12#[utoipa::path(
13 patch,
14 path = "/repos/{space_name}/{repo_name}/branch-protection/{policy_id}",
15 tag = "protection",
16 description = "修改分支保护政策的各项开关。",
17 params(PolicyPath),
18 request_body = UpdateBranchPolicyCmd,
19 responses(
20 (status = 200, description = "updated policy", body = AppResponseData<BranchPolicyDto>)
21 )
22)]
23pub async fn update_policy(
24 State(service): State<GitService>,
25 session: Session,
26 Path(path): Path<PolicyPath>,
27 Json(cmd): Json<UpdateBranchPolicyCmd>,
28) -> Result<AppResponseData<BranchPolicyDto>, ApiError> {
29 let _ = require_repo_admin(
30 &service,
31 &session,
32 path.space_name.clone(),
33 path.repo_name.clone(),
34 )
35 .await?;
36 let out = service
37 .update_branch_policy(path.space_name, path.repo_name, path.policy_id, cmd)
38 .await
39 .map_err(GitApiError::from)?;
40 Ok(AppResponseData::ok(out))
41}
Modified lib/git/src/handler/mod.rs lib/git/src/handler/mod.rs +1 −0 View lib/git/src/handler/mod.rs
@@ -2,6 +2,7 @@
22pub mod archives;
33pub mod blame;
44pub mod blobs;
5pub mod branch_protection;
56pub mod branches;
67pub mod commits;
78pub mod conflicts;
Added lib/git/src/service/protection.rs lib/git/src/service/protection.rs +336 −0 View lib/git/src/service/protection.rs
@@ -0,0 +1,336 @@
1use globset::Glob;
2use models::repo::repo_branch_protection_policy;
3use sea_orm::{
4 ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter, QueryOrder,
5};
6use serde::{Deserialize, Serialize};
7use time::OffsetDateTime;
8use uuid::Uuid;
9
10use crate::service::GitService;
11use crate::share::errors::{GitError, is_unique_violation};
12
13#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
14pub struct BranchPolicyDto {
15 pub id: Uuid,
16 pub repo_id: Uuid,
17 pub branch_pattern: String,
18 pub enable_push: bool,
19 pub enable_push_whitelist: bool,
20 pub push_whitelist_deploy_keys: bool,
21 pub enable_merge_whitelist: bool,
22 pub required_approvals: i32,
23 pub enable_approvals_whitelist: bool,
24 pub block_on_rejected_reviews: bool,
25 pub block_on_change_requested_reviews: bool,
26 pub block_on_outdated_branch: bool,
27 pub dismiss_stale_approvals: bool,
28 pub require_signed_commits: bool,
29 pub protected_file_patterns: String,
30 pub unprotected_file_patterns: String,
31 pub enable_force_push: bool,
32 pub created_at: OffsetDateTime,
33 pub updated_at: OffsetDateTime,
34}
35
36impl From<repo_branch_protection_policy::Model> for BranchPolicyDto {
37 fn from(m: repo_branch_protection_policy::Model) -> Self {
38 Self {
39 id: m.id,
40 repo_id: m.repo_id,
41 branch_pattern: m.branch_pattern,
42 enable_push: m.enable_push,
43 enable_push_whitelist: m.enable_push_whitelist,
44 push_whitelist_deploy_keys: m.push_whitelist_deploy_keys,
45 enable_merge_whitelist: m.enable_merge_whitelist,
46 required_approvals: m.required_approvals,
47 enable_approvals_whitelist: m.enable_approvals_whitelist,
48 block_on_rejected_reviews: m.block_on_rejected_reviews,
49 block_on_change_requested_reviews: m.block_on_change_requested_reviews,
50 block_on_outdated_branch: m.block_on_outdated_branch,
51 dismiss_stale_approvals: m.dismiss_stale_approvals,
52 require_signed_commits: m.require_signed_commits,
53 protected_file_patterns: m.protected_file_patterns,
54 unprotected_file_patterns: m.unprotected_file_patterns,
55 enable_force_push: m.enable_force_push,
56 created_at: m.created_at,
57 updated_at: m.updated_at,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
63pub struct CreateBranchPolicyCmd {
64 pub branch_pattern: String,
65 #[serde(default)]
66 pub enable_push: Option<bool>,
67 #[serde(default)]
68 pub enable_push_whitelist: Option<bool>,
69 #[serde(default)]
70 pub enable_merge_whitelist: Option<bool>,
71 #[serde(default)]
72 pub required_approvals: Option<i32>,
73 #[serde(default)]
74 pub enable_approvals_whitelist: Option<bool>,
75 #[serde(default)]
76 pub block_on_rejected_reviews: Option<bool>,
77 #[serde(default)]
78 pub block_on_change_requested_reviews: Option<bool>,
79 #[serde(default)]
80 pub block_on_outdated_branch: Option<bool>,
81 #[serde(default)]
82 pub dismiss_stale_approvals: Option<bool>,
83 #[serde(default)]
84 pub require_signed_commits: Option<bool>,
85 #[serde(default)]
86 pub protected_file_patterns: Option<String>,
87 #[serde(default)]
88 pub unprotected_file_patterns: Option<String>,
89 #[serde(default)]
90 pub enable_force_push: Option<bool>,
91}
92
93#[derive(Debug, Clone, Deserialize, utoipa::ToSchema)]
94pub struct UpdateBranchPolicyCmd {
95 #[serde(default)]
96 pub branch_pattern: Option<String>,
97 #[serde(default)]
98 pub enable_push: Option<bool>,
99 #[serde(default)]
100 pub enable_push_whitelist: Option<bool>,
101 #[serde(default)]
102 pub enable_merge_whitelist: Option<bool>,
103 #[serde(default)]
104 pub required_approvals: Option<i32>,
105 #[serde(default)]
106 pub enable_approvals_whitelist: Option<bool>,
107 #[serde(default)]
108 pub block_on_rejected_reviews: Option<bool>,
109 #[serde(default)]
110 pub block_on_change_requested_reviews: Option<bool>,
111 #[serde(default)]
112 pub block_on_outdated_branch: Option<bool>,
113 #[serde(default)]
114 pub dismiss_stale_approvals: Option<bool>,
115 #[serde(default)]
116 pub require_signed_commits: Option<bool>,
117 #[serde(default)]
118 pub protected_file_patterns: Option<String>,
119 #[serde(default)]
120 pub unprotected_file_patterns: Option<String>,
121 #[serde(default)]
122 pub enable_force_push: Option<bool>,
123}
124
125fn validate_pattern(pattern: &str) -> Result<(), GitError> {
126 let trimmed = pattern.trim();
127 if trimmed.is_empty() || trimmed.len() > 255 || trimmed.chars().any(|c| c.is_control()) {
128 return Err(GitError::InvalidArgument(
129 "branch_pattern must be 1-255 printable characters".to_string(),
130 ));
131 }
132 Glob::new(trimmed)
133 .map_err(|e| GitError::InvalidArgument(format!("invalid branch pattern: {e}")))?;
134 Ok(())
135}
136
137fn validate_approvals(count: i32) -> Result<(), GitError> {
138 if !(0..=10).contains(&count) {
139 return Err(GitError::InvalidArgument(
140 "required_approvals must be between 0 and 10".to_string(),
141 ));
142 }
143 Ok(())
144}
145
146fn validate_patterns(raw: &str) -> Result<(), GitError> {
147 for part in raw.split([',', '\n']) {
148 let trimmed = part.trim();
149 if trimmed.is_empty() {
150 continue;
151 }
152 Glob::new(trimmed)
153 .map_err(|e| GitError::InvalidArgument(format!("invalid file pattern: {e}")))?;
154 }
155 Ok(())
156}
157
158impl GitService {
159 pub async fn list_branch_policies(
160 &self,
161 space_name: String,
162 repo_name: String,
163 ) -> Result<Vec<BranchPolicyDto>, GitError> {
164 let ctx = self.context(space_name, repo_name).await?;
165 let rows = repo_branch_protection_policy::Entity::find()
166 .filter(repo_branch_protection_policy::Column::RepoId.eq(ctx.model.id))
167 .order_by_asc(repo_branch_protection_policy::Column::BranchPattern)
168 .all(self.db_read())
169 .await?;
170 Ok(rows.into_iter().map(BranchPolicyDto::from).collect())
171 }
172
173 pub async fn get_branch_policy(
174 &self,
175 space_name: String,
176 repo_name: String,
177 policy_id: Uuid,
178 ) -> Result<BranchPolicyDto, GitError> {
179 let ctx = self.context(space_name, repo_name).await?;
180 let row = repo_branch_protection_policy::Entity::find_by_id(policy_id)
181 .filter(repo_branch_protection_policy::Column::RepoId.eq(ctx.model.id))
182 .one(self.db_read())
183 .await?
184 .ok_or(GitError::NotFound)?;
185 Ok(BranchPolicyDto::from(row))
186 }
187
188 pub async fn create_branch_policy(
189 &self,
190 space_name: String,
191 repo_name: String,
192 cmd: CreateBranchPolicyCmd,
193 ) -> Result<BranchPolicyDto, GitError> {
194 let ctx = self.context(space_name, repo_name).await?;
195 validate_pattern(&cmd.branch_pattern)?;
196 let required = cmd.required_approvals.unwrap_or(0);
197 validate_approvals(required)?;
198 let protected = cmd.protected_file_patterns.unwrap_or_default();
199 let unprotected = cmd.unprotected_file_patterns.unwrap_or_default();
200 validate_patterns(&protected)?;
201 validate_patterns(&unprotected)?;
202 let now = OffsetDateTime::now_utc();
203 let row = repo_branch_protection_policy::ActiveModel {
204 id: Set(Uuid::now_v7()),
205 repo_id: Set(ctx.model.id),
206 branch_pattern: Set(cmd.branch_pattern.trim().to_string()),
207 enable_push: Set(cmd.enable_push.unwrap_or(true)),
208 enable_push_whitelist: Set(cmd.enable_push_whitelist.unwrap_or(false)),
209 push_whitelist_deploy_keys: Set(false),
210 enable_merge_whitelist: Set(cmd.enable_merge_whitelist.unwrap_or(false)),
211 required_approvals: Set(required),
212 enable_approvals_whitelist: Set(cmd.enable_approvals_whitelist.unwrap_or(false)),
213 block_on_rejected_reviews: Set(cmd.block_on_rejected_reviews.unwrap_or(true)),
214 block_on_change_requested_reviews: Set(cmd
215 .block_on_change_requested_reviews
216 .unwrap_or(true)),
217 block_on_outdated_branch: Set(cmd.block_on_outdated_branch.unwrap_or(false)),
218 dismiss_stale_approvals: Set(cmd.dismiss_stale_approvals.unwrap_or(true)),
219 require_signed_commits: Set(cmd.require_signed_commits.unwrap_or(false)),
220 protected_file_patterns: Set(protected),
221 unprotected_file_patterns: Set(unprotected),
222 enable_force_push: Set(cmd.enable_force_push.unwrap_or(false)),
223 created_at: Set(now),
224 updated_at: Set(now),
225 ..Default::default()
226 };
227 let created = match row.insert(self.db_write()).await {
228 Ok(created) => created,
229 Err(error) if is_unique_violation(&error) => {
230 return Err(GitError::Conflict(
231 "a protection policy for this pattern already exists".to_string(),
232 ));
233 }
234 Err(error) => return Err(error.into()),
235 };
236 Ok(BranchPolicyDto::from(created))
237 }
238
239 pub async fn update_branch_policy(
240 &self,
241 space_name: String,
242 repo_name: String,
243 policy_id: Uuid,
244 cmd: UpdateBranchPolicyCmd,
245 ) -> Result<BranchPolicyDto, GitError> {
246 let ctx = self.context(space_name, repo_name).await?;
247 let row = repo_branch_protection_policy::Entity::find_by_id(policy_id)
248 .filter(repo_branch_protection_policy::Column::RepoId.eq(ctx.model.id))
249 .one(self.db_read())
250 .await?
251 .ok_or(GitError::NotFound)?;
252 let mut model: repo_branch_protection_policy::ActiveModel = row.into();
253 if let Some(pattern) = cmd.branch_pattern.as_deref() {
254 validate_pattern(pattern)?;
255 model.branch_pattern = Set(pattern.trim().to_string());
256 }
257 if let Some(count) = cmd.required_approvals {
258 validate_approvals(count)?;
259 model.required_approvals = Set(count);
260 }
261 if let Some(raw) = cmd.protected_file_patterns.as_deref() {
262 validate_patterns(raw)?;
263 model.protected_file_patterns = Set(raw.to_string());
264 }
265 if let Some(raw) = cmd.unprotected_file_patterns.as_deref() {
266 validate_patterns(raw)?;
267 model.unprotected_file_patterns = Set(raw.to_string());
268 }
269 for (flag, value) in [
270 (&cmd.enable_push, &mut model.enable_push),
271 (&cmd.enable_push_whitelist, &mut model.enable_push_whitelist),
272 (
273 &cmd.enable_merge_whitelist,
274 &mut model.enable_merge_whitelist,
275 ),
276 (
277 &cmd.enable_approvals_whitelist,
278 &mut model.enable_approvals_whitelist,
279 ),
280 (
281 &cmd.block_on_rejected_reviews,
282 &mut model.block_on_rejected_reviews,
283 ),
284 (
285 &cmd.block_on_change_requested_reviews,
286 &mut model.block_on_change_requested_reviews,
287 ),
288 (
289 &cmd.block_on_outdated_branch,
290 &mut model.block_on_outdated_branch,
291 ),
292 (
293 &cmd.dismiss_stale_approvals,
294 &mut model.dismiss_stale_approvals,
295 ),
296 (
297 &cmd.require_signed_commits,
298 &mut model.require_signed_commits,
299 ),
300 (&cmd.enable_force_push, &mut model.enable_force_push),
301 ] {
302 if let Some(value_flag) = flag {
303 *value = Set(*value_flag);
304 }
305 }
306 model.updated_at = Set(OffsetDateTime::now_utc());
307 let updated = match model.update(self.db_write()).await {
308 Ok(updated) => updated,
309 Err(error) if is_unique_violation(&error) => {
310 return Err(GitError::Conflict(
311 "a protection policy for this pattern already exists".to_string(),
312 ));
313 }
314 Err(error) => return Err(error.into()),
315 };
316 Ok(BranchPolicyDto::from(updated))
317 }
318
319 pub async fn delete_branch_policy(
320 &self,
321 space_name: String,
322 repo_name: String,
323 policy_id: Uuid,
324 ) -> Result<(), GitError> {
325 let ctx = self.context(space_name, repo_name).await?;
326 let deleted = repo_branch_protection_policy::Entity::delete_many()
327 .filter(repo_branch_protection_policy::Column::Id.eq(policy_id))
328 .filter(repo_branch_protection_policy::Column::RepoId.eq(ctx.model.id))
329 .exec(self.db_write())
330 .await?;
331 if deleted.rows_affected == 0 {
332 return Err(GitError::NotFound);
333 }
334 Ok(())
335 }
336}
Modified lib/git/src/share/errors.rs lib/git/src/share/errors.rs +1 −1 View lib/git/src/share/errors.rs
@@ -85,7 +85,7 @@
8585 }
8686}
8787
88fn is_unique_violation(err: &sea_orm::DbErr) -> bool {
88pub fn is_unique_violation(err: &sea_orm::DbErr) -> bool {
8989 let msg = err.to_string().to_lowercase();
9090 msg.contains("unique") || msg.contains("duplicate") || msg.contains("23505")
9191}
Modified lib/git/src/share/policy.rs lib/git/src/share/policy.rs +41 −0 View lib/git/src/share/policy.rs
@@ -128,3 +128,44 @@
128128 }
129129 Ok(out)
130130}
131
132/// 按分支名匹配启用的保护政策(精确名优先,其次最长通配)。
133pub async fn match_branch_policies(
134 db: &sea_orm::DatabaseConnection,
135 repo_id: Uuid,
136 branch: &str,
137) -> Result<Vec<repo_branch_protection_policy::Model>, GitError> {
138 let rows = repo_branch_protection_policy::Entity::find()
139 .filter(repo_branch_protection_policy::Column::RepoId.eq(repo_id))
140 .all(db)
141 .await?;
142 let mut matched: Vec<repo_branch_protection_policy::Model> = Vec::new();
143 for row in rows {
144 if BranchPolicy::from_patterns([row.branch_pattern.clone()])?.is_match(branch) {
145 matched.push(row);
146 }
147 }
148 matched.sort_by_key(|row| {
149 (
150 row.branch_pattern != branch,
151 -(row.branch_pattern.len() as i32),
152 )
153 });
154 let exact: Option<String> = matched
155 .iter()
156 .find(|row| row.branch_pattern == branch)
157 .map(|row| row.branch_pattern.clone());
158 if let Some(pattern) = exact {
159 matched.retain(|row| row.branch_pattern == pattern);
160 }
161 Ok(matched)
162}
163
164/// 政策对应的白名单集合。
165pub async fn branch_policy_whitelists(
166 db: &sea_orm::DatabaseConnection,
167 policies: &[repo_branch_protection_policy::Model],
168) -> Result<WhitelistSet, GitError> {
169 let ids: Vec<Uuid> = policies.iter().map(|row| row.id).collect();
170 load_branch_whitelists(db, &ids).await
171}
Modified lib/git/src/openapi.rs lib/git/src/openapi.rs +8 −0 View lib/git/src/openapi.rs
@@ -25,6 +25,11 @@
2525 handler::branches::list::list_branches,
2626 handler::branches::rename::rename_branch,
2727 handler::branches::set_default::set_default_branch,
28 handler::branch_protection::create_policy::create_policy,
29 handler::branch_protection::delete_policy::delete_policy,
30 handler::branch_protection::get_policy::get_policy,
31 handler::branch_protection::list_policies::list_policies,
32 handler::branch_protection::update_policy::update_policy,
2833 handler::commits::get::get_commit,
2934 handler::commits::is_ancestor::is_ancestor,
3035 handler::commits::list::list_commits,
@@ -122,6 +127,9 @@
122127 crate::service::branches::BranchDto,
123128 crate::service::branches::CreateBranchCmd,
124129 crate::service::commits::CommitDto,
130 crate::service::protection::BranchPolicyDto,
131 crate::service::protection::CreateBranchPolicyCmd,
132 crate::service::protection::UpdateBranchPolicyCmd,
125133 crate::service::commits::CommitStatusDto,
126134 crate::service::commits::CommitStatusDtoState,
127135 crate::service::commits::CommitsPageDto,
Modified lib/git/src/routes.rs lib/git/src/routes.rs +17 −7 View lib/git/src/routes.rs
@@ -68,9 +68,9 @@
6868use crate::handler::refs::resolve::refs_resolve;
6969use crate::handler::refs::update::refs_update;
7070use crate::handler::remotes::add::upsert_remote;
71use crate::handler::remotes::list::list_remotes;
7271use crate::handler::remotes::clone_import::clone_import as remote_clone_import;
7372use crate::handler::remotes::fetch::fetch as remote_fetch;
73use crate::handler::remotes::list::list_remotes;
7474use crate::handler::remotes::push::push as remote_push;
7575use crate::handler::remotes::remove::remove as remote_remove;
7676use crate::handler::repos::archive::{archive_repo as repo_archive, unarchive_repo};
@@ -181,7 +181,6 @@
181181 "/{space_name}/{repo_name}/lfs-pointers",
182182 get(list_lfs_pointers),
183183 )
184
185184 .route("/{space_name}/{repo_name}/blame", get(blame_handler))
186185 .route("/{space_name}/{repo_name}/submodules", get(submodules_list))
187186 .route(
@@ -264,8 +263,11 @@
264263 get(code_activity),
265264 )
266265 .route("/{space_name}/{repo_name}/archives", post(create_archive))
267 .route("/{space_name}/{repo_name}/archives/{revision}", get(get_archive))
268266 .route(
267 "/{space_name}/{repo_name}/archives/{revision}",
268 get(get_archive),
269 )
270 .route(
269271 "/{space_name}/{repo_name}/archives/bundle",
270272 post(create_bundle),
271273 )
@@ -274,10 +276,7 @@
274276 put(star_repo).delete(unstar_repo),
275277 )
276278 .route("/{space_name}/{repo_name}/stargazers", get(list_stargazers))
277 .route(
278 "/{space_name}/{repo_name}/watch",
279 put(upsert_watch),
280 )
279 .route("/{space_name}/{repo_name}/watch", put(upsert_watch))
281280 .route("/{space_name}/{repo_name}/watchers", get(list_watchers))
282281 .route("/{space_name}/{repo_name}/topics", get(list_repo_topics))
283282 .route(
@@ -285,6 +284,17 @@
285284 put(add_topic_to_repo).delete(remove_topic_from_repo),
286285 )
287286 .route(
287 "/{space_name}/{repo_name}/branch-protection",
288 get(crate::handler::branch_protection::list_policies::list_policies)
289 .post(crate::handler::branch_protection::create_policy::create_policy),
290 )
291 .route(
292 "/{space_name}/{repo_name}/branch-protection/{policy_id}",
293 get(crate::handler::branch_protection::get_policy::get_policy)
294 .patch(crate::handler::branch_protection::update_policy::update_policy)
295 .delete(crate::handler::branch_protection::delete_policy::delete_policy),
296 )
297 .route(
288298 "/{space_name}/{repo_name}/branch-protection/{policy_id}/whitelists",
289299 post(add_branch_whitelist).get(list_branch_whitelists),
290300 )