ephemeris_forge_forgejo/plan/
branch_rules.rs1use std::collections::BTreeMap;
2
3use ephemeris_forge::{Collection, Limitation, Report, Shadowing};
4use globset::GlobBuilder;
5
6use crate::api::model::BranchProtection;
7use crate::api::request::EditBranchProtectionOption;
8use crate::operation::branch_rules::Operation;
9use crate::{BranchRule, BranchRules, Live, RuleName};
10
11pub(super) fn plan(
12 config: Option<&BranchRules>,
13 live: &Live,
14 report: &mut Report,
15) -> Vec<Operation> {
16 let Some(config) = config else {
17 return Vec::new();
18 };
19 report.governance = governance(&config.rules, live);
20 Collection {
21 noun: "branch rule",
22 mode: config.mode,
23 }
24 .plan(
25 &config.rules,
26 &live.branch_rules,
27 report,
28 |rule, name| rule.rule_name == name,
29 |name, desired, actual, _| {
30 let fallback = BranchProtection::default();
31 let (edit, diff) =
32 EditBranchProtectionOption::between(desired, actual.unwrap_or(&fallback));
33 let operation = match actual {
34 Some(_) if edit.is_empty() => None,
35 Some(_) => Some(Operation::Patch {
36 name: name.to_owned(),
37 edit,
38 }),
39 None => Some(Operation::Create {
40 name: name.to_owned(),
41 rule: desired.clone(),
42 }),
43 };
44 (diff, operation.into_iter().collect())
45 },
46 |rule, report| {
47 if RuleName::is_addressable(&rule.rule_name) {
48 Some((
49 rule.rule_name.clone(),
50 Operation::Delete {
51 name: rule.rule_name.clone(),
52 },
53 ))
54 } else {
55 report.limitations.push(Limitation::UnaddressableRule {
56 name: rule.rule_name.clone(),
57 });
58 None
59 }
60 },
61 )
62}
63
64fn governance(config: &BTreeMap<RuleName, BranchRule>, live: &Live) -> Option<Shadowing> {
65 let branch = live.repository.default_branch.as_deref()?;
66 let governing_rule = live
67 .branch_rules
68 .iter()
69 .map(|rule| &rule.rule_name)
70 .find(|name| matches_branch(name, branch))
71 .cloned();
72 let declared = governing_rule
73 .as_ref()
74 .is_some_and(|name| config.contains_key(name.as_str()));
75 Some(Shadowing {
76 default_branch: branch.to_owned(),
77 governing_rule,
78 declared,
79 })
80}
81
82fn matches_branch(rule_name: &str, branch: &str) -> bool {
85 const GLOB_CHARS: [char; 7] = ['*', '?', '[', ']', '{', '}', '\\'];
86 if !rule_name.contains(GLOB_CHARS) {
87 return rule_name.eq_ignore_ascii_case(branch);
88 }
89 GlobBuilder::new(rule_name)
90 .literal_separator(true)
91 .build()
92 .map_or_else(
93 |_| rule_name.eq_ignore_ascii_case(branch),
94 |glob| glob.compile_matcher().is_match(branch),
95 )
96}