Skip to main content

ephemeris_forge_forgejo/config/
branch_rules.rs

1use std::borrow::Borrow;
2use std::collections::BTreeMap;
3use std::fmt;
4
5use ephemeris_forge::{Mode, NameSet};
6use facet::Facet;
7
8use super::PushAccess;
9
10/// The name of a branch rule, which fits the Forgejo branch protection routes.
11#[derive(Clone, Debug, Eq, Facet, Ord, PartialEq, PartialOrd)]
12#[facet(proxy = String)]
13pub struct RuleName(String);
14
15/// A branch rule name cannot address a Forgejo branch protection route.
16#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
17#[error("branch rule name \"{name}\" cannot address a Forgejo branch protection route")]
18pub struct InvalidRuleName {
19    /// The rejected name.
20    pub name: String,
21}
22
23impl RuleName {
24    /// Whether `name` can be part of a branch protection route.
25    #[must_use]
26    pub fn is_addressable(name: &str) -> bool {
27        !name.is_empty() && !name.contains(['/', '?', '#'])
28    }
29
30    /// Borrow the name.
31    #[must_use]
32    pub fn as_str(&self) -> &str {
33        &self.0
34    }
35}
36
37impl TryFrom<String> for RuleName {
38    type Error = InvalidRuleName;
39
40    fn try_from(name: String) -> Result<Self, Self::Error> {
41        if Self::is_addressable(&name) {
42            Ok(Self(name))
43        } else {
44            Err(InvalidRuleName { name })
45        }
46    }
47}
48
49impl From<&RuleName> for String {
50    fn from(value: &RuleName) -> Self {
51        value.0.clone()
52    }
53}
54
55impl AsRef<str> for RuleName {
56    fn as_ref(&self) -> &str {
57        &self.0
58    }
59}
60
61impl Borrow<str> for RuleName {
62    fn borrow(&self) -> &str {
63        &self.0
64    }
65}
66
67impl fmt::Display for RuleName {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        f.write_str(&self.0)
70    }
71}
72
73/// Desired settings for one branch-protection rule.
74#[derive(Clone, Debug, Eq, Facet, PartialEq)]
75#[facet(deny_unknown_fields)]
76#[expect(
77    clippy::struct_excessive_bools,
78    reason = "these are independent forge switches"
79)]
80pub struct BranchRule {
81    /// Whether the protection also applies to repository administrators.
82    pub apply_to_admins: bool,
83    /// The teams whose approvals count, when the approvals whitelist is on.
84    pub approvals_whitelist_teams: Vec<String>,
85    /// The users whose approvals count, when the approvals whitelist is on.
86    pub approvals_whitelist_username: Vec<String>,
87    /// Whether an official code owner review request blocks merging until
88    /// resolved.
89    pub block_on_official_review_requests: bool,
90    /// Whether merging is blocked while the branch is behind its base.
91    pub block_on_outdated_branch: bool,
92    /// Whether a requested change blocks merging until it is resolved.
93    pub block_on_rejected_reviews: bool,
94    /// Whether a new commit dismisses existing approvals.
95    pub dismiss_stale_approvals: bool,
96    /// Whether only the approvals whitelist counts toward the required
97    /// approvals.
98    pub enable_approvals_whitelist: bool,
99    /// Whether only the merge whitelist may merge.
100    pub enable_merge_whitelist: bool,
101    /// Whether status checks must pass before merging.
102    pub enable_status_check: bool,
103    /// Whether approvals on an outdated commit stop counting.
104    pub ignore_stale_approvals: bool,
105    /// The teams that may merge, when the merge whitelist is on.
106    pub merge_whitelist_teams: Vec<String>,
107    /// The users that may merge, when the merge whitelist is on.
108    pub merge_whitelist_usernames: Vec<String>,
109    /// Glob patterns for files that only whitelisted pushers may change.
110    pub protected_file_patterns: Vec<String>,
111    /// Who may push directly to the branch.
112    pub push: PushAccess,
113    /// Whether deploy keys may push, when the push whitelist is on.
114    pub push_whitelist_deploy_keys: bool,
115    /// The teams that may push, when the push whitelist is on.
116    pub push_whitelist_teams: Vec<String>,
117    /// The users that may push, when the push whitelist is on.
118    pub push_whitelist_usernames: Vec<String>,
119    /// Whether every commit on the branch must be signed.
120    pub require_signed_commits: bool,
121    /// The number of approving reviews required before merging.
122    pub required_approvals: i64,
123    /// The status check contexts that must report success.
124    pub status_check_contexts: Vec<String>,
125    /// Glob patterns for files that any pusher may change despite the
126    /// protected patterns.
127    pub unprotected_file_patterns: Vec<String>,
128}
129
130/// Desired branch-protection rules.
131#[derive(Clone, Debug, Eq, Facet, PartialEq)]
132#[facet(deny_unknown_fields)]
133pub struct BranchRules {
134    /// Whether undeclared branch rules are deleted.
135    pub mode: Mode,
136    /// The desired rules, keyed by rule name.
137    pub rules: BTreeMap<RuleName, BranchRule>,
138}
139
140impl BranchRule {
141    /// Return contexts in the comparison and write order.
142    #[must_use]
143    pub fn contexts(&self) -> NameSet {
144        NameSet::new(self.status_check_contexts.iter().cloned())
145    }
146}