Skip to main content

grit_lib/
reftable.rs

1//! Reftable format — binary reference storage.
2//!
3//! Implements the [reftable file format](https://git-scm.com/docs/reftable)
4//! for efficient, sorted reference storage.  A reftable file contains
5//! ref blocks (sorted ref records with prefix compression), optional log
6//! blocks (reflog entries), optional index blocks, and a footer.
7//!
8//! # Architecture
9//!
10//! - [`ReftableWriter`] writes a single `.ref` (or `.log`) reftable file.
11//! - [`ReftableReader`] reads and searches a single reftable file.
12//! - [`ReftableStack`] manages the `tables.list` stack, providing a
13//!   merged view of all tables and auto-compaction on writes.
14//!
15//! # On-disk layout
16//!
17//! ```text
18//! first_block { header, first_ref_block }
19//! ref_block*
20//! ref_index?
21//! obj_block*    (not yet implemented)
22//! obj_index?    (not yet implemented)
23//! log_block*
24//! log_index?
25//! footer
26//! ```
27
28use std::collections::BTreeMap;
29use std::fs;
30use std::io::{Read, Write};
31use std::path::{Path, PathBuf};
32
33use crate::config::ConfigSet;
34use crate::error::{Error, Result};
35use crate::objects::ObjectId;
36
37// ---------------------------------------------------------------------------
38// Constants
39// ---------------------------------------------------------------------------
40
41/// Magic bytes at the start of every reftable file.
42const REFTABLE_MAGIC: &[u8; 4] = b"REFT";
43
44/// File header size (version 1): magic(4) + version(1) + block_size(3)
45/// + min_update_index(8) + max_update_index(8) = 24 bytes.
46const HEADER_SIZE: usize = 24;
47
48/// Footer size for version 1.
49const FOOTER_V1_SIZE: usize = 68;
50
51/// Block type: ref block.
52const BLOCK_TYPE_REF: u8 = b'r';
53/// Block type: index block.
54const BLOCK_TYPE_INDEX: u8 = b'i';
55/// Block type: log block (zlib-compressed).
56const BLOCK_TYPE_LOG: u8 = b'g';
57
58/// Value types encoded in the low 3 bits of the suffix_length varint.
59const VALUE_DELETION: u8 = 0;
60const VALUE_ONE_OID: u8 = 1;
61const VALUE_TWO_OID: u8 = 2;
62const VALUE_SYMREF: u8 = 3;
63
64/// Hash size (SHA-1).
65const HASH_SIZE: usize = 20;
66
67/// Default block size when none is configured (4 KiB).
68const DEFAULT_BLOCK_SIZE: u32 = 4096;
69
70/// How many records between restart points.
71const RESTART_INTERVAL: usize = 16;
72
73// ---------------------------------------------------------------------------
74// Varint encoding (Git pack-style)
75// ---------------------------------------------------------------------------
76
77/// Encode a u64 as a varint into `out`. Returns number of bytes written.
78fn put_varint(mut val: u64, out: &mut Vec<u8>) -> usize {
79    // First, collect 7-bit groups.
80    let mut buf = [0u8; 10];
81    let mut i = 0;
82    buf[i] = (val & 0x7f) as u8;
83    i += 1;
84    val >>= 7;
85    while val > 0 {
86        val -= 1;
87        buf[i] = (val & 0x7f) as u8;
88        i += 1;
89        val >>= 7;
90    }
91    // Write in reverse, with continuation bits.
92    let len = i;
93    for j in (1..len).rev() {
94        out.push(buf[j] | 0x80);
95    }
96    out.push(buf[0]);
97    len
98}
99
100/// Decode a varint from `data` starting at `pos`. Returns (value, new_pos).
101fn get_varint(data: &[u8], mut pos: usize) -> Result<(u64, usize)> {
102    if pos >= data.len() {
103        return Err(Error::InvalidRef("varint: unexpected end of data".into()));
104    }
105    let mut val = (data[pos] & 0x7f) as u64;
106    while data[pos] & 0x80 != 0 {
107        pos += 1;
108        if pos >= data.len() {
109            return Err(Error::InvalidRef("varint: unexpected end of data".into()));
110        }
111        val = ((val + 1) << 7) | (data[pos] & 0x7f) as u64;
112    }
113    Ok((val, pos + 1))
114}
115
116// ---------------------------------------------------------------------------
117// Ref record types
118// ---------------------------------------------------------------------------
119
120/// A single reference record as stored in a reftable.
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub enum RefValue {
123    /// Deletion tombstone (value_type 0x0).
124    Deletion,
125    /// A direct ref pointing to one OID (value_type 0x1).
126    Val1(ObjectId),
127    /// An annotated tag: value + peeled target (value_type 0x2).
128    Val2(ObjectId, ObjectId),
129    /// A symbolic reference (value_type 0x3).
130    Symref(String),
131}
132
133/// A decoded ref record.
134#[derive(Debug, Clone)]
135pub struct RefRecord {
136    /// Full reference name.
137    pub name: String,
138    /// Update index (absolute).
139    pub update_index: u64,
140    /// The value.
141    pub value: RefValue,
142}
143
144/// A decoded log record.
145#[derive(Debug, Clone)]
146pub struct LogRecord {
147    /// Reference name.
148    pub refname: String,
149    /// Update index.
150    pub update_index: u64,
151    /// Old object ID.
152    pub old_id: ObjectId,
153    /// New object ID.
154    pub new_id: ObjectId,
155    /// Committer name.
156    pub name: String,
157    /// Committer email (without angle brackets).
158    pub email: String,
159    /// Time in seconds since epoch.
160    pub time_seconds: u64,
161    /// Timezone offset in minutes (signed).
162    pub tz_offset: i16,
163    /// Log message.
164    pub message: String,
165}
166
167/// Write options for reftable creation.
168#[derive(Debug, Clone)]
169pub struct WriteOptions {
170    /// Block size in bytes. 0 means unaligned (variable-sized blocks).
171    pub block_size: u32,
172    /// Restart interval (number of records between restart points).
173    pub restart_interval: usize,
174    /// Whether to write log blocks.
175    pub write_log: bool,
176}
177
178impl Default for WriteOptions {
179    fn default() -> Self {
180        Self {
181            block_size: DEFAULT_BLOCK_SIZE,
182            restart_interval: RESTART_INTERVAL,
183            write_log: true,
184        }
185    }
186}
187
188// ---------------------------------------------------------------------------
189// Writer
190// ---------------------------------------------------------------------------
191
192/// Writes a single reftable file.
193///
194/// Usage:
195/// ```ignore
196/// let mut w = ReftableWriter::new(opts, min_idx, max_idx);
197/// w.add_ref(&RefRecord { .. })?;
198/// w.add_log(&LogRecord { .. })?;
199/// let bytes = w.finish()?;
200/// ```
201pub struct ReftableWriter {
202    opts: WriteOptions,
203    min_update_index: u64,
204    max_update_index: u64,
205
206    // Accumulated ref records (must be added in sorted order).
207    refs: Vec<RefRecord>,
208    // Accumulated log records.
209    logs: Vec<LogRecord>,
210}
211
212impl ReftableWriter {
213    /// Create a new writer.
214    pub fn new(opts: WriteOptions, min_update_index: u64, max_update_index: u64) -> Self {
215        Self {
216            opts,
217            min_update_index,
218            max_update_index,
219            refs: Vec::new(),
220            logs: Vec::new(),
221        }
222    }
223
224    /// Add a ref record. Records **must** be added in sorted name order.
225    pub fn add_ref(&mut self, rec: RefRecord) -> Result<()> {
226        if let Some(last) = self.refs.last() {
227            if rec.name <= last.name {
228                return Err(Error::InvalidRef(format!(
229                    "reftable: refs must be sorted, got '{}' after '{}'",
230                    rec.name, last.name
231                )));
232            }
233        }
234        self.refs.push(rec);
235        Ok(())
236    }
237
238    /// Add a log record.
239    pub fn add_log(&mut self, rec: LogRecord) -> Result<()> {
240        self.logs.push(rec);
241        Ok(())
242    }
243
244    /// Finish writing and return the complete reftable file bytes.
245    pub fn finish(mut self) -> Result<Vec<u8>> {
246        let mut out = Vec::new();
247        let block_size = self.opts.block_size;
248
249        // --- Header (24 bytes) ---
250        out.extend_from_slice(REFTABLE_MAGIC);
251        out.push(1); // version
252        out.push(((block_size >> 16) & 0xff) as u8);
253        out.push(((block_size >> 8) & 0xff) as u8);
254        out.push((block_size & 0xff) as u8);
255        out.extend_from_slice(&self.min_update_index.to_be_bytes());
256        out.extend_from_slice(&self.max_update_index.to_be_bytes());
257
258        assert_eq!(out.len(), HEADER_SIZE);
259
260        // --- Ref blocks ---
261        let ref_block_positions = self.write_ref_blocks(&mut out)?;
262
263        // --- Ref index (if ≥ 4 ref blocks) ---
264        let ref_index_position = if ref_block_positions.len() >= 4 {
265            let pos = out.len() as u64;
266            self.write_ref_index(&mut out, &ref_block_positions)?;
267            pos
268        } else {
269            0
270        };
271
272        // --- Log blocks ---
273        let log_position = if self.opts.write_log && !self.logs.is_empty() {
274            let pos = out.len() as u64;
275            self.write_log_blocks(&mut out)?;
276            pos
277        } else {
278            0
279        };
280
281        // --- Footer ---
282        let footer_start = out.len();
283        // Repeat header
284        out.extend_from_slice(REFTABLE_MAGIC);
285        out.push(1);
286        out.push(((block_size >> 16) & 0xff) as u8);
287        out.push(((block_size >> 8) & 0xff) as u8);
288        out.push((block_size & 0xff) as u8);
289        out.extend_from_slice(&self.min_update_index.to_be_bytes());
290        out.extend_from_slice(&self.max_update_index.to_be_bytes());
291
292        // ref_index_position
293        out.extend_from_slice(&ref_index_position.to_be_bytes());
294        // (obj_position << 5) | obj_id_len — no obj blocks yet
295        out.extend_from_slice(&0u64.to_be_bytes());
296        // obj_index_position
297        out.extend_from_slice(&0u64.to_be_bytes());
298        // log_position
299        out.extend_from_slice(&log_position.to_be_bytes());
300        // log_index_position (we skip log index for simplicity)
301        out.extend_from_slice(&0u64.to_be_bytes());
302
303        // CRC-32 of footer (everything from footer_start to here)
304        let crc = crc32(&out[footer_start..]);
305        out.extend_from_slice(&crc.to_be_bytes());
306
307        Ok(out)
308    }
309
310    /// Write ref blocks, returning (block_start_position, last_refname) per block.
311    fn write_ref_blocks(&self, out: &mut Vec<u8>) -> Result<Vec<(u64, String)>> {
312        if self.refs.is_empty() {
313            return Ok(Vec::new());
314        }
315
316        let block_size = self.opts.block_size as usize;
317        let restart_interval = self.opts.restart_interval;
318        let mut block_positions: Vec<(u64, String)> = Vec::new();
319        let mut i = 0;
320
321        while i < self.refs.len() {
322            let block_start = out.len();
323            let is_first_block = block_start == HEADER_SIZE;
324
325            // We accumulate records into a buffer, then write the block.
326            let mut records_buf = Vec::new();
327            let mut restart_offsets: Vec<u32> = Vec::new();
328            let mut prev_name = String::new();
329            let mut count = 0;
330            let mut last_name = String::new();
331
332            while i < self.refs.len() {
333                let rec = &self.refs[i];
334                let is_restart = count % restart_interval == 0;
335
336                let mut rec_buf = Vec::new();
337                let prefix_len = if is_restart {
338                    0
339                } else {
340                    common_prefix_len(prev_name.as_bytes(), rec.name.as_bytes())
341                };
342                let suffix = &rec.name.as_bytes()[prefix_len..];
343                let suffix_len = suffix.len();
344
345                let value_type = match &rec.value {
346                    RefValue::Deletion => VALUE_DELETION,
347                    RefValue::Val1(_) => VALUE_ONE_OID,
348                    RefValue::Val2(_, _) => VALUE_TWO_OID,
349                    RefValue::Symref(_) => VALUE_SYMREF,
350                };
351
352                put_varint(prefix_len as u64, &mut rec_buf);
353                put_varint(((suffix_len as u64) << 3) | value_type as u64, &mut rec_buf);
354                rec_buf.extend_from_slice(suffix);
355
356                let update_index_delta = rec.update_index.saturating_sub(self.min_update_index);
357                put_varint(update_index_delta, &mut rec_buf);
358
359                match &rec.value {
360                    RefValue::Deletion => {}
361                    RefValue::Val1(oid) => {
362                        rec_buf.extend_from_slice(oid.as_bytes());
363                    }
364                    RefValue::Val2(oid, peeled) => {
365                        rec_buf.extend_from_slice(oid.as_bytes());
366                        rec_buf.extend_from_slice(peeled.as_bytes());
367                    }
368                    RefValue::Symref(target) => {
369                        put_varint(target.len() as u64, &mut rec_buf);
370                        rec_buf.extend_from_slice(target.as_bytes());
371                    }
372                }
373
374                // Check if adding this record would overflow the block.
375                // Block overhead: 4 (block header) + restart table
376                let restart_count = restart_offsets.len() + if is_restart { 1 } else { 0 };
377                let trailer_size = restart_count * 3 + 2;
378                let total = 4 + records_buf.len() + rec_buf.len() + trailer_size;
379                let effective_block_size = if is_first_block && block_size > 0 {
380                    block_size // first block includes header
381                } else if block_size > 0 {
382                    block_size
383                } else {
384                    usize::MAX // unaligned
385                };
386                // For first block, block_len includes the 24-byte header
387                let block_len = if is_first_block {
388                    HEADER_SIZE + total
389                } else {
390                    total
391                };
392
393                if block_size > 0 && block_len > effective_block_size && count > 0 {
394                    break; // Start a new block
395                }
396
397                if is_restart {
398                    let offset = if is_first_block {
399                        HEADER_SIZE + 4 + records_buf.len()
400                    } else {
401                        4 + records_buf.len()
402                    };
403                    restart_offsets.push(offset as u32);
404                }
405
406                records_buf.extend_from_slice(&rec_buf);
407                last_name = rec.name.clone();
408                prev_name = rec.name.clone();
409                count += 1;
410                i += 1;
411            }
412
413            if count == 0 {
414                return Err(Error::InvalidRef(
415                    "reftable: ref record too large for block size".into(),
416                ));
417            }
418
419            // Ensure at least one restart point
420            if restart_offsets.is_empty() {
421                restart_offsets.push(if is_first_block {
422                    HEADER_SIZE as u32 + 4
423                } else {
424                    4
425                });
426            }
427
428            // Compute block_len
429            let trailer_size = restart_offsets.len() * 3 + 2;
430            let block_len_val = if is_first_block {
431                HEADER_SIZE + 4 + records_buf.len() + trailer_size
432            } else {
433                4 + records_buf.len() + trailer_size
434            };
435
436            // Write block header: type(1) + block_len(3)
437            out.push(BLOCK_TYPE_REF);
438            out.push(((block_len_val >> 16) & 0xff) as u8);
439            out.push(((block_len_val >> 8) & 0xff) as u8);
440            out.push((block_len_val & 0xff) as u8);
441
442            // Write records
443            out.extend_from_slice(&records_buf);
444
445            // Write restart offsets (3 bytes each)
446            for &off in &restart_offsets {
447                out.push(((off >> 16) & 0xff) as u8);
448                out.push(((off >> 8) & 0xff) as u8);
449                out.push((off & 0xff) as u8);
450            }
451
452            // Write restart count (2 bytes)
453            let rc = restart_offsets.len() as u16;
454            out.push((rc >> 8) as u8);
455            out.push((rc & 0xff) as u8);
456
457            // Pad to block alignment if needed
458            if block_size > 0 {
459                let written = out.len() - block_start;
460                let target = if is_first_block {
461                    block_size
462                } else {
463                    block_size
464                };
465                if written < target {
466                    out.resize(block_start + target, 0);
467                }
468            }
469
470            block_positions.push((block_start as u64, last_name.clone()));
471        }
472
473        Ok(block_positions)
474    }
475
476    /// Write a single-level ref index block.
477    fn write_ref_index(&self, out: &mut Vec<u8>, block_positions: &[(u64, String)]) -> Result<()> {
478        let mut records_buf = Vec::new();
479        let mut restart_offsets: Vec<u32> = Vec::new();
480        let mut prev_name = String::new();
481
482        for (idx, (block_pos, last_ref)) in block_positions.iter().enumerate() {
483            let is_restart = idx % self.opts.restart_interval == 0;
484            let prefix_len = if is_restart {
485                0
486            } else {
487                common_prefix_len(prev_name.as_bytes(), last_ref.as_bytes())
488            };
489            let suffix = &last_ref.as_bytes()[prefix_len..];
490
491            if is_restart {
492                restart_offsets.push(4 + records_buf.len() as u32);
493            }
494
495            put_varint(prefix_len as u64, &mut records_buf);
496            put_varint((suffix.len() as u64) << 3, &mut records_buf);
497            records_buf.extend_from_slice(suffix);
498            put_varint(*block_pos, &mut records_buf);
499
500            prev_name = last_ref.clone();
501        }
502
503        if restart_offsets.is_empty() {
504            restart_offsets.push(4);
505        }
506
507        let trailer_size = restart_offsets.len() * 3 + 2;
508        let block_len = 4 + records_buf.len() + trailer_size;
509
510        out.push(BLOCK_TYPE_INDEX);
511        out.push(((block_len >> 16) & 0xff) as u8);
512        out.push(((block_len >> 8) & 0xff) as u8);
513        out.push((block_len & 0xff) as u8);
514
515        out.extend_from_slice(&records_buf);
516
517        for &off in &restart_offsets {
518            out.push(((off >> 16) & 0xff) as u8);
519            out.push(((off >> 8) & 0xff) as u8);
520            out.push((off & 0xff) as u8);
521        }
522        let rc = restart_offsets.len() as u16;
523        out.push((rc >> 8) as u8);
524        out.push((rc & 0xff) as u8);
525
526        Ok(())
527    }
528
529    /// Write log blocks (zlib-compressed).
530    fn write_log_blocks(&mut self, out: &mut Vec<u8>) -> Result<()> {
531        use flate2::write::DeflateEncoder;
532        use flate2::Compression;
533
534        // Sort logs by (refname, reverse update_index)
535        self.logs.sort_by(|a, b| {
536            a.refname
537                .cmp(&b.refname)
538                .then_with(|| b.update_index.cmp(&a.update_index))
539        });
540
541        // Build the uncompressed log block content
542        let mut inner = Vec::new();
543        let mut restart_offsets: Vec<u32> = Vec::new();
544        let mut prev_key = Vec::<u8>::new();
545
546        for (idx, log) in self.logs.iter().enumerate() {
547            let is_restart = idx % self.opts.restart_interval == 0;
548
549            // Log key: refname \0 reverse_int64(update_index)
550            let mut key = Vec::new();
551            key.extend_from_slice(log.refname.as_bytes());
552            key.push(0);
553            key.extend_from_slice(&(0xffffffffffffffffu64 - log.update_index).to_be_bytes());
554
555            let prefix_len = if is_restart {
556                0
557            } else {
558                common_prefix_len(&prev_key, &key)
559            };
560            let suffix = &key[prefix_len..];
561
562            if is_restart {
563                // Offset within the decompressed block (4 byte header + inner.len())
564                restart_offsets.push(4 + inner.len() as u32);
565            }
566
567            // log_type = 1 (standard reflog data)
568            let log_type: u8 = 1;
569            put_varint(prefix_len as u64, &mut inner);
570            put_varint(((suffix.len() as u64) << 3) | log_type as u64, &mut inner);
571            inner.extend_from_slice(suffix);
572
573            // log_data
574            inner.extend_from_slice(log.old_id.as_bytes());
575            inner.extend_from_slice(log.new_id.as_bytes());
576            put_varint(log.name.len() as u64, &mut inner);
577            inner.extend_from_slice(log.name.as_bytes());
578            put_varint(log.email.len() as u64, &mut inner);
579            inner.extend_from_slice(log.email.as_bytes());
580            put_varint(log.time_seconds, &mut inner);
581            inner.extend_from_slice(&log.tz_offset.to_be_bytes());
582            put_varint(log.message.len() as u64, &mut inner);
583            inner.extend_from_slice(log.message.as_bytes());
584
585            prev_key = key;
586        }
587
588        if restart_offsets.is_empty() {
589            restart_offsets.push(4);
590        }
591
592        // Append restart table
593        for &off in &restart_offsets {
594            inner.push(((off >> 16) & 0xff) as u8);
595            inner.push(((off >> 8) & 0xff) as u8);
596            inner.push((off & 0xff) as u8);
597        }
598        let rc = restart_offsets.len() as u16;
599        inner.push((rc >> 8) as u8);
600        inner.push((rc & 0xff) as u8);
601
602        // block_len is the *inflated* size including the 4-byte block header
603        let block_len = 4 + inner.len();
604
605        // Deflate the inner content
606        let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
607        encoder
608            .write_all(&inner)
609            .map_err(|e| Error::Zlib(e.to_string()))?;
610        let compressed = encoder.finish().map_err(|e| Error::Zlib(e.to_string()))?;
611
612        // Write block header + compressed data
613        out.push(BLOCK_TYPE_LOG);
614        out.push(((block_len >> 16) & 0xff) as u8);
615        out.push(((block_len >> 8) & 0xff) as u8);
616        out.push((block_len & 0xff) as u8);
617        out.extend_from_slice(&compressed);
618
619        Ok(())
620    }
621}
622
623// ---------------------------------------------------------------------------
624// Reader
625// ---------------------------------------------------------------------------
626
627/// Reads a single reftable file from a byte buffer.
628pub struct ReftableReader {
629    data: Vec<u8>,
630    version: u8,
631    block_size: u32,
632    min_update_index: u64,
633    max_update_index: u64,
634    ref_index_position: u64,
635    log_position: u64,
636}
637
638/// Parsed footer fields.
639#[derive(Debug)]
640#[allow(dead_code)]
641struct Footer {
642    version: u8,
643    block_size: u32,
644    min_update_index: u64,
645    max_update_index: u64,
646    ref_index_position: u64,
647    obj_position_and_id_len: u64,
648    obj_index_position: u64,
649    log_position: u64,
650    log_index_position: u64,
651}
652
653impl ReftableReader {
654    /// Open a reftable from bytes.
655    pub fn new(data: Vec<u8>) -> Result<Self> {
656        if data.len() < HEADER_SIZE + FOOTER_V1_SIZE {
657            // Could be an empty table (header + footer only = 24 + 68 = 92)
658            if data.len() < HEADER_SIZE {
659                return Err(Error::InvalidRef("reftable: file too small".into()));
660            }
661        }
662
663        // Parse header
664        if &data[0..4] != REFTABLE_MAGIC {
665            return Err(Error::InvalidRef("reftable: bad magic".into()));
666        }
667        let version = data[4];
668        if version != 1 && version != 2 {
669            return Err(Error::InvalidRef(format!(
670                "reftable: unsupported version {version}"
671            )));
672        }
673        let _block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
674        let _min_update_index = u64::from_be_bytes(data[8..16].try_into().unwrap());
675        let _max_update_index = u64::from_be_bytes(data[16..24].try_into().unwrap());
676
677        // Parse footer
678        let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
679        if data.len() < footer_size {
680            return Err(Error::InvalidRef(
681                "reftable: file too small for footer".into(),
682            ));
683        }
684        let footer_start = data.len() - footer_size;
685        let footer = parse_footer(&data[footer_start..], version)?;
686
687        Ok(Self {
688            data,
689            version,
690            block_size: footer.block_size,
691            min_update_index: footer.min_update_index,
692            max_update_index: footer.max_update_index,
693            ref_index_position: footer.ref_index_position,
694            log_position: footer.log_position,
695        })
696    }
697
698    /// Read all ref records from the table.
699    pub fn read_refs(&self) -> Result<Vec<RefRecord>> {
700        let mut refs = Vec::new();
701        let footer_size = if self.version == 2 {
702            72
703        } else {
704            FOOTER_V1_SIZE
705        };
706        let file_end = self.data.len() - footer_size;
707
708        // Determine where ref blocks end
709        let ref_end = if self.ref_index_position > 0 {
710            self.ref_index_position as usize
711        } else if self.log_position > 0 {
712            self.log_position as usize
713        } else {
714            file_end
715        };
716
717        let mut pos = 0usize;
718        // Skip the header — first ref block starts at offset 24 but shares
719        // the same physical block as the header.
720        if pos < HEADER_SIZE {
721            pos = HEADER_SIZE;
722        }
723
724        while pos < ref_end {
725            if pos >= self.data.len() {
726                break;
727            }
728            let block_type = self.data[pos];
729            if block_type == 0 {
730                // Padding — skip to next block boundary
731                if self.block_size > 0 {
732                    let bs = self.block_size as usize;
733                    pos = ((pos / bs) + 1) * bs;
734                    continue;
735                } else {
736                    break;
737                }
738            }
739            if block_type != BLOCK_TYPE_REF {
740                break;
741            }
742
743            let block_len = read_u24(&self.data, pos + 1);
744            // Determine the data range for this block
745            let block_data_start = pos + 4; // after type(1) + len(3)
746
747            // The first block's block_len includes the 24-byte header
748            let is_first = pos == HEADER_SIZE;
749            let records_end = if is_first {
750                // block_len is from file start
751                block_len
752            } else {
753                pos + block_len
754            };
755
756            if records_end > ref_end {
757                break;
758            }
759
760            // Read restart count (last 2 bytes before padding)
761            let rc = read_u16(&self.data, records_end - 2);
762            // Restart table is rc * 3 bytes before the restart_count
763            let restart_table_start = records_end - 2 - (rc * 3);
764
765            // Read records from block_data_start to restart_table_start
766            let mut rpos = block_data_start;
767            let mut prev_name = Vec::<u8>::new();
768
769            while rpos < restart_table_start {
770                let (rec, new_pos) =
771                    decode_ref_record(&self.data, rpos, &prev_name, self.min_update_index)?;
772                prev_name = rec.name.as_bytes().to_vec();
773                refs.push(rec);
774                rpos = new_pos;
775            }
776
777            // Advance to next block
778            if self.block_size > 0 {
779                let bs = self.block_size as usize;
780                if is_first {
781                    pos = bs;
782                } else {
783                    pos += bs;
784                }
785            } else {
786                pos = records_end;
787            }
788        }
789
790        Ok(refs)
791    }
792
793    /// Look up a single ref by name.
794    pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
795        // Simple: scan all refs. For large files the index would speed this up.
796        let refs = self.read_refs()?;
797        Ok(refs.into_iter().find(|r| r.name == name))
798    }
799
800    /// Read all log records from the table.
801    pub fn read_logs(&self) -> Result<Vec<LogRecord>> {
802        if self.log_position == 0 {
803            return Ok(Vec::new());
804        }
805
806        let footer_size = if self.version == 2 {
807            72
808        } else {
809            FOOTER_V1_SIZE
810        };
811        let file_end = self.data.len() - footer_size;
812        let mut pos = self.log_position as usize;
813        let mut logs = Vec::new();
814
815        while pos < file_end {
816            if pos >= self.data.len() {
817                break;
818            }
819            let block_type = self.data[pos];
820            if block_type != BLOCK_TYPE_LOG {
821                break;
822            }
823            let block_len = read_u24(&self.data, pos + 1);
824            let compressed_start = pos + 4;
825
826            // The inflated size is block_len - 4 (block_len includes the 4-byte header)
827            let inflated_size = block_len - 4;
828
829            // Decompress
830            use flate2::read::DeflateDecoder;
831            let remaining = &self.data[compressed_start..file_end];
832            let mut decoder = DeflateDecoder::new(remaining);
833            let mut inflated = vec![0u8; inflated_size];
834            decoder
835                .read_exact(&mut inflated)
836                .map_err(|e| Error::Zlib(e.to_string()))?;
837
838            // How many compressed bytes were consumed?
839            let consumed = decoder.total_in() as usize;
840
841            // Parse log records from inflated data
842            // Read restart_count from end
843            if inflated.len() < 2 {
844                break;
845            }
846            let rc = read_u16(&inflated, inflated.len() - 2);
847            let restart_table_start = inflated.len() - 2 - (rc * 3);
848
849            let mut rpos = 0usize;
850            let mut prev_key = Vec::<u8>::new();
851
852            while rpos < restart_table_start {
853                let (log, new_pos) = decode_log_record(&inflated, rpos, &prev_key)?;
854                // Reconstruct key for prefix compression
855                let mut key = Vec::new();
856                key.extend_from_slice(log.refname.as_bytes());
857                key.push(0);
858                key.extend_from_slice(&(0xffffffffffffffffu64 - log.update_index).to_be_bytes());
859                prev_key = key;
860                logs.push(log);
861                rpos = new_pos;
862            }
863
864            pos = compressed_start + consumed;
865        }
866
867        Ok(logs)
868    }
869
870    /// Get the block size from the header.
871    pub fn block_size(&self) -> u32 {
872        self.block_size
873    }
874
875    /// Get the min update index.
876    pub fn min_update_index(&self) -> u64 {
877        self.min_update_index
878    }
879
880    /// Get the max update index.
881    pub fn max_update_index(&self) -> u64 {
882        self.max_update_index
883    }
884}
885
886// ---------------------------------------------------------------------------
887// Record decoding helpers
888// ---------------------------------------------------------------------------
889
890fn decode_ref_record(
891    data: &[u8],
892    pos: usize,
893    prev_name: &[u8],
894    min_update_index: u64,
895) -> Result<(RefRecord, usize)> {
896    let (prefix_len, p) = get_varint(data, pos)?;
897    let (suffix_and_type, mut p) = get_varint(data, p)?;
898    let suffix_len = (suffix_and_type >> 3) as usize;
899    let value_type = (suffix_and_type & 0x7) as u8;
900
901    // Reconstruct name
902    let mut name = Vec::with_capacity(prefix_len as usize + suffix_len);
903    if prefix_len > 0 {
904        if (prefix_len as usize) > prev_name.len() {
905            return Err(Error::InvalidRef(
906                "reftable: prefix_len exceeds prev name".into(),
907            ));
908        }
909        name.extend_from_slice(&prev_name[..prefix_len as usize]);
910    }
911    if p + suffix_len > data.len() {
912        return Err(Error::InvalidRef("reftable: suffix overflows block".into()));
913    }
914    name.extend_from_slice(&data[p..p + suffix_len]);
915    p += suffix_len;
916
917    let name_str = String::from_utf8(name)
918        .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in ref name".into()))?;
919
920    let (update_index_delta, mut p) = get_varint(data, p)?;
921    let update_index = min_update_index + update_index_delta;
922
923    let value = match value_type {
924        VALUE_DELETION => RefValue::Deletion,
925        VALUE_ONE_OID => {
926            if p + HASH_SIZE > data.len() {
927                return Err(Error::InvalidRef("reftable: truncated OID".into()));
928            }
929            let oid = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
930            p += HASH_SIZE;
931            RefValue::Val1(oid)
932        }
933        VALUE_TWO_OID => {
934            if p + 2 * HASH_SIZE > data.len() {
935                return Err(Error::InvalidRef("reftable: truncated OID pair".into()));
936            }
937            let oid = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
938            p += HASH_SIZE;
939            let peeled = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
940            p += HASH_SIZE;
941            RefValue::Val2(oid, peeled)
942        }
943        VALUE_SYMREF => {
944            let (target_len, p2) = get_varint(data, p)?;
945            p = p2;
946            let target_len = target_len as usize;
947            if p + target_len > data.len() {
948                return Err(Error::InvalidRef(
949                    "reftable: truncated symref target".into(),
950                ));
951            }
952            let target = String::from_utf8(data[p..p + target_len].to_vec())
953                .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in symref".into()))?;
954            p += target_len;
955            RefValue::Symref(target)
956        }
957        _ => {
958            return Err(Error::InvalidRef(format!(
959                "reftable: unknown value_type {value_type}"
960            )));
961        }
962    };
963
964    Ok((
965        RefRecord {
966            name: name_str,
967            update_index,
968            value,
969        },
970        p,
971    ))
972}
973
974fn decode_log_record(data: &[u8], pos: usize, prev_key: &[u8]) -> Result<(LogRecord, usize)> {
975    let (prefix_len, p) = get_varint(data, pos)?;
976    let (suffix_and_type, mut p) = get_varint(data, p)?;
977    let suffix_len = (suffix_and_type >> 3) as usize;
978    let log_type = (suffix_and_type & 0x7) as u8;
979
980    // Reconstruct key
981    let mut key = Vec::with_capacity(prefix_len as usize + suffix_len);
982    if prefix_len > 0 {
983        if (prefix_len as usize) > prev_key.len() {
984            return Err(Error::InvalidRef(
985                "reftable: log prefix_len exceeds prev key".into(),
986            ));
987        }
988        key.extend_from_slice(&prev_key[..prefix_len as usize]);
989    }
990    if p + suffix_len > data.len() {
991        return Err(Error::InvalidRef("reftable: log suffix overflows".into()));
992    }
993    key.extend_from_slice(&data[p..p + suffix_len]);
994    p += suffix_len;
995
996    // Parse key: refname \0 reverse_int64(update_index)
997    let null_pos = key
998        .iter()
999        .position(|&b| b == 0)
1000        .ok_or_else(|| Error::InvalidRef("reftable: log key missing null separator".into()))?;
1001    let refname = String::from_utf8(key[..null_pos].to_vec())
1002        .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log refname".into()))?;
1003    if null_pos + 9 > key.len() {
1004        return Err(Error::InvalidRef("reftable: log key too short".into()));
1005    }
1006    let reversed_idx = u64::from_be_bytes(key[null_pos + 1..null_pos + 9].try_into().unwrap());
1007    let update_index = 0xffffffffffffffffu64 - reversed_idx;
1008
1009    if log_type == 0 {
1010        // Deletion
1011        let zero_oid = ObjectId::from_bytes(&[0u8; 20])?;
1012        return Ok((
1013            LogRecord {
1014                refname,
1015                update_index,
1016                old_id: zero_oid,
1017                new_id: zero_oid,
1018                name: String::new(),
1019                email: String::new(),
1020                time_seconds: 0,
1021                tz_offset: 0,
1022                message: String::new(),
1023            },
1024            p,
1025        ));
1026    }
1027
1028    // log_type == 1: standard log data
1029    if p + 2 * HASH_SIZE > data.len() {
1030        return Err(Error::InvalidRef("reftable: truncated log OIDs".into()));
1031    }
1032    let old_id = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
1033    p += HASH_SIZE;
1034    let new_id = ObjectId::from_bytes(&data[p..p + HASH_SIZE])?;
1035    p += HASH_SIZE;
1036
1037    let (name_len, p2) = get_varint(data, p)?;
1038    p = p2;
1039    let name_len = name_len as usize;
1040    if p + name_len > data.len() {
1041        return Err(Error::InvalidRef("reftable: truncated log name".into()));
1042    }
1043    let name = String::from_utf8(data[p..p + name_len].to_vec())
1044        .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log name".into()))?;
1045    p += name_len;
1046
1047    let (email_len, p2) = get_varint(data, p)?;
1048    p = p2;
1049    let email_len = email_len as usize;
1050    if p + email_len > data.len() {
1051        return Err(Error::InvalidRef("reftable: truncated log email".into()));
1052    }
1053    let email = String::from_utf8(data[p..p + email_len].to_vec())
1054        .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log email".into()))?;
1055    p += email_len;
1056
1057    let (time_seconds, p2) = get_varint(data, p)?;
1058    p = p2;
1059
1060    if p + 2 > data.len() {
1061        return Err(Error::InvalidRef("reftable: truncated tz_offset".into()));
1062    }
1063    let tz_offset = i16::from_be_bytes([data[p], data[p + 1]]);
1064    p += 2;
1065
1066    let (msg_len, p2) = get_varint(data, p)?;
1067    p = p2;
1068    let msg_len = msg_len as usize;
1069    if p + msg_len > data.len() {
1070        return Err(Error::InvalidRef("reftable: truncated log message".into()));
1071    }
1072    let message = String::from_utf8(data[p..p + msg_len].to_vec())
1073        .map_err(|_| Error::InvalidRef("reftable: invalid UTF-8 in log message".into()))?;
1074    p += msg_len;
1075
1076    Ok((
1077        LogRecord {
1078            refname,
1079            update_index,
1080            old_id,
1081            new_id,
1082            name,
1083            email,
1084            time_seconds,
1085            tz_offset,
1086            message,
1087        },
1088        p,
1089    ))
1090}
1091
1092// ---------------------------------------------------------------------------
1093// Stack management
1094// ---------------------------------------------------------------------------
1095
1096/// Manages the `$GIT_DIR/reftable/` directory and `tables.list` stack.
1097///
1098/// The stack provides a merged view of all tables, with later tables
1099/// taking precedence over earlier ones.
1100pub struct ReftableStack {
1101    /// Path to the `reftable/` directory.
1102    reftable_dir: PathBuf,
1103    /// Ordered list of table file names (oldest first).
1104    table_names: Vec<String>,
1105}
1106
1107impl ReftableStack {
1108    /// Open an existing reftable stack.
1109    pub fn open(git_dir: &Path) -> Result<Self> {
1110        let reftable_dir = git_dir.join("reftable");
1111        let tables_list = reftable_dir.join("tables.list");
1112        let content = fs::read_to_string(&tables_list).map_err(Error::Io)?;
1113        let table_names: Vec<String> = content
1114            .lines()
1115            .filter(|l| !l.is_empty())
1116            .map(|l| l.to_owned())
1117            .collect();
1118        Ok(Self {
1119            reftable_dir,
1120            table_names,
1121        })
1122    }
1123
1124    /// Read a merged view of all ref records.
1125    ///
1126    /// Later tables override earlier ones. Deletion records cause the
1127    /// ref to be omitted from the result.
1128    pub fn read_refs(&self) -> Result<Vec<RefRecord>> {
1129        let mut merged: BTreeMap<String, RefRecord> = BTreeMap::new();
1130
1131        for name in &self.table_names {
1132            let path = self.reftable_dir.join(name);
1133            let data = fs::read(&path).map_err(Error::Io)?;
1134            let reader = ReftableReader::new(data)?;
1135            for rec in reader.read_refs()? {
1136                match &rec.value {
1137                    RefValue::Deletion => {
1138                        merged.remove(&rec.name);
1139                    }
1140                    _ => {
1141                        merged.insert(rec.name.clone(), rec);
1142                    }
1143                }
1144            }
1145        }
1146
1147        Ok(merged.into_values().collect())
1148    }
1149
1150    /// Look up a single ref across all tables (most recent wins).
1151    pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
1152        // Search tables in reverse (newest first)
1153        for table_name in self.table_names.iter().rev() {
1154            let path = self.reftable_dir.join(table_name);
1155            let data = fs::read(&path).map_err(Error::Io)?;
1156            let reader = ReftableReader::new(data)?;
1157            if let Some(rec) = reader.lookup_ref(name)? {
1158                return match rec.value {
1159                    RefValue::Deletion => Ok(None),
1160                    _ => Ok(Some(rec)),
1161                };
1162            }
1163        }
1164        Ok(None)
1165    }
1166
1167    /// Read merged log records for a specific ref.
1168    pub fn read_logs_for_ref(&self, refname: &str) -> Result<Vec<LogRecord>> {
1169        let mut logs = Vec::new();
1170        for table_name in &self.table_names {
1171            let path = self.reftable_dir.join(table_name);
1172            let data = fs::read(&path).map_err(Error::Io)?;
1173            let reader = ReftableReader::new(data)?;
1174            for log in reader.read_logs()? {
1175                if log.refname == refname {
1176                    logs.push(log);
1177                }
1178            }
1179        }
1180        // Sort by update_index descending (most recent first)
1181        logs.sort_by(|a, b| b.update_index.cmp(&a.update_index));
1182        Ok(logs)
1183    }
1184
1185    /// Read all log records across all tables.
1186    pub fn read_all_logs(&self) -> Result<Vec<LogRecord>> {
1187        let mut logs = Vec::new();
1188        for table_name in &self.table_names {
1189            let path = self.reftable_dir.join(table_name);
1190            let data = fs::read(&path).map_err(Error::Io)?;
1191            let reader = ReftableReader::new(data)?;
1192            logs.extend(reader.read_logs()?);
1193        }
1194        logs.sort_by(|a, b| {
1195            a.refname
1196                .cmp(&b.refname)
1197                .then_with(|| b.update_index.cmp(&a.update_index))
1198        });
1199        Ok(logs)
1200    }
1201
1202    /// Get the current max update index across all tables.
1203    pub fn max_update_index(&self) -> Result<u64> {
1204        let mut max_idx = 0u64;
1205        for name in &self.table_names {
1206            let path = self.reftable_dir.join(name);
1207            let data = fs::read(&path).map_err(Error::Io)?;
1208            let reader = ReftableReader::new(data)?;
1209            max_idx = max_idx.max(reader.max_update_index());
1210        }
1211        Ok(max_idx)
1212    }
1213
1214    /// Add a new reftable to the stack.
1215    ///
1216    /// Writes the table bytes to a new file, then atomically updates
1217    /// `tables.list`.
1218    pub fn add_table(&mut self, data: &[u8], update_index: u64) -> Result<String> {
1219        let random: u64 = {
1220            // Simple random from /dev/urandom or time-based fallback
1221            let mut buf = [0u8; 8];
1222            if let Ok(mut f) = fs::File::open("/dev/urandom") {
1223                let _ = f.read(&mut buf);
1224            }
1225            u64::from_le_bytes(buf)
1226        };
1227        let filename = format!(
1228            "{:08x}-{:08x}-{:08x}.ref",
1229            update_index, update_index, random as u32
1230        );
1231        let path = self.reftable_dir.join(&filename);
1232        fs::write(&path, data).map_err(Error::Io)?;
1233
1234        self.table_names.push(filename.clone());
1235        self.write_tables_list()?;
1236
1237        // Auto-compact small write bursts into a single table. A plain commit writes several small
1238        // ref/log updates and should settle back to one table; a following tag write remains as a
1239        // second table until explicit `pack-refs`.
1240        if self.table_names.len() > 3
1241            && std::env::var("GIT_TEST_REFTABLE_AUTOCOMPACTION")
1242                .map(|value| value != "false")
1243                .unwrap_or(true)
1244        {
1245            self.compact()?;
1246        }
1247
1248        Ok(filename)
1249    }
1250
1251    /// Write a ref update (add/update/delete) as a new reftable.
1252    ///
1253    /// This is the main entry point for updating refs in a reftable repo.
1254    pub fn write_ref(
1255        &mut self,
1256        refname: &str,
1257        value: RefValue,
1258        log: Option<LogRecord>,
1259        opts: &WriteOptions,
1260    ) -> Result<()> {
1261        let update_index = self.max_update_index()? + 1;
1262        let mut writer = ReftableWriter::new(opts.clone(), update_index, update_index);
1263
1264        // For a single-ref update we need to write all existing refs + the update
1265        // into a proper sorted order, OR we can write a single-record table.
1266        // The stack handles merging, so a single-record table is fine.
1267        writer.add_ref(RefRecord {
1268            name: refname.to_owned(),
1269            update_index,
1270            value,
1271        })?;
1272
1273        if let Some(log_rec) = log {
1274            let mut log_rec = log_rec;
1275            log_rec.update_index = update_index;
1276            writer.add_log(log_rec)?;
1277        }
1278
1279        let data = writer.finish()?;
1280        self.add_table(&data, update_index)?;
1281        Ok(())
1282    }
1283
1284    /// Compact all tables into a single table.
1285    pub fn compact(&mut self) -> Result<()> {
1286        if self.table_names.len() <= 1 {
1287            return Ok(());
1288        }
1289
1290        // Read all refs and logs
1291        let refs = self.read_refs()?;
1292        let logs = self.read_all_logs()?;
1293
1294        // Determine update index range
1295        let mut min_idx = u64::MAX;
1296        let mut max_idx = 0u64;
1297        for name in &self.table_names {
1298            let path = self.reftable_dir.join(name);
1299            let data = fs::read(&path).map_err(Error::Io)?;
1300            let reader = ReftableReader::new(data)?;
1301            min_idx = min_idx.min(reader.min_update_index());
1302            max_idx = max_idx.max(reader.max_update_index());
1303        }
1304        if min_idx == u64::MAX {
1305            min_idx = 0;
1306        }
1307
1308        let mut writer = ReftableWriter::new(WriteOptions::default(), min_idx, max_idx);
1309        for rec in refs {
1310            writer.add_ref(rec)?;
1311        }
1312        for log in logs {
1313            writer.add_log(log)?;
1314        }
1315
1316        let data = writer.finish()?;
1317
1318        // Write new compacted table
1319        let old_names = self.table_names.clone();
1320        self.table_names.clear();
1321        let _name = self.add_table(&data, max_idx)?;
1322
1323        // Remove old table files
1324        for old in &old_names {
1325            let path = self.reftable_dir.join(old);
1326            let _ = fs::remove_file(&path);
1327        }
1328
1329        Ok(())
1330    }
1331
1332    /// Write `tables.list` atomically.
1333    fn write_tables_list(&self) -> Result<()> {
1334        let tables_list = self.reftable_dir.join("tables.list");
1335        let lock = self.reftable_dir.join("tables.list.lock");
1336        let content = self.table_names.join("\n")
1337            + if self.table_names.is_empty() {
1338                ""
1339            } else {
1340                "\n"
1341            };
1342        fs::write(&lock, &content).map_err(Error::Io)?;
1343        fs::rename(&lock, &tables_list).map_err(Error::Io)?;
1344        Ok(())
1345    }
1346
1347    /// Return the list of table filenames in this stack.
1348    pub fn table_names(&self) -> &[String] {
1349        &self.table_names
1350    }
1351}
1352
1353// ---------------------------------------------------------------------------
1354// Integration helpers — used by refs.rs and commands
1355// ---------------------------------------------------------------------------
1356
1357/// Detect whether a git directory uses the reftable backend.
1358pub fn is_reftable_repo(git_dir: &Path) -> bool {
1359    fn config_uses_reftable(config_path: &Path) -> bool {
1360        let Ok(content) = fs::read_to_string(config_path) else {
1361            return false;
1362        };
1363
1364        let mut in_extensions = false;
1365        for line in content.lines() {
1366            let trimmed = line.trim();
1367            if trimmed.starts_with('[') {
1368                in_extensions = trimmed.eq_ignore_ascii_case("[extensions]");
1369                continue;
1370            }
1371            if in_extensions {
1372                if let Some((key, value)) = trimmed.split_once('=') {
1373                    if key.trim().eq_ignore_ascii_case("refstorage")
1374                        && value.trim().eq_ignore_ascii_case("reftable")
1375                    {
1376                        return true;
1377                    }
1378                }
1379            }
1380        }
1381        false
1382    }
1383
1384    let local_config = git_dir.join("config");
1385    if config_uses_reftable(&local_config) {
1386        return true;
1387    }
1388
1389    // Linked worktrees typically store the shared repository configuration
1390    // in the common directory pointed to by `commondir`.
1391    if let Ok(raw) = fs::read_to_string(git_dir.join("commondir")) {
1392        let rel = raw.trim();
1393        if !rel.is_empty() {
1394            let common = if Path::new(rel).is_absolute() {
1395                PathBuf::from(rel)
1396            } else {
1397                git_dir.join(rel)
1398            };
1399            let common_config = common.canonicalize().unwrap_or(common).join("config");
1400            if config_uses_reftable(&common_config) {
1401                return true;
1402            }
1403        }
1404    }
1405
1406    false
1407}
1408
1409/// Resolve a ref in a reftable repo, following symbolic refs.
1410pub fn reftable_resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
1411    reftable_resolve_ref_depth(git_dir, refname, 0)
1412}
1413
1414fn reftable_storage_location(git_dir: &Path, refname: &str) -> (PathBuf, String) {
1415    if let Some(rest) = refname.strip_prefix("worktrees/") {
1416        if let Some((worktree_id, per_worktree_ref)) = rest.split_once('/') {
1417            if per_worktree_ref.starts_with("refs/") {
1418                let common =
1419                    crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
1420                return (
1421                    common.join("worktrees").join(worktree_id),
1422                    per_worktree_ref.to_owned(),
1423                );
1424            }
1425        }
1426    }
1427
1428    if refname == "HEAD"
1429        || refname.starts_with("refs/worktree/")
1430        || (git_dir.join("commondir").exists() && refname.starts_with("refs/bisect/"))
1431    {
1432        return (git_dir.to_path_buf(), refname.to_owned());
1433    }
1434
1435    (
1436        crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf()),
1437        refname.to_owned(),
1438    )
1439}
1440
1441fn reftable_resolve_ref_depth(git_dir: &Path, refname: &str, depth: usize) -> Result<ObjectId> {
1442    if depth > 10 {
1443        return Err(Error::InvalidRef(format!(
1444            "reftable: symlink too deep: {refname}"
1445        )));
1446    }
1447
1448    // HEAD is special — stored as a file even in reftable repos
1449    if refname == "HEAD" {
1450        let head_path = git_dir.join("HEAD");
1451        if head_path.exists() {
1452            let content = fs::read_to_string(&head_path).map_err(Error::Io)?;
1453            let content = content.trim();
1454            if let Some(target) = content.strip_prefix("ref: ") {
1455                if target.trim() == "refs/heads/.invalid" {
1456                    return reftable_resolve_ref_depth(git_dir, "refs/worktree/HEAD", depth + 1);
1457                }
1458                return reftable_resolve_ref_depth(git_dir, target.trim(), depth + 1);
1459            }
1460            // Detached HEAD
1461            if content.len() == 40 && content.chars().all(|c| c.is_ascii_hexdigit()) {
1462                return content.parse();
1463            }
1464        }
1465    }
1466
1467    let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1468    let stack = ReftableStack::open(&store_git_dir)?;
1469    match stack.lookup_ref(&storage_refname)? {
1470        Some(rec) => match rec.value {
1471            RefValue::Val1(oid) => Ok(oid),
1472            RefValue::Val2(oid, _) => Ok(oid),
1473            RefValue::Symref(target) => {
1474                reftable_resolve_ref_depth(&store_git_dir, &target, depth + 1)
1475            }
1476            RefValue::Deletion => Err(Error::InvalidRef(format!("ref not found: {refname}"))),
1477        },
1478        None => Err(Error::InvalidRef(format!("ref not found: {refname}"))),
1479    }
1480}
1481
1482/// Write a ref to a reftable repo.
1483pub fn reftable_write_ref(
1484    git_dir: &Path,
1485    refname: &str,
1486    oid: &ObjectId,
1487    log_identity: Option<&str>,
1488    log_message: Option<&str>,
1489) -> Result<()> {
1490    let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1491    let mut stack = ReftableStack::open(&store_git_dir)?;
1492    let old_oid = stack
1493        .lookup_ref(&storage_refname)?
1494        .and_then(|r| match r.value {
1495            RefValue::Val1(oid) => Some(oid),
1496            RefValue::Val2(oid, _) => Some(oid),
1497            _ => None,
1498        })
1499        .unwrap_or_else(|| ObjectId::from_bytes(&[0u8; 20]).unwrap());
1500
1501    let log = if let Some(identity) = log_identity {
1502        let (name, email, time_secs, tz) = parse_identity_string(identity);
1503        Some(LogRecord {
1504            refname: storage_refname.clone(),
1505            update_index: 0, // will be set by write_ref
1506            old_id: old_oid,
1507            new_id: *oid,
1508            name,
1509            email,
1510            time_seconds: time_secs,
1511            tz_offset: tz,
1512            message: log_message.unwrap_or("").to_owned(),
1513        })
1514    } else {
1515        None
1516    };
1517
1518    // Check config for logAllRefUpdates
1519    let write_log = log.is_some() || should_log_ref_updates(&store_git_dir);
1520    let log = if write_log { log } else { None };
1521
1522    let opts = read_write_options(&store_git_dir);
1523    stack.write_ref(&storage_refname, RefValue::Val1(*oid), log, &opts)
1524}
1525
1526/// Write a symbolic ref to a reftable repo.
1527pub fn reftable_write_symref(
1528    git_dir: &Path,
1529    refname: &str,
1530    target: &str,
1531    log_identity: Option<&str>,
1532    log_message: Option<&str>,
1533) -> Result<()> {
1534    let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1535    let mut stack = ReftableStack::open(&store_git_dir)?;
1536    let opts = read_write_options(&store_git_dir);
1537
1538    let log = if let Some(identity) = log_identity {
1539        let (name, email, time_secs, tz) = parse_identity_string(identity);
1540        let zero_oid = ObjectId::from_bytes(&[0u8; 20])?;
1541        Some(LogRecord {
1542            refname: storage_refname.clone(),
1543            update_index: 0,
1544            old_id: zero_oid,
1545            new_id: zero_oid,
1546            name,
1547            email,
1548            time_seconds: time_secs,
1549            tz_offset: tz,
1550            message: log_message.unwrap_or("").to_owned(),
1551        })
1552    } else {
1553        None
1554    };
1555
1556    stack.write_ref(
1557        &storage_refname,
1558        RefValue::Symref(target.to_owned()),
1559        log,
1560        &opts,
1561    )
1562}
1563
1564/// Delete a ref from a reftable repo.
1565pub fn reftable_delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
1566    let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1567    let mut stack = ReftableStack::open(&store_git_dir)?;
1568    let opts = read_write_options(&store_git_dir);
1569    stack.write_ref(&storage_refname, RefValue::Deletion, None, &opts)
1570}
1571
1572/// Read the symbolic target of a ref in a reftable repo.
1573pub fn reftable_read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
1574    let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1575    let stack = ReftableStack::open(&store_git_dir)?;
1576    match stack.lookup_ref(&storage_refname)? {
1577        Some(rec) => match rec.value {
1578            RefValue::Symref(target) => Ok(Some(target)),
1579            _ => Ok(None),
1580        },
1581        None => Ok(None),
1582    }
1583}
1584
1585/// List all refs in a reftable repo under a given prefix.
1586pub fn reftable_list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
1587    let stack = ReftableStack::open(git_dir)?;
1588    let refs = stack.read_refs()?;
1589    let mut result = Vec::new();
1590    for rec in refs {
1591        let matches_prefix = rec.name.starts_with(prefix)
1592            || (prefix.ends_with('/') && rec.name == prefix.trim_end_matches('/'));
1593        if matches_prefix {
1594            match rec.value {
1595                RefValue::Val1(oid) => result.push((rec.name, oid)),
1596                RefValue::Val2(oid, _) => result.push((rec.name, oid)),
1597                RefValue::Symref(target) => {
1598                    // Try to resolve the symref
1599                    if let Ok(oid) = reftable_resolve_ref(git_dir, &target) {
1600                        result.push((rec.name, oid));
1601                    }
1602                }
1603                RefValue::Deletion => {}
1604            }
1605        }
1606    }
1607    result.sort_by(|a, b| a.0.cmp(&b.0));
1608    Ok(result)
1609}
1610
1611/// Read reflog entries for a ref from the reftable stack.
1612pub fn reftable_read_reflog(
1613    git_dir: &Path,
1614    refname: &str,
1615) -> Result<Vec<crate::reflog::ReflogEntry>> {
1616    let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1617    let stack = ReftableStack::open(&store_git_dir)?;
1618    let logs = stack.read_logs_for_ref(&storage_refname)?;
1619    let mut entries = Vec::new();
1620    for log in logs {
1621        // Reconstruct the identity string
1622        let tz_sign = if log.tz_offset >= 0 { '+' } else { '-' };
1623        let tz_abs = log.tz_offset.unsigned_abs();
1624        let tz_hours = tz_abs / 60;
1625        let tz_mins = tz_abs % 60;
1626        let identity = format!(
1627            "{} <{}> {} {}{:02}{:02}",
1628            log.name, log.email, log.time_seconds, tz_sign, tz_hours, tz_mins
1629        );
1630        entries.push(crate::reflog::ReflogEntry {
1631            old_oid: log.old_id,
1632            new_oid: log.new_id,
1633            identity,
1634            message: log.message,
1635        });
1636    }
1637    Ok(entries)
1638}
1639
1640/// Append a reflog entry for a reftable repo.
1641pub fn reftable_append_reflog(
1642    git_dir: &Path,
1643    refname: &str,
1644    old_oid: &ObjectId,
1645    new_oid: &ObjectId,
1646    identity: &str,
1647    message: &str,
1648    force_create: bool,
1649) -> Result<()> {
1650    use crate::refs::should_autocreate_reflog;
1651    let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1652    if !force_create
1653        && !should_autocreate_reflog(&store_git_dir, &storage_refname)
1654        && message.is_empty()
1655        && !reftable_reflog_exists(&store_git_dir, &storage_refname)
1656    {
1657        return Ok(());
1658    }
1659    let (name, email, time_secs, tz) = parse_identity_string(identity);
1660    let mut stack = ReftableStack::open(&store_git_dir)?;
1661    let update_index = stack.max_update_index()? + 1;
1662    let opts = read_write_options(&store_git_dir);
1663
1664    let mut writer = ReftableWriter::new(opts, update_index, update_index);
1665    writer.add_log(LogRecord {
1666        refname: storage_refname,
1667        update_index,
1668        old_id: *old_oid,
1669        new_id: *new_oid,
1670        name,
1671        email,
1672        time_seconds: time_secs,
1673        tz_offset: tz,
1674        message: message.to_owned(),
1675    })?;
1676
1677    let data = writer.finish()?;
1678    stack.add_table(&data, update_index)?;
1679    Ok(())
1680}
1681
1682/// Check whether a reftable repo has reflogs for the given ref.
1683pub fn reftable_reflog_exists(git_dir: &Path, refname: &str) -> bool {
1684    let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
1685    if let Ok(stack) = ReftableStack::open(&store_git_dir) {
1686        if let Ok(logs) = stack.read_logs_for_ref(&storage_refname) {
1687            return !logs.is_empty();
1688        }
1689    }
1690    false
1691}
1692
1693// ---------------------------------------------------------------------------
1694// Write options helpers
1695// ---------------------------------------------------------------------------
1696
1697/// Read reftable write options from the repository config.
1698pub fn read_write_options(git_dir: &Path) -> WriteOptions {
1699    let mut opts = WriteOptions::default();
1700
1701    if let Ok(config) = ConfigSet::load(Some(git_dir), true) {
1702        if let Some(value) = config.get("reftable.blockSize") {
1703            if let Ok(v) = value.parse::<u32>() {
1704                opts.block_size = v;
1705            }
1706        }
1707        if let Some(value) = config.get("reftable.restartInterval") {
1708            if let Ok(v) = value.parse::<usize>() {
1709                opts.restart_interval = v;
1710            }
1711        }
1712        if let Some(value) = config.get("core.logAllRefUpdates") {
1713            let value = value.to_lowercase();
1714            if !(value == "true" || value == "always") {
1715                opts.write_log = false;
1716            }
1717        }
1718        return opts;
1719    }
1720
1721    let config_path = git_dir.join("config");
1722    if let Ok(content) = fs::read_to_string(&config_path) {
1723        let mut in_reftable = false;
1724        let mut in_core = false;
1725        let mut log_all_ref_updates: Option<bool> = None;
1726
1727        for line in content.lines() {
1728            let trimmed = line.trim();
1729            if trimmed.starts_with('[') {
1730                let section_lower = trimmed.to_lowercase();
1731                in_reftable = section_lower.starts_with("[reftable]");
1732                in_core = section_lower.starts_with("[core]");
1733                continue;
1734            }
1735            if in_reftable {
1736                if let Some((key, value)) = trimmed.split_once('=') {
1737                    let key = key.trim().to_lowercase();
1738                    let value = value.trim();
1739                    match key.as_str() {
1740                        "blocksize" => {
1741                            if let Ok(v) = value.parse::<u32>() {
1742                                opts.block_size = v;
1743                            }
1744                        }
1745                        "restartinterval" => {
1746                            if let Ok(v) = value.parse::<usize>() {
1747                                opts.restart_interval = v;
1748                            }
1749                        }
1750                        _ => {}
1751                    }
1752                }
1753            }
1754            if in_core {
1755                if let Some((key, value)) = trimmed.split_once('=') {
1756                    let key = key.trim().to_lowercase();
1757                    let value = value.trim().to_lowercase();
1758                    if key == "logallrefupdates" {
1759                        log_all_ref_updates = Some(value == "true" || value == "always");
1760                    }
1761                }
1762            }
1763        }
1764
1765        if let Some(false) = log_all_ref_updates {
1766            opts.write_log = false;
1767        }
1768    }
1769
1770    opts
1771}
1772
1773/// Check if logAllRefUpdates is enabled.
1774fn should_log_ref_updates(git_dir: &Path) -> bool {
1775    let config_path = git_dir.join("config");
1776    if let Ok(content) = fs::read_to_string(&config_path) {
1777        let mut in_core = false;
1778        for line in content.lines() {
1779            let trimmed = line.trim();
1780            if trimmed.starts_with('[') {
1781                in_core = trimmed.to_lowercase().starts_with("[core]");
1782                continue;
1783            }
1784            if in_core {
1785                if let Some((key, value)) = trimmed.split_once('=') {
1786                    if key.trim().eq_ignore_ascii_case("logallrefupdates") {
1787                        let v = value.trim().to_lowercase();
1788                        return v == "true" || v == "always";
1789                    }
1790                }
1791            }
1792        }
1793    }
1794    false
1795}
1796
1797// ---------------------------------------------------------------------------
1798// Utility functions
1799// ---------------------------------------------------------------------------
1800
1801/// Compute the CRC-32 of a byte slice (ISO 3309 / ITU-T V.42).
1802fn crc32(data: &[u8]) -> u32 {
1803    let mut crc: u32 = 0xffffffff;
1804    for &byte in data {
1805        crc ^= byte as u32;
1806        for _ in 0..8 {
1807            if crc & 1 != 0 {
1808                crc = (crc >> 1) ^ 0xedb88320;
1809            } else {
1810                crc >>= 1;
1811            }
1812        }
1813    }
1814    !crc
1815}
1816
1817/// Compute common prefix length between two byte slices.
1818fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
1819    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
1820}
1821
1822/// Read a big-endian u24 from 3 bytes at `pos`.
1823fn read_u24(data: &[u8], pos: usize) -> usize {
1824    ((data[pos] as usize) << 16) | ((data[pos + 1] as usize) << 8) | (data[pos + 2] as usize)
1825}
1826
1827/// Read a big-endian u16 from 2 bytes at `pos`.
1828fn read_u16(data: &[u8], pos: usize) -> usize {
1829    ((data[pos] as usize) << 8) | (data[pos + 1] as usize)
1830}
1831
1832/// Parse the footer of a reftable file.
1833fn parse_footer(data: &[u8], version: u8) -> Result<Footer> {
1834    let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
1835    if data.len() < footer_size {
1836        return Err(Error::InvalidRef("reftable: footer too small".into()));
1837    }
1838
1839    // Verify magic
1840    if &data[0..4] != REFTABLE_MAGIC {
1841        return Err(Error::InvalidRef("reftable: bad footer magic".into()));
1842    }
1843    let fver = data[4];
1844    if fver != version {
1845        return Err(Error::InvalidRef(format!(
1846            "reftable: footer version mismatch: header={version}, footer={fver}"
1847        )));
1848    }
1849
1850    let block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
1851    let min_update_index = u64::from_be_bytes(data[8..16].try_into().unwrap());
1852    let max_update_index = u64::from_be_bytes(data[16..24].try_into().unwrap());
1853
1854    let off = 24;
1855    let ref_index_position = u64::from_be_bytes(data[off..off + 8].try_into().unwrap());
1856    let obj_position_and_id_len = u64::from_be_bytes(data[off + 8..off + 16].try_into().unwrap());
1857    let obj_index_position = u64::from_be_bytes(data[off + 16..off + 24].try_into().unwrap());
1858    let log_position = u64::from_be_bytes(data[off + 24..off + 32].try_into().unwrap());
1859    let log_index_position = u64::from_be_bytes(data[off + 32..off + 40].try_into().unwrap());
1860
1861    // CRC-32 check
1862    let crc_stored = u32::from_be_bytes(data[footer_size - 4..footer_size].try_into().unwrap());
1863    let crc_computed = crc32(&data[..footer_size - 4]);
1864    if crc_stored != crc_computed {
1865        return Err(Error::InvalidRef(format!(
1866            "reftable: footer CRC mismatch: stored={crc_stored:08x}, computed={crc_computed:08x}"
1867        )));
1868    }
1869
1870    Ok(Footer {
1871        version: fver,
1872        block_size,
1873        min_update_index,
1874        max_update_index,
1875        ref_index_position,
1876        obj_position_and_id_len,
1877        obj_index_position,
1878        log_position,
1879        log_index_position,
1880    })
1881}
1882
1883/// Parse an identity string like `"Name <email> 1234567890 +0100"`.
1884fn parse_identity_string(identity: &str) -> (String, String, u64, i16) {
1885    // Format: "Name <email> timestamp tz"
1886    let parts: Vec<&str> = identity.rsplitn(3, ' ').collect();
1887    if parts.len() < 3 {
1888        return (identity.to_owned(), String::new(), 0, 0);
1889    }
1890    let tz_str = parts[0]; // e.g. "+0100"
1891    let time_str = parts[1]; // e.g. "1234567890"
1892    let name_email = parts[2]; // e.g. "Name <email>"
1893
1894    let time_secs = time_str.parse::<u64>().unwrap_or(0);
1895
1896    // Parse timezone: +HHMM or -HHMM
1897    let tz_minutes = if tz_str.len() >= 5 {
1898        let sign = if tz_str.starts_with('-') { -1i16 } else { 1 };
1899        let hours = tz_str[1..3].parse::<i16>().unwrap_or(0);
1900        let mins = tz_str[3..5].parse::<i16>().unwrap_or(0);
1901        sign * (hours * 60 + mins)
1902    } else {
1903        0
1904    };
1905
1906    // Split name and email
1907    let (name, email) = if let Some(lt_pos) = name_email.find('<') {
1908        let name = name_email[..lt_pos].trim().to_owned();
1909        let email = if let Some(gt_pos) = name_email.find('>') {
1910            name_email[lt_pos + 1..gt_pos].to_owned()
1911        } else {
1912            name_email[lt_pos + 1..].to_owned()
1913        };
1914        (name, email)
1915    } else {
1916        (name_email.to_owned(), String::new())
1917    };
1918
1919    (name, email, time_secs, tz_minutes)
1920}
1921
1922// ---------------------------------------------------------------------------
1923// Tests
1924// ---------------------------------------------------------------------------
1925
1926#[cfg(test)]
1927mod tests {
1928    use super::*;
1929
1930    #[test]
1931    fn test_varint_roundtrip() {
1932        for val in [0u64, 1, 127, 128, 255, 256, 16383, 16384, u64::MAX] {
1933            let mut buf = Vec::new();
1934            put_varint(val, &mut buf);
1935            let (decoded, end) = get_varint(&buf, 0).unwrap();
1936            assert_eq!(decoded, val, "varint roundtrip failed for {val}");
1937            assert_eq!(end, buf.len());
1938        }
1939    }
1940
1941    #[test]
1942    fn test_crc32() {
1943        // Known test vector: "123456789" => 0xCBF43926
1944        assert_eq!(crc32(b"123456789"), 0xCBF43926);
1945    }
1946
1947    #[test]
1948    fn test_empty_table() {
1949        let writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
1950        let data = writer.finish().unwrap();
1951        let reader = ReftableReader::new(data).unwrap();
1952        let refs = reader.read_refs().unwrap();
1953        assert!(refs.is_empty());
1954    }
1955
1956    #[test]
1957    fn test_write_read_single_ref() {
1958        let oid = ObjectId::from_bytes(&[0xab; 20]).unwrap();
1959        let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
1960        writer
1961            .add_ref(RefRecord {
1962                name: "refs/heads/main".to_owned(),
1963                update_index: 1,
1964                value: RefValue::Val1(oid),
1965            })
1966            .unwrap();
1967        let data = writer.finish().unwrap();
1968
1969        let reader = ReftableReader::new(data).unwrap();
1970        let refs = reader.read_refs().unwrap();
1971        assert_eq!(refs.len(), 1);
1972        assert_eq!(refs[0].name, "refs/heads/main");
1973        assert_eq!(refs[0].value, RefValue::Val1(oid));
1974        assert_eq!(refs[0].update_index, 1);
1975    }
1976
1977    #[test]
1978    fn test_write_read_multiple_refs() {
1979        let oid1 = ObjectId::from_bytes(&[0x11; 20]).unwrap();
1980        let oid2 = ObjectId::from_bytes(&[0x22; 20]).unwrap();
1981        let oid3 = ObjectId::from_bytes(&[0x33; 20]).unwrap();
1982
1983        let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
1984        writer
1985            .add_ref(RefRecord {
1986                name: "refs/heads/a".to_owned(),
1987                update_index: 1,
1988                value: RefValue::Val1(oid1),
1989            })
1990            .unwrap();
1991        writer
1992            .add_ref(RefRecord {
1993                name: "refs/heads/b".to_owned(),
1994                update_index: 1,
1995                value: RefValue::Val1(oid2),
1996            })
1997            .unwrap();
1998        writer
1999            .add_ref(RefRecord {
2000                name: "refs/tags/v1.0".to_owned(),
2001                update_index: 1,
2002                value: RefValue::Val2(oid3, oid1),
2003            })
2004            .unwrap();
2005        let data = writer.finish().unwrap();
2006
2007        let reader = ReftableReader::new(data).unwrap();
2008        let refs = reader.read_refs().unwrap();
2009        assert_eq!(refs.len(), 3);
2010        assert_eq!(refs[0].name, "refs/heads/a");
2011        assert_eq!(refs[1].name, "refs/heads/b");
2012        assert_eq!(refs[2].name, "refs/tags/v1.0");
2013        assert_eq!(refs[2].value, RefValue::Val2(oid3, oid1));
2014    }
2015
2016    #[test]
2017    fn test_symref_roundtrip() {
2018        let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
2019        writer
2020            .add_ref(RefRecord {
2021                name: "refs/heads/sym".to_owned(),
2022                update_index: 1,
2023                value: RefValue::Symref("refs/heads/main".to_owned()),
2024            })
2025            .unwrap();
2026        let data = writer.finish().unwrap();
2027
2028        let reader = ReftableReader::new(data).unwrap();
2029        let refs = reader.read_refs().unwrap();
2030        assert_eq!(refs.len(), 1);
2031        assert_eq!(
2032            refs[0].value,
2033            RefValue::Symref("refs/heads/main".to_owned())
2034        );
2035    }
2036
2037    #[test]
2038    fn test_log_roundtrip() {
2039        let old_oid = ObjectId::from_bytes(&[0; 20]).unwrap();
2040        let new_oid = ObjectId::from_bytes(&[0xaa; 20]).unwrap();
2041
2042        let mut opts = WriteOptions::default();
2043        opts.write_log = true;
2044        let mut writer = ReftableWriter::new(opts, 1, 1);
2045        writer
2046            .add_log(LogRecord {
2047                refname: "refs/heads/main".to_owned(),
2048                update_index: 1,
2049                old_id: old_oid,
2050                new_id: new_oid,
2051                name: "Test User".to_owned(),
2052                email: "test@example.com".to_owned(),
2053                time_seconds: 1700000000,
2054                tz_offset: -480,
2055                message: "initial commit".to_owned(),
2056            })
2057            .unwrap();
2058        let data = writer.finish().unwrap();
2059
2060        let reader = ReftableReader::new(data).unwrap();
2061        let logs = reader.read_logs().unwrap();
2062        assert_eq!(logs.len(), 1);
2063        assert_eq!(logs[0].refname, "refs/heads/main");
2064        assert_eq!(logs[0].old_id, old_oid);
2065        assert_eq!(logs[0].new_id, new_oid);
2066        assert_eq!(logs[0].name, "Test User");
2067        assert_eq!(logs[0].email, "test@example.com");
2068        assert_eq!(logs[0].time_seconds, 1700000000);
2069        assert_eq!(logs[0].tz_offset, -480);
2070        assert_eq!(logs[0].message, "initial commit");
2071    }
2072
2073    #[test]
2074    fn test_unaligned_table() {
2075        let oid = ObjectId::from_bytes(&[0xcc; 20]).unwrap();
2076        let opts = WriteOptions {
2077            block_size: 0, // unaligned
2078            restart_interval: 16,
2079            write_log: false,
2080        };
2081        let mut writer = ReftableWriter::new(opts, 1, 1);
2082        writer
2083            .add_ref(RefRecord {
2084                name: "refs/heads/main".to_owned(),
2085                update_index: 1,
2086                value: RefValue::Val1(oid),
2087            })
2088            .unwrap();
2089        let data = writer.finish().unwrap();
2090
2091        let reader = ReftableReader::new(data).unwrap();
2092        assert_eq!(reader.block_size(), 0);
2093        let refs = reader.read_refs().unwrap();
2094        assert_eq!(refs.len(), 1);
2095        assert_eq!(refs[0].value, RefValue::Val1(oid));
2096    }
2097
2098    #[test]
2099    fn test_parse_identity() {
2100        let (name, email, ts, tz) =
2101            parse_identity_string("Test User <test@example.com> 1700000000 -0800");
2102        assert_eq!(name, "Test User");
2103        assert_eq!(email, "test@example.com");
2104        assert_eq!(ts, 1700000000);
2105        assert_eq!(tz, -480);
2106    }
2107
2108    #[test]
2109    fn test_deletion_record() {
2110        let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
2111        writer
2112            .add_ref(RefRecord {
2113                name: "refs/heads/gone".to_owned(),
2114                update_index: 1,
2115                value: RefValue::Deletion,
2116            })
2117            .unwrap();
2118        let data = writer.finish().unwrap();
2119
2120        let reader = ReftableReader::new(data).unwrap();
2121        let refs = reader.read_refs().unwrap();
2122        assert_eq!(refs.len(), 1);
2123        assert_eq!(refs[0].value, RefValue::Deletion);
2124    }
2125}