crates/gunnar-policy/src/gates/refname.rs
//! Gate 2 decides which reference names may exist.
//!
//! The cheapest gate and the one most likely to be written blind, because a
//! test that pushes `refs/heads/main` through an allowlist containing
//! `refs/heads/*` passes whether or not the matcher does anything at all. The
//! tests behind it therefore always assert a *refused* name alongside an accepted
//! one, and the `**`-versus-`*` distinction is pinned separately in
//! [`crate::pattern`].
use crate::config::RefNamePolicy;
use crate::context::PushContext;
use crate::error::Result;
use crate::pattern;
use crate::{Gate, Verdict};
/// Refuses reference names the namespace has not sanctioned.
#[derive(Clone, Debug)]
pub struct RefNameGate {
policy: RefNamePolicy,
}
impl RefNameGate {
/// A gate over `policy`.
pub fn new(policy: RefNamePolicy) -> Self {
RefNameGate { policy }
}
/// The policy this gate enforces.
pub fn policy(&self) -> &RefNamePolicy {
&self.policy
}
}
impl Gate for RefNameGate {
fn name(&self) -> &str {
"ref-name"
}
fn check(&self, ctx: &PushContext<'_>) -> Result<Verdict> {
for update in ctx.updates {
// A delete is judged on the name too: a reference that should never
// have existed is still a reference somebody may not touch, and
// exempting deletes here would make the denylist a suggestion.
if let Some(denied) = pattern::first_match(&self.policy.deny, &update.name) {
return Ok(Verdict::reject_ref(
update.name.clone(),
format!(
"reference name {} is denied by pattern {denied}",
update.name
),
));
}
if !self.policy.allow.is_empty()
&& !pattern::any_matches(&self.policy.allow, &update.name)
{
let allowed = self
.policy
.allow
.iter()
.map(|p| p.as_str())
.collect::<Vec<_>>()
.join(", ");
return Ok(Verdict::reject_ref(
update.name.clone(),
format!(
"reference name {} matches no allowed pattern ({allowed})",
update.name
),
));
}
}
Ok(Verdict::Allow)
}
}