1use std::collections::{BTreeMap, BTreeSet};
29use std::fs;
30use std::io::{Read, Write};
31use std::path::{Path, PathBuf};
32use std::thread;
33use std::time::{Duration, Instant};
34
35use crate::config::ConfigSet;
36use crate::error::{Error, Result};
37use crate::objects::ObjectId;
38
39const REFTABLE_MAGIC: &[u8; 4] = b"REFT";
45
46const HEADER_SIZE: usize = 24;
49
50const FOOTER_V1_SIZE: usize = 68;
52
53const BLOCK_TYPE_REF: u8 = b'r';
55const BLOCK_TYPE_INDEX: u8 = b'i';
57const BLOCK_TYPE_LOG: u8 = b'g';
59
60const VALUE_DELETION: u8 = 0;
62const VALUE_ONE_OID: u8 = 1;
63const VALUE_TWO_OID: u8 = 2;
64const VALUE_SYMREF: u8 = 3;
65
66const HASH_SIZE: usize = 20;
68
69const DEFAULT_BLOCK_SIZE: u32 = 4096;
71
72const RESTART_INTERVAL: usize = 16;
74
75fn put_varint(mut val: u64, out: &mut Vec<u8>) -> usize {
81 let mut buf = [0u8; 10];
83 let mut i = 0;
84 buf[i] = (val & 0x7f) as u8;
85 i += 1;
86 val >>= 7;
87 while val > 0 {
88 val -= 1;
89 buf[i] = (val & 0x7f) as u8;
90 i += 1;
91 val >>= 7;
92 }
93 let len = i;
95 for j in (1..len).rev() {
96 out.push(buf[j] | 0x80);
97 }
98 out.push(buf[0]);
99 len
100}
101
102fn get_varint(data: &[u8], mut pos: usize) -> Result<(u64, usize)> {
104 if pos >= data.len() {
105 return Err(Error::InvalidRef("varint: unexpected end of data".into()));
106 }
107 let mut val = (data[pos] & 0x7f) as u64;
108 while data[pos] & 0x80 != 0 {
109 pos += 1;
110 if pos >= data.len() {
111 return Err(Error::InvalidRef("varint: unexpected end of data".into()));
112 }
113 val = ((val + 1) << 7) | (data[pos] & 0x7f) as u64;
114 }
115 Ok((val, pos + 1))
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum RefValue {
125 Deletion,
127 Val1(ObjectId),
129 Val2(ObjectId, ObjectId),
131 Symref(String),
133}
134
135#[derive(Debug, Clone)]
137pub struct RefRecord {
138 pub name: String,
140 pub update_index: u64,
142 pub value: RefValue,
144}
145
146#[derive(Debug, Clone)]
148pub struct LogRecord {
149 pub refname: String,
151 pub update_index: u64,
153 pub old_id: ObjectId,
155 pub new_id: ObjectId,
157 pub name: String,
159 pub email: String,
161 pub time_seconds: u64,
163 pub tz_offset: i16,
165 pub message: String,
167}
168
169#[derive(Debug, Clone)]
171pub struct WriteOptions {
172 pub block_size: u32,
174 pub restart_interval: usize,
176 pub write_log: bool,
178}
179
180impl Default for WriteOptions {
181 fn default() -> Self {
182 Self {
183 block_size: DEFAULT_BLOCK_SIZE,
184 restart_interval: RESTART_INTERVAL,
185 write_log: true,
186 }
187 }
188}
189
190pub struct ReftableWriter {
204 opts: WriteOptions,
205 min_update_index: u64,
206 max_update_index: u64,
207
208 refs: Vec<RefRecord>,
210 logs: Vec<LogRecord>,
212}
213
214impl ReftableWriter {
215 pub fn new(opts: WriteOptions, min_update_index: u64, max_update_index: u64) -> Self {
217 Self {
218 opts,
219 min_update_index,
220 max_update_index,
221 refs: Vec::new(),
222 logs: Vec::new(),
223 }
224 }
225
226 pub fn add_ref(&mut self, rec: RefRecord) -> Result<()> {
228 if let Some(last) = self.refs.last() {
229 if rec.name <= last.name {
230 return Err(Error::InvalidRef(format!(
231 "reftable: refs must be sorted, got '{}' after '{}'",
232 rec.name, last.name
233 )));
234 }
235 }
236 self.refs.push(rec);
237 Ok(())
238 }
239
240 pub fn add_log(&mut self, rec: LogRecord) -> Result<()> {
242 self.logs.push(rec);
243 Ok(())
244 }
245
246 pub fn finish(mut self) -> Result<Vec<u8>> {
248 let mut out = Vec::new();
249 let block_size = self.opts.block_size;
250
251 out.extend_from_slice(REFTABLE_MAGIC);
253 out.push(1); out.push(((block_size >> 16) & 0xff) as u8);
255 out.push(((block_size >> 8) & 0xff) as u8);
256 out.push((block_size & 0xff) as u8);
257 out.extend_from_slice(&self.min_update_index.to_be_bytes());
258 out.extend_from_slice(&self.max_update_index.to_be_bytes());
259
260 assert_eq!(out.len(), HEADER_SIZE);
261
262 let ref_block_positions = self.write_ref_blocks(&mut out)?;
264
265 let ref_index_position = if ref_block_positions.len() >= 4 {
267 let pos = out.len() as u64;
268 self.write_ref_index(&mut out, &ref_block_positions)?;
269 pos
270 } else {
271 0
272 };
273
274 let log_position = if self.opts.write_log && !self.logs.is_empty() {
276 let pos = out.len() as u64;
277 self.write_log_blocks(&mut out)?;
278 pos
279 } else {
280 0
281 };
282
283 let footer_start = out.len();
285 out.extend_from_slice(REFTABLE_MAGIC);
287 out.push(1);
288 out.push(((block_size >> 16) & 0xff) as u8);
289 out.push(((block_size >> 8) & 0xff) as u8);
290 out.push((block_size & 0xff) as u8);
291 out.extend_from_slice(&self.min_update_index.to_be_bytes());
292 out.extend_from_slice(&self.max_update_index.to_be_bytes());
293
294 out.extend_from_slice(&ref_index_position.to_be_bytes());
296 out.extend_from_slice(&0u64.to_be_bytes());
298 out.extend_from_slice(&0u64.to_be_bytes());
300 out.extend_from_slice(&log_position.to_be_bytes());
302 out.extend_from_slice(&0u64.to_be_bytes());
304
305 let crc = crc32(&out[footer_start..]);
307 out.extend_from_slice(&crc.to_be_bytes());
308
309 Ok(out)
310 }
311
312 fn write_ref_blocks(&self, out: &mut Vec<u8>) -> Result<Vec<(u64, String)>> {
314 if self.refs.is_empty() {
315 return Ok(Vec::new());
316 }
317
318 let block_size = self.opts.block_size as usize;
319 let restart_interval = self.opts.restart_interval;
320 let mut block_positions: Vec<(u64, String)> = Vec::new();
321 let mut i = 0;
322
323 while i < self.refs.len() {
324 let block_start = out.len();
325 let is_first_block = block_start == HEADER_SIZE;
326
327 let mut records_buf = Vec::new();
329 let mut restart_offsets: Vec<u32> = Vec::new();
330 let mut prev_name = String::new();
331 let mut count = 0;
332 let mut last_name = String::new();
333
334 while i < self.refs.len() {
335 let rec = &self.refs[i];
336 let is_restart = count % restart_interval == 0;
337
338 let mut rec_buf = Vec::new();
339 let prefix_len = if is_restart {
340 0
341 } else {
342 common_prefix_len(prev_name.as_bytes(), rec.name.as_bytes())
343 };
344 let suffix = &rec.name.as_bytes()[prefix_len..];
345 let suffix_len = suffix.len();
346
347 let value_type = match &rec.value {
348 RefValue::Deletion => VALUE_DELETION,
349 RefValue::Val1(_) => VALUE_ONE_OID,
350 RefValue::Val2(_, _) => VALUE_TWO_OID,
351 RefValue::Symref(_) => VALUE_SYMREF,
352 };
353
354 put_varint(prefix_len as u64, &mut rec_buf);
355 put_varint(((suffix_len as u64) << 3) | value_type as u64, &mut rec_buf);
356 rec_buf.extend_from_slice(suffix);
357
358 let update_index_delta = rec.update_index.saturating_sub(self.min_update_index);
359 put_varint(update_index_delta, &mut rec_buf);
360
361 match &rec.value {
362 RefValue::Deletion => {}
363 RefValue::Val1(oid) => {
364 rec_buf.extend_from_slice(oid.as_bytes());
365 }
366 RefValue::Val2(oid, peeled) => {
367 rec_buf.extend_from_slice(oid.as_bytes());
368 rec_buf.extend_from_slice(peeled.as_bytes());
369 }
370 RefValue::Symref(target) => {
371 put_varint(target.len() as u64, &mut rec_buf);
372 rec_buf.extend_from_slice(target.as_bytes());
373 }
374 }
375
376 let restart_count = restart_offsets.len() + if is_restart { 1 } else { 0 };
379 let trailer_size = restart_count * 3 + 2;
380 let total = 4 + records_buf.len() + rec_buf.len() + trailer_size;
381 let effective_block_size = if is_first_block && block_size > 0 {
382 block_size } else if block_size > 0 {
384 block_size
385 } else {
386 usize::MAX };
388 let block_len = if is_first_block {
390 HEADER_SIZE + total
391 } else {
392 total
393 };
394
395 if block_size > 0 && block_len > effective_block_size && count > 0 {
396 break; }
398
399 if is_restart {
400 let offset = if is_first_block {
401 HEADER_SIZE + 4 + records_buf.len()
402 } else {
403 4 + records_buf.len()
404 };
405 restart_offsets.push(offset as u32);
406 }
407
408 records_buf.extend_from_slice(&rec_buf);
409 last_name = rec.name.clone();
410 prev_name = rec.name.clone();
411 count += 1;
412 i += 1;
413 }
414
415 if count == 0 {
416 return Err(Error::InvalidRef(
417 "reftable: ref record too large for block size".into(),
418 ));
419 }
420
421 if restart_offsets.is_empty() {
423 restart_offsets.push(if is_first_block {
424 HEADER_SIZE as u32 + 4
425 } else {
426 4
427 });
428 }
429
430 let trailer_size = restart_offsets.len() * 3 + 2;
432 let block_len_val = if is_first_block {
433 HEADER_SIZE + 4 + records_buf.len() + trailer_size
434 } else {
435 4 + records_buf.len() + trailer_size
436 };
437
438 out.push(BLOCK_TYPE_REF);
440 out.push(((block_len_val >> 16) & 0xff) as u8);
441 out.push(((block_len_val >> 8) & 0xff) as u8);
442 out.push((block_len_val & 0xff) as u8);
443
444 out.extend_from_slice(&records_buf);
446
447 for &off in &restart_offsets {
449 out.push(((off >> 16) & 0xff) as u8);
450 out.push(((off >> 8) & 0xff) as u8);
451 out.push((off & 0xff) as u8);
452 }
453
454 let rc = restart_offsets.len() as u16;
456 out.push((rc >> 8) as u8);
457 out.push((rc & 0xff) as u8);
458
459 if block_size > 0 {
461 let written = out.len() - block_start;
462 let target = if is_first_block {
463 block_size
464 } else {
465 block_size
466 };
467 if written < target {
468 out.resize(block_start + target, 0);
469 }
470 }
471
472 block_positions.push((block_start as u64, last_name.clone()));
473 }
474
475 Ok(block_positions)
476 }
477
478 fn write_ref_index(&self, out: &mut Vec<u8>, block_positions: &[(u64, String)]) -> Result<()> {
480 let mut records_buf = Vec::new();
481 let mut restart_offsets: Vec<u32> = Vec::new();
482 let mut prev_name = String::new();
483
484 for (idx, (block_pos, last_ref)) in block_positions.iter().enumerate() {
485 let is_restart = idx % self.opts.restart_interval == 0;
486 let prefix_len = if is_restart {
487 0
488 } else {
489 common_prefix_len(prev_name.as_bytes(), last_ref.as_bytes())
490 };
491 let suffix = &last_ref.as_bytes()[prefix_len..];
492
493 if is_restart {
494 restart_offsets.push(4 + records_buf.len() as u32);
495 }
496
497 put_varint(prefix_len as u64, &mut records_buf);
498 put_varint((suffix.len() as u64) << 3, &mut records_buf);
499 records_buf.extend_from_slice(suffix);
500 put_varint(*block_pos, &mut records_buf);
501
502 prev_name = last_ref.clone();
503 }
504
505 if restart_offsets.is_empty() {
506 restart_offsets.push(4);
507 }
508
509 let trailer_size = restart_offsets.len() * 3 + 2;
510 let block_len = 4 + records_buf.len() + trailer_size;
511
512 out.push(BLOCK_TYPE_INDEX);
513 out.push(((block_len >> 16) & 0xff) as u8);
514 out.push(((block_len >> 8) & 0xff) as u8);
515 out.push((block_len & 0xff) as u8);
516
517 out.extend_from_slice(&records_buf);
518
519 for &off in &restart_offsets {
520 out.push(((off >> 16) & 0xff) as u8);
521 out.push(((off >> 8) & 0xff) as u8);
522 out.push((off & 0xff) as u8);
523 }
524 let rc = restart_offsets.len() as u16;
525 out.push((rc >> 8) as u8);
526 out.push((rc & 0xff) as u8);
527
528 Ok(())
529 }
530
531 fn write_log_blocks(&mut self, out: &mut Vec<u8>) -> Result<()> {
533 use flate2::write::DeflateEncoder;
534 use flate2::Compression;
535
536 self.logs.sort_by(|a, b| {
538 a.refname
539 .cmp(&b.refname)
540 .then_with(|| b.update_index.cmp(&a.update_index))
541 });
542
543 let mut inner = Vec::new();
545 let mut restart_offsets: Vec<u32> = Vec::new();
546 let mut prev_key = Vec::<u8>::new();
547
548 for (idx, log) in self.logs.iter().enumerate() {
549 let is_restart = idx % self.opts.restart_interval == 0;
550
551 let mut key = Vec::new();
553 key.extend_from_slice(log.refname.as_bytes());
554 key.push(0);
555 key.extend_from_slice(&(0xffffffffffffffffu64 - log.update_index).to_be_bytes());
556
557 let prefix_len = if is_restart {
558 0
559 } else {
560 common_prefix_len(&prev_key, &key)
561 };
562 let suffix = &key[prefix_len..];
563
564 if is_restart {
565 restart_offsets.push(4 + inner.len() as u32);
567 }
568
569 let log_type: u8 = 1;
571 put_varint(prefix_len as u64, &mut inner);
572 put_varint(((suffix.len() as u64) << 3) | log_type as u64, &mut inner);
573 inner.extend_from_slice(suffix);
574
575 inner.extend_from_slice(log.old_id.as_bytes());
577 inner.extend_from_slice(log.new_id.as_bytes());
578 put_varint(log.name.len() as u64, &mut inner);
579 inner.extend_from_slice(log.name.as_bytes());
580 put_varint(log.email.len() as u64, &mut inner);
581 inner.extend_from_slice(log.email.as_bytes());
582 put_varint(log.time_seconds, &mut inner);
583 inner.extend_from_slice(&log.tz_offset.to_be_bytes());
584 put_varint(log.message.len() as u64, &mut inner);
585 inner.extend_from_slice(log.message.as_bytes());
586
587 prev_key = key;
588 }
589
590 if restart_offsets.is_empty() {
591 restart_offsets.push(4);
592 }
593
594 for &off in &restart_offsets {
596 inner.push(((off >> 16) & 0xff) as u8);
597 inner.push(((off >> 8) & 0xff) as u8);
598 inner.push((off & 0xff) as u8);
599 }
600 let rc = restart_offsets.len() as u16;
601 inner.push((rc >> 8) as u8);
602 inner.push((rc & 0xff) as u8);
603
604 let block_len = 4 + inner.len();
606
607 let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
609 encoder
610 .write_all(&inner)
611 .map_err(|e| Error::Zlib(e.to_string()))?;
612 let compressed = encoder.finish().map_err(|e| Error::Zlib(e.to_string()))?;
613
614 out.push(BLOCK_TYPE_LOG);
616 out.push(((block_len >> 16) & 0xff) as u8);
617 out.push(((block_len >> 8) & 0xff) as u8);
618 out.push((block_len & 0xff) as u8);
619 out.extend_from_slice(&compressed);
620
621 Ok(())
622 }
623}
624
625pub struct ReftableReader {
631 data: Vec<u8>,
632 version: u8,
633 block_size: u32,
634 min_update_index: u64,
635 max_update_index: u64,
636 ref_index_position: u64,
637 log_position: u64,
638}
639
640#[derive(Debug)]
642#[allow(dead_code)]
643struct Footer {
644 version: u8,
645 block_size: u32,
646 min_update_index: u64,
647 max_update_index: u64,
648 ref_index_position: u64,
649 obj_position_and_id_len: u64,
650 obj_index_position: u64,
651 log_position: u64,
652 log_index_position: u64,
653}
654
655impl ReftableReader {
656 pub fn new(data: Vec<u8>) -> Result<Self> {
658 if data.len() < HEADER_SIZE + FOOTER_V1_SIZE {
659 if data.len() < HEADER_SIZE {
661 return Err(Error::InvalidRef("reftable: file too small".into()));
662 }
663 }
664
665 if &data[0..4] != REFTABLE_MAGIC {
667 return Err(Error::InvalidRef("reftable: bad magic".into()));
668 }
669 let version = data[4];
670 if version != 1 && version != 2 {
671 return Err(Error::InvalidRef(format!(
672 "reftable: unsupported version {version}"
673 )));
674 }
675 let _block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
676 let _min_update_index = u64::from_be_bytes(data[8..16].try_into().unwrap());
677 let _max_update_index = u64::from_be_bytes(data[16..24].try_into().unwrap());
678
679 let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
681 if data.len() < footer_size {
682 return Err(Error::InvalidRef(
683 "reftable: file too small for footer".into(),
684 ));
685 }
686 let footer_start = data.len() - footer_size;
687 let footer = parse_footer(&data[footer_start..], version)?;
688
689 Ok(Self {
690 data,
691 version,
692 block_size: footer.block_size,
693 min_update_index: footer.min_update_index,
694 max_update_index: footer.max_update_index,
695 ref_index_position: footer.ref_index_position,
696 log_position: footer.log_position,
697 })
698 }
699
700 pub fn read_refs(&self) -> Result<Vec<RefRecord>> {
702 let mut refs = Vec::new();
703 let footer_size = if self.version == 2 {
704 72
705 } else {
706 FOOTER_V1_SIZE
707 };
708 let file_end = self.data.len() - footer_size;
709
710 let ref_end = if self.ref_index_position > 0 {
712 self.ref_index_position as usize
713 } else if self.log_position > 0 {
714 self.log_position as usize
715 } else {
716 file_end
717 };
718
719 let mut pos = 0usize;
720 if pos < HEADER_SIZE {
723 pos = HEADER_SIZE;
724 }
725
726 while pos < ref_end {
727 if pos >= self.data.len() {
728 break;
729 }
730 let block_type = self.data[pos];
731 if block_type == 0 {
732 if self.block_size > 0 {
734 let bs = self.block_size as usize;
735 pos = ((pos / bs) + 1) * bs;
736 continue;
737 } else {
738 break;
739 }
740 }
741 if block_type != BLOCK_TYPE_REF {
742 break;
743 }
744
745 let block_len = read_u24(&self.data, pos + 1);
746 let block_data_start = pos + 4; let is_first = pos == HEADER_SIZE;
751 let records_end = if is_first {
752 block_len
754 } else {
755 pos + block_len
756 };
757
758 if records_end > ref_end {
759 break;
760 }
761
762 let rc = read_u16(&self.data, records_end - 2);
764 let restart_table_start = records_end - 2 - (rc * 3);
766
767 let mut rpos = block_data_start;
769 let mut prev_name = Vec::<u8>::new();
770
771 while rpos < restart_table_start {
772 let (rec, new_pos) =
773 decode_ref_record(&self.data, rpos, &prev_name, self.min_update_index)?;
774 prev_name = rec.name.as_bytes().to_vec();
775 refs.push(rec);
776 rpos = new_pos;
777 }
778
779 if self.block_size > 0 {
781 let bs = self.block_size as usize;
782 if is_first {
783 pos = bs;
784 } else {
785 pos += bs;
786 }
787 } else {
788 pos = records_end;
789 }
790 }
791
792 Ok(refs)
793 }
794
795 pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
797 let refs = self.read_refs()?;
799 Ok(refs.into_iter().find(|r| r.name == name))
800 }
801
802 pub fn read_logs(&self) -> Result<Vec<LogRecord>> {
804 if self.log_position == 0 {
805 return Ok(Vec::new());
806 }
807
808 let footer_size = if self.version == 2 {
809 72
810 } else {
811 FOOTER_V1_SIZE
812 };
813 let file_end = self.data.len() - footer_size;
814 let mut pos = self.log_position as usize;
815 let mut logs = Vec::new();
816
817 while pos < file_end {
818 if pos >= self.data.len() {
819 break;
820 }
821 let block_type = self.data[pos];
822 if block_type != BLOCK_TYPE_LOG {
823 break;
824 }
825 let block_len = read_u24(&self.data, pos + 1);
826 let compressed_start = pos + 4;
827
828 let inflated_size = block_len - 4;
830
831 use flate2::read::DeflateDecoder;
833 let remaining = &self.data[compressed_start..file_end];
834 let mut decoder = DeflateDecoder::new(remaining);
835 let mut inflated = vec![0u8; inflated_size];
836 decoder
837 .read_exact(&mut inflated)
838 .map_err(|e| Error::Zlib(e.to_string()))?;
839
840 let consumed = decoder.total_in() as usize;
842
843 if inflated.len() < 2 {
846 break;
847 }
848 let rc = read_u16(&inflated, inflated.len() - 2);
849 let restart_table_start = inflated.len() - 2 - (rc * 3);
850
851 let mut rpos = 0usize;
852 let mut prev_key = Vec::<u8>::new();
853
854 while rpos < restart_table_start {
855 let (log, new_pos) = decode_log_record(&inflated, rpos, &prev_key)?;
856 let mut key = Vec::new();
858 key.extend_from_slice(log.refname.as_bytes());
859 key.push(0);
860 key.extend_from_slice(&(0xffffffffffffffffu64 - log.update_index).to_be_bytes());
861 prev_key = key;
862 logs.push(log);
863 rpos = new_pos;
864 }
865
866 pos = compressed_start + consumed;
867 }
868
869 Ok(logs)
870 }
871
872 pub fn block_size(&self) -> u32 {
874 self.block_size
875 }
876
877 pub fn min_update_index(&self) -> u64 {
879 self.min_update_index
880 }
881
882 pub fn max_update_index(&self) -> u64 {
884 self.max_update_index
885 }
886}
887
888fn decode_ref_record(
893 data: &[u8],
894 pos: usize,
895 prev_name: &[u8],
896 min_update_index: u64,
897) -> Result<(RefRecord, usize)> {
898 let (prefix_len, p) = get_varint(data, pos)?;
899 let (suffix_and_type, mut p) = get_varint(data, p)?;
900 let suffix_len = (suffix_and_type >> 3) as usize;
901 let value_type = (suffix_and_type & 0x7) as u8;
902
903 let mut name = Vec::with_capacity(prefix_len as usize + suffix_len);
905 if prefix_len > 0 {
906 if (prefix_len as usize) > prev_name.len() {
907 return Err(Error::InvalidRef(
908 "reftable: prefix_len exceeds prev name".into(),
909 ));
910 }
911 name.extend_from_slice(&prev_name[..prefix_len as usize]);
912 }
913 if p + suffix_len > data.len() {
914 return Err(Error::InvalidRef("reftable: suffix overflows block".into()));
915 }
916 name.extend_from_slice(&data[p..p + suffix_len]);
917 p += suffix_len;
918
919 let name_str = String::from_utf8(name)
920 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in ref name".into()))?;
921
922 let (update_index_delta, mut p) = get_varint(data, p)?;
923 let update_index = min_update_index + update_index_delta;
924
925 let value = match value_type {
926 VALUE_DELETION => RefValue::Deletion,
927 VALUE_ONE_OID => {
928 if p + HASH_SIZE > data.len() {
929 return Err(Error::InvalidRef("reftable: truncated OID".into()));
930 }
931 let oid = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
932 p += HASH_SIZE;
933 RefValue::Val1(oid)
934 }
935 VALUE_TWO_OID => {
936 if p + 2 * HASH_SIZE > data.len() {
937 return Err(Error::InvalidRef("reftable: truncated OID pair".into()));
938 }
939 let oid = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
940 p += HASH_SIZE;
941 let peeled = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
942 p += HASH_SIZE;
943 RefValue::Val2(oid, peeled)
944 }
945 VALUE_SYMREF => {
946 let (target_len, p2) = get_varint(data, p)?;
947 p = p2;
948 let target_len = target_len as usize;
949 if p + target_len > data.len() {
950 return Err(Error::InvalidRef(
951 "reftable: truncated symref target".into(),
952 ));
953 }
954 let target = String::from_utf8(data[p..p + target_len].to_vec())
955 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in symref".into()))?;
956 p += target_len;
957 RefValue::Symref(target)
958 }
959 _ => {
960 return Err(Error::InvalidRef(format!(
961 "reftable: unknown value_type {value_type}"
962 )));
963 }
964 };
965
966 Ok((
967 RefRecord {
968 name: name_str,
969 update_index,
970 value,
971 },
972 p,
973 ))
974}
975
976fn decode_log_record(data: &[u8], pos: usize, prev_key: &[u8]) -> Result<(LogRecord, usize)> {
977 let (prefix_len, p) = get_varint(data, pos)?;
978 let (suffix_and_type, mut p) = get_varint(data, p)?;
979 let suffix_len = (suffix_and_type >> 3) as usize;
980 let log_type = (suffix_and_type & 0x7) as u8;
981
982 let mut key = Vec::with_capacity(prefix_len as usize + suffix_len);
984 if prefix_len > 0 {
985 if (prefix_len as usize) > prev_key.len() {
986 return Err(Error::InvalidRef(
987 "reftable: log prefix_len exceeds prev key".into(),
988 ));
989 }
990 key.extend_from_slice(&prev_key[..prefix_len as usize]);
991 }
992 if p + suffix_len > data.len() {
993 return Err(Error::InvalidRef("reftable: log suffix overflows".into()));
994 }
995 key.extend_from_slice(&data[p..p + suffix_len]);
996 p += suffix_len;
997
998 let null_pos = key
1000 .iter()
1001 .position(|&b| b == 0)
1002 .ok_or_else(|| Error::InvalidRef("reftable: log key missing null separator".into()))?;
1003 let refname = String::from_utf8(key[..null_pos].to_vec())
1004 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log refname".into()))?;
1005 if null_pos + 9 > key.len() {
1006 return Err(Error::InvalidRef("reftable: log key too short".into()));
1007 }
1008 let reversed_idx = u64::from_be_bytes(key[null_pos + 1..null_pos + 9].try_into().unwrap());
1009 let update_index = 0xffffffffffffffffu64 - reversed_idx;
1010
1011 if log_type == 0 {
1012 let zero_oid = ObjectId::from_bytes(&[0u8; 20])?;
1014 return Ok((
1015 LogRecord {
1016 refname,
1017 update_index,
1018 old_id: zero_oid,
1019 new_id: zero_oid,
1020 name: String::new(),
1021 email: String::new(),
1022 time_seconds: 0,
1023 tz_offset: 0,
1024 message: String::new(),
1025 },
1026 p,
1027 ));
1028 }
1029
1030 if p + 2 * HASH_SIZE > data.len() {
1032 return Err(Error::InvalidRef("reftable: truncated log OIDs".into()));
1033 }
1034 let old_id = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
1035 p += HASH_SIZE;
1036 let new_id = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
1037 p += HASH_SIZE;
1038
1039 let (name_len, p2) = get_varint(data, p)?;
1040 p = p2;
1041 let name_len = name_len as usize;
1042 if p + name_len > data.len() {
1043 return Err(Error::InvalidRef("reftable: truncated log name".into()));
1044 }
1045 let name = String::from_utf8(data[p..p + name_len].to_vec())
1046 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log name".into()))?;
1047 p += name_len;
1048
1049 let (email_len, p2) = get_varint(data, p)?;
1050 p = p2;
1051 let email_len = email_len as usize;
1052 if p + email_len > data.len() {
1053 return Err(Error::InvalidRef("reftable: truncated log email".into()));
1054 }
1055 let email = String::from_utf8(data[p..p + email_len].to_vec())
1056 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log email".into()))?;
1057 p += email_len;
1058
1059 let (time_seconds, p2) = get_varint(data, p)?;
1060 p = p2;
1061
1062 if p + 2 > data.len() {
1063 return Err(Error::InvalidRef("reftable: truncated tz_offset".into()));
1064 }
1065 let tz_offset = i16::from_be_bytes([data[p], data[p + 1]]);
1066 p += 2;
1067
1068 let (msg_len, p2) = get_varint(data, p)?;
1069 p = p2;
1070 let msg_len = msg_len as usize;
1071 if p + msg_len > data.len() {
1072 return Err(Error::InvalidRef("reftable: truncated log message".into()));
1073 }
1074 let message = String::from_utf8(data[p..p + msg_len].to_vec())
1075 .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log message".into()))?;
1076 p += msg_len;
1077
1078 Ok((
1079 LogRecord {
1080 refname,
1081 update_index,
1082 old_id,
1083 new_id,
1084 name,
1085 email,
1086 time_seconds,
1087 tz_offset,
1088 message,
1089 },
1090 p,
1091 ))
1092}
1093
1094pub struct ReftableStack {
1103 reftable_dir: PathBuf,
1105 table_names: Vec<String>,
1107}
1108
1109impl ReftableStack {
1110 pub fn open(git_dir: &Path) -> Result<Self> {
1112 let reftable_dir = git_dir.join("reftable");
1113 let tables_list = reftable_dir.join("tables.list");
1114 let content = fs::read_to_string(&tables_list).map_err(Error::Io)?;
1115 let table_names: Vec<String> = content
1116 .lines()
1117 .filter(|l| !l.is_empty())
1118 .map(|l| l.to_owned())
1119 .collect();
1120 Ok(Self {
1121 reftable_dir,
1122 table_names,
1123 })
1124 }
1125
1126 pub fn read_refs(&self) -> Result<Vec<RefRecord>> {
1131 let mut merged: BTreeMap<String, RefRecord> = BTreeMap::new();
1132
1133 for name in &self.table_names {
1134 let path = self.reftable_dir.join(name);
1135 let data = fs::read(&path).map_err(Error::Io)?;
1136 let reader = ReftableReader::new(data)?;
1137 for rec in reader.read_refs()? {
1138 match &rec.value {
1139 RefValue::Deletion => {
1140 merged.remove(&rec.name);
1141 }
1142 _ => {
1143 merged.insert(rec.name.clone(), rec);
1144 }
1145 }
1146 }
1147 }
1148
1149 Ok(merged.into_values().collect())
1150 }
1151
1152 pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
1154 for table_name in self.table_names.iter().rev() {
1156 let path = self.reftable_dir.join(table_name);
1157 let data = fs::read(&path).map_err(Error::Io)?;
1158 let reader = ReftableReader::new(data)?;
1159 if let Some(rec) = reader.lookup_ref(name)? {
1160 return match rec.value {
1161 RefValue::Deletion => Ok(None),
1162 _ => Ok(Some(rec)),
1163 };
1164 }
1165 }
1166 Ok(None)
1167 }
1168
1169 pub fn read_logs_for_ref(&self, refname: &str) -> Result<Vec<LogRecord>> {
1171 let mut logs = Vec::new();
1172 for table_name in &self.table_names {
1173 let path = self.reftable_dir.join(table_name);
1174 let data = fs::read(&path).map_err(Error::Io)?;
1175 let reader = ReftableReader::new(data)?;
1176 for log in reader.read_logs()? {
1177 if log.refname == refname {
1178 logs.push(log);
1179 }
1180 }
1181 }
1182 logs.sort_by(|a, b| b.update_index.cmp(&a.update_index));
1184 Ok(logs)
1185 }
1186
1187 pub fn replace_logs_for_ref(
1189 &mut self,
1190 refname: &str,
1191 entries: &[crate::reflog::ReflogEntry],
1192 ) -> Result<()> {
1193 let refs = self.read_refs()?;
1194 let mut logs: Vec<LogRecord> = self
1195 .read_all_logs()?
1196 .into_iter()
1197 .filter(|log| log.refname != refname)
1198 .collect();
1199 let mut next_update_index = self.max_update_index()? + 1;
1200 for entry in entries {
1201 let (name, email, time_secs, tz) = parse_identity_string(&entry.identity);
1202 logs.push(LogRecord {
1203 refname: refname.to_owned(),
1204 update_index: next_update_index,
1205 old_id: entry.old_oid,
1206 new_id: entry.new_oid,
1207 name,
1208 email,
1209 time_seconds: time_secs,
1210 tz_offset: tz,
1211 message: entry.message.clone(),
1212 });
1213 next_update_index += 1;
1214 }
1215
1216 let mut min_idx = u64::MAX;
1217 let mut max_idx = 0u64;
1218 for name in &self.table_names {
1219 let path = self.reftable_dir.join(name);
1220 let data = fs::read(&path).map_err(Error::Io)?;
1221 let reader = ReftableReader::new(data)?;
1222 min_idx = min_idx.min(reader.min_update_index());
1223 max_idx = max_idx.max(reader.max_update_index());
1224 }
1225 if min_idx == u64::MAX {
1226 min_idx = 0;
1227 }
1228 max_idx = max_idx.max(next_update_index.saturating_sub(1));
1229
1230 let mut writer = ReftableWriter::new(WriteOptions::default(), min_idx, max_idx);
1231 for rec in refs {
1232 writer.add_ref(rec)?;
1233 }
1234 for log in logs {
1235 writer.add_log(log)?;
1236 }
1237 let data = writer.finish()?;
1238 let old_names = self.table_names.clone();
1239 let name = self.write_table_file(&data, max_idx)?;
1240 self.table_names = vec![name];
1241 self.write_tables_list()?;
1242 for old in &old_names {
1243 let _ = fs::remove_file(self.reftable_dir.join(old));
1244 }
1245 Ok(())
1246 }
1247
1248 pub fn read_all_logs(&self) -> Result<Vec<LogRecord>> {
1250 let mut logs = Vec::new();
1251 for table_name in &self.table_names {
1252 let path = self.reftable_dir.join(table_name);
1253 let data = fs::read(&path).map_err(Error::Io)?;
1254 let reader = ReftableReader::new(data)?;
1255 logs.extend(reader.read_logs()?);
1256 }
1257 logs.sort_by(|a, b| {
1258 a.refname
1259 .cmp(&b.refname)
1260 .then_with(|| b.update_index.cmp(&a.update_index))
1261 });
1262 Ok(logs)
1263 }
1264
1265 pub fn max_update_index(&self) -> Result<u64> {
1267 let mut max_idx = 0u64;
1268 for name in &self.table_names {
1269 let path = self.reftable_dir.join(name);
1270 let data = fs::read(&path).map_err(Error::Io)?;
1271 let reader = ReftableReader::new(data)?;
1272 max_idx = max_idx.max(reader.max_update_index());
1273 }
1274 Ok(max_idx)
1275 }
1276
1277 pub fn add_table(&mut self, data: &[u8], update_index: u64) -> Result<String> {
1282 let table_has_deletion = ReftableReader::new(data.to_vec())
1283 .and_then(|reader| reader.read_refs())
1284 .map(|records| {
1285 records
1286 .iter()
1287 .any(|record| matches!(record.value, RefValue::Deletion))
1288 })
1289 .unwrap_or(false);
1290 let random: u64 = {
1291 let mut buf = [0u8; 8];
1293 if let Ok(mut f) = fs::File::open("/dev/urandom") {
1294 let _ = f.read(&mut buf);
1295 }
1296 u64::from_le_bytes(buf)
1297 };
1298 let filename = format!(
1299 "{:08x}-{:08x}-{:08x}.ref",
1300 update_index, update_index, random as u32
1301 );
1302 let path = self.reftable_dir.join(&filename);
1303 fs::write(&path, data).map_err(Error::Io)?;
1304
1305 self.table_names.push(filename.clone());
1306 self.write_tables_list()?;
1307
1308 if table_has_deletion && self.table_names.len() > 2 {
1312 self.compact_prefix_preserving_newest()?;
1313 } else if self.table_names.len() > 3
1314 && std::env::var("GIT_TEST_REFTABLE_AUTOCOMPACTION")
1315 .map(|value| value != "false")
1316 .unwrap_or(true)
1317 {
1318 if self
1319 .table_names
1320 .iter()
1321 .any(|name| self.table_is_locked(name))
1322 {
1323 self.compact_unlocked_suffix()?;
1324 } else {
1325 self.compact()?;
1326 }
1327 }
1328
1329 Ok(filename)
1330 }
1331
1332 fn compact_prefix_preserving_newest(&mut self) -> Result<()> {
1333 if std::env::var("GIT_TEST_REFTABLE_AUTOCOMPACTION")
1334 .map(|value| value == "false")
1335 .unwrap_or(false)
1336 {
1337 return Ok(());
1338 }
1339 if self.table_names.len() <= 2 {
1340 return Ok(());
1341 }
1342 let newest = self
1343 .table_names
1344 .last()
1345 .cloned()
1346 .expect("length checked above");
1347 let old_names: Vec<String> = self.table_names[..self.table_names.len() - 1].to_vec();
1348 let prefix_stack = Self {
1349 reftable_dir: self.reftable_dir.clone(),
1350 table_names: old_names.clone(),
1351 };
1352 let refs = prefix_stack.read_refs()?;
1353 let logs = prefix_stack.read_all_logs()?;
1354
1355 let mut min_idx = u64::MAX;
1356 let mut max_idx = 0u64;
1357 for name in &old_names {
1358 let path = self.reftable_dir.join(name);
1359 let data = fs::read(&path).map_err(Error::Io)?;
1360 let reader = ReftableReader::new(data)?;
1361 min_idx = min_idx.min(reader.min_update_index());
1362 max_idx = max_idx.max(reader.max_update_index());
1363 }
1364 if min_idx == u64::MAX {
1365 min_idx = 0;
1366 }
1367
1368 let mut writer = ReftableWriter::new(WriteOptions::default(), min_idx, max_idx);
1369 for rec in refs {
1370 writer.add_ref(rec)?;
1371 }
1372 for log in logs {
1373 writer.add_log(log)?;
1374 }
1375 let data = writer.finish()?;
1376 let filename = self.write_table_file(&data, max_idx)?;
1377 self.table_names = vec![filename, newest];
1378 self.write_tables_list()?;
1379 for old in &old_names {
1380 let _ = fs::remove_file(self.reftable_dir.join(old));
1381 }
1382 Ok(())
1383 }
1384
1385 fn table_is_locked(&self, name: &str) -> bool {
1386 self.reftable_dir.join(format!("{name}.lock")).exists()
1387 }
1388
1389 fn compact_unlocked_suffix(&mut self) -> Result<()> {
1390 let first_unlocked = self
1391 .table_names
1392 .iter()
1393 .position(|name| !self.table_is_locked(name))
1394 .unwrap_or(self.table_names.len());
1395 if self.table_names.len().saturating_sub(first_unlocked) <= 1 {
1396 return Ok(());
1397 }
1398
1399 let locked_prefix: Vec<String> = self.table_names[..first_unlocked].to_vec();
1400 let old_suffix: Vec<String> = self.table_names[first_unlocked..].to_vec();
1401 let suffix_stack = Self {
1402 reftable_dir: self.reftable_dir.clone(),
1403 table_names: old_suffix.clone(),
1404 };
1405 let refs = suffix_stack.read_refs()?;
1406 let logs = suffix_stack.read_all_logs()?;
1407
1408 let mut min_idx = u64::MAX;
1409 let mut max_idx = 0u64;
1410 for name in &old_suffix {
1411 let path = self.reftable_dir.join(name);
1412 let data = fs::read(&path).map_err(Error::Io)?;
1413 let reader = ReftableReader::new(data)?;
1414 min_idx = min_idx.min(reader.min_update_index());
1415 max_idx = max_idx.max(reader.max_update_index());
1416 }
1417 if min_idx == u64::MAX {
1418 min_idx = 0;
1419 }
1420
1421 let mut writer = ReftableWriter::new(WriteOptions::default(), min_idx, max_idx);
1422 for rec in refs {
1423 writer.add_ref(rec)?;
1424 }
1425 for log in logs {
1426 writer.add_log(log)?;
1427 }
1428 let data = writer.finish()?;
1429 let compacted = self.write_table_file(&data, max_idx)?;
1430
1431 self.table_names = locked_prefix;
1432 self.table_names.push(compacted);
1433 self.write_tables_list()?;
1434 for old in &old_suffix {
1435 let _ = fs::remove_file(self.reftable_dir.join(old));
1436 }
1437 Ok(())
1438 }
1439
1440 pub fn write_ref(
1444 &mut self,
1445 refname: &str,
1446 value: RefValue,
1447 log: Option<LogRecord>,
1448 opts: &WriteOptions,
1449 ) -> Result<()> {
1450 let update_index = self.max_update_index()? + 1;
1451 let mut writer = ReftableWriter::new(opts.clone(), update_index, update_index);
1452
1453 writer.add_ref(RefRecord {
1457 name: refname.to_owned(),
1458 update_index,
1459 value,
1460 })?;
1461
1462 if let Some(log_rec) = log {
1463 let mut log_rec = log_rec;
1464 log_rec.update_index = update_index;
1465 writer.add_log(log_rec)?;
1466 }
1467
1468 let data = writer.finish()?;
1469 self.add_table(&data, update_index)?;
1470 Ok(())
1471 }
1472
1473 pub fn compact(&mut self) -> Result<()> {
1475 if self.table_names.len() <= 1 {
1476 return Ok(());
1477 }
1478
1479 let refs = self.read_refs()?;
1481 let logs = self.read_all_logs()?;
1482
1483 let mut min_idx = u64::MAX;
1485 let mut max_idx = 0u64;
1486 for name in &self.table_names {
1487 let path = self.reftable_dir.join(name);
1488 let data = fs::read(&path).map_err(Error::Io)?;
1489 let reader = ReftableReader::new(data)?;
1490 min_idx = min_idx.min(reader.min_update_index());
1491 max_idx = max_idx.max(reader.max_update_index());
1492 }
1493 if min_idx == u64::MAX {
1494 min_idx = 0;
1495 }
1496
1497 let mut writer = ReftableWriter::new(WriteOptions::default(), min_idx, max_idx);
1498 for rec in refs {
1499 writer.add_ref(rec)?;
1500 }
1501 for log in logs {
1502 writer.add_log(log)?;
1503 }
1504
1505 let data = writer.finish()?;
1506
1507 let old_names = self.table_names.clone();
1509 self.table_names.clear();
1510 let name = self.write_table_file(&data, max_idx)?;
1511 self.table_names.push(name);
1512 self.write_tables_list()?;
1513
1514 for old in &old_names {
1516 let path = self.reftable_dir.join(old);
1517 let _ = fs::remove_file(&path);
1518 }
1519
1520 Ok(())
1521 }
1522
1523 fn write_table_file(&self, data: &[u8], update_index: u64) -> Result<String> {
1524 let random: u64 = {
1525 let mut buf = [0u8; 8];
1526 if let Ok(mut f) = fs::File::open("/dev/urandom") {
1527 let _ = f.read(&mut buf);
1528 }
1529 u64::from_le_bytes(buf)
1530 };
1531 let filename = format!(
1532 "{:08x}-{:08x}-{:08x}.ref",
1533 update_index, update_index, random as u32
1534 );
1535 let path = self.reftable_dir.join(&filename);
1536 fs::write(&path, data).map_err(Error::Io)?;
1537 Ok(filename)
1538 }
1539
1540 fn write_tables_list(&self) -> Result<()> {
1542 let tables_list = self.reftable_dir.join("tables.list");
1543 let lock = self.reftable_dir.join("tables.list.lock");
1544 self.wait_for_tables_list_lock(&lock)?;
1545 let content = self.table_names.join("\n")
1546 + if self.table_names.is_empty() {
1547 ""
1548 } else {
1549 "\n"
1550 };
1551 fs::write(&lock, &content).map_err(Error::Io)?;
1552 fs::rename(&lock, &tables_list).map_err(Error::Io)?;
1553 Ok(())
1554 }
1555
1556 fn wait_for_tables_list_lock(&self, lock: &Path) -> Result<()> {
1557 let git_dir = self
1558 .reftable_dir
1559 .parent()
1560 .unwrap_or(self.reftable_dir.as_path());
1561 let config = ConfigSet::load(Some(git_dir), true).unwrap_or_else(|_| ConfigSet::new());
1562 let timeout_ms = config
1563 .get("reftable.lockTimeout")
1564 .and_then(|value| value.parse::<u64>().ok())
1565 .unwrap_or(0);
1566 let deadline = Instant::now() + Duration::from_millis(timeout_ms);
1567 while lock.exists() {
1568 if timeout_ms == 0 || Instant::now() >= deadline {
1569 return Err(Error::InvalidRef(
1570 "cannot lock references: data is locked".to_owned(),
1571 ));
1572 }
1573 thread::sleep(Duration::from_millis(50));
1574 }
1575 Ok(())
1576 }
1577
1578 pub fn table_names(&self) -> &[String] {
1580 &self.table_names
1581 }
1582}
1583
1584pub fn is_reftable_repo(git_dir: &Path) -> bool {
1590 fn config_uses_reftable(config_path: &Path) -> bool {
1591 let Ok(content) = fs::read_to_string(config_path) else {
1592 return false;
1593 };
1594
1595 let mut in_extensions = false;
1596 for line in content.lines() {
1597 let trimmed = line.trim();
1598 if trimmed.starts_with('[') {
1599 in_extensions = trimmed.eq_ignore_ascii_case("[extensions]");
1600 continue;
1601 }
1602 if in_extensions {
1603 if let Some((key, value)) = trimmed.split_once('=') {
1604 if key.trim().eq_ignore_ascii_case("refstorage")
1605 && value.trim().eq_ignore_ascii_case("reftable")
1606 {
1607 return true;
1608 }
1609 }
1610 }
1611 }
1612 false
1613 }
1614
1615 let local_config = git_dir.join("config");
1616 if config_uses_reftable(&local_config) {
1617 return true;
1618 }
1619
1620 if let Ok(raw) = fs::read_to_string(git_dir.join("commondir")) {
1623 let rel = raw.trim();
1624 if !rel.is_empty() {
1625 let common = if Path::new(rel).is_absolute() {
1626 PathBuf::from(rel)
1627 } else {
1628 git_dir.join(rel)
1629 };
1630 let common_config = common.canonicalize().unwrap_or(common).join("config");
1631 if config_uses_reftable(&common_config) {
1632 return true;
1633 }
1634 }
1635 }
1636
1637 false
1638}
1639
1640pub fn reftable_resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
1642 reftable_resolve_ref_depth(git_dir, refname, 0)
1643}
1644
1645fn reftable_storage_location(git_dir: &Path, refname: &str) -> (PathBuf, String) {
1646 if let Some(rest) = refname.strip_prefix("worktrees/") {
1647 if let Some((worktree_id, per_worktree_ref)) = rest.split_once('/') {
1648 if per_worktree_ref.starts_with("refs/") {
1649 let common =
1650 crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
1651 return (
1652 common.join("worktrees").join(worktree_id),
1653 per_worktree_ref.to_owned(),
1654 );
1655 }
1656 }
1657 }
1658
1659 if refname == "HEAD"
1660 || refname.starts_with("refs/worktree/")
1661 || (git_dir.join("commondir").exists() && refname.starts_with("refs/bisect/"))
1662 {
1663 return (git_dir.to_path_buf(), refname.to_owned());
1664 }
1665
1666 (
1667 crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf()),
1668 refname.to_owned(),
1669 )
1670}
1671
1672fn reftable_resolve_ref_depth(git_dir: &Path, refname: &str, depth: usize) -> Result<ObjectId> {
1673 if depth > 10 {
1674 return Err(Error::InvalidRef(format!(
1675 "reftable: symlink too deep: {refname}"
1676 )));
1677 }
1678
1679 if refname == "HEAD" {
1681 let head_path = git_dir.join("HEAD");
1682 if head_path.exists() {
1683 let content = fs::read_to_string(&head_path).map_err(Error::Io)?;
1684 let content = content.trim();
1685 if let Some(target) = content.strip_prefix("ref: ") {
1686 if target.trim() == "refs/heads/.invalid" {
1687 return reftable_resolve_ref_depth(git_dir, "refs/worktree/HEAD", depth + 1);
1688 }
1689 return reftable_resolve_ref_depth(git_dir, target.trim(), depth + 1);
1690 }
1691 if content.len() == 40 && content.chars().all(|c| c.is_ascii_hexdigit()) {
1693 return content.parse();
1694 }
1695 }
1696 }
1697
1698 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1699 let stack = ReftableStack::open(&store_git_dir)?;
1700 match stack.lookup_ref(&storage_refname)? {
1701 Some(rec) => match rec.value {
1702 RefValue::Val1(oid) => Ok(oid),
1703 RefValue::Val2(oid, _) => Ok(oid),
1704 RefValue::Symref(target) => {
1705 reftable_resolve_ref_depth(&store_git_dir, &target, depth + 1)
1706 }
1707 RefValue::Deletion => Err(Error::InvalidRef(format!("ref not found: {refname}"))),
1708 },
1709 None => Err(Error::InvalidRef(format!("ref not found: {refname}"))),
1710 }
1711}
1712
1713pub fn reftable_write_ref(
1715 git_dir: &Path,
1716 refname: &str,
1717 oid: &ObjectId,
1718 log_identity: Option<&str>,
1719 log_message: Option<&str>,
1720) -> Result<()> {
1721 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1722 let mut stack = ReftableStack::open(&store_git_dir)?;
1723 let old_oid = stack
1724 .lookup_ref(&storage_refname)?
1725 .and_then(|r| match r.value {
1726 RefValue::Val1(oid) => Some(oid),
1727 RefValue::Val2(oid, _) => Some(oid),
1728 _ => None,
1729 })
1730 .unwrap_or_else(|| ObjectId::from_bytes(&[0u8; 20]).unwrap());
1731
1732 let log = if let Some(identity) = log_identity {
1733 let (name, email, time_secs, tz) = parse_identity_string(identity);
1734 Some(LogRecord {
1735 refname: storage_refname.clone(),
1736 update_index: 0, old_id: old_oid,
1738 new_id: *oid,
1739 name,
1740 email,
1741 time_seconds: time_secs,
1742 tz_offset: tz,
1743 message: log_message.unwrap_or("").to_owned(),
1744 })
1745 } else {
1746 None
1747 };
1748
1749 let write_log = log.is_some() || should_log_ref_updates(&store_git_dir);
1751 let log = if write_log { log } else { None };
1752
1753 let opts = read_write_options(&store_git_dir);
1754 stack.write_ref(&storage_refname, RefValue::Val1(*oid), log, &opts)
1755}
1756
1757pub fn reftable_write_symref(
1759 git_dir: &Path,
1760 refname: &str,
1761 target: &str,
1762 log_identity: Option<&str>,
1763 log_message: Option<&str>,
1764) -> Result<()> {
1765 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1766 let mut stack = ReftableStack::open(&store_git_dir)?;
1767 let opts = read_write_options(&store_git_dir);
1768
1769 let log = if let Some(identity) = log_identity {
1770 let (name, email, time_secs, tz) = parse_identity_string(identity);
1771 let zero_oid = ObjectId::from_bytes(&[0u8; 20])?;
1772 Some(LogRecord {
1773 refname: storage_refname.clone(),
1774 update_index: 0,
1775 old_id: zero_oid,
1776 new_id: zero_oid,
1777 name,
1778 email,
1779 time_seconds: time_secs,
1780 tz_offset: tz,
1781 message: log_message.unwrap_or("").to_owned(),
1782 })
1783 } else {
1784 None
1785 };
1786
1787 stack.write_ref(
1788 &storage_refname,
1789 RefValue::Symref(target.to_owned()),
1790 log,
1791 &opts,
1792 )
1793}
1794
1795pub fn reftable_delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
1797 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1798 let mut stack = ReftableStack::open(&store_git_dir)?;
1799 let opts = read_write_options(&store_git_dir);
1800 stack.write_ref(&storage_refname, RefValue::Deletion, None, &opts)
1801}
1802
1803pub fn reftable_read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
1805 if refname == "HEAD" {
1806 let head_path = git_dir.join("HEAD");
1807 let content = match fs::read_to_string(&head_path) {
1808 Ok(content) => content,
1809 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
1810 Err(err) => return Err(Error::Io(err)),
1811 };
1812 return Ok(content
1813 .trim()
1814 .strip_prefix("ref: ")
1815 .map(|target| target.trim().to_owned()));
1816 }
1817 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1818 let stack = ReftableStack::open(&store_git_dir)?;
1819 match stack.lookup_ref(&storage_refname)? {
1820 Some(rec) => match rec.value {
1821 RefValue::Symref(target) => Ok(Some(target)),
1822 _ => Ok(None),
1823 },
1824 None => Ok(None),
1825 }
1826}
1827
1828pub fn reftable_list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
1830 let stack = ReftableStack::open(git_dir)?;
1831 let refs = stack.read_refs()?;
1832 let mut result = Vec::new();
1833 for rec in refs {
1834 let matches_prefix = rec.name.starts_with(prefix)
1835 || (prefix.ends_with('/') && rec.name == prefix.trim_end_matches('/'));
1836 if matches_prefix {
1837 match rec.value {
1838 RefValue::Val1(oid) => result.push((rec.name, oid)),
1839 RefValue::Val2(oid, _) => result.push((rec.name, oid)),
1840 RefValue::Symref(target) => {
1841 if let Ok(oid) = reftable_resolve_ref(git_dir, &target) {
1843 result.push((rec.name, oid));
1844 }
1845 }
1846 RefValue::Deletion => {}
1847 }
1848 }
1849 }
1850 result.sort_by(|a, b| a.0.cmp(&b.0));
1851 Ok(result)
1852}
1853
1854pub fn reftable_read_reflog(
1856 git_dir: &Path,
1857 refname: &str,
1858) -> Result<Vec<crate::reflog::ReflogEntry>> {
1859 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1860 let stack = ReftableStack::open(&store_git_dir)?;
1861 let logs = stack.read_logs_for_ref(&storage_refname)?;
1862 let mut entries = Vec::new();
1863 for log in logs {
1864 let tz_sign = if log.tz_offset >= 0 { '+' } else { '-' };
1866 let tz_abs = log.tz_offset.unsigned_abs();
1867 let tz_hours = tz_abs / 60;
1868 let tz_mins = tz_abs % 60;
1869 let identity = format!(
1870 "{} <{}> {} {}{:02}{:02}",
1871 log.name, log.email, log.time_seconds, tz_sign, tz_hours, tz_mins
1872 );
1873 entries.push(crate::reflog::ReflogEntry {
1874 old_oid: log.old_id,
1875 new_oid: log.new_id,
1876 identity,
1877 message: log.message,
1878 });
1879 }
1880 entries.reverse();
1881 Ok(entries)
1882}
1883
1884pub fn reftable_replace_reflog(
1886 git_dir: &Path,
1887 refname: &str,
1888 entries: &[crate::reflog::ReflogEntry],
1889) -> Result<()> {
1890 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1891 let mut markers = read_empty_reflog_markers(&store_git_dir);
1892 if entries.is_empty() {
1893 markers.insert(storage_refname.clone());
1894 } else {
1895 markers.remove(&storage_refname);
1896 }
1897 write_empty_reflog_markers(&store_git_dir, &markers)?;
1898 let mut stack = ReftableStack::open(&store_git_dir)?;
1899 stack.replace_logs_for_ref(&storage_refname, entries)
1900}
1901
1902pub fn reftable_append_reflog(
1904 git_dir: &Path,
1905 refname: &str,
1906 old_oid: &ObjectId,
1907 new_oid: &ObjectId,
1908 identity: &str,
1909 message: &str,
1910 force_create: bool,
1911) -> Result<()> {
1912 use crate::refs::should_autocreate_reflog;
1913 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1914 if !force_create
1915 && !should_autocreate_reflog(&store_git_dir, &storage_refname)
1916 && message.is_empty()
1917 && !reftable_reflog_exists(&store_git_dir, &storage_refname)
1918 {
1919 return Ok(());
1920 }
1921 let (name, email, time_secs, tz) = parse_identity_string(identity);
1922 let mut stack = ReftableStack::open(&store_git_dir)?;
1923 let update_index = stack.max_update_index()? + 1;
1924 let opts = read_write_options(&store_git_dir);
1925
1926 let mut writer = ReftableWriter::new(opts, update_index, update_index);
1927 writer.add_log(LogRecord {
1928 refname: storage_refname,
1929 update_index,
1930 old_id: *old_oid,
1931 new_id: *new_oid,
1932 name,
1933 email,
1934 time_seconds: time_secs,
1935 tz_offset: tz,
1936 message: message.to_owned(),
1937 })?;
1938
1939 let data = writer.finish()?;
1940 stack.add_table(&data, update_index)?;
1941 Ok(())
1942}
1943
1944pub fn reftable_reflog_exists(git_dir: &Path, refname: &str) -> bool {
1946 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1947 if read_empty_reflog_markers(&store_git_dir).contains(&storage_refname) {
1948 return true;
1949 }
1950 if let Ok(stack) = ReftableStack::open(&store_git_dir) {
1951 if let Ok(logs) = stack.read_logs_for_ref(&storage_refname) {
1952 return !logs.is_empty();
1953 }
1954 }
1955 false
1956}
1957
1958pub fn reftable_list_reflog_refs(git_dir: &Path) -> Result<Vec<String>> {
1960 let stack = ReftableStack::open(git_dir)?;
1961 let mut refs: BTreeSet<String> = read_empty_reflog_markers(git_dir);
1962 for log in stack.read_all_logs()? {
1963 refs.insert(log.refname);
1964 }
1965 Ok(refs.into_iter().collect())
1966}
1967
1968fn empty_reflog_markers_path(git_dir: &Path) -> PathBuf {
1969 git_dir.join("reftable").join("empty-reflogs")
1970}
1971
1972fn read_empty_reflog_markers(git_dir: &Path) -> BTreeSet<String> {
1973 fs::read_to_string(empty_reflog_markers_path(git_dir))
1974 .map(|content| {
1975 content
1976 .lines()
1977 .filter(|line| !line.trim().is_empty())
1978 .map(ToOwned::to_owned)
1979 .collect()
1980 })
1981 .unwrap_or_default()
1982}
1983
1984fn write_empty_reflog_markers(git_dir: &Path, markers: &BTreeSet<String>) -> Result<()> {
1985 let path = empty_reflog_markers_path(git_dir);
1986 let content = markers.iter().cloned().collect::<Vec<_>>().join("\n");
1987 fs::write(
1988 path,
1989 if content.is_empty() {
1990 content
1991 } else {
1992 content + "\n"
1993 },
1994 )?;
1995 Ok(())
1996}
1997
1998pub fn reftable_create_reflog(git_dir: &Path, refname: &str) -> Result<()> {
2000 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
2001 let mut markers = read_empty_reflog_markers(&store_git_dir);
2002 markers.insert(storage_refname);
2003 write_empty_reflog_markers(&store_git_dir, &markers)
2004}
2005
2006pub fn reftable_delete_reflog(git_dir: &Path, refname: &str) -> Result<()> {
2008 let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
2009 let mut markers = read_empty_reflog_markers(&store_git_dir);
2010 markers.remove(&storage_refname);
2011 write_empty_reflog_markers(&store_git_dir, &markers)?;
2012 let mut stack = ReftableStack::open(&store_git_dir)?;
2013 stack.replace_logs_for_ref(&storage_refname, &[])
2014}
2015
2016pub fn read_write_options(git_dir: &Path) -> WriteOptions {
2022 let mut opts = WriteOptions::default();
2023
2024 if let Ok(config) = ConfigSet::load(Some(git_dir), true) {
2025 if let Some(value) = config.get("reftable.blockSize") {
2026 if let Ok(v) = value.parse::<u32>() {
2027 opts.block_size = v;
2028 }
2029 }
2030 if let Some(value) = config.get("reftable.restartInterval") {
2031 if let Ok(v) = value.parse::<usize>() {
2032 opts.restart_interval = v;
2033 }
2034 }
2035 if let Some(value) = config.get("core.logAllRefUpdates") {
2036 let value = value.to_lowercase();
2037 if !(value == "true" || value == "always") {
2038 opts.write_log = false;
2039 }
2040 }
2041 return opts;
2042 }
2043
2044 let config_path = git_dir.join("config");
2045 if let Ok(content) = fs::read_to_string(&config_path) {
2046 let mut in_reftable = false;
2047 let mut in_core = false;
2048 let mut log_all_ref_updates: Option<bool> = None;
2049
2050 for line in content.lines() {
2051 let trimmed = line.trim();
2052 if trimmed.starts_with('[') {
2053 let section_lower = trimmed.to_lowercase();
2054 in_reftable = section_lower.starts_with("[reftable]");
2055 in_core = section_lower.starts_with("[core]");
2056 continue;
2057 }
2058 if in_reftable {
2059 if let Some((key, value)) = trimmed.split_once('=') {
2060 let key = key.trim().to_lowercase();
2061 let value = value.trim();
2062 match key.as_str() {
2063 "blocksize" => {
2064 if let Ok(v) = value.parse::<u32>() {
2065 opts.block_size = v;
2066 }
2067 }
2068 "restartinterval" => {
2069 if let Ok(v) = value.parse::<usize>() {
2070 opts.restart_interval = v;
2071 }
2072 }
2073 _ => {}
2074 }
2075 }
2076 }
2077 if in_core {
2078 if let Some((key, value)) = trimmed.split_once('=') {
2079 let key = key.trim().to_lowercase();
2080 let value = value.trim().to_lowercase();
2081 if key == "logallrefupdates" {
2082 log_all_ref_updates = Some(value == "true" || value == "always");
2083 }
2084 }
2085 }
2086 }
2087
2088 if let Some(false) = log_all_ref_updates {
2089 opts.write_log = false;
2090 }
2091 }
2092
2093 opts
2094}
2095
2096fn should_log_ref_updates(git_dir: &Path) -> bool {
2098 let config_path = git_dir.join("config");
2099 if let Ok(content) = fs::read_to_string(&config_path) {
2100 let mut in_core = false;
2101 for line in content.lines() {
2102 let trimmed = line.trim();
2103 if trimmed.starts_with('[') {
2104 in_core = trimmed.to_lowercase().starts_with("[core]");
2105 continue;
2106 }
2107 if in_core {
2108 if let Some((key, value)) = trimmed.split_once('=') {
2109 if key.trim().eq_ignore_ascii_case("logallrefupdates") {
2110 let v = value.trim().to_lowercase();
2111 return v == "true" || v == "always";
2112 }
2113 }
2114 }
2115 }
2116 }
2117 false
2118}
2119
2120fn crc32(data: &[u8]) -> u32 {
2126 let mut crc: u32 = 0xffffffff;
2127 for &byte in data {
2128 crc ^= byte as u32;
2129 for _ in 0..8 {
2130 if crc & 1 != 0 {
2131 crc = (crc >> 1) ^ 0xedb88320;
2132 } else {
2133 crc >>= 1;
2134 }
2135 }
2136 }
2137 !crc
2138}
2139
2140fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
2142 a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
2143}
2144
2145fn read_u24(data: &[u8], pos: usize) -> usize {
2147 ((data[pos] as usize) << 16) | ((data[pos + 1] as usize) << 8) | (data[pos + 2] as usize)
2148}
2149
2150fn read_u16(data: &[u8], pos: usize) -> usize {
2152 ((data[pos] as usize) << 8) | (data[pos + 1] as usize)
2153}
2154
2155fn parse_footer(data: &[u8], version: u8) -> Result<Footer> {
2157 let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
2158 if data.len() < footer_size {
2159 return Err(Error::InvalidRef("reftable: footer too small".into()));
2160 }
2161
2162 if &data[0..4] != REFTABLE_MAGIC {
2164 return Err(Error::InvalidRef("reftable: bad footer magic".into()));
2165 }
2166 let fver = data[4];
2167 if fver != version {
2168 return Err(Error::InvalidRef(format!(
2169 "reftable: footer version mismatch: header={version}, footer={fver}"
2170 )));
2171 }
2172
2173 let block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
2174 let min_update_index = u64::from_be_bytes(data[8..16].try_into().unwrap());
2175 let max_update_index = u64::from_be_bytes(data[16..24].try_into().unwrap());
2176
2177 let off = 24;
2178 let ref_index_position = u64::from_be_bytes(data[off..off + 8].try_into().unwrap());
2179 let obj_position_and_id_len = u64::from_be_bytes(data[off + 8..off + 16].try_into().unwrap());
2180 let obj_index_position = u64::from_be_bytes(data[off + 16..off + 24].try_into().unwrap());
2181 let log_position = u64::from_be_bytes(data[off + 24..off + 32].try_into().unwrap());
2182 let log_index_position = u64::from_be_bytes(data[off + 32..off + 40].try_into().unwrap());
2183
2184 let crc_stored = u32::from_be_bytes(data[footer_size - 4..footer_size].try_into().unwrap());
2186 let crc_computed = crc32(&data[..footer_size - 4]);
2187 if crc_stored != crc_computed {
2188 return Err(Error::InvalidRef(format!(
2189 "reftable: footer CRC mismatch: stored={crc_stored:08x}, computed={crc_computed:08x}"
2190 )));
2191 }
2192
2193 Ok(Footer {
2194 version: fver,
2195 block_size,
2196 min_update_index,
2197 max_update_index,
2198 ref_index_position,
2199 obj_position_and_id_len,
2200 obj_index_position,
2201 log_position,
2202 log_index_position,
2203 })
2204}
2205
2206fn parse_identity_string(identity: &str) -> (String, String, u64, i16) {
2208 let parts: Vec<&str> = identity.rsplitn(3, ' ').collect();
2210 if parts.len() < 3 {
2211 return (identity.to_owned(), String::new(), 0, 0);
2212 }
2213 let tz_str = parts[0]; let time_str = parts[1]; let name_email = parts[2]; let time_secs = time_str.parse::<u64>().unwrap_or(0);
2218
2219 let tz_minutes = if tz_str.len() >= 5 {
2221 let sign = if tz_str.starts_with('-') { -1i16 } else { 1 };
2222 let hours = tz_str[1..3].parse::<i16>().unwrap_or(0);
2223 let mins = tz_str[3..5].parse::<i16>().unwrap_or(0);
2224 sign * (hours * 60 + mins)
2225 } else {
2226 0
2227 };
2228
2229 let (name, email) = if let Some(lt_pos) = name_email.find('<') {
2231 let name = name_email[..lt_pos].trim().to_owned();
2232 let email = if let Some(gt_pos) = name_email.find('>') {
2233 name_email[lt_pos + 1..gt_pos].to_owned()
2234 } else {
2235 name_email[lt_pos + 1..].to_owned()
2236 };
2237 (name, email)
2238 } else {
2239 (name_email.to_owned(), String::new())
2240 };
2241
2242 (name, email, time_secs, tz_minutes)
2243}
2244
2245#[cfg(test)]
2250mod tests {
2251 use super::*;
2252
2253 #[test]
2254 fn test_varint_roundtrip() {
2255 for val in [0u64, 1, 127, 128, 255, 256, 16383, 16384, u64::MAX] {
2256 let mut buf = Vec::new();
2257 put_varint(val, &mut buf);
2258 let (decoded, end) = get_varint(&buf, 0).unwrap();
2259 assert_eq!(decoded, val, "varint roundtrip failed for {val}");
2260 assert_eq!(end, buf.len());
2261 }
2262 }
2263
2264 #[test]
2265 fn test_crc32() {
2266 assert_eq!(crc32(b"123456789"), 0xCBF43926);
2268 }
2269
2270 #[test]
2271 fn test_empty_table() {
2272 let writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
2273 let data = writer.finish().unwrap();
2274 let reader = ReftableReader::new(data).unwrap();
2275 let refs = reader.read_refs().unwrap();
2276 assert!(refs.is_empty());
2277 }
2278
2279 #[test]
2280 fn test_write_read_single_ref() {
2281 let oid = ObjectId::from_bytes(&[0xab; 20]).unwrap();
2282 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
2283 writer
2284 .add_ref(RefRecord {
2285 name: "refs/heads/main".to_owned(),
2286 update_index: 1,
2287 value: RefValue::Val1(oid),
2288 })
2289 .unwrap();
2290 let data = writer.finish().unwrap();
2291
2292 let reader = ReftableReader::new(data).unwrap();
2293 let refs = reader.read_refs().unwrap();
2294 assert_eq!(refs.len(), 1);
2295 assert_eq!(refs[0].name, "refs/heads/main");
2296 assert_eq!(refs[0].value, RefValue::Val1(oid));
2297 assert_eq!(refs[0].update_index, 1);
2298 }
2299
2300 #[test]
2301 fn test_write_read_multiple_refs() {
2302 let oid1 = ObjectId::from_bytes(&[0x11; 20]).unwrap();
2303 let oid2 = ObjectId::from_bytes(&[0x22; 20]).unwrap();
2304 let oid3 = ObjectId::from_bytes(&[0x33; 20]).unwrap();
2305
2306 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
2307 writer
2308 .add_ref(RefRecord {
2309 name: "refs/heads/a".to_owned(),
2310 update_index: 1,
2311 value: RefValue::Val1(oid1),
2312 })
2313 .unwrap();
2314 writer
2315 .add_ref(RefRecord {
2316 name: "refs/heads/b".to_owned(),
2317 update_index: 1,
2318 value: RefValue::Val1(oid2),
2319 })
2320 .unwrap();
2321 writer
2322 .add_ref(RefRecord {
2323 name: "refs/tags/v1.0".to_owned(),
2324 update_index: 1,
2325 value: RefValue::Val2(oid3, oid1),
2326 })
2327 .unwrap();
2328 let data = writer.finish().unwrap();
2329
2330 let reader = ReftableReader::new(data).unwrap();
2331 let refs = reader.read_refs().unwrap();
2332 assert_eq!(refs.len(), 3);
2333 assert_eq!(refs[0].name, "refs/heads/a");
2334 assert_eq!(refs[1].name, "refs/heads/b");
2335 assert_eq!(refs[2].name, "refs/tags/v1.0");
2336 assert_eq!(refs[2].value, RefValue::Val2(oid3, oid1));
2337 }
2338
2339 #[test]
2340 fn test_symref_roundtrip() {
2341 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
2342 writer
2343 .add_ref(RefRecord {
2344 name: "refs/heads/sym".to_owned(),
2345 update_index: 1,
2346 value: RefValue::Symref("refs/heads/main".to_owned()),
2347 })
2348 .unwrap();
2349 let data = writer.finish().unwrap();
2350
2351 let reader = ReftableReader::new(data).unwrap();
2352 let refs = reader.read_refs().unwrap();
2353 assert_eq!(refs.len(), 1);
2354 assert_eq!(
2355 refs[0].value,
2356 RefValue::Symref("refs/heads/main".to_owned())
2357 );
2358 }
2359
2360 #[test]
2361 fn test_log_roundtrip() {
2362 let old_oid = ObjectId::from_bytes(&[0; 20]).unwrap();
2363 let new_oid = ObjectId::from_bytes(&[0xaa; 20]).unwrap();
2364
2365 let mut opts = WriteOptions::default();
2366 opts.write_log = true;
2367 let mut writer = ReftableWriter::new(opts, 1, 1);
2368 writer
2369 .add_log(LogRecord {
2370 refname: "refs/heads/main".to_owned(),
2371 update_index: 1,
2372 old_id: old_oid,
2373 new_id: new_oid,
2374 name: "Test User".to_owned(),
2375 email: "test@example.com".to_owned(),
2376 time_seconds: 1700000000,
2377 tz_offset: -480,
2378 message: "initial commit".to_owned(),
2379 })
2380 .unwrap();
2381 let data = writer.finish().unwrap();
2382
2383 let reader = ReftableReader::new(data).unwrap();
2384 let logs = reader.read_logs().unwrap();
2385 assert_eq!(logs.len(), 1);
2386 assert_eq!(logs[0].refname, "refs/heads/main");
2387 assert_eq!(logs[0].old_id, old_oid);
2388 assert_eq!(logs[0].new_id, new_oid);
2389 assert_eq!(logs[0].name, "Test User");
2390 assert_eq!(logs[0].email, "test@example.com");
2391 assert_eq!(logs[0].time_seconds, 1700000000);
2392 assert_eq!(logs[0].tz_offset, -480);
2393 assert_eq!(logs[0].message, "initial commit");
2394 }
2395
2396 #[test]
2397 fn test_unaligned_table() {
2398 let oid = ObjectId::from_bytes(&[0xcc; 20]).unwrap();
2399 let opts = WriteOptions {
2400 block_size: 0, restart_interval: 16,
2402 write_log: false,
2403 };
2404 let mut writer = ReftableWriter::new(opts, 1, 1);
2405 writer
2406 .add_ref(RefRecord {
2407 name: "refs/heads/main".to_owned(),
2408 update_index: 1,
2409 value: RefValue::Val1(oid),
2410 })
2411 .unwrap();
2412 let data = writer.finish().unwrap();
2413
2414 let reader = ReftableReader::new(data).unwrap();
2415 assert_eq!(reader.block_size(), 0);
2416 let refs = reader.read_refs().unwrap();
2417 assert_eq!(refs.len(), 1);
2418 assert_eq!(refs[0].value, RefValue::Val1(oid));
2419 }
2420
2421 #[test]
2422 fn test_parse_identity() {
2423 let (name, email, ts, tz) =
2424 parse_identity_string("Test User <test@example.com> 1700000000 -0800");
2425 assert_eq!(name, "Test User");
2426 assert_eq!(email, "test@example.com");
2427 assert_eq!(ts, 1700000000);
2428 assert_eq!(tz, -480);
2429 }
2430
2431 #[test]
2432 fn test_deletion_record() {
2433 let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
2434 writer
2435 .add_ref(RefRecord {
2436 name: "refs/heads/gone".to_owned(),
2437 update_index: 1,
2438 value: RefValue::Deletion,
2439 })
2440 .unwrap();
2441 let data = writer.finish().unwrap();
2442
2443 let reader = ReftableReader::new(data).unwrap();
2444 let refs = reader.read_refs().unwrap();
2445 assert_eq!(refs.len(), 1);
2446 assert_eq!(refs[0].value, RefValue::Deletion);
2447 }
2448}