1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//! Reference exclusion rules for `rev-list` / `rev-parse` (`--exclude`, `--exclude-hidden`).
//!
//! Mirrors Git's `ref_exclusions` / `parse_hide_refs_config` / `ref_is_hidden` in `revision.c`
//! and `refs.c`.
use crate::config::ConfigSet;
use crate::wildmatch::wildmatch;
/// One `transfer.hideRefs` / `<section>.hideRefs` prefix rule (after normalization).
#[derive(Debug, Clone)]
struct HideRefRule {
/// Pattern without leading `!` or `^`.
pattern: String,
/// If true, the rule negates a previous hide (Git `!` prefix).
negated: bool,
/// If true, match against the full ref name (`^` prefix in config).
full_ref: bool,
}
/// Patterns that exclude refs from `--all` / glob expansion, including hidden-ref config.
#[derive(Debug, Clone, Default)]
pub struct RefExclusions {
/// `wildmatch` patterns from `--exclude=<pat>` (full ref names).
excluded_refs: Vec<String>,
/// Rules from config when `--exclude-hidden=<section>` is active.
hidden_rules: Vec<HideRefRule>,
/// Set after `--exclude-hidden=` is parsed; cleared by [`RefExclusions::clear`].
/// Used to reject a second `--exclude-hidden` before the next pseudo-ref clears state.
pub hidden_configured: bool,
}
impl RefExclusions {
/// Reset exclusions after `--all` / `--glob` / `--branches` / … (matches Git `clear_ref_exclusions`).
pub fn clear(&mut self) {
self.excluded_refs.clear();
self.hidden_rules.clear();
self.hidden_configured = false;
}
/// Append a `--exclude=<pattern>` entry (Git wildmatch on the ref name).
pub fn add_excluded_ref(&mut self, pattern: impl Into<String>) {
self.excluded_refs.push(pattern.into());
}
/// Load `transfer.hideRefs` and `<section>.hideRefs` into this set.
///
/// `section` must be one of `fetch`, `receive`, or `uploadpack`.
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));
}
}
}
}
/// Whether this ref should be omitted from ref listing (exclude + hidden rules).
///
/// - `stripped_name` — ref name with `GIT_NAMESPACE` prefix removed, when applicable.
/// - `full_name` — storage path of the ref (e.g. `refs/heads/main`).
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(), 0) {
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
}
/// Git `skip_prefix` semantics: `subject` must begin with `prefix` byte-for-byte; returns the tail.
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
}
}
/// `GIT_NAMESPACE` value (e.g. `namespace` / `a/b`) expanded to the `refs/namespaces/.../`
/// prefix, or empty string when unset.
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
}
/// Strip a leading namespace prefix from `refname`, returning `None` when not under the namespace.
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));
}
}