use crate::config::ConfigSet;
use crate::wildmatch::{wildmatch, WM_PATHNAME};
#[derive(Debug, Clone)]
struct HideRefRule {
pattern: String,
negated: bool,
full_ref: bool,
}
#[derive(Debug, Clone, Default)]
pub struct RefExclusions {
excluded_refs: Vec<String>,
hidden_rules: Vec<HideRefRule>,
pub hidden_configured: bool,
}
impl RefExclusions {
pub fn clear(&mut self) {
self.excluded_refs.clear();
self.hidden_rules.clear();
self.hidden_configured = false;
}
pub fn add_excluded_ref(&mut self, pattern: impl Into<String>) {
self.excluded_refs.push(pattern.into());
}
pub fn load_hidden_refs_from_config(&mut self, config: &ConfigSet, section: &str) {
self.hidden_configured = true;
let section_key = format!("{section}.hiderefs");
for e in config.entries() {
if e.key == "transfer.hiderefs" || e.key == section_key {
if let Some(v) = e.value.as_deref() {
self.hidden_rules.push(parse_hide_refs_value(v));
}
}
}
}
pub fn ref_excluded(&self, stripped_name: Option<&str>, full_name: &str) -> bool {
for pat in &self.excluded_refs {
if wildmatch(pat.as_bytes(), full_name.as_bytes(), WM_PATHNAME) {
return true;
}
}
ref_is_hidden(stripped_name, full_name, &self.hidden_rules)
}
}
fn trim_trailing_slashes(mut s: String) -> String {
while s.ends_with('/') {
s.pop();
}
s
}
fn parse_hide_refs_value(raw: &str) -> HideRefRule {
let mut rest = raw;
let mut negated = false;
if let Some(stripped) = rest.strip_prefix('!') {
negated = true;
rest = stripped;
}
let mut full_ref = false;
if let Some(stripped) = rest.strip_prefix('^') {
full_ref = true;
rest = stripped;
}
HideRefRule {
pattern: trim_trailing_slashes(rest.to_owned()),
negated,
full_ref,
}
}
fn ref_is_hidden(stripped_name: Option<&str>, full_name: &str, rules: &[HideRefRule]) -> bool {
for rule in rules.iter().rev() {
let subject = if rule.full_ref {
full_name
} else {
match stripped_name {
Some(s) => s,
None => continue,
}
};
if subject.is_empty() {
continue;
}
let pat = rule.pattern.as_str();
if pat.is_empty() {
continue;
}
if skip_prefix_git(subject, pat)
.is_some_and(|tail| tail.is_empty() || tail.starts_with('/'))
{
return !rule.negated;
}
}
false
}
fn skip_prefix_git<'a>(subject: &'a str, prefix: &str) -> Option<&'a str> {
let b = subject.as_bytes();
let p = prefix.as_bytes();
if p.is_empty() {
return Some(subject);
}
if b.len() < p.len() {
return None;
}
if &b[..p.len()] == p {
subject.get(p.len()..)
} else {
None
}
}
pub fn git_namespace_prefix() -> String {
let raw = std::env::var("GIT_NAMESPACE").unwrap_or_default();
if raw.is_empty() {
return String::new();
}
let mut out = String::new();
for comp in raw.split('/') {
if comp.is_empty() {
continue;
}
out.push_str("refs/namespaces/");
out.push_str(comp);
out.push('/');
}
while out.ends_with('/') {
out.pop();
}
if !out.is_empty() {
out.push('/');
}
out
}
pub fn strip_git_namespace<'a>(refname: &'a str, namespace_prefix: &str) -> Option<&'a str> {
if namespace_prefix.is_empty() {
return Some(refname);
}
refname.strip_prefix(namespace_prefix)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hide_refs_prefix_match() {
let rules = vec![parse_hide_refs_value("refs/hidden/")];
assert!(ref_is_hidden(
Some("refs/hidden/foo"),
"refs/hidden/foo",
&rules
));
assert!(!ref_is_hidden(
Some("refs/heads/main"),
"refs/heads/main",
&rules
));
}
#[test]
fn hide_refs_negation() {
let rules = vec![
parse_hide_refs_value("refs/foo/"),
parse_hide_refs_value("!refs/foo/bar"),
];
assert!(!ref_is_hidden(Some("refs/foo/bar"), "refs/foo/bar", &rules));
assert!(ref_is_hidden(Some("refs/foo/baz"), "refs/foo/baz", &rules));
}
}