ephemeris_forge_forgejo/api/request/
branch_rules.rs1use ephemeris_forge::{FieldDiff, NameSet};
2use facet::Facet;
3
4use crate::api::model::BranchProtection;
5use crate::{BranchRule, PushAccess};
6
7macro_rules! compare {
8 ($diff:ident, $edit:ident, $desired:ident, $live:ident, $($field:ident),+ $(,)?) => {$(
9 if let Some(wanted) =
10 $diff.want(stringify!($field), &$desired.$field, $live.$field.as_ref())
11 {
12 $edit.$field = Some(wanted);
13 }
14 )+};
15}
16
17macro_rules! compare_sets {
18 ($diff:ident, $edit:ident, $desired:ident, $live:ident, $($field:ident),+ $(,)?) => {$(
19 let wanted = NameSet::new($desired.$field.iter().cloned());
20 let current = $live.$field.clone().map(NameSet::new);
21 if let Some(set) = $diff.want_set(stringify!($field), &wanted, current.as_ref()) {
22 $edit.$field = Some(set.into_vec());
23 }
24 )+};
25}
26
27macro_rules! compare_patterns {
28 ($diff:ident, $edit:ident, $desired:ident, $live:ident, $($field:ident),+ $(,)?) => {$(
29 let wanted = NameSet::new($desired.$field.iter().cloned());
30 let current = $live.$field.as_deref().map(NameSet::from_patterns);
31 if let Some(set) = $diff.want_set(stringify!($field), &wanted, current.as_ref()) {
32 $edit.$field = Some(set.to_patterns());
33 }
34 )+};
35}
36
37#[derive(Clone, Debug, Default, Eq, Facet, PartialEq)]
41#[facet(skip_all_unless_truthy)]
42pub struct EditBranchProtectionOption {
43 pub apply_to_admins: Option<bool>,
45 pub approvals_whitelist_teams: Option<Vec<String>>,
47 pub approvals_whitelist_username: Option<Vec<String>>,
49 pub block_on_official_review_requests: Option<bool>,
52 pub block_on_outdated_branch: Option<bool>,
54 pub block_on_rejected_reviews: Option<bool>,
56 pub dismiss_stale_approvals: Option<bool>,
58 pub enable_approvals_whitelist: Option<bool>,
61 pub enable_merge_whitelist: Option<bool>,
63 pub enable_push: Option<bool>,
65 pub enable_push_whitelist: Option<bool>,
67 pub enable_status_check: Option<bool>,
69 pub ignore_stale_approvals: Option<bool>,
71 pub merge_whitelist_teams: Option<Vec<String>>,
73 pub merge_whitelist_usernames: Option<Vec<String>>,
75 pub protected_file_patterns: Option<String>,
78 pub push_whitelist_deploy_keys: Option<bool>,
80 pub push_whitelist_teams: Option<Vec<String>>,
82 pub push_whitelist_usernames: Option<Vec<String>>,
84 pub require_signed_commits: Option<bool>,
86 pub required_approvals: Option<i64>,
88 pub status_check_contexts: Option<Vec<String>>,
90 pub unprotected_file_patterns: Option<String>,
93}
94
95#[derive(Clone, Debug, Eq, Facet, PartialEq)]
105#[expect(
106 clippy::struct_excessive_bools,
107 reason = "this mirrors CreateBranchProtectionOption, whose plain bools are the point"
108)]
109pub struct CreateBranchProtectionOption {
110 pub rule_name: String,
112 pub apply_to_admins: bool,
114 pub approvals_whitelist_teams: Vec<String>,
116 pub approvals_whitelist_username: Vec<String>,
118 pub block_on_official_review_requests: bool,
121 pub block_on_outdated_branch: bool,
123 pub block_on_rejected_reviews: bool,
125 pub dismiss_stale_approvals: bool,
127 pub enable_approvals_whitelist: bool,
130 pub enable_merge_whitelist: bool,
132 pub enable_push: bool,
134 pub enable_push_whitelist: bool,
136 pub enable_status_check: bool,
138 pub ignore_stale_approvals: bool,
140 pub merge_whitelist_teams: Vec<String>,
142 pub merge_whitelist_usernames: Vec<String>,
144 pub protected_file_patterns: String,
147 pub push_whitelist_deploy_keys: bool,
149 pub push_whitelist_teams: Vec<String>,
151 pub push_whitelist_usernames: Vec<String>,
153 pub require_signed_commits: bool,
155 pub required_approvals: i64,
157 pub status_check_contexts: Vec<String>,
159 pub unprotected_file_patterns: String,
162}
163
164impl EditBranchProtectionOption {
165 #[must_use]
168 pub fn between(desired: &BranchRule, actual: &BranchProtection) -> (Self, FieldDiff) {
169 let mut diff = FieldDiff::default();
170 let mut edit = Self::default();
171 compare!(
172 diff,
173 edit,
174 desired,
175 actual,
176 apply_to_admins,
177 block_on_official_review_requests,
178 block_on_outdated_branch,
179 block_on_rejected_reviews,
180 dismiss_stale_approvals,
181 enable_approvals_whitelist,
182 enable_merge_whitelist,
183 enable_status_check,
184 ignore_stale_approvals,
185 push_whitelist_deploy_keys,
186 require_signed_commits,
187 required_approvals,
188 );
189 compare_sets!(
190 diff,
191 edit,
192 desired,
193 actual,
194 approvals_whitelist_teams,
195 approvals_whitelist_username,
196 merge_whitelist_teams,
197 merge_whitelist_usernames,
198 push_whitelist_teams,
199 push_whitelist_usernames,
200 status_check_contexts,
201 );
202 compare_patterns!(
203 diff,
204 edit,
205 desired,
206 actual,
207 protected_file_patterns,
208 unprotected_file_patterns,
209 );
210 let push_differs = diff.check(
211 "push",
212 actual.push_access() == Some(desired.push),
213 &actual.push_access(),
214 &desired.push,
215 );
216 if push_differs || edit.push_whitelist_deploy_keys.is_some() {
217 edit.carry_push(desired.push);
218 }
219 (edit, diff)
220 }
221
222 #[must_use]
224 pub fn is_empty(&self) -> bool {
225 *self == Self::default()
226 }
227
228 pub const fn carry_push(&mut self, push: PushAccess) {
231 self.enable_push = Some(push.enables_push());
232 self.enable_push_whitelist = Some(push.restricts_to_whitelist());
233 }
234}
235
236impl CreateBranchProtectionOption {
237 #[must_use]
239 pub fn new(rule_name: &str, desired: &BranchRule) -> Self {
240 Self {
241 rule_name: rule_name.to_owned(),
242 apply_to_admins: desired.apply_to_admins,
243 approvals_whitelist_teams: NameSet::new(
244 desired.approvals_whitelist_teams.iter().cloned(),
245 )
246 .into_vec(),
247 approvals_whitelist_username: NameSet::new(
248 desired.approvals_whitelist_username.iter().cloned(),
249 )
250 .into_vec(),
251 block_on_official_review_requests: desired.block_on_official_review_requests,
252 block_on_outdated_branch: desired.block_on_outdated_branch,
253 block_on_rejected_reviews: desired.block_on_rejected_reviews,
254 dismiss_stale_approvals: desired.dismiss_stale_approvals,
255 enable_approvals_whitelist: desired.enable_approvals_whitelist,
256 enable_merge_whitelist: desired.enable_merge_whitelist,
257 enable_push: desired.push.enables_push(),
258 enable_push_whitelist: desired.push.restricts_to_whitelist(),
259 enable_status_check: desired.enable_status_check,
260 ignore_stale_approvals: desired.ignore_stale_approvals,
261 merge_whitelist_teams: NameSet::new(desired.merge_whitelist_teams.iter().cloned())
262 .into_vec(),
263 merge_whitelist_usernames: NameSet::new(
264 desired.merge_whitelist_usernames.iter().cloned(),
265 )
266 .into_vec(),
267 protected_file_patterns: NameSet::new(desired.protected_file_patterns.iter().cloned())
268 .to_patterns(),
269 push_whitelist_deploy_keys: desired.push_whitelist_deploy_keys,
270 push_whitelist_teams: NameSet::new(desired.push_whitelist_teams.iter().cloned())
271 .into_vec(),
272 push_whitelist_usernames: NameSet::new(
273 desired.push_whitelist_usernames.iter().cloned(),
274 )
275 .into_vec(),
276 require_signed_commits: desired.require_signed_commits,
277 required_approvals: desired.required_approvals,
278 status_check_contexts: desired.contexts().into_vec(),
279 unprotected_file_patterns: NameSet::new(
280 desired.unprotected_file_patterns.iter().cloned(),
281 )
282 .to_patterns(),
283 }
284 }
285}