grit_lib/
ref_exclusions.rs1use crate::config::ConfigSet;
7use crate::wildmatch::{wildmatch, WM_PATHNAME};
8
9#[derive(Debug, Clone)]
11struct HideRefRule {
12 pattern: String,
14 negated: bool,
16 full_ref: bool,
18}
19
20#[derive(Debug, Clone, Default)]
22pub struct RefExclusions {
23 excluded_refs: Vec<String>,
25 hidden_rules: Vec<HideRefRule>,
27 pub hidden_configured: bool,
30}
31
32impl RefExclusions {
33 pub fn clear(&mut self) {
35 self.excluded_refs.clear();
36 self.hidden_rules.clear();
37 self.hidden_configured = false;
38 }
39
40 pub fn add_excluded_ref(&mut self, pattern: impl Into<String>) {
42 self.excluded_refs.push(pattern.into());
43 }
44
45 pub fn load_hidden_refs_from_config(&mut self, config: &ConfigSet, section: &str) {
49 self.hidden_configured = true;
50 let section_key = format!("{section}.hiderefs");
51 for e in config.entries() {
52 if e.key == "transfer.hiderefs" || e.key == section_key {
53 if let Some(v) = e.value.as_deref() {
54 self.hidden_rules.push(parse_hide_refs_value(v));
55 }
56 }
57 }
58 }
59
60 pub fn ref_excluded(&self, stripped_name: Option<&str>, full_name: &str) -> bool {
65 for pat in &self.excluded_refs {
66 if wildmatch(pat.as_bytes(), full_name.as_bytes(), WM_PATHNAME) {
67 return true;
68 }
69 }
70 ref_is_hidden(stripped_name, full_name, &self.hidden_rules)
71 }
72}
73
74fn trim_trailing_slashes(mut s: String) -> String {
75 while s.ends_with('/') {
76 s.pop();
77 }
78 s
79}
80
81fn parse_hide_refs_value(raw: &str) -> HideRefRule {
82 let mut rest = raw;
83 let mut negated = false;
84 if let Some(stripped) = rest.strip_prefix('!') {
85 negated = true;
86 rest = stripped;
87 }
88 let mut full_ref = false;
89 if let Some(stripped) = rest.strip_prefix('^') {
90 full_ref = true;
91 rest = stripped;
92 }
93 HideRefRule {
94 pattern: trim_trailing_slashes(rest.to_owned()),
95 negated,
96 full_ref,
97 }
98}
99
100fn ref_is_hidden(stripped_name: Option<&str>, full_name: &str, rules: &[HideRefRule]) -> bool {
101 for rule in rules.iter().rev() {
102 let subject = if rule.full_ref {
103 full_name
104 } else {
105 match stripped_name {
106 Some(s) => s,
107 None => continue,
108 }
109 };
110 if subject.is_empty() {
111 continue;
112 }
113 let pat = rule.pattern.as_str();
114 if pat.is_empty() {
115 continue;
116 }
117 if skip_prefix_git(subject, pat)
118 .is_some_and(|tail| tail.is_empty() || tail.starts_with('/'))
119 {
120 return !rule.negated;
121 }
122 }
123 false
124}
125
126fn skip_prefix_git<'a>(subject: &'a str, prefix: &str) -> Option<&'a str> {
128 let b = subject.as_bytes();
129 let p = prefix.as_bytes();
130 if p.is_empty() {
131 return Some(subject);
132 }
133 if b.len() < p.len() {
134 return None;
135 }
136 if &b[..p.len()] == p {
137 subject.get(p.len()..)
138 } else {
139 None
140 }
141}
142
143pub fn git_namespace_prefix() -> String {
146 let raw = std::env::var("GIT_NAMESPACE").unwrap_or_default();
147 if raw.is_empty() {
148 return String::new();
149 }
150 let mut out = String::new();
151 for comp in raw.split('/') {
152 if comp.is_empty() {
153 continue;
154 }
155 out.push_str("refs/namespaces/");
156 out.push_str(comp);
157 out.push('/');
158 }
159 while out.ends_with('/') {
160 out.pop();
161 }
162 if !out.is_empty() {
163 out.push('/');
164 }
165 out
166}
167
168pub fn strip_git_namespace<'a>(refname: &'a str, namespace_prefix: &str) -> Option<&'a str> {
170 if namespace_prefix.is_empty() {
171 return Some(refname);
172 }
173 refname.strip_prefix(namespace_prefix)
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn hide_refs_prefix_match() {
182 let rules = vec![parse_hide_refs_value("refs/hidden/")];
183 assert!(ref_is_hidden(
184 Some("refs/hidden/foo"),
185 "refs/hidden/foo",
186 &rules
187 ));
188 assert!(!ref_is_hidden(
189 Some("refs/heads/main"),
190 "refs/heads/main",
191 &rules
192 ));
193 }
194
195 #[test]
196 fn hide_refs_negation() {
197 let rules = vec![
198 parse_hide_refs_value("refs/foo/"),
199 parse_hide_refs_value("!refs/foo/bar"),
200 ];
201 assert!(!ref_is_hidden(Some("refs/foo/bar"), "refs/foo/bar", &rules));
202 assert!(ref_is_hidden(Some("refs/foo/baz"), "refs/foo/baz", &rules));
203 }
204}