Skip to main content

wire/
message_refs.rs

1// SPDX-License-Identifier: Apache-2.0
2use objects::object::StateId;
3use serde::{Deserialize, Serialize};
4
5/// Filter applied when listing repository refs.
6///
7/// Retained as part of the published `heddle-wire` 0.11 surface while Weft
8/// migrates to the generated hosted API types.
9#[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/// Repository HEAD shape returned alongside a ref listing.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub enum HeadInfo {
74    Attached { thread: String },
75    Detached { state: StateId },
76}
77
78/// Published repository-ref listing response.
79#[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}