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, 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
39// ---------------------------------------------------------------------------
40// Constants
41// ---------------------------------------------------------------------------
42
43/// Magic bytes at the start of every reftable file.
44const REFTABLE_MAGIC: &[u8; 4] = b"REFT";
45
46/// File header size (version 1): magic(4) + version(1) + block_size(3)
47/// + min_update_index(8) + max_update_index(8) = 24 bytes.
48const HEADER_SIZE: usize = 24;
49
50/// Footer size for version 1.
51const FOOTER_V1_SIZE: usize = 68;
52
53/// Block type: ref block.
54const BLOCK_TYPE_REF: u8 = b'r';
55/// Block type: index block.
56const BLOCK_TYPE_INDEX: u8 = b'i';
57/// Block type: log block (zlib-compressed).
58const BLOCK_TYPE_LOG: u8 = b'g';
59
60/// Value types encoded in the low 3 bits of the suffix_length varint.
61const VALUE_DELETION: u8 = 0;
62const VALUE_ONE_OID: u8 = 1;
63const VALUE_TWO_OID: u8 = 2;
64const VALUE_SYMREF: u8 = 3;
65
66/// Hash size (SHA-1).
67const HASH_SIZE: usize = 20;
68
69/// Default block size when none is configured (4 KiB).
70const DEFAULT_BLOCK_SIZE: u32 = 4096;
71
72/// How many records between restart points.
73const RESTART_INTERVAL: usize = 16;
74
75// ---------------------------------------------------------------------------
76// Varint encoding (Git pack-style)
77// ---------------------------------------------------------------------------
78
79/// Encode a u64 as a varint into `out`. Returns number of bytes written.
80fn put_varint(mut val: u64, out: &mut Vec<u8>) -> usize {
81    // First, collect 7-bit groups.
82    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    // Write in reverse, with continuation bits.
94    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
102/// Decode a varint from `data` starting at `pos`. Returns (value, new_pos).
103fn 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// ---------------------------------------------------------------------------
119// Ref record types
120// ---------------------------------------------------------------------------
121
122/// A single reference record as stored in a reftable.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub enum RefValue {
125    /// Deletion tombstone (value_type 0x0).
126    Deletion,
127    /// A direct ref pointing to one OID (value_type 0x1).
128    Val1(ObjectId),
129    /// An annotated tag: value + peeled target (value_type 0x2).
130    Val2(ObjectId, ObjectId),
131    /// A symbolic reference (value_type 0x3).
132    Symref(String),
133}
134
135/// A decoded ref record.
136#[derive(Debug, Clone)]
137pub struct RefRecord {
138    /// Full reference name.
139    pub name: String,
140    /// Update index (absolute).
141    pub update_index: u64,
142    /// The value.
143    pub value: RefValue,
144}
145
146/// A decoded log record.
147#[derive(Debug, Clone)]
148pub struct LogRecord {
149    /// Reference name.
150    pub refname: String,
151    /// Update index.
152    pub update_index: u64,
153    /// Old object ID.
154    pub old_id: ObjectId,
155    /// New object ID.
156    pub new_id: ObjectId,
157    /// Committer name.
158    pub name: String,
159    /// Committer email (without angle brackets).
160    pub email: String,
161    /// Time in seconds since epoch.
162    pub time_seconds: u64,
163    /// Timezone offset in minutes (signed).
164    pub tz_offset: i16,
165    /// Log message.
166    pub message: String,
167}
168
169/// Write options for reftable creation.
170#[derive(Debug, Clone)]
171pub struct WriteOptions {
172    /// Block size in bytes. 0 means unaligned (variable-sized blocks).
173    pub block_size: u32,
174    /// Restart interval (number of records between restart points).
175    pub restart_interval: usize,
176    /// Whether to write log blocks.
177    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
190// ---------------------------------------------------------------------------
191// Writer
192// ---------------------------------------------------------------------------
193
194/// Writes a single reftable file.
195///
196/// Usage:
197/// ```ignore
198/// let mut w = ReftableWriter::new(opts, min_idx, max_idx);
199/// w.add_ref(&RefRecord { .. })?;
200/// w.add_log(&LogRecord { .. })?;
201/// let bytes = w.finish()?;
202/// ```
203pub struct ReftableWriter {
204    opts: WriteOptions,
205    min_update_index: u64,
206    max_update_index: u64,
207
208    // Accumulated ref records (must be added in sorted order).
209    refs: Vec<RefRecord>,
210    // Accumulated log records.
211    logs: Vec<LogRecord>,
212}
213
214impl ReftableWriter {
215    /// Create a new writer.
216    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    /// Add a ref record. Records **must** be added in sorted name order.
227    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    /// Add a log record.
241    pub fn add_log(&mut self, rec: LogRecord) -> Result<()> {
242        self.logs.push(rec);
243        Ok(())
244    }
245
246    /// Finish writing and return the complete reftable file bytes.
247    pub fn finish(mut self) -> Result<Vec<u8>> {
248        let mut out = Vec::new();
249        let block_size = self.opts.block_size;
250
251        // --- Header (24 bytes) ---
252        out.extend_from_slice(REFTABLE_MAGIC);
253        out.push(1); // version
254        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        // --- Ref blocks ---
263        let ref_block_positions = self.write_ref_blocks(&mut out)?;
264
265        // --- Ref index (if ≥ 4 ref blocks) ---
266        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        // --- Log blocks ---
275        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        // --- Footer ---
284        let footer_start = out.len();
285        // Repeat header
286        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        // ref_index_position
295        out.extend_from_slice(&ref_index_position.to_be_bytes());
296        // (obj_position << 5) | obj_id_len — no obj blocks yet
297        out.extend_from_slice(&0u64.to_be_bytes());
298        // obj_index_position
299        out.extend_from_slice(&0u64.to_be_bytes());
300        // log_position
301        out.extend_from_slice(&log_position.to_be_bytes());
302        // log_index_position (we skip log index for simplicity)
303        out.extend_from_slice(&0u64.to_be_bytes());
304
305        // CRC-32 of footer (everything from footer_start to here)
306        let crc = crc32(&out[footer_start..]);
307        out.extend_from_slice(&crc.to_be_bytes());
308
309        Ok(out)
310    }
311
312    /// Write ref blocks, returning (block_start_position, last_refname) per block.
313    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            // We accumulate records into a buffer, then write the block.
328            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                // Check if adding this record would overflow the block.
377                // Block overhead: 4 (block header) + restart table
378                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 // first block includes header
383                } else if block_size > 0 {
384                    block_size
385                } else {
386                    usize::MAX // unaligned
387                };
388                // For first block, block_len includes the 24-byte header
389                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; // Start a new block
397                }
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            // Ensure at least one restart point
422            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            // Compute block_len
431            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            // Write block header: type(1) + block_len(3)
439            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            // Write records
445            out.extend_from_slice(&records_buf);
446
447            // Write restart offsets (3 bytes each)
448            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            // Write restart count (2 bytes)
455            let rc = restart_offsets.len() as u16;
456            out.push((rc >> 8) as u8);
457            out.push((rc & 0xff) as u8);
458
459            // Pad to block alignment if needed
460            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    /// Write a single-level ref index block.
479    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    /// Write log blocks (zlib-compressed).
532    fn write_log_blocks(&mut self, out: &mut Vec<u8>) -> Result<()> {
533        use flate2::write::DeflateEncoder;
534        use flate2::Compression;
535
536        // Sort logs by (refname, reverse update_index)
537        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        // Build the uncompressed log block content
544        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            // Log key: refname \0 reverse_int64(update_index)
552            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                // Offset within the decompressed block (4 byte header + inner.len())
566                restart_offsets.push(4 + inner.len() as u32);
567            }
568
569            // log_type = 1 (standard reflog data)
570            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            // log_data
576            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        // Append restart table
595        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        // block_len is the *inflated* size including the 4-byte block header
605        let block_len = 4 + inner.len();
606
607        // Deflate the inner content
608        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        // Write block header + compressed data
615        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
625// ---------------------------------------------------------------------------
626// Reader
627// ---------------------------------------------------------------------------
628
629/// Reads a single reftable file from a byte buffer.
630pub 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/// Parsed footer fields.
641#[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    /// Open a reftable from bytes.
657    pub fn new(data: Vec<u8>) -> Result<Self> {
658        if data.len() < HEADER_SIZE + FOOTER_V1_SIZE {
659            // Could be an empty table (header + footer only = 24 + 68 = 92)
660            if data.len() < HEADER_SIZE {
661                return Err(Error::InvalidRef("reftable: file too small".into()));
662            }
663        }
664
665        // Parse header
666        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        // Parse footer
680        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    /// Read all ref records from the table.
701    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        // Determine where ref blocks end
711        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        // Skip the header — first ref block starts at offset 24 but shares
721        // the same physical block as the header.
722        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                // Padding — skip to next block boundary
733                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            // Determine the data range for this block
747            let block_data_start = pos + 4; // after type(1) + len(3)
748
749            // The first block's block_len includes the 24-byte header
750            let is_first = pos == HEADER_SIZE;
751            let records_end = if is_first {
752                // block_len is from file start
753                block_len
754            } else {
755                pos + block_len
756            };
757
758            if records_end > ref_end {
759                break;
760            }
761
762            // Read restart count (last 2 bytes before padding)
763            let rc = read_u16(&self.data, records_end - 2);
764            // Restart table is rc * 3 bytes before the restart_count
765            let restart_table_start = records_end - 2 - (rc * 3);
766
767            // Read records from block_data_start to restart_table_start
768            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            // Advance to next block
780            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    /// Look up a single ref by name.
796    pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
797        // Simple: scan all refs. For large files the index would speed this up.
798        let refs = self.read_refs()?;
799        Ok(refs.into_iter().find(|r| r.name == name))
800    }
801
802    /// Read all log records from the table.
803    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            // The inflated size is block_len - 4 (block_len includes the 4-byte header)
829            let inflated_size = block_len - 4;
830
831            // Decompress
832            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            // How many compressed bytes were consumed?
841            let consumed = decoder.total_in() as usize;
842
843            // Parse log records from inflated data
844            // Read restart_count from end
845            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                // Reconstruct key for prefix compression
857                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    /// Get the block size from the header.
873    pub fn block_size(&self) -> u32 {
874        self.block_size
875    }
876
877    /// Get the min update index.
878    pub fn min_update_index(&self) -> u64 {
879        self.min_update_index
880    }
881
882    /// Get the max update index.
883    pub fn max_update_index(&self) -> u64 {
884        self.max_update_index
885    }
886}
887
888// ---------------------------------------------------------------------------
889// Record decoding helpers
890// ---------------------------------------------------------------------------
891
892fn 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    // Reconstruct name
904    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    // Reconstruct key
983    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    // Parse key: refname \0 reverse_int64(update_index)
999    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        // Deletion
1013        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    // log_type == 1: standard log data
1031    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
1094// ---------------------------------------------------------------------------
1095// Stack management
1096// ---------------------------------------------------------------------------
1097
1098/// Manages the `$GIT_DIR/reftable/` directory and `tables.list` stack.
1099///
1100/// The stack provides a merged view of all tables, with later tables
1101/// taking precedence over earlier ones.
1102pub struct ReftableStack {
1103    /// Path to the `reftable/` directory.
1104    reftable_dir: PathBuf,
1105    /// Ordered list of table file names (oldest first).
1106    table_names: Vec<String>,
1107}
1108
1109impl ReftableStack {
1110    /// Open an existing reftable stack.
1111    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    /// Read a merged view of all ref records.
1127    ///
1128    /// Later tables override earlier ones. Deletion records cause the
1129    /// ref to be omitted from the result.
1130    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    /// Look up a single ref across all tables (most recent wins).
1153    pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
1154        // Search tables in reverse (newest first)
1155        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    /// Read merged log records for a specific ref.
1170    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        // Sort by update_index descending (most recent first)
1183        logs.sort_by(|a, b| b.update_index.cmp(&a.update_index));
1184        Ok(logs)
1185    }
1186
1187    /// Replace all log records for one ref and compact the stack.
1188    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    /// Read all log records across all tables.
1249    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    /// Get the current max update index across all tables.
1266    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    /// Add a new reftable to the stack.
1278    ///
1279    /// Writes the table bytes to a new file, then atomically updates
1280    /// `tables.list`.
1281    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            // Simple random from /dev/urandom or time-based fallback
1292            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        // Auto-compact small write bursts into a single table. A plain commit writes several small
1309        // ref/log updates and should settle back to one table; a following tag write remains as a
1310        // second table until explicit `pack-refs`.
1311        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    /// Write a ref update (add/update/delete) as a new reftable.
1441    ///
1442    /// This is the main entry point for updating refs in a reftable repo.
1443    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        // For a single-ref update we need to write all existing refs + the update
1454        // into a proper sorted order, OR we can write a single-record table.
1455        // The stack handles merging, so a single-record table is fine.
1456        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    /// Compact all tables into a single table.
1474    pub fn compact(&mut self) -> Result<()> {
1475        if self.table_names.len() <= 1 {
1476            return Ok(());
1477        }
1478
1479        // Read all refs and logs
1480        let refs = self.read_refs()?;
1481        let logs = self.read_all_logs()?;
1482
1483        // Determine update index range
1484        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        // Write new compacted table
1508        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        // Remove old table files
1515        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    /// Write `tables.list` atomically.
1541    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    /// Return the list of table filenames in this stack.
1579    pub fn table_names(&self) -> &[String] {
1580        &self.table_names
1581    }
1582}
1583
1584// ---------------------------------------------------------------------------
1585// Integration helpers — used by refs.rs and commands
1586// ---------------------------------------------------------------------------
1587
1588/// Detect whether a git directory uses the reftable backend.
1589pub 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    // Linked worktrees typically store the shared repository configuration
1621    // in the common directory pointed to by `commondir`.
1622    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
1640/// Resolve a ref in a reftable repo, following symbolic refs.
1641pub 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    // HEAD is special — stored as a file even in reftable repos
1680    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            // Detached HEAD
1692            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
1713/// Write a ref to a reftable repo.
1714pub 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, // will be set by write_ref
1737            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    // Check config for logAllRefUpdates
1750    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
1757/// Write a symbolic ref to a reftable repo.
1758pub 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
1795/// Delete a ref from a reftable repo.
1796pub 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
1803/// Read the symbolic target of a ref in a reftable repo.
1804pub 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
1828/// List all refs in a reftable repo under a given prefix.
1829pub 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                    // Try to resolve the symref
1842                    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
1854/// Read reflog entries for a ref from the reftable stack.
1855pub 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        // Reconstruct the identity string
1865        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
1884/// Replace the reflog entries for a ref in a reftable repo.
1885pub 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
1902/// Append a reflog entry for a reftable repo.
1903pub 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
1944/// Check whether a reftable repo has reflogs for the given ref.
1945pub 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
1958/// List refs that have reflogs in a reftable repo.
1959pub 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
1998/// Create an empty reflog marker in a reftable repo.
1999pub 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
2006/// Delete all reflog records and empty-log marker for a ref in a reftable repo.
2007pub 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
2016// ---------------------------------------------------------------------------
2017// Write options helpers
2018// ---------------------------------------------------------------------------
2019
2020/// Read reftable write options from the repository config.
2021pub 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
2096/// Check if logAllRefUpdates is enabled.
2097fn 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
2120// ---------------------------------------------------------------------------
2121// Utility functions
2122// ---------------------------------------------------------------------------
2123
2124/// Compute the CRC-32 of a byte slice (ISO 3309 / ITU-T V.42).
2125fn 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
2140/// Compute common prefix length between two byte slices.
2141fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
2142    a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
2143}
2144
2145/// Read a big-endian u24 from 3 bytes at `pos`.
2146fn 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
2150/// Read a big-endian u16 from 2 bytes at `pos`.
2151fn read_u16(data: &[u8], pos: usize) -> usize {
2152    ((data[pos] as usize) << 8) | (data[pos + 1] as usize)
2153}
2154
2155/// Parse the footer of a reftable file.
2156fn 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    // Verify magic
2163    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    // CRC-32 check
2185    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
2206/// Parse an identity string like `"Name <email> 1234567890 +0100"`.
2207fn parse_identity_string(identity: &str) -> (String, String, u64, i16) {
2208    // Format: "Name <email> timestamp tz"
2209    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]; // e.g. "+0100"
2214    let time_str = parts[1]; // e.g. "1234567890"
2215    let name_email = parts[2]; // e.g. "Name <email>"
2216
2217    let time_secs = time_str.parse::<u64>().unwrap_or(0);
2218
2219    // Parse timezone: +HHMM or -HHMM
2220    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    // Split name and email
2230    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// ---------------------------------------------------------------------------
2246// Tests
2247// ---------------------------------------------------------------------------
2248
2249#[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        // Known test vector: "123456789" => 0xCBF43926
2267        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, // unaligned
2401            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}