heddle_object_model/
refs.rs1use 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}