1use objects::object::StateId;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct RefFilter {
11 #[serde(default)]
12 pub names: Vec<String>,
13 #[serde(default)]
14 pub patterns: Vec<String>,
15 #[serde(default = "default_true")]
16 pub include_threads: bool,
17 #[serde(default = "default_true")]
18 pub include_markers: bool,
19 #[serde(default)]
20 pub limit: Option<usize>,
21}
22
23fn default_true() -> bool {
24 true
25}
26
27impl Default for RefFilter {
28 fn default() -> Self {
29 Self {
30 names: Vec::new(),
31 patterns: Vec::new(),
32 include_threads: true,
33 include_markers: true,
34 limit: None,
35 }
36 }
37}
38
39impl RefFilter {
40 pub fn matches(&self, name: &str) -> bool {
41 if !self.names.is_empty() && self.names.iter().any(|candidate| candidate == name) {
42 return true;
43 }
44
45 if self.patterns.is_empty() {
46 return self.names.is_empty();
47 }
48
49 self.patterns
50 .iter()
51 .any(|pattern| Self::matches_pattern(name, pattern))
52 }
53
54 fn matches_pattern(name: &str, pattern: &str) -> bool {
55 if pattern == "*" {
56 return true;
57 }
58 if pattern.starts_with('*') && pattern.ends_with('*') && pattern.len() >= 2 {
59 return name.contains(&pattern[1..pattern.len() - 1]);
60 }
61 if let Some(suffix) = pattern.strip_prefix('*') {
62 return name.ends_with(suffix);
63 }
64 if let Some(prefix) = pattern.strip_suffix('*') {
65 return name.starts_with(prefix);
66 }
67 name == pattern
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub enum HeadInfo {
74 Attached { thread: String },
75 Detached { state: StateId },
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct RefsList {
81 pub head: HeadInfo,
82 pub head_state: Option<StateId>,
83 pub refs: Vec<RefEntry>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct RefEntry {
88 pub name: String,
89 pub state_id: StateId,
90 pub is_thread: bool,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct RefUpdated {
95 pub success: bool,
96 pub old_value: Option<StateId>,
97 pub error: Option<String>,
98}
99
100#[cfg(test)]
101mod tests {
102 use super::RefFilter;
103
104 #[test]
105 fn ref_filter_matches_union_of_names_and_patterns() {
106 let filter = RefFilter {
107 names: vec!["refs/heads/main".to_string()],
108 patterns: vec!["refs/tags/v*".to_string()],
109 ..RefFilter::default()
110 };
111
112 assert!(filter.matches("refs/heads/main"));
113 assert!(filter.matches("refs/tags/v1.0.0"));
114 assert!(!filter.matches("refs/heads/feature"));
115 assert!(!filter.matches("refs/tags/nightly"));
116 }
117
118 #[test]
119 fn ref_filter_without_names_or_patterns_matches_everything() {
120 let filter = RefFilter::default();
121
122 assert!(filter.matches("refs/heads/main"));
123 assert!(filter.matches("refs/tags/v1.0.0"));
124 assert!(filter.matches("threads/alice"));
125 }
126
127 #[test]
128 fn ref_filter_exact_names_do_not_expand_without_patterns() {
129 let filter = RefFilter {
130 names: vec!["refs/heads/main".to_string()],
131 patterns: Vec::new(),
132 ..RefFilter::default()
133 };
134
135 assert!(filter.matches("refs/heads/main"));
136 assert!(!filter.matches("refs/heads/mainline"));
137 }
138}