Skip to main content

wire/message_refs/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2use objects::object::StateId;
3use serde::{Deserialize, Serialize};
4
5mod kind;
6
7pub use kind::{AdvertisedRef, AdvertisedRefError, RefKind};
8
9/// Filter applied when listing repository refs.
10///
11/// Retained as part of the published `heddle-wire` 0.11 surface while Weft
12/// migrates to the generated hosted API types.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct RefFilter {
15    #[serde(default)]
16    pub names: Vec<String>,
17    #[serde(default)]
18    pub patterns: Vec<String>,
19    #[serde(default = "default_true")]
20    pub include_threads: bool,
21    #[serde(default = "default_true")]
22    pub include_markers: bool,
23    /// Synthetic frontier roots are an internal embargo mechanism and stay
24    /// hidden unless a caller opts in.
25    #[serde(default)]
26    pub include_synthetic: bool,
27    #[serde(default)]
28    pub limit: Option<usize>,
29}
30
31fn default_true() -> bool {
32    true
33}
34
35impl Default for RefFilter {
36    fn default() -> Self {
37        Self {
38            names: Vec::new(),
39            patterns: Vec::new(),
40            include_threads: true,
41            include_markers: true,
42            include_synthetic: false,
43            limit: None,
44        }
45    }
46}
47
48impl RefFilter {
49    pub fn matches(&self, name: &str) -> bool {
50        if !self.names.is_empty() && self.names.iter().any(|candidate| candidate == name) {
51            return true;
52        }
53
54        if self.patterns.is_empty() {
55            return self.names.is_empty();
56        }
57
58        self.patterns
59            .iter()
60            .any(|pattern| Self::matches_pattern(name, pattern))
61    }
62
63    pub fn includes_kind(&self, kind: RefKind) -> bool {
64        match kind {
65            RefKind::Thread => self.include_threads,
66            RefKind::Marker => self.include_markers,
67            RefKind::SyntheticFrontierRoot => self.include_synthetic,
68        }
69    }
70
71    fn matches_pattern(name: &str, pattern: &str) -> bool {
72        if pattern == "*" {
73            return true;
74        }
75        if pattern.starts_with('*') && pattern.ends_with('*') && pattern.len() >= 2 {
76            return name.contains(&pattern[1..pattern.len() - 1]);
77        }
78        if let Some(suffix) = pattern.strip_prefix('*') {
79            return name.ends_with(suffix);
80        }
81        if let Some(prefix) = pattern.strip_suffix('*') {
82            return name.starts_with(prefix);
83        }
84        name == pattern
85    }
86}
87
88/// Repository HEAD shape returned alongside a ref listing.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub enum HeadInfo {
91    Attached { thread: String },
92    Detached { state: StateId },
93}
94
95/// Published repository-ref listing response.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct RefsList {
98    pub head: HeadInfo,
99    pub head_state: Option<StateId>,
100    pub refs: Vec<RefEntry>,
101}
102
103/// One advertised repository ref.
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct RefEntry {
106    pub name: String,
107    pub state_id: StateId,
108    pub kind: RefKind,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct RefUpdated {
113    pub success: bool,
114    pub old_value: Option<StateId>,
115    pub error: Option<String>,
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn ref_filter_matches_union_of_names_and_patterns() {
124        let filter = RefFilter {
125            names: vec!["refs/heads/main".to_string()],
126            patterns: vec!["refs/tags/v*".to_string()],
127            ..RefFilter::default()
128        };
129
130        assert!(filter.matches("refs/heads/main"));
131        assert!(filter.matches("refs/tags/v1.0.0"));
132        assert!(!filter.matches("refs/heads/feature"));
133        assert!(!filter.matches("refs/tags/nightly"));
134    }
135
136    #[test]
137    fn ref_filter_without_names_or_patterns_matches_everything() {
138        let filter = RefFilter::default();
139
140        assert!(filter.matches("refs/heads/main"));
141        assert!(filter.matches("refs/tags/v1.0.0"));
142        assert!(filter.matches("threads/alice"));
143        assert!(!filter.includes_kind(RefKind::SyntheticFrontierRoot));
144        assert!(filter.includes_kind(RefKind::Thread));
145        assert!(filter.includes_kind(RefKind::Marker));
146    }
147
148    #[test]
149    fn ref_filter_exact_names_do_not_expand_without_patterns() {
150        let filter = RefFilter {
151            names: vec!["refs/heads/main".to_string()],
152            patterns: Vec::new(),
153            ..RefFilter::default()
154        };
155
156        assert!(filter.matches("refs/heads/main"));
157        assert!(!filter.matches("refs/heads/mainline"));
158    }
159}