Skip to main content

ephemeris_forge_forgejo/plan/
branch_rules.rs

1use 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
82/// Mirrors `Match` in Forgejo `models/git/protected_branch.go`: plain names
83/// compare case-insensitively, globs case-sensitively.
84fn 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}