Skip to main content

heddle_object_model/
refs.rs

1// SPDX-License-Identifier: Apache-2.0
2//! In-memory packed refs model and text format.
3
4use std::collections::HashMap;
5
6use crate::object::StateId;
7
8const THREADS_PREFIX: &str = "refs/threads/";
9const MARKERS_PREFIX: &str = "refs/markers/";
10
11#[derive(Clone, Debug)]
12pub struct PackedRefsModel {
13    threads: HashMap<String, StateId>,
14    markers: HashMap<String, StateId>,
15}
16
17impl PackedRefsModel {
18    pub fn new() -> Self {
19        Self {
20            threads: HashMap::new(),
21            markers: HashMap::new(),
22        }
23    }
24
25    pub fn parse(contents: &str) -> Self {
26        let mut packed = Self::new();
27        for line in contents.lines() {
28            if line.is_empty() || line.starts_with('#') {
29                continue;
30            }
31            let mut parts = line.splitn(2, ' ');
32            let (Some(id_str), Some(refname)) = (parts.next(), parts.next()) else {
33                continue;
34            };
35            let id = match StateId::parse(id_str) {
36                Ok(id) => id,
37                Err(_) => continue,
38            };
39            if let Some(name) = refname.strip_prefix(THREADS_PREFIX) {
40                packed.threads.insert(name.to_string(), id);
41            } else if let Some(name) = refname.strip_prefix(MARKERS_PREFIX) {
42                packed.markers.insert(name.to_string(), id);
43            }
44        }
45        packed
46    }
47
48    pub fn to_text(&self) -> String {
49        let mut lines: Vec<String> =
50            vec!["# packed-refs with: peeled fully-peeled sorted".to_string()];
51        for (name, id) in &self.threads {
52            lines.push(format!(
53                "{} {}{}",
54                id.to_string_full(),
55                THREADS_PREFIX,
56                name
57            ));
58        }
59        for (name, id) in &self.markers {
60            lines.push(format!(
61                "{} {}{}",
62                id.to_string_full(),
63                MARKERS_PREFIX,
64                name
65            ));
66        }
67        lines.sort();
68        lines.join("\n") + "\n"
69    }
70
71    pub fn get_thread(&self, name: &str) -> Option<StateId> {
72        self.threads.get(name).copied()
73    }
74    pub fn get_marker(&self, name: &str) -> Option<StateId> {
75        self.markers.get(name).copied()
76    }
77    pub fn set_thread(&mut self, name: &str, id: StateId) {
78        self.threads.insert(name.to_string(), id);
79    }
80    pub fn set_marker(&mut self, name: &str, id: StateId) {
81        self.markers.insert(name.to_string(), id);
82    }
83    pub fn remove_track(&mut self, name: &str) {
84        self.threads.remove(name);
85    }
86    pub fn remove_marker(&mut self, name: &str) {
87        self.markers.remove(name);
88    }
89    pub fn list_threads(&self) -> Vec<String> {
90        self.threads.keys().cloned().collect()
91    }
92    pub fn list_markers(&self) -> Vec<String> {
93        self.markers.keys().cloned().collect()
94    }
95    pub fn is_empty(&self) -> bool {
96        self.threads.is_empty() && self.markers.is_empty()
97    }
98}
99
100impl Default for PackedRefsModel {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn packed_refs_keep_exact_unicode_suffixes() {
112        let state = StateId::from_bytes([3; 32]);
113        let name = "trailing\u{a0}";
114        let mut refs = PackedRefsModel::new();
115        refs.set_thread(name, state);
116        refs.set_marker(name, state);
117        let parsed = PackedRefsModel::parse(&refs.to_text().replace('\n', "\r\n"));
118        assert_eq!(parsed.get_thread(name), Some(state));
119        assert_eq!(parsed.get_marker(name), Some(state));
120        assert_eq!(parsed.get_thread("trailing"), None);
121    }
122}