1use arrow::array::{BooleanArray, RecordBatch, StringArray, TimestampMillisecondArray};
7use arrow::datatypes::{DataType, Field, Schema, TimeUnit};
8use std::sync::Arc;
9
10pub fn refs_schema() -> Schema {
12 Schema::new(vec![
13 Field::new("ref_name", DataType::Utf8, false),
14 Field::new("commit_id", DataType::Utf8, false),
15 Field::new("ref_type", DataType::Utf8, false), Field::new("is_head", DataType::Boolean, false),
17 Field::new(
18 "created_at",
19 DataType::Timestamp(TimeUnit::Millisecond, Some("UTC".into())),
20 false,
21 ),
22 ])
23}
24
25#[derive(Debug, Clone)]
27pub struct Ref {
28 pub ref_name: String,
29 pub commit_id: String,
30 pub ref_type: RefType,
31 pub is_head: bool,
32 pub created_at_ms: i64,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum RefType {
38 Branch,
39 Tag,
40}
41
42impl RefType {
43 pub fn as_str(&self) -> &'static str {
44 match self {
45 RefType::Branch => "branch",
46 RefType::Tag => "tag",
47 }
48 }
49}
50
51pub struct RefsTable {
53 refs: Vec<Ref>,
54}
55
56impl RefsTable {
57 pub fn new() -> Self {
59 RefsTable { refs: Vec::new() }
60 }
61
62 pub fn init_main(&mut self, commit_id: &str) {
64 let now_ms = chrono::Utc::now().timestamp_millis();
65 self.refs.push(Ref {
66 ref_name: "main".to_string(),
67 commit_id: commit_id.to_string(),
68 ref_type: RefType::Branch,
69 is_head: true,
70 created_at_ms: now_ms,
71 });
72 }
73
74 pub fn head(&self) -> Option<&Ref> {
76 self.refs.iter().find(|r| r.is_head)
77 }
78
79 pub fn get(&self, name: &str) -> Option<&Ref> {
81 self.refs.iter().find(|r| r.ref_name == name)
82 }
83
84 pub fn resolve(&self, name: &str) -> Option<&str> {
86 self.get(name).map(|r| r.commit_id.as_str())
87 }
88
89 pub fn create_branch(&mut self, name: &str, commit_id: &str) -> Result<(), RefsError> {
91 if self.get(name).is_some() {
92 return Err(RefsError::RefExists(name.to_string()));
93 }
94 let now_ms = chrono::Utc::now().timestamp_millis();
95 self.refs.push(Ref {
96 ref_name: name.to_string(),
97 commit_id: commit_id.to_string(),
98 ref_type: RefType::Branch,
99 is_head: false,
100 created_at_ms: now_ms,
101 });
102 Ok(())
103 }
104
105 pub fn switch_head(&mut self, name: &str) -> Result<(), RefsError> {
107 if self.get(name).is_none() {
108 return Err(RefsError::RefNotFound(name.to_string()));
109 }
110 for r in &mut self.refs {
111 r.is_head = r.ref_name == name;
112 }
113 Ok(())
114 }
115
116 pub fn update_ref(&mut self, name: &str, commit_id: &str) -> Result<(), RefsError> {
120 let r = self
121 .refs
122 .iter_mut()
123 .find(|r| r.ref_name == name)
124 .ok_or_else(|| RefsError::RefNotFound(name.to_string()))?;
125 if r.ref_type == RefType::Tag {
126 return Err(RefsError::TagImmutable(name.to_string()));
127 }
128 r.commit_id = commit_id.to_string();
129 Ok(())
130 }
131
132 pub fn delete_branch(&mut self, name: &str) -> Result<(), RefsError> {
137 let r = self
138 .refs
139 .iter()
140 .find(|r| r.ref_name == name)
141 .ok_or_else(|| RefsError::RefNotFound(name.to_string()))?;
142
143 if r.is_head {
144 return Err(RefsError::DeleteHead(name.to_string()));
145 }
146
147 if r.ref_type != RefType::Branch {
148 return Err(RefsError::NotABranch(name.to_string()));
149 }
150
151 self.refs.retain(|r| r.ref_name != name);
152 Ok(())
153 }
154
155 pub fn create_tag(&mut self, name: &str, commit_id: &str) -> Result<(), RefsError> {
159 if self.get(name).is_some() {
160 return Err(RefsError::RefExists(name.to_string()));
161 }
162 let now_ms = chrono::Utc::now().timestamp_millis();
163 self.refs.push(Ref {
164 ref_name: name.to_string(),
165 commit_id: commit_id.to_string(),
166 ref_type: RefType::Tag,
167 is_head: false,
168 created_at_ms: now_ms,
169 });
170 Ok(())
171 }
172
173 pub fn tags(&self) -> Vec<&Ref> {
175 self.refs
176 .iter()
177 .filter(|r| r.ref_type == RefType::Tag)
178 .collect()
179 }
180
181 pub fn branches(&self) -> Vec<&Ref> {
183 self.refs
184 .iter()
185 .filter(|r| r.ref_type == RefType::Branch)
186 .collect()
187 }
188
189 pub fn to_record_batch(&self) -> Result<RecordBatch, arrow::error::ArrowError> {
191 let schema = Arc::new(refs_schema());
192 if self.refs.is_empty() {
193 return Ok(RecordBatch::new_empty(schema));
194 }
195
196 let names: Vec<&str> = self.refs.iter().map(|r| r.ref_name.as_str()).collect();
197 let commits: Vec<&str> = self.refs.iter().map(|r| r.commit_id.as_str()).collect();
198 let types: Vec<&str> = self.refs.iter().map(|r| r.ref_type.as_str()).collect();
199 let heads: Vec<bool> = self.refs.iter().map(|r| r.is_head).collect();
200 let times: Vec<i64> = self.refs.iter().map(|r| r.created_at_ms).collect();
201
202 RecordBatch::try_new(
203 schema,
204 vec![
205 Arc::new(StringArray::from(names)),
206 Arc::new(StringArray::from(commits)),
207 Arc::new(StringArray::from(types)),
208 Arc::new(BooleanArray::from(heads)),
209 Arc::new(TimestampMillisecondArray::from(times).with_timezone("UTC")),
210 ],
211 )
212 }
213}
214
215impl Default for RefsTable {
216 fn default() -> Self {
217 Self::new()
218 }
219}
220
221#[derive(Debug, thiserror::Error)]
223pub enum RefsError {
224 #[error("Ref already exists: {0}")]
225 RefExists(String),
226
227 #[error("Ref not found: {0}")]
228 RefNotFound(String),
229
230 #[error("Cannot delete HEAD branch: {0}")]
231 DeleteHead(String),
232
233 #[error("Tags are immutable and cannot be moved: {0}")]
234 TagImmutable(String),
235
236 #[error("Ref is not a branch: {0}")]
237 NotABranch(String),
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_init_main_and_head() {
246 let mut refs = RefsTable::new();
247 refs.init_main("c1");
248
249 let head = refs.head().unwrap();
250 assert_eq!(head.ref_name, "main");
251 assert_eq!(head.commit_id, "c1");
252 assert!(head.is_head);
253 }
254
255 #[test]
256 fn test_create_branch_and_switch() {
257 let mut refs = RefsTable::new();
258 refs.init_main("c1");
259 refs.create_branch("feature", "c1").unwrap();
260
261 assert_eq!(refs.branches().len(), 2);
262
263 refs.switch_head("feature").unwrap();
264 assert_eq!(refs.head().unwrap().ref_name, "feature");
265 }
266
267 #[test]
268 fn test_duplicate_branch_fails() {
269 let mut refs = RefsTable::new();
270 refs.init_main("c1");
271 let result = refs.create_branch("main", "c1");
272 assert!(result.is_err());
273 }
274
275 #[test]
276 fn test_switch_nonexistent_branch_fails() {
277 let mut refs = RefsTable::new();
278 refs.init_main("c1");
279 let result = refs.switch_head("nonexistent");
280 assert!(result.is_err());
281 }
282
283 #[test]
284 fn test_update_ref() {
285 let mut refs = RefsTable::new();
286 refs.init_main("c1");
287 refs.update_ref("main", "c2").unwrap();
288 assert_eq!(refs.resolve("main"), Some("c2"));
289 }
290
291 #[test]
292 fn test_to_record_batch() {
293 let mut refs = RefsTable::new();
294 refs.init_main("c1");
295 refs.create_branch("dev", "c1").unwrap();
296
297 let batch = refs.to_record_batch().unwrap();
298 assert_eq!(batch.num_rows(), 2);
299 assert_eq!(batch.num_columns(), 5);
300 }
301
302 #[test]
305 fn test_delete_branch_works() {
306 let mut refs = RefsTable::new();
307 refs.init_main("c1");
308 refs.create_branch("feature", "c1").unwrap();
309 assert_eq!(refs.branches().len(), 2);
310
311 refs.delete_branch("feature").unwrap();
312 assert_eq!(refs.branches().len(), 1);
313 assert!(refs.get("feature").is_none());
314 }
315
316 #[test]
317 fn test_delete_head_branch_fails() {
318 let mut refs = RefsTable::new();
319 refs.init_main("c1");
320 let result = refs.delete_branch("main");
321 assert!(result.is_err());
322 match result.unwrap_err() {
323 RefsError::DeleteHead(name) => assert_eq!(name, "main"),
324 other => panic!("Expected DeleteHead, got: {other:?}"),
325 }
326 }
327
328 #[test]
329 fn test_delete_nonexistent_branch_fails() {
330 let mut refs = RefsTable::new();
331 refs.init_main("c1");
332 let result = refs.delete_branch("ghost");
333 assert!(result.is_err());
334 match result.unwrap_err() {
335 RefsError::RefNotFound(name) => assert_eq!(name, "ghost"),
336 other => panic!("Expected RefNotFound, got: {other:?}"),
337 }
338 }
339
340 #[test]
343 fn test_create_tag() {
344 let mut refs = RefsTable::new();
345 refs.init_main("c1");
346 refs.create_tag("v1.0", "c1").unwrap();
347
348 let tags = refs.tags();
349 assert_eq!(tags.len(), 1);
350 assert_eq!(tags[0].ref_name, "v1.0");
351 assert_eq!(tags[0].commit_id, "c1");
352 assert_eq!(tags[0].ref_type, RefType::Tag);
353 }
354
355 #[test]
356 fn test_duplicate_tag_fails() {
357 let mut refs = RefsTable::new();
358 refs.init_main("c1");
359 refs.create_tag("v1.0", "c1").unwrap();
360 let result = refs.create_tag("v1.0", "c2");
361 assert!(result.is_err());
362 }
363
364 #[test]
365 fn test_tag_survives_branch_delete() {
366 let mut refs = RefsTable::new();
367 refs.init_main("c1");
368 refs.create_branch("feature", "c2").unwrap();
369 refs.create_tag("v1.0", "c2").unwrap();
370
371 refs.delete_branch("feature").unwrap();
372
373 assert_eq!(refs.resolve("v1.0"), Some("c2"));
375 assert_eq!(refs.tags().len(), 1);
376 }
377
378 #[test]
379 fn test_update_ref_rejects_tag() {
380 let mut refs = RefsTable::new();
381 refs.init_main("c1");
382 refs.create_tag("v1.0", "c1").unwrap();
383 let result = refs.update_ref("v1.0", "c2");
384 assert!(result.is_err());
385 match result.unwrap_err() {
386 RefsError::TagImmutable(name) => assert_eq!(name, "v1.0"),
387 other => panic!("Expected TagImmutable, got: {other:?}"),
388 }
389 assert_eq!(refs.resolve("v1.0"), Some("c1"));
391 }
392
393 #[test]
394 fn test_delete_branch_rejects_tag() {
395 let mut refs = RefsTable::new();
396 refs.init_main("c1");
397 refs.create_tag("v1.0", "c1").unwrap();
398 let result = refs.delete_branch("v1.0");
399 assert!(result.is_err());
400 match result.unwrap_err() {
401 RefsError::NotABranch(name) => assert_eq!(name, "v1.0"),
402 other => panic!("Expected NotABranch, got: {other:?}"),
403 }
404 }
405
406 #[test]
407 fn test_tags_not_in_branches() {
408 let mut refs = RefsTable::new();
409 refs.init_main("c1");
410 refs.create_tag("v1.0", "c1").unwrap();
411
412 assert_eq!(refs.branches().len(), 1);
414 assert_eq!(refs.tags().len(), 1);
415 }
416}