use crate::config::ConfigSet;
use crate::error::{Error, Result};
use crate::objects::ObjectId;
use crate::refs::store::RefStoreError;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fs;
use std::io::{Read, Write};
use std::ops::ControlFlow;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
const REFTABLE_MAGIC: &[u8; 4] = b"REFT";
const HEADER_SIZE: usize = 24;
const FOOTER_V1_SIZE: usize = 68;
const BLOCK_TYPE_REF: u8 = b'r';
const BLOCK_TYPE_INDEX: u8 = b'i';
const BLOCK_TYPE_LOG: u8 = b'g';
const BLOCK_TYPE_OBJ: u8 = b'o';
const VALUE_DELETION: u8 = 0;
const VALUE_ONE_OID: u8 = 1;
const VALUE_TWO_OID: u8 = 2;
const VALUE_SYMREF: u8 = 3;
const HASH_SIZE: usize = 20;
const DEFAULT_BLOCK_SIZE: u32 = 4096;
const RESTART_INTERVAL: usize = 16;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ReftableError {
#[error("reftable: file is too small ({len} bytes)")]
TooSmall {
len: usize,
},
#[error("reftable: bad magic bytes")]
BadMagic,
#[error("reftable: unsupported format version {version}")]
UnsupportedVersion {
version: u8,
},
#[error("reftable: footer version {footer} does not match header version {header}")]
FooterVersionMismatch {
header: u8,
footer: u8,
},
#[error("reftable: footer checksum mismatch (stored {stored:08x}, computed {computed:08x})")]
FooterChecksumMismatch {
stored: u32,
computed: u32,
},
#[error("reftable: truncated {field}")]
Truncated {
field: ReftableField,
},
#[error("reftable: {field} is not valid UTF-8")]
InvalidUtf8 {
field: ReftableField,
},
#[error("reftable: {field} prefix is longer than the previous key")]
PrefixTooLong {
field: ReftableField,
},
#[error("reftable: unknown ref value type {value_type}")]
UnknownValueType {
value_type: u8,
},
#[error("reftable: invalid block length")]
InvalidBlockLength,
#[error("reftable: invalid restart table")]
InvalidRestartTable,
#[error("reftable: malformed log record key")]
InvalidLogKey,
#[error("reftable: tables.list names missing table {name}")]
MissingTable {
name: String,
},
#[error("reftable: refs must be added in sorted order ('{next}' after '{previous}')")]
UnsortedRefs {
previous: String,
next: String,
},
#[error("reftable: record does not fit in a block")]
EntryTooLarge,
#[error("reftable: record has an empty key")]
EmptyRecordKey,
#[error("reftable: stack is locked by another writer ({})", lock_path.display())]
Locked {
lock_path: PathBuf,
},
#[error("reftable: tables.list lock was already released")]
LockReleased,
#[error(
"reftable: '{refname}' expected {expected} but found {}",
actual.map_or_else(|| "<absent>".to_owned(), |oid| oid.to_hex())
)]
ExpectedMismatch {
refname: String,
expected: ObjectId,
actual: Option<ObjectId>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReftableField {
Varint,
BlockHeader,
IndexKey,
RefName,
ObjectId,
ObjectIdPair,
SymrefTarget,
LogKey,
LogName,
LogEmail,
LogTimezone,
LogMessage,
Footer,
}
impl std::fmt::Display for ReftableField {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Varint => "varint",
Self::BlockHeader => "block header",
Self::IndexKey => "index key",
Self::RefName => "ref name",
Self::ObjectId => "object id",
Self::ObjectIdPair => "object id pair",
Self::SymrefTarget => "symref target",
Self::LogKey => "log key",
Self::LogName => "log name",
Self::LogEmail => "log email",
Self::LogTimezone => "log timezone offset",
Self::LogMessage => "log message",
Self::Footer => "footer",
})
}
}
#[cold]
#[inline(never)]
fn truncated(field: ReftableField) -> Error {
ReftableError::Truncated { field }.into()
}
#[cold]
#[inline(never)]
fn invalid_utf8(field: ReftableField) -> Error {
ReftableError::InvalidUtf8 { field }.into()
}
#[cold]
#[inline(never)]
fn corrupt(err: ReftableError) -> Error {
err.into()
}
fn put_varint(mut val: u64, out: &mut Vec<u8>) -> usize {
let mut buf = [0u8; 10];
let mut i = 0;
buf[i] = (val & 0x7f) as u8;
i += 1;
val >>= 7;
while val > 0 {
val -= 1;
buf[i] = (val & 0x7f) as u8;
i += 1;
val >>= 7;
}
let len = i;
for j in (1..len).rev() {
out.push(buf[j] | 0x80);
}
out.push(buf[0]);
len
}
fn get_varint(data: &[u8], mut pos: usize) -> Result<(u64, usize)> {
if pos >= data.len() {
return Err(truncated(ReftableField::Varint));
}
let mut val = (data[pos] & 0x7f) as u64;
while data[pos] & 0x80 != 0 {
pos += 1;
if pos >= data.len() {
return Err(truncated(ReftableField::Varint));
}
val = ((val + 1) << 7) | (data[pos] & 0x7f) as u64;
}
Ok((val, pos + 1))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RefValue {
Deletion,
Val1(ObjectId),
Val2(ObjectId, ObjectId),
Symref(String),
}
#[derive(Debug, Clone)]
pub struct RefRecord {
pub name: String,
pub update_index: u64,
pub value: RefValue,
}
#[derive(Debug, Clone)]
pub struct LogRecord {
pub refname: String,
pub update_index: u64,
pub old_id: ObjectId,
pub new_id: ObjectId,
pub name: String,
pub email: String,
pub time_seconds: u64,
pub tz_offset: i16,
pub message: String,
}
#[derive(Debug, Clone)]
pub struct WriteOptions {
pub block_size: u32,
pub restart_interval: usize,
pub write_log: bool,
pub skip_index_objects: bool,
pub unpadded: bool,
pub hash_size: usize,
pub auto_compaction_factor: u8,
pub autocompaction_enabled: bool,
}
impl Default for WriteOptions {
fn default() -> Self {
Self {
block_size: DEFAULT_BLOCK_SIZE,
restart_interval: RESTART_INTERVAL,
write_log: true,
skip_index_objects: false,
unpadded: false,
hash_size: HASH_SIZE,
auto_compaction_factor: 2,
autocompaction_enabled: true,
}
}
}
#[derive(Debug, Clone)]
pub struct ReftableTransactionUpdate {
pub refname: String,
pub value: Option<RefValue>,
pub log: Option<LogRecord>,
pub expected_old: Option<ObjectId>,
}
pub struct ReftableWriter {
opts: WriteOptions,
min_update_index: u64,
max_update_index: u64,
refs: Vec<RefRecord>,
logs: Vec<LogRecord>,
}
impl ReftableWriter {
pub fn new(opts: WriteOptions, min_update_index: u64, max_update_index: u64) -> Self {
Self {
opts,
min_update_index,
max_update_index,
refs: Vec::new(),
logs: Vec::new(),
}
}
pub fn add_ref(&mut self, rec: RefRecord) -> Result<()> {
if let Some(last) = self.refs.last() {
if rec.name <= last.name {
return Err(ReftableError::UnsortedRefs {
previous: last.name.clone(),
next: rec.name,
}
.into());
}
}
self.refs.push(rec);
Ok(())
}
pub fn add_log(&mut self, rec: LogRecord) -> Result<()> {
self.logs.push(rec);
Ok(())
}
pub fn finish(self) -> Result<Vec<u8>> {
let refs = self.refs;
let logs = self.logs;
let opts = self.opts;
let mut w = WriterState::new(opts, self.min_update_index, self.max_update_index);
for rec in &refs {
w.add_ref(rec)?;
}
let mut logs = logs;
logs.sort_by(|a, b| {
a.refname
.cmp(&b.refname)
.then_with(|| b.update_index.cmp(&a.update_index))
});
if w.opts.write_log {
for log in &logs {
w.add_log(log)?;
}
}
w.close()
}
}
const REFTABLE_DEFAULT_BLOCK_SIZE: u32 = 4096;
const MAX_RESTARTS: usize = (1 << 16) - 1;
enum EncRecord<'a> {
Ref(&'a RefRecord, u64),
Log(&'a LogRecord),
Obj { prefix: Vec<u8>, offsets: Vec<u64> },
Index { last_key: Vec<u8>, offset: u64 },
}
impl EncRecord<'_> {
fn block_type(&self) -> u8 {
match self {
EncRecord::Ref(..) => BLOCK_TYPE_REF,
EncRecord::Log(_) => BLOCK_TYPE_LOG,
EncRecord::Obj { .. } => BLOCK_TYPE_OBJ,
EncRecord::Index { .. } => BLOCK_TYPE_INDEX,
}
}
fn key(&self) -> Vec<u8> {
match self {
EncRecord::Ref(r, _) => r.name.as_bytes().to_vec(),
EncRecord::Log(l) => {
let mut k = Vec::with_capacity(l.refname.len() + 9);
k.extend_from_slice(l.refname.as_bytes());
k.push(0);
let ts = u64::MAX - l.update_index;
k.extend_from_slice(&ts.to_be_bytes());
k
}
EncRecord::Obj { prefix, .. } => prefix.clone(),
EncRecord::Index { last_key, .. } => last_key.clone(),
}
}
fn val_type(&self) -> u8 {
match self {
EncRecord::Ref(r, _) => match r.value {
RefValue::Deletion => VALUE_DELETION,
RefValue::Val1(_) => VALUE_ONE_OID,
RefValue::Val2(..) => VALUE_TWO_OID,
RefValue::Symref(_) => VALUE_SYMREF,
},
EncRecord::Log(_) => 1,
EncRecord::Obj { offsets, .. } => {
if !offsets.is_empty() && offsets.len() < 8 {
offsets.len() as u8
} else {
0
}
}
EncRecord::Index { .. } => 0,
}
}
fn encode_value(&self, opts: &WriteOptions, out: &mut Vec<u8>) {
match self {
EncRecord::Ref(r, update_index_delta) => {
put_varint(*update_index_delta, out);
match &r.value {
RefValue::Deletion => {}
RefValue::Val1(oid) => out.extend_from_slice(oid.as_bytes()),
RefValue::Val2(oid, peeled) => {
out.extend_from_slice(oid.as_bytes());
out.extend_from_slice(peeled.as_bytes());
}
RefValue::Symref(target) => {
put_varint(target.len() as u64, out);
out.extend_from_slice(target.as_bytes());
}
}
}
EncRecord::Log(l) => {
out.extend_from_slice(l.old_id.as_bytes());
out.extend_from_slice(l.new_id.as_bytes());
put_varint(l.name.len() as u64, out);
out.extend_from_slice(l.name.as_bytes());
put_varint(l.email.len() as u64, out);
out.extend_from_slice(l.email.as_bytes());
put_varint(l.time_seconds, out);
out.extend_from_slice(&l.tz_offset.to_be_bytes());
let msg = clean_log_message(&l.message, opts);
put_varint(msg.len() as u64, out);
out.extend_from_slice(&msg);
}
EncRecord::Obj { offsets, .. } => {
if offsets.is_empty() || offsets.len() >= 8 {
put_varint(offsets.len() as u64, out);
}
if offsets.is_empty() {
return;
}
put_varint(offsets[0], out);
let mut last = offsets[0];
for &o in &offsets[1..] {
put_varint(o - last, out);
last = o;
}
}
EncRecord::Index { offset, .. } => {
put_varint(*offset, out);
}
}
}
}
fn clean_log_message(msg: &str, opts: &WriteOptions) -> Vec<u8> {
let limit = (opts.block_size as usize / 2).max(1);
let msg = if msg.len() > limit {
let mut end = limit;
while end > 0 && !msg.is_char_boundary(end) {
end -= 1;
}
&msg[..end]
} else {
msg
};
let trimmed = msg.trim_end_matches('\n');
let mut out = trimmed.as_bytes().to_vec();
out.push(b'\n');
out
}
fn encode_key(prev: &[u8], key: &[u8], extra: u8, out: &mut Vec<u8>) -> bool {
let prefix_len = common_prefix_len(prev, key);
let suffix_len = key.len() - prefix_len;
put_varint(prefix_len as u64, out);
put_varint(((suffix_len as u64) << 3) | (extra as u64), out);
out.extend_from_slice(&key[prefix_len..]);
prefix_len == 0
}
struct BlockWriter {
typ: u8,
buf: Vec<u8>,
header_off: usize,
block_size: usize,
restart_interval: usize,
restarts: Vec<u32>,
last_key: Vec<u8>,
entries: usize,
}
impl BlockWriter {
fn new(typ: u8, block_size: usize, header_off: usize, restart_interval: usize) -> Self {
let mut buf = vec![0u8; header_off + 4];
buf[header_off] = typ;
Self {
typ,
buf,
header_off,
block_size,
restart_interval,
restarts: Vec::new(),
last_key: Vec::new(),
entries: 0,
}
}
fn next(&self) -> usize {
self.buf.len()
}
fn add(&mut self, rec: &EncRecord, opts: &WriteOptions) -> Result<bool> {
let key = rec.key();
if key.is_empty() {
return Err(ReftableError::EmptyRecordKey.into());
}
let restart = self.entries.is_multiple_of(self.restart_interval);
let prev: &[u8] = if restart { &[] } else { &self.last_key };
let mut encoded = Vec::new();
let is_restart = encode_key(prev, &key, rec.val_type(), &mut encoded);
rec.encode_value(opts, &mut encoded);
let n = encoded.len();
let mut rlen = self.restarts.len();
let mut is_restart = is_restart;
if rlen >= MAX_RESTARTS {
is_restart = false;
}
if is_restart {
rlen += 1;
}
if self.block_size > 0 && 2 + 3 * rlen + n > self.block_size - self.next() {
return Ok(false);
}
if is_restart {
self.restarts.push(self.next() as u32);
}
self.buf.extend_from_slice(&encoded);
self.last_key = key;
self.entries += 1;
Ok(true)
}
fn finish(&mut self) -> Result<usize> {
for &r in &self.restarts {
self.buf.push(((r >> 16) & 0xff) as u8);
self.buf.push(((r >> 8) & 0xff) as u8);
self.buf.push((r & 0xff) as u8);
}
let rc = self.restarts.len() as u16;
self.buf.push((rc >> 8) as u8);
self.buf.push((rc & 0xff) as u8);
let block_len = self.buf.len();
self.buf[self.header_off + 1] = ((block_len >> 16) & 0xff) as u8;
self.buf[self.header_off + 2] = ((block_len >> 8) & 0xff) as u8;
self.buf[self.header_off + 3] = (block_len & 0xff) as u8;
if self.typ == BLOCK_TYPE_LOG {
use flate2::write::ZlibEncoder;
use flate2::Compression;
let skip = 4 + self.header_off;
let mut enc = ZlibEncoder::new(Vec::new(), Compression::new(9));
enc.write_all(&self.buf[skip..])
.map_err(|e| Error::Zlib(e.to_string()))?;
let compressed = enc.finish().map_err(|e| Error::Zlib(e.to_string()))?;
self.buf.truncate(skip);
self.buf.extend_from_slice(&compressed);
}
Ok(self.buf.len())
}
}
#[derive(Default, Clone)]
struct SectionStats {
blocks: usize,
index_blocks: usize,
offset: u64,
index_offset: u64,
}
struct ObjEntry {
hash: Vec<u8>,
offsets: Vec<u64>,
}
struct WriterState {
opts: WriteOptions,
min_update_index: u64,
max_update_index: u64,
out: Vec<u8>,
next: u64,
pending_padding: usize,
block: Option<BlockWriter>,
block_type: u8,
index: Vec<(Vec<u8>, u64)>,
obj_entries: Vec<ObjEntry>,
object_id_len: usize,
ref_stats: SectionStats,
obj_stats: SectionStats,
log_stats: SectionStats,
idx_blocks_total: usize,
}
impl WriterState {
fn new(mut opts: WriteOptions, min: u64, max: u64) -> Self {
if opts.restart_interval == 0 {
opts.restart_interval = RESTART_INTERVAL;
}
if opts.block_size == 0 {
opts.block_size = REFTABLE_DEFAULT_BLOCK_SIZE;
}
Self {
opts,
min_update_index: min,
max_update_index: max,
out: Vec::new(),
next: 0,
pending_padding: 0,
block: None,
block_type: 0,
index: Vec::new(),
obj_entries: Vec::new(),
object_id_len: 0,
ref_stats: SectionStats::default(),
obj_stats: SectionStats::default(),
log_stats: SectionStats::default(),
idx_blocks_total: 0,
}
}
fn header_size(&self) -> usize {
if self.opts.hash_size == 32 {
28
} else {
24
}
}
fn write_header(&self, dest: &mut [u8]) {
dest[0..4].copy_from_slice(REFTABLE_MAGIC);
dest[4] = if self.opts.hash_size == 32 { 2 } else { 1 };
dest[5] = ((self.opts.block_size >> 16) & 0xff) as u8;
dest[6] = ((self.opts.block_size >> 8) & 0xff) as u8;
dest[7] = (self.opts.block_size & 0xff) as u8;
dest[8..16].copy_from_slice(&self.min_update_index.to_be_bytes());
dest[16..24].copy_from_slice(&self.max_update_index.to_be_bytes());
if self.opts.hash_size == 32 {
dest[24..28].copy_from_slice(b"s256");
}
}
fn stats_mut(&mut self, typ: u8) -> &mut SectionStats {
match typ {
BLOCK_TYPE_REF => &mut self.ref_stats,
BLOCK_TYPE_OBJ => &mut self.obj_stats,
BLOCK_TYPE_LOG => &mut self.log_stats,
_ => &mut self.ref_stats,
}
}
fn padded_write(&mut self, data: &[u8], padding: usize) {
if self.pending_padding > 0 {
self.out
.extend(std::iter::repeat_n(0u8, self.pending_padding));
self.pending_padding = 0;
}
self.pending_padding = padding;
self.out.extend_from_slice(data);
}
fn reinit_block(&mut self, typ: u8) {
let header_off = if self.next == 0 {
self.header_size()
} else {
0
};
self.block = Some(BlockWriter::new(
typ,
self.opts.block_size as usize,
header_off,
self.opts.restart_interval,
));
self.block_type = typ;
}
fn add_record(&mut self, rec: &EncRecord) -> Result<()> {
let typ = rec.block_type();
if self.block.is_none() {
self.reinit_block(typ);
}
if self.add_to_block(rec)? {
return Ok(());
}
self.flush_block()?;
self.reinit_block(typ);
if !self.add_to_block(rec)? {
return Err(ReftableError::EntryTooLarge.into());
}
Ok(())
}
fn add_to_block(&mut self, rec: &EncRecord) -> Result<bool> {
match self.block.as_mut() {
Some(bw) => bw.add(rec, &self.opts),
None => Ok(false),
}
}
fn add_ref(&mut self, r: &RefRecord) -> Result<()> {
let delta = r.update_index.saturating_sub(self.min_update_index);
self.add_record(&EncRecord::Ref(r, delta))?;
if !self.opts.skip_index_objects {
match &r.value {
RefValue::Val1(oid) => self.index_hash(oid.as_bytes()),
RefValue::Val2(oid, peeled) => {
self.index_hash(oid.as_bytes());
self.index_hash(peeled.as_bytes());
}
_ => {}
}
}
Ok(())
}
fn add_log(&mut self, l: &LogRecord) -> Result<()> {
if matches!(&self.block, Some(b) if b.typ == BLOCK_TYPE_REF) {
self.finish_public_section()?;
}
self.next -= self.pending_padding as u64;
self.pending_padding = 0;
self.add_record(&EncRecord::Log(l))
}
fn index_hash(&mut self, hash: &[u8]) {
let off = self.next;
match self
.obj_entries
.binary_search_by(|e| e.hash.as_slice().cmp(hash))
{
Ok(idx) => {
let e = &mut self.obj_entries[idx];
if e.offsets.last() != Some(&off) {
e.offsets.push(off);
}
}
Err(idx) => {
self.obj_entries.insert(
idx,
ObjEntry {
hash: hash.to_vec(),
offsets: vec![off],
},
);
}
}
}
fn flush_block(&mut self) -> Result<()> {
let Some(mut bw) = self.block.take() else {
return Ok(());
};
if bw.entries == 0 {
self.block = Some(bw);
return Ok(());
}
let typ = bw.typ;
let raw_bytes = bw.finish()?;
let mut padding = 0;
if !self.opts.unpadded && typ != BLOCK_TYPE_LOG {
padding = (self.opts.block_size as usize).saturating_sub(raw_bytes);
}
let block_typ_off = if self.stats_mut(typ).blocks == 0 {
self.next
} else {
0
};
{
let next = self.next;
let st = self.stats_mut(typ);
if block_typ_off > 0 {
st.offset = next;
}
st.blocks += 1;
}
if self.next == 0 {
let hs = self.header_size();
self.write_header_into_block(&mut bw, hs);
}
let data = bw.buf.clone();
self.padded_write(&data, padding);
self.index.push((bw.last_key.clone(), self.next));
self.next += (padding + raw_bytes) as u64;
self.block = None;
Ok(())
}
fn write_header_into_block(&self, bw: &mut BlockWriter, hs: usize) {
let mut hdr = vec![0u8; hs];
self.write_header(&mut hdr);
bw.buf[..hs].copy_from_slice(&hdr);
}
fn flush_block_if_nonempty(&mut self) -> Result<()> {
if matches!(&self.block, Some(b) if b.entries == 0) {
return Ok(());
}
self.flush_block()
}
fn finish_section(&mut self) -> Result<()> {
let typ = self.block_type;
let threshold = if self.opts.unpadded { 1 } else { 3 };
let before_blocks = self.idx_blocks_total;
self.flush_block_if_nonempty()?;
let mut max_level = 0;
let mut index_start = 0u64;
while self.index.len() > threshold {
max_level += 1;
index_start = self.next;
self.reinit_block(BLOCK_TYPE_INDEX);
let idx = std::mem::take(&mut self.index);
for (last_key, offset) in &idx {
self.add_record(&EncRecord::Index {
last_key: last_key.clone(),
offset: *offset,
})?;
}
let blocks_before = self.count_index_blocks_marker();
self.flush_index_block()?;
let _ = blocks_before;
}
self.index.clear();
let index_blocks = self.idx_blocks_total - before_blocks;
{
let st = self.stats_mut(typ);
st.index_blocks = index_blocks;
st.index_offset = index_start;
}
let _ = max_level;
Ok(())
}
fn count_index_blocks_marker(&self) -> usize {
self.idx_blocks_total
}
fn flush_index_block(&mut self) -> Result<()> {
let Some(mut bw) = self.block.take() else {
return Ok(());
};
if bw.entries == 0 {
self.block = Some(bw);
return Ok(());
}
let raw_bytes = bw.finish()?;
let mut padding = 0;
if !self.opts.unpadded {
padding = (self.opts.block_size as usize).saturating_sub(raw_bytes);
}
if self.next == 0 {
let hs = self.header_size();
self.write_header_into_block(&mut bw, hs);
}
let data = bw.buf.clone();
self.padded_write(&data, padding);
self.index.push((bw.last_key.clone(), self.next));
self.next += (padding + raw_bytes) as u64;
self.idx_blocks_total += 1;
self.block = None;
Ok(())
}
fn dump_object_index(&mut self) -> Result<()> {
let mut max_common = 1usize;
for w in self.obj_entries.windows(2) {
let n = common_prefix_len(&w[0].hash, &w[1].hash);
if n > max_common {
max_common = n;
}
}
self.object_id_len = max_common + 1;
let id_len = self.object_id_len;
self.reinit_block(BLOCK_TYPE_OBJ);
let entries = std::mem::take(&mut self.obj_entries);
for e in &entries {
let prefix = e.hash[..id_len.min(e.hash.len())].to_vec();
self.add_obj_record(prefix, &e.offsets)?;
}
self.obj_entries = entries;
self.finish_section()
}
fn add_obj_record(&mut self, prefix: Vec<u8>, offsets: &[u64]) -> Result<()> {
let typ = BLOCK_TYPE_OBJ;
if self.block.is_none() {
self.reinit_block(typ);
}
let rec = EncRecord::Obj {
prefix: prefix.clone(),
offsets: offsets.to_vec(),
};
if self.add_to_block(&rec)? {
return Ok(());
}
self.flush_block()?;
self.reinit_block(typ);
if self.add_to_block(&rec)? {
return Ok(());
}
let rec = EncRecord::Obj {
prefix,
offsets: Vec::new(),
};
self.add_to_block(&rec)?;
Ok(())
}
fn finish_public_section(&mut self) -> Result<()> {
let Some(bw) = &self.block else {
return Ok(());
};
let typ = bw.typ;
self.finish_section()?;
if typ == BLOCK_TYPE_REF && !self.opts.skip_index_objects && self.ref_stats.index_blocks > 0
{
self.dump_object_index()?;
}
self.obj_entries.clear();
self.block = None;
self.block_type = 0;
Ok(())
}
fn close(mut self) -> Result<Vec<u8>> {
self.finish_public_section()?;
let empty_table = self.next == 0;
self.pending_padding = 0;
if empty_table {
let hs = self.header_size();
let mut header = vec![0u8; hs];
self.write_header(&mut header);
self.padded_write(&header, 0);
}
let mut footer = vec![0u8; self.header_size()];
self.write_header(&mut footer);
footer.extend_from_slice(&self.ref_stats.index_offset.to_be_bytes());
let obj_field = (self.obj_stats.offset << 5) | (self.object_id_len as u64);
footer.extend_from_slice(&obj_field.to_be_bytes());
footer.extend_from_slice(&self.obj_stats.index_offset.to_be_bytes());
footer.extend_from_slice(&self.log_stats.offset.to_be_bytes());
footer.extend_from_slice(&self.log_stats.index_offset.to_be_bytes());
let crc = crc32(&footer);
footer.extend_from_slice(&crc.to_be_bytes());
self.pending_padding = 0;
self.out.extend_from_slice(&footer);
Ok(self.out)
}
}
pub struct ReftableReader {
data: Arc<[u8]>,
version: u8,
block_size: u32,
min_update_index: u64,
max_update_index: u64,
ref_index_position: u64,
log_position: u64,
}
#[derive(Debug)]
struct Footer {
block_size: u32,
min_update_index: u64,
max_update_index: u64,
ref_index_position: u64,
log_position: u64,
}
impl ReftableReader {
pub fn new(data: Vec<u8>) -> Result<Self> {
Self::from_arc(Arc::from(data))
}
pub fn from_arc(data: Arc<[u8]>) -> Result<Self> {
if data.len() < HEADER_SIZE + FOOTER_V1_SIZE {
if data.len() < HEADER_SIZE {
return Err(ReftableError::TooSmall { len: data.len() }.into());
}
}
if &data[0..4] != REFTABLE_MAGIC {
return Err(ReftableError::BadMagic.into());
}
let version = data[4];
if version != 1 && version != 2 {
return Err(ReftableError::UnsupportedVersion { version }.into());
}
let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
if data.len() < footer_size {
return Err(ReftableError::TooSmall { len: data.len() }.into());
}
let footer_start = data.len() - footer_size;
let footer = parse_footer(&data[footer_start..], version)?;
Ok(Self {
data,
version,
block_size: footer.block_size,
min_update_index: footer.min_update_index,
max_update_index: footer.max_update_index,
ref_index_position: footer.ref_index_position,
log_position: footer.log_position,
})
}
fn hash_size(&self) -> usize {
if self.version == 2 {
32
} else {
20
}
}
fn header_len(&self) -> usize {
if self.version == 2 {
28
} else {
HEADER_SIZE
}
}
pub fn read_refs(&self) -> Result<Vec<RefRecord>> {
let mut refs = Vec::new();
self.for_each_ref(|rec| {
refs.push(rec.clone());
ControlFlow::<()>::Continue(())
})?;
Ok(refs)
}
pub fn for_each_ref(&self, f: impl FnMut(&RefRecord) -> ControlFlow<()>) -> Result<()> {
self.for_each_ref_in_range("", None, f)
}
pub fn for_each_ref_in_range(
&self,
lower: &str,
upper_exclusive: Option<&str>,
mut f: impl FnMut(&RefRecord) -> ControlFlow<()>,
) -> Result<()> {
let footer_size = if self.version == 2 {
72
} else {
FOOTER_V1_SIZE
};
let file_end = self.data.len() - footer_size;
let ref_end = if self.ref_index_position > 0 {
self.ref_index_position as usize
} else if self.log_position > 0 {
self.log_position as usize
} else {
file_end
};
let header_len = self.header_len();
let mut pos = if self.ref_index_position > 0 && !lower.is_empty() {
self.locate_ref_block_via_index(lower.as_bytes())?
.unwrap_or(header_len)
} else {
header_len
};
while pos < ref_end {
if pos >= self.data.len() {
break;
}
let block_type = self.data[pos];
if block_type == 0 {
if self.block_size > 0 {
let bs = self.block_size as usize;
pos = ((pos / bs) + 1) * bs;
continue;
}
break;
}
if block_type != BLOCK_TYPE_REF {
break;
}
let block_len = read_u24(&self.data, pos + 1);
let block_data_start = pos + 4;
let is_first = pos == header_len;
let records_end = if is_first { block_len } else { pos + block_len };
if records_end > ref_end {
break;
}
let restart_table_start =
restart_table_start_for_block(&self.data, records_end, block_data_start)?;
let mut rpos = block_data_start;
let mut prev_name = Vec::<u8>::new();
while rpos < restart_table_start {
let (rec, new_pos) = decode_ref_record(
&self.data,
rpos,
&prev_name,
self.min_update_index,
self.hash_size(),
)?;
prev_name = rec.name.as_bytes().to_vec();
if rec.name.as_str() < lower {
rpos = new_pos;
continue;
}
if let Some(upper) = upper_exclusive {
if rec.name.as_str() >= upper {
return Ok(());
}
}
if f(&rec).is_break() {
return Ok(());
}
rpos = new_pos;
}
if self.block_size > 0 {
let bs = self.block_size as usize;
pos = if is_first { bs } else { pos + bs };
} else {
pos = records_end;
}
}
Ok(())
}
pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
if self.ref_index_position > 0 {
if let Some(rec) = self.lookup_ref_via_index(name)? {
return Ok(Some(rec));
}
}
let refs = self.read_refs()?;
Ok(refs.into_iter().find(|r| r.name == name))
}
fn lookup_ref_via_index(&self, name: &str) -> Result<Option<RefRecord>> {
let Some(block_off) = self.locate_ref_block_via_index(name.as_bytes())? else {
return Ok(None);
};
self.find_ref_in_block(block_off, name)
}
fn locate_ref_block_via_index(&self, name: &[u8]) -> Result<Option<usize>> {
let mut pos = self.ref_index_position as usize;
if pos == 0 {
return Ok(None);
}
for _ in 0..64 {
if pos >= self.data.len() {
return Ok(None);
}
match self.data[pos] {
BLOCK_TYPE_REF => return Ok(Some(pos)),
BLOCK_TYPE_INDEX => {
let Some(offset) = self.index_block_seek_offset(pos, name)? else {
return Ok(None);
};
let next = offset as usize;
if next == pos {
return Ok(None);
}
pos = next;
}
_ => return Ok(None),
}
}
Ok(None)
}
fn index_block_seek_offset(&self, block_pos: usize, name: &[u8]) -> Result<Option<u64>> {
let (records_start, records_end) = self.block_record_range(block_pos)?;
let mut rpos = records_start;
let mut prev_key = Vec::<u8>::new();
let mut last: Option<u64> = None;
while rpos < records_end {
let (key, offset, new_pos) = decode_index_record(&self.data, rpos, &prev_key)?;
prev_key = key;
if prev_key.as_slice() >= name {
return Ok(Some(offset));
}
last = Some(offset);
rpos = new_pos;
}
Ok(last)
}
fn find_ref_in_block(&self, block_pos: usize, name: &str) -> Result<Option<RefRecord>> {
let (records_start, records_end) = self.block_record_range(block_pos)?;
let mut rpos = records_start;
let mut prev_name = Vec::<u8>::new();
while rpos < records_end {
let (rec, new_pos) = decode_ref_record(
&self.data,
rpos,
&prev_name,
self.min_update_index,
self.hash_size(),
)?;
prev_name = rec.name.as_bytes().to_vec();
match rec.name.as_str().cmp(name) {
std::cmp::Ordering::Equal => return Ok(Some(rec)),
std::cmp::Ordering::Greater => return Ok(None),
std::cmp::Ordering::Less => {}
}
rpos = new_pos;
}
Ok(None)
}
fn block_record_range(&self, block_pos: usize) -> Result<(usize, usize)> {
if block_pos + 4 > self.data.len() {
return Err(truncated(ReftableField::BlockHeader));
}
let block_len = read_u24(&self.data, block_pos + 1);
let header_len = self.header_len();
let is_first = block_pos == header_len;
let records_end = if is_first {
block_len
} else {
block_pos + block_len
};
if records_end > self.data.len() || records_end < 2 {
return Err(corrupt(ReftableError::InvalidBlockLength));
}
let records_data_start = block_pos + 4;
let restart_table_start =
restart_table_start_for_block(&self.data, records_end, records_data_start)?;
Ok((records_data_start, restart_table_start))
}
pub fn read_logs(&self) -> Result<Vec<LogRecord>> {
let footer_size = if self.version == 2 {
72
} else {
FOOTER_V1_SIZE
};
let file_end = self.data.len() - footer_size;
let mut pos = if self.log_position > 0 {
self.log_position as usize
} else if self.data.len() > self.header_len()
&& self.data[self.header_len()] == BLOCK_TYPE_LOG
{
self.header_len()
} else {
return Ok(Vec::new());
};
let mut logs = Vec::new();
while pos < file_end {
if pos >= self.data.len() {
break;
}
let block_type = self.data[pos];
if block_type != BLOCK_TYPE_LOG {
break;
}
let is_first = pos == self.header_len() && self.log_position == 0;
let block_len = read_u24(&self.data, pos + 1);
let compressed_start = pos + 4;
let header_prefix = if is_first { self.header_len() } else { 0 };
let inflated_size = block_len.saturating_sub(4 + header_prefix);
let remaining = &self.data[compressed_start..file_end];
let (inflated, consumed) = decompress_log_payload(remaining, inflated_size)?;
if inflated.len() < 2 {
break;
}
let records_end = inflated.len();
let restart_table_start = restart_table_start_for_block(&inflated, records_end, 0)?;
let mut rpos = 0usize;
let mut prev_key = Vec::<u8>::new();
while rpos < restart_table_start {
let (log, new_pos) =
decode_log_record(&inflated, rpos, &prev_key, self.hash_size())?;
let mut key = Vec::new();
key.extend_from_slice(log.refname.as_bytes());
key.push(0);
key.extend_from_slice(&(0xffffffffffffffffu64 - log.update_index).to_be_bytes());
prev_key = key;
logs.push(log);
rpos = new_pos;
}
pos = compressed_start + consumed;
}
Ok(logs)
}
pub fn block_size(&self) -> u32 {
self.block_size
}
pub fn min_update_index(&self) -> u64 {
self.min_update_index
}
pub fn max_update_index(&self) -> u64 {
self.max_update_index
}
pub fn ref_index_offset(&self) -> u64 {
self.ref_index_position
}
pub fn payload_size(&self) -> u64 {
let overhead = if self.version == 2 { 28 } else { 24 };
self.data.len().saturating_sub(overhead) as u64
}
}
fn decode_index_record(data: &[u8], pos: usize, prev_key: &[u8]) -> Result<(Vec<u8>, u64, usize)> {
let (prefix_len, p) = get_varint(data, pos)?;
let (suffix_and_type, mut p) = get_varint(data, p)?;
let suffix_len = (suffix_and_type >> 3) as usize;
let mut key = Vec::with_capacity(prefix_len as usize + suffix_len);
if prefix_len > 0 {
if (prefix_len as usize) > prev_key.len() {
return Err(corrupt(ReftableError::PrefixTooLong {
field: ReftableField::IndexKey,
}));
}
key.extend_from_slice(&prev_key[..prefix_len as usize]);
}
if p + suffix_len > data.len() {
return Err(truncated(ReftableField::IndexKey));
}
key.extend_from_slice(&data[p..p + suffix_len]);
p += suffix_len;
let (offset, p) = get_varint(data, p)?;
Ok((key, offset, p))
}
fn decode_ref_record(
data: &[u8],
pos: usize,
prev_name: &[u8],
min_update_index: u64,
hash_size: usize,
) -> Result<(RefRecord, usize)> {
let (prefix_len, p) = get_varint(data, pos)?;
let (suffix_and_type, mut p) = get_varint(data, p)?;
let suffix_len = (suffix_and_type >> 3) as usize;
let value_type = (suffix_and_type & 0x7) as u8;
let mut name = Vec::with_capacity(prefix_len as usize + suffix_len);
if prefix_len > 0 {
if (prefix_len as usize) > prev_name.len() {
return Err(corrupt(ReftableError::PrefixTooLong {
field: ReftableField::RefName,
}));
}
name.extend_from_slice(&prev_name[..prefix_len as usize]);
}
if p + suffix_len > data.len() {
return Err(truncated(ReftableField::RefName));
}
name.extend_from_slice(&data[p..p + suffix_len]);
p += suffix_len;
let name_str = String::from_utf8(name).map_err(|_| invalid_utf8(ReftableField::RefName))?;
let (update_index_delta, mut p) = get_varint(data, p)?;
let update_index = min_update_index + update_index_delta;
let value = match value_type {
VALUE_DELETION => RefValue::Deletion,
VALUE_ONE_OID => {
if p + hash_size > data.len() {
return Err(truncated(ReftableField::ObjectId));
}
let oid = ObjectId::from_bytes(&data[p..p + hash_size])?;
p += hash_size;
RefValue::Val1(oid)
}
VALUE_TWO_OID => {
if p + 2 * hash_size > data.len() {
return Err(truncated(ReftableField::ObjectIdPair));
}
let oid = ObjectId::from_bytes(&data[p..p + hash_size])?;
p += hash_size;
let peeled = ObjectId::from_bytes(&data[p..p + hash_size])?;
p += hash_size;
RefValue::Val2(oid, peeled)
}
VALUE_SYMREF => {
let (target_len, p2) = get_varint(data, p)?;
p = p2;
let target_len = target_len as usize;
if p + target_len > data.len() {
return Err(truncated(ReftableField::SymrefTarget));
}
let target = String::from_utf8(data[p..p + target_len].to_vec())
.map_err(|_| invalid_utf8(ReftableField::SymrefTarget))?;
p += target_len;
RefValue::Symref(target)
}
_ => {
return Err(corrupt(ReftableError::UnknownValueType { value_type }));
}
};
Ok((
RefRecord {
name: name_str,
update_index,
value,
},
p,
))
}
fn decode_log_record(
data: &[u8],
pos: usize,
prev_key: &[u8],
hash_size: usize,
) -> Result<(LogRecord, usize)> {
let (prefix_len, p) = get_varint(data, pos)?;
let (suffix_and_type, mut p) = get_varint(data, p)?;
let suffix_len = (suffix_and_type >> 3) as usize;
let log_type = (suffix_and_type & 0x7) as u8;
let mut key = Vec::with_capacity(prefix_len as usize + suffix_len);
if prefix_len > 0 {
if (prefix_len as usize) > prev_key.len() {
return Err(corrupt(ReftableError::PrefixTooLong {
field: ReftableField::LogKey,
}));
}
key.extend_from_slice(&prev_key[..prefix_len as usize]);
}
if p + suffix_len > data.len() {
return Err(truncated(ReftableField::LogKey));
}
key.extend_from_slice(&data[p..p + suffix_len]);
p += suffix_len;
let null_pos = key
.iter()
.position(|&b| b == 0)
.ok_or_else(|| corrupt(ReftableError::InvalidLogKey))?;
let refname = String::from_utf8(key[..null_pos].to_vec())
.map_err(|_| invalid_utf8(ReftableField::RefName))?;
if null_pos + 9 > key.len() {
return Err(corrupt(ReftableError::InvalidLogKey));
}
let reversed_idx = u64::from_be_bytes(
key[null_pos + 1..null_pos + 9]
.try_into()
.map_err(|_| corrupt(ReftableError::InvalidLogKey))?,
);
let update_index = 0xffffffffffffffffu64 - reversed_idx;
if log_type == 0 {
let zero_oid = ObjectId::from_bytes(&vec![0u8; hash_size])?;
return Ok((
LogRecord {
refname,
update_index,
old_id: zero_oid,
new_id: zero_oid,
name: String::new(),
email: String::new(),
time_seconds: 0,
tz_offset: 0,
message: String::new(),
},
p,
));
}
if p + 2 * hash_size > data.len() {
return Err(truncated(ReftableField::ObjectIdPair));
}
let old_id = ObjectId::from_bytes(&data[p..p + hash_size])?;
p += hash_size;
let new_id = ObjectId::from_bytes(&data[p..p + hash_size])?;
p += hash_size;
let (name_len, p2) = get_varint(data, p)?;
p = p2;
let name_len = name_len as usize;
if p + name_len > data.len() {
return Err(truncated(ReftableField::LogName));
}
let name = String::from_utf8(data[p..p + name_len].to_vec())
.map_err(|_| invalid_utf8(ReftableField::LogName))?;
p += name_len;
let (email_len, p2) = get_varint(data, p)?;
p = p2;
let email_len = email_len as usize;
if p + email_len > data.len() {
return Err(truncated(ReftableField::LogEmail));
}
let email = String::from_utf8(data[p..p + email_len].to_vec())
.map_err(|_| invalid_utf8(ReftableField::LogEmail))?;
p += email_len;
let (time_seconds, p2) = get_varint(data, p)?;
p = p2;
if p + 2 > data.len() {
return Err(truncated(ReftableField::LogTimezone));
}
let tz_offset = i16::from_be_bytes([data[p], data[p + 1]]);
p += 2;
let (msg_len, p2) = get_varint(data, p)?;
p = p2;
let msg_len = msg_len as usize;
if p + msg_len > data.len() {
return Err(truncated(ReftableField::LogMessage));
}
let message = String::from_utf8(data[p..p + msg_len].to_vec())
.map_err(|_| invalid_utf8(ReftableField::LogMessage))?;
p += msg_len;
Ok((
LogRecord {
refname,
update_index,
old_id,
new_id,
name,
email,
time_seconds,
tz_offset,
message,
},
p,
))
}
pub(crate) fn widen_oid_to(oid: ObjectId, hash_size: usize) -> ObjectId {
if oid.is_zero() && oid.as_bytes().len() != hash_size {
ObjectId::from_bytes(&vec![0u8; hash_size]).unwrap_or(oid)
} else {
oid
}
}
fn reftable_hash_size_for_git_dir(git_dir: &Path) -> usize {
let cfg = crate::config::ConfigSet::load(
&crate::environment::Environment::empty(),
Some(git_dir),
true,
)
.unwrap_or_default();
match cfg
.get("extensions.objectformat")
.and_then(|v| crate::objects::HashAlgo::from_name(&v))
{
Some(crate::objects::HashAlgo::Sha256) => 32,
_ => 20,
}
}
fn prefix_upper_exclusive(prefix: &str) -> Option<String> {
if prefix.is_empty() || prefix.ends_with('/') {
return None;
}
let mut end = prefix.as_bytes().to_vec();
while let Some(last) = end.last_mut() {
if *last == b'/' {
break;
}
if *last < 0xff {
*last += 1;
return String::from_utf8(end).ok();
}
*last = 0;
end.pop();
}
None
}
fn suggest_compaction_segment(sizes: &[u64]) -> (usize, usize) {
let n = sizes.len();
if n <= 1 {
return (0, 0);
}
let mut seg_end = 0usize;
let mut bytes = 0u64;
let mut i = n - 1;
while i > 0 {
if sizes[i - 1] < sizes[i].saturating_mul(2) {
seg_end = i + 1;
bytes = sizes[i];
break;
}
i -= 1;
}
if seg_end == 0 {
return (0, 0);
}
let mut seg_start = 0usize;
i = seg_end.saturating_sub(1);
while i > 0 {
let curr = bytes;
bytes = bytes.saturating_add(sizes[i - 1]);
if sizes[i - 1] < curr.saturating_mul(2) {
seg_start = i - 1;
}
i -= 1;
}
if seg_end <= seg_start + 1 {
return (0, 0);
}
(seg_start, seg_end)
}
#[derive(Clone, PartialEq, Eq, Hash)]
struct StackCacheKey {
reftable_dir: PathBuf,
tables_list_len: u64,
tables_list_modified: i64,
}
struct CachedStackTables {
table_names: Vec<String>,
readers: Vec<ReftableReader>,
}
static STACK_TABLE_CACHE: OnceLock<Mutex<HashMap<StackCacheKey, Arc<CachedStackTables>>>> =
OnceLock::new();
fn stack_table_cache() -> &'static Mutex<HashMap<StackCacheKey, Arc<CachedStackTables>>> {
STACK_TABLE_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
fn invalidate_stack_table_cache(reftable_dir: &Path) {
let Ok(mut guard) = stack_table_cache().lock() else {
return;
};
guard.retain(|k, _| k.reftable_dir != reftable_dir);
}
fn tables_list_cache_key(reftable_dir: &Path) -> Result<StackCacheKey> {
let tables_list = reftable_dir.join("tables.list");
match fs::metadata(&tables_list) {
Ok(meta) => Ok(StackCacheKey {
reftable_dir: reftable_dir.to_path_buf(),
tables_list_len: meta.len(),
tables_list_modified: meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_secs() as i64)
.unwrap_or(0),
}),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(StackCacheKey {
reftable_dir: reftable_dir.to_path_buf(),
tables_list_len: 0,
tables_list_modified: 0,
}),
Err(e) => Err(Error::Io(e)),
}
}
fn load_cached_stack_tables(reftable_dir: &Path) -> Result<Arc<CachedStackTables>> {
let key = tables_list_cache_key(reftable_dir)?;
if let Ok(guard) = stack_table_cache().lock() {
if let Some(hit) = guard.get(&key) {
return Ok(Arc::clone(hit));
}
}
let tables_list = reftable_dir.join("tables.list");
let table_names: Vec<String> = match fs::read_to_string(&tables_list) {
Ok(content) => content
.lines()
.filter(|l| !l.is_empty())
.map(|l| l.to_owned())
.collect(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
Err(e) => return Err(Error::Io(e)),
};
let mut readers = Vec::with_capacity(table_names.len());
for name in &table_names {
let path = reftable_dir.join(name);
let data = fs::read(&path).map_err(Error::Io)?;
readers.push(ReftableReader::from_arc(Arc::from(data))?);
}
let cached = Arc::new(CachedStackTables {
table_names,
readers,
});
if let Ok(mut guard) = stack_table_cache().lock() {
guard.insert(key, Arc::clone(&cached));
}
Ok(cached)
}
fn cached_reader_for_name<'a>(
cached: &'a CachedStackTables,
table_name: &str,
) -> Result<&'a ReftableReader> {
let idx = cached
.table_names
.iter()
.position(|n| n == table_name)
.ok_or_else(|| ReftableError::MissingTable {
name: table_name.to_owned(),
})?;
Ok(&cached.readers[idx])
}
pub struct ReftableStack {
reftable_dir: PathBuf,
table_names: Vec<String>,
fixed_table_names: bool,
}
fn parse_tables_list_content(content: &str) -> Vec<String> {
content
.lines()
.filter(|l| !l.is_empty())
.map(|l| l.to_owned())
.collect()
}
fn read_tables_list_names(reftable_dir: &Path) -> Result<Vec<String>> {
let tables_list = reftable_dir.join("tables.list");
match fs::read_to_string(&tables_list) {
Ok(content) => Ok(parse_tables_list_content(&content)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Vec::new()),
Err(e) => Err(Error::Io(e)),
}
}
pub(crate) struct TablesListLock {
lock: Option<crate::lockfile::LockFile>,
}
impl TablesListLock {
fn new(lock: crate::lockfile::LockFile) -> Self {
Self { lock: Some(lock) }
}
fn path(&self) -> Result<&Path> {
self.lock
.as_ref()
.map(|lock| lock.lock_path())
.ok_or_else(|| ReftableError::LockReleased.into())
}
}
impl Drop for TablesListLock {
fn drop(&mut self) {
drop(self.lock.take());
}
}
impl ReftableStack {
fn hash_size(&self) -> usize {
match self.reftable_dir.parent() {
Some(git_dir) => reftable_hash_size_for_git_dir(git_dir),
None => 20,
}
}
pub fn open(git_dir: &Path) -> Result<Self> {
let reftable_dir = git_dir.join("reftable");
fs::create_dir_all(&reftable_dir).map_err(Error::Io)?;
let tables_list = reftable_dir.join("tables.list");
let table_names = match fs::read_to_string(&tables_list) {
Ok(content) => parse_tables_list_content(&content),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Vec::new(),
Err(e) => return Err(Error::Io(e)),
};
Ok(Self {
reftable_dir,
table_names,
fixed_table_names: false,
})
}
fn subset_stack(reftable_dir: PathBuf, table_names: Vec<String>) -> Self {
Self {
reftable_dir,
table_names,
fixed_table_names: true,
}
}
fn table_names_for_read(&self) -> Result<Vec<String>> {
if self.fixed_table_names {
Ok(self.table_names.clone())
} else {
read_tables_list_names(&self.reftable_dir)
}
}
fn first_ref_name_with_prefix(&self, prefix: &str) -> Result<Option<String>> {
let cached = load_cached_stack_tables(&self.reftable_dir)?;
let names = self.table_names_for_read()?;
let readers = self.stack_readers(&cached, &names)?;
let upper = prefix_upper_exclusive(prefix.trim_end_matches('/'));
let mut merged: BTreeMap<String, RefRecord> = BTreeMap::new();
for reader in readers {
reader.for_each_ref_in_range(prefix, upper.as_deref(), |rec| {
if !rec.name.starts_with(prefix) || rec.name.len() <= prefix.len() {
return ControlFlow::<()>::Continue(());
}
match &rec.value {
RefValue::Deletion => {
merged.remove(&rec.name);
}
_ => {
merged.insert(rec.name.clone(), rec.clone());
}
}
ControlFlow::<()>::Continue(())
})?;
}
Ok(merged.keys().next().cloned())
}
fn inject_head_ref(&self, refs: &mut Vec<RefRecord>, min_idx: u64) -> Option<LogRecord> {
let git_dir = self.reftable_dir.parent()?;
let head_path = git_dir.join("HEAD");
let content = fs::read_to_string(&head_path).ok()?;
let target = content.strip_prefix("ref: ")?.trim();
if target.is_empty() || target == "refs/heads/.invalid" {
return None;
}
if refs.iter().any(|r| r.name == "HEAD") {
return None;
}
refs.push(RefRecord {
name: "HEAD".to_owned(),
update_index: min_idx,
value: RefValue::Symref(target.to_owned()),
});
refs.sort_by(|a, b| a.name.cmp(&b.name));
if self
.read_logs_for_ref("HEAD")
.map(|logs| !logs.is_empty())
.unwrap_or(false)
{
return None;
}
let target_logs = self.read_logs_for_ref(target).ok()?;
let newest = target_logs.into_iter().next()?;
Some(LogRecord {
refname: "HEAD".to_owned(),
update_index: newest.update_index,
old_id: newest.old_id,
new_id: newest.new_id,
name: newest.name,
email: newest.email,
time_seconds: newest.time_seconds,
tz_offset: newest.tz_offset,
message: newest.message,
})
}
fn write_options(&self) -> WriteOptions {
let git_dir = self
.reftable_dir
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| self.reftable_dir.clone());
read_write_options(&git_dir)
}
fn stack_readers<'a>(
&self,
cached: &'a CachedStackTables,
table_names: &[String],
) -> Result<Vec<&'a ReftableReader>> {
table_names
.iter()
.map(|name| cached_reader_for_name(cached, name))
.collect()
}
pub fn read_refs(&self) -> Result<Vec<RefRecord>> {
let cached = load_cached_stack_tables(&self.reftable_dir)?;
let names = self.table_names_for_read()?;
let readers = self.stack_readers(&cached, &names)?;
let mut merged: BTreeMap<String, RefRecord> = BTreeMap::new();
for reader in readers {
reader.for_each_ref(|rec| {
match &rec.value {
RefValue::Deletion => {
merged.remove(&rec.name);
}
_ => {
merged.insert(rec.name.clone(), rec.clone());
}
}
ControlFlow::<()>::Continue(())
})?;
}
Ok(merged.into_values().collect())
}
pub fn list_refs_under_prefix(
&self,
git_dir: &Path,
prefix: &str,
) -> Result<Vec<(String, ObjectId)>> {
let cached = load_cached_stack_tables(&self.reftable_dir)?;
let names = self.table_names_for_read()?;
let readers = self.stack_readers(&cached, &names)?;
let lower = prefix;
let upper = prefix_upper_exclusive(prefix);
let mut out = Vec::new();
if readers.len() == 1 {
let reader = readers[0];
reader.for_each_ref_in_range(lower, upper.as_deref(), |rec| {
if prefix.ends_with('/') && !rec.name.starts_with(prefix) {
return ControlFlow::<()>::Continue(());
}
push_list_ref_entry(git_dir, rec, &mut out);
ControlFlow::<()>::Continue(())
})?;
out.sort_by(|a, b| a.0.cmp(&b.0));
return Ok(out);
}
let mut merged: BTreeMap<String, RefRecord> = BTreeMap::new();
for reader in readers {
reader.for_each_ref_in_range(lower, upper.as_deref(), |rec| {
if prefix.ends_with('/') && !rec.name.starts_with(prefix) {
return ControlFlow::<()>::Continue(());
}
match &rec.value {
RefValue::Deletion => {
merged.remove(&rec.name);
}
_ => {
merged.insert(rec.name.clone(), rec.clone());
}
}
ControlFlow::<()>::Continue(())
})?;
}
for rec in merged.into_values() {
push_list_ref_entry(git_dir, &rec, &mut out);
}
out.sort_by(|a, b| a.0.cmp(&b.0));
Ok(out)
}
fn table_payload_sizes(&self) -> Result<Vec<u64>> {
let cached = load_cached_stack_tables(&self.reftable_dir)?;
let names = self.table_names_for_read()?;
Ok(self
.stack_readers(&cached, &names)?
.into_iter()
.map(ReftableReader::payload_size)
.collect())
}
fn auto_compact_geometric(&mut self, opts: &WriteOptions) -> Result<()> {
if !opts.autocompaction_enabled {
return Ok(());
}
self.reload_table_names();
if self.table_names.len() <= 2 {
return Ok(());
}
let sizes = self.table_payload_sizes()?;
let (start, end) = suggest_compaction_segment(&sizes);
if end <= start + 1 {
return Ok(());
}
self.compact_range(start, end - 1, opts)
}
fn compact_range(&mut self, first: usize, last: usize, opts: &WriteOptions) -> Result<()> {
let mut guard = self.acquire_tables_list_lock()?;
self.reload_table_names();
if first >= self.table_names.len() || last >= self.table_names.len() || first > last {
return Ok(());
}
let compact_names: Vec<String> = self.table_names[first..=last].to_vec();
let prefix_stack = Self::subset_stack(self.reftable_dir.clone(), compact_names.clone());
let refs = prefix_stack.read_refs()?;
let logs = prefix_stack.read_all_logs()?;
let mut min_idx = u64::MAX;
let mut max_idx = 0u64;
for name in &compact_names {
let path = self.reftable_dir.join(name);
let data = fs::read(&path).map_err(Error::Io)?;
let reader = ReftableReader::new(data)?;
min_idx = min_idx.min(reader.min_update_index());
max_idx = max_idx.max(reader.max_update_index());
}
if min_idx == u64::MAX {
min_idx = 0;
}
let wopts = WriteOptions {
hash_size: self.hash_size(),
..opts.clone()
};
let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
for rec in refs {
writer.add_ref(rec)?;
}
for log in logs {
writer.add_log(log)?;
}
let data = writer.finish()?;
let filename = self.write_table_file(&data, max_idx)?;
let mut new_names: Vec<String> = self.table_names[..first].to_vec();
new_names.push(filename.clone());
new_names.extend_from_slice(&self.table_names[last + 1..]);
self.table_names = new_names;
self.write_tables_list_locked(&mut guard)?;
invalidate_stack_table_cache(&self.reftable_dir);
for old in &compact_names {
if old == &filename {
continue;
}
let _ = fs::remove_file(self.reftable_dir.join(old));
}
Ok(())
}
pub fn lookup_ref_oid(&self, name: &str) -> Result<Option<ObjectId>> {
Ok(self.lookup_ref(name)?.and_then(|rec| match rec.value {
RefValue::Val1(oid) => Some(oid),
RefValue::Val2(oid, _) => Some(oid),
RefValue::Deletion | RefValue::Symref(_) => None,
}))
}
pub fn lookup_ref(&self, name: &str) -> Result<Option<RefRecord>> {
let cached = load_cached_stack_tables(&self.reftable_dir)?;
let names = self.table_names_for_read()?;
for reader in self.stack_readers(&cached, &names)?.into_iter().rev() {
if let Some(rec) = reader.lookup_ref(name)? {
return match rec.value {
RefValue::Deletion => Ok(None),
_ => Ok(Some(rec)),
};
}
}
Ok(None)
}
pub fn read_logs_for_ref(&self, refname: &str) -> Result<Vec<LogRecord>> {
let mut logs = Vec::new();
for table_name in &self.table_names {
let path = self.reftable_dir.join(table_name);
let data = fs::read(&path).map_err(Error::Io)?;
let reader = ReftableReader::new(data)?;
for log in reader.read_logs()? {
if log.refname == refname {
logs.push(log);
}
}
}
logs.sort_by_key(|a| std::cmp::Reverse(a.update_index));
Ok(logs)
}
pub fn replace_logs_for_ref(
&mut self,
refname: &str,
entries: &[crate::reflog::ReflogEntry],
) -> Result<()> {
let refs = self.read_refs()?;
let mut logs: Vec<LogRecord> = self
.read_all_logs()?
.into_iter()
.filter(|log| log.refname != refname)
.collect();
let mut next_update_index = self.max_update_index()? + 1;
let hash_size = self.hash_size();
for entry in entries {
let (name, email, time_secs, tz) = parse_identity_string(&entry.identity);
logs.push(LogRecord {
refname: refname.to_owned(),
update_index: next_update_index,
old_id: widen_oid_to(entry.old_oid, hash_size),
new_id: widen_oid_to(entry.new_oid, hash_size),
name,
email,
time_seconds: time_secs,
tz_offset: tz,
message: entry.message.clone(),
});
next_update_index += 1;
}
let mut min_idx = u64::MAX;
let mut max_idx = 0u64;
for name in &self.table_names {
let path = self.reftable_dir.join(name);
let data = fs::read(&path).map_err(Error::Io)?;
let reader = ReftableReader::new(data)?;
min_idx = min_idx.min(reader.min_update_index());
max_idx = max_idx.max(reader.max_update_index());
}
if min_idx == u64::MAX {
min_idx = 0;
}
max_idx = max_idx.max(next_update_index.saturating_sub(1));
let wopts = WriteOptions {
hash_size: self.hash_size(),
..Default::default()
};
let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
for rec in refs {
writer.add_ref(rec)?;
}
for log in logs {
writer.add_log(log)?;
}
let data = writer.finish()?;
let old_names = self.table_names.clone();
let name = self.write_table_file(&data, max_idx)?;
self.table_names = vec![name];
self.write_tables_list()?;
for old in &old_names {
let _ = fs::remove_file(self.reftable_dir.join(old));
}
Ok(())
}
pub fn read_all_logs(&self) -> Result<Vec<LogRecord>> {
let mut logs = Vec::new();
for table_name in &self.table_names {
let path = self.reftable_dir.join(table_name);
let data = fs::read(&path).map_err(Error::Io)?;
let reader = ReftableReader::new(data)?;
logs.extend(reader.read_logs()?);
}
logs.sort_by(|a, b| {
a.refname
.cmp(&b.refname)
.then_with(|| b.update_index.cmp(&a.update_index))
});
Ok(logs)
}
pub fn max_update_index(&self) -> Result<u64> {
let names: Vec<String> = match fs::read_to_string(self.reftable_dir.join("tables.list")) {
Ok(content) => content
.lines()
.filter(|line| !line.is_empty())
.map(ToOwned::to_owned)
.collect(),
Err(_) => self.table_names.clone(),
};
let mut max_idx = 0u64;
for name in &names {
let path = self.reftable_dir.join(name);
let data = match fs::read(&path) {
Ok(data) => data,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
Err(err) => return Err(Error::Io(err)),
};
let reader = ReftableReader::new(data)?;
max_idx = max_idx.max(reader.max_update_index());
}
Ok(max_idx)
}
pub fn add_table(
&mut self,
data: &[u8],
update_index: u64,
opts: &WriteOptions,
) -> Result<String> {
let table_has_deletion = ReftableReader::new(data.to_vec())
.and_then(|reader| reader.read_refs())
.map(|records| {
records
.iter()
.any(|record| matches!(record.value, RefValue::Deletion))
})
.unwrap_or(false);
let random: u64 = {
let mut buf = [0u8; 8];
if let Ok(mut f) = fs::File::open("/dev/urandom") {
let _ = f.read(&mut buf);
}
u64::from_le_bytes(buf)
};
let filename = format!(
"{:08x}-{:08x}-{:08x}.ref",
update_index, update_index, random as u32
);
let path = self.reftable_dir.join(&filename);
fs::write(&path, data).map_err(Error::Io)?;
{
let mut guard = self.acquire_tables_list_lock()?;
self.reload_table_names();
self.table_names.push(filename.clone());
self.write_tables_list_locked(&mut guard)?;
}
if table_has_deletion && self.table_names.len() > 2 {
self.compact_prefix_preserving_newest(opts)?;
} else if opts.autocompaction_enabled {
if self
.table_names
.iter()
.any(|name| self.table_is_locked(name))
{
self.compact_unlocked_suffix()?;
} else {
self.auto_compact_geometric(opts)?;
}
}
Ok(filename)
}
fn compact_prefix_preserving_newest(&mut self, opts: &WriteOptions) -> Result<()> {
if !opts.autocompaction_enabled {
return Ok(());
}
let mut guard = self.acquire_tables_list_lock()?;
self.reload_table_names();
if self.table_names.len() <= 2 {
return Ok(());
}
let Some((newest, old_names)) = self.table_names.split_last() else {
return Ok(());
};
let (newest, old_names) = (newest.clone(), old_names.to_vec());
let prefix_stack = Self::subset_stack(self.reftable_dir.clone(), old_names.clone());
let refs = prefix_stack.read_refs()?;
let logs = prefix_stack.read_all_logs()?;
let mut min_idx = u64::MAX;
let mut max_idx = 0u64;
for name in &old_names {
let path = self.reftable_dir.join(name);
let data = fs::read(&path).map_err(Error::Io)?;
let reader = ReftableReader::new(data)?;
min_idx = min_idx.min(reader.min_update_index());
max_idx = max_idx.max(reader.max_update_index());
}
if min_idx == u64::MAX {
min_idx = 0;
}
let wopts = WriteOptions {
hash_size: self.hash_size(),
..Default::default()
};
let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
for rec in refs {
writer.add_ref(rec)?;
}
for log in logs {
writer.add_log(log)?;
}
let data = writer.finish()?;
let filename = self.write_table_file(&data, max_idx)?;
let keep: Vec<String> = vec![filename.clone(), newest.clone()];
self.table_names = keep;
self.write_tables_list_locked(&mut guard)?;
for old in &old_names {
if old == &filename || old == &newest {
continue;
}
let _ = fs::remove_file(self.reftable_dir.join(old));
}
Ok(())
}
fn table_is_locked(&self, name: &str) -> bool {
self.reftable_dir.join(format!("{name}.lock")).exists()
}
fn compact_unlocked_suffix(&mut self) -> Result<()> {
let mut guard = self.acquire_tables_list_lock()?;
self.reload_table_names();
let first_unlocked = self
.table_names
.iter()
.position(|name| !self.table_is_locked(name))
.unwrap_or(self.table_names.len());
if self.table_names.len().saturating_sub(first_unlocked) <= 1 {
return Ok(());
}
let locked_prefix: Vec<String> = self.table_names[..first_unlocked].to_vec();
let old_suffix: Vec<String> = self.table_names[first_unlocked..].to_vec();
let suffix_stack = Self::subset_stack(self.reftable_dir.clone(), old_suffix.clone());
let refs = suffix_stack.read_refs()?;
let logs = suffix_stack.read_all_logs()?;
let mut min_idx = u64::MAX;
let mut max_idx = 0u64;
for name in &old_suffix {
let path = self.reftable_dir.join(name);
let data = fs::read(&path).map_err(Error::Io)?;
let reader = ReftableReader::new(data)?;
min_idx = min_idx.min(reader.min_update_index());
max_idx = max_idx.max(reader.max_update_index());
}
if min_idx == u64::MAX {
min_idx = 0;
}
let wopts = WriteOptions {
hash_size: self.hash_size(),
..Default::default()
};
let mut writer = ReftableWriter::new(wopts, min_idx, max_idx);
for rec in refs {
writer.add_ref(rec)?;
}
for log in logs {
writer.add_log(log)?;
}
let data = writer.finish()?;
let compacted = self.write_table_file(&data, max_idx)?;
self.table_names = locked_prefix;
self.table_names.push(compacted.clone());
self.write_tables_list_locked(&mut guard)?;
for old in &old_suffix {
if old == &compacted {
continue;
}
let _ = fs::remove_file(self.reftable_dir.join(old));
}
Ok(())
}
pub fn write_ref(
&mut self,
refname: &str,
value: RefValue,
log: Option<LogRecord>,
opts: &WriteOptions,
) -> Result<()> {
{
let mut guard = self.acquire_tables_list_lock()?;
self.reload_table_names();
let update_index = self.max_update_index_unlocked()? + 1;
let mut writer = ReftableWriter::new(opts.clone(), update_index, update_index);
writer.add_ref(RefRecord {
name: refname.to_owned(),
update_index,
value,
})?;
if let Some(log_rec) = log {
let mut log_rec = log_rec;
log_rec.update_index = update_index;
writer.add_log(log_rec)?;
}
let data = writer.finish()?;
let filename = self.write_table_file(&data, update_index)?;
self.table_names.push(filename);
self.write_tables_list_locked(&mut guard)?;
}
self.maybe_auto_compact(opts)?;
Ok(())
}
pub fn write_transaction(
&mut self,
updates: Vec<ReftableTransactionUpdate>,
opts: &WriteOptions,
) -> Result<()> {
if updates.is_empty() {
return Ok(());
}
let mut guard = self.acquire_tables_list_lock()?;
self.write_transaction_under_lock(&mut guard, updates, opts)?;
self.maybe_auto_compact(opts)?;
Ok(())
}
pub(crate) fn write_transaction_under_lock(
&mut self,
guard: &mut TablesListLock,
mut updates: Vec<ReftableTransactionUpdate>,
opts: &WriteOptions,
) -> Result<()> {
if updates.is_empty() {
return Ok(());
}
test_inject_reftable_transaction_fail()?;
self.reload_table_names();
let cas_names: Vec<String> = updates
.iter()
.filter_map(|u| u.expected_old.map(|_| u.refname.clone()))
.collect();
if !cas_names.is_empty() {
let current_by_name = self.lookup_ref_oids_batch(&cas_names)?;
for update in &updates {
if let Some(expected) = update.expected_old {
let current = current_by_name.get(&update.refname).copied();
if current != Some(expected) {
return Err(ReftableError::ExpectedMismatch {
refname: update.refname.clone(),
expected,
actual: current,
}
.into());
}
}
}
}
let update_index = self.max_update_index_unlocked()? + 1;
let mut writer = ReftableWriter::new(opts.clone(), update_index, update_index);
updates.sort_by(|a, b| a.refname.cmp(&b.refname));
for update in &updates {
if let Some(value) = &update.value {
writer.add_ref(RefRecord {
name: update.refname.clone(),
update_index,
value: value.clone(),
})?;
}
}
for update in updates {
if let Some(mut log) = update.log {
log.update_index = update_index;
writer.add_log(log)?;
}
}
let data = writer.finish()?;
let filename = self.write_table_file(&data, update_index)?;
self.table_names.push(filename);
self.write_tables_list_locked(guard)?;
Ok(())
}
pub(crate) fn acquire_tables_list_lock_for_store(&self) -> Result<TablesListLock> {
self.acquire_tables_list_lock()
}
fn lookup_ref_oids_batch(&self, names: &[String]) -> Result<HashMap<String, ObjectId>> {
let want: HashSet<String> = names.iter().cloned().collect();
let mut found = HashMap::with_capacity(want.len());
let mut resolved = HashSet::with_capacity(want.len());
if want.is_empty() {
return Ok(found);
}
let cached = load_cached_stack_tables(&self.reftable_dir)?;
let table_names = self.table_names_for_read()?;
let readers = self.stack_readers(&cached, &table_names)?;
for reader in readers.iter().rev() {
reader.for_each_ref(|rec| {
if !want.contains(&rec.name) || resolved.contains(&rec.name) {
return ControlFlow::<()>::Continue(());
}
match rec.value {
RefValue::Val1(oid) | RefValue::Val2(oid, _) => {
found.insert(rec.name.clone(), oid);
resolved.insert(rec.name.clone());
}
RefValue::Deletion | RefValue::Symref(_) => {
found.remove(&rec.name);
resolved.insert(rec.name.clone());
}
}
if resolved.len() == want.len() {
return ControlFlow::Break(());
}
ControlFlow::<()>::Continue(())
})?;
if resolved.len() == want.len() {
break;
}
}
Ok(found)
}
fn max_update_index_unlocked(&self) -> Result<u64> {
let mut max_idx = 0u64;
for name in &self.table_names {
let path = self.reftable_dir.join(name);
let data = match fs::read(&path) {
Ok(data) => data,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
Err(err) => return Err(Error::Io(err)),
};
let reader = ReftableReader::new(data)?;
max_idx = max_idx.max(reader.max_update_index());
}
Ok(max_idx)
}
pub(crate) fn maybe_auto_compact(&mut self, opts: &WriteOptions) -> Result<()> {
self.reload_table_names();
if self
.table_names
.iter()
.any(|name| self.table_is_locked(name))
{
self.compact_unlocked_suffix()?;
return Ok(());
}
self.auto_compact_geometric(opts)?;
Ok(())
}
pub fn compact(&mut self) -> Result<()> {
let mut guard = self.acquire_tables_list_lock()?;
self.reload_table_names();
if self.table_names.is_empty() {
return Ok(());
}
let refs = self.read_refs()?;
let logs = self.read_all_logs()?;
let mut min_idx = u64::MAX;
let mut max_idx = 0u64;
for name in &self.table_names {
let path = self.reftable_dir.join(name);
let data = fs::read(&path).map_err(Error::Io)?;
let reader = ReftableReader::new(data)?;
min_idx = min_idx.min(reader.min_update_index());
max_idx = max_idx.max(reader.max_update_index());
}
if min_idx == u64::MAX {
min_idx = 0;
}
let opts = self.write_options();
let mut refs = refs;
let head_log = self.inject_head_ref(&mut refs, min_idx);
let mut writer = ReftableWriter::new(opts.clone(), min_idx, max_idx);
for rec in refs {
writer.add_ref(rec)?;
}
if opts.write_log {
let mut logs = logs;
if let Some(hl) = head_log {
logs.push(hl);
}
for log in logs {
writer.add_log(log)?;
}
}
let data = writer.finish()?;
let old_names = self.table_names.clone();
self.table_names.clear();
let name = self.write_table_file(&data, max_idx)?;
self.table_names.push(name.clone());
self.write_tables_list_locked(&mut guard)?;
for old in &old_names {
if old == &name {
continue;
}
let path = self.reftable_dir.join(old);
let _ = fs::remove_file(&path);
}
Ok(())
}
fn write_table_file(&self, data: &[u8], update_index: u64) -> Result<String> {
let random: u64 = {
let mut buf = [0u8; 8];
if let Ok(mut f) = fs::File::open("/dev/urandom") {
let _ = f.read(&mut buf);
}
u64::from_le_bytes(buf)
};
let filename = format!(
"{:08x}-{:08x}-{:08x}.ref",
update_index, update_index, random as u32
);
let path = self.reftable_dir.join(&filename);
fs::write(&path, data).map_err(Error::Io)?;
Ok(filename)
}
fn write_tables_list(&self) -> Result<()> {
let mut guard = self.acquire_tables_list_lock()?;
self.write_tables_list_locked(&mut guard)
}
fn write_tables_list_locked(&self, guard: &mut TablesListLock) -> Result<()> {
let tables_list = self.reftable_dir.join("tables.list");
let content = self.table_names.join("\n")
+ if self.table_names.is_empty() {
""
} else {
"\n"
};
fs::write(guard.path()?, &content).map_err(Error::Io)?;
let lock = guard.lock.take().ok_or(ReftableError::LockReleased)?;
lock.commit_to(&tables_list).map_err(|err| match err {
crate::lockfile::LockError::Io { source, .. } => Error::Io(source),
crate::lockfile::LockError::Held { path } => {
ReftableError::Locked { lock_path: path }.into()
}
})?;
invalidate_stack_table_cache(&self.reftable_dir);
Ok(())
}
fn acquire_tables_list_lock(&self) -> Result<TablesListLock> {
use crate::lockfile::{LockError, LockFile, LockWait};
let lock = self.reftable_dir.join("tables.list.lock");
let git_dir = self
.reftable_dir
.parent()
.unwrap_or(self.reftable_dir.as_path());
let config = ConfigSet::load(
&crate::environment::Environment::empty(),
Some(git_dir),
true,
)
.unwrap_or_else(|_| ConfigSet::new());
let wait = LockWait::reftable_tables_list(Some(&config));
let file_lock = LockFile::acquire(&lock, wait).map_err(|err| match err {
LockError::Held { path } => ReftableError::Locked { lock_path: path }.into(),
LockError::Io { source, .. } => Error::Io(source),
})?;
Ok(TablesListLock::new(file_lock))
}
pub(crate) fn reload_table_names(&mut self) {
if let Ok(content) = fs::read_to_string(self.reftable_dir.join("tables.list")) {
self.table_names = content
.lines()
.filter(|line| !line.is_empty())
.map(ToOwned::to_owned)
.collect();
}
}
pub fn table_names(&self) -> &[String] {
&self.table_names
}
}
pub fn reftable_resolve_ref(git_dir: &Path, refname: &str) -> Result<ObjectId> {
reftable_resolve_ref_depth(git_dir, refname, 0)
}
pub(crate) fn reftable_storage_location(git_dir: &Path, refname: &str) -> (PathBuf, String) {
if let Some(rest) = refname.strip_prefix("worktrees/") {
if let Some((worktree_id, per_worktree_ref)) = rest.split_once('/') {
if per_worktree_ref.starts_with("refs/") {
let common =
crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf());
return (
common.join("worktrees").join(worktree_id),
per_worktree_ref.to_owned(),
);
}
}
}
if refname == "HEAD"
|| refname.starts_with("refs/worktree/")
|| (git_dir.join("commondir").exists() && refname.starts_with("refs/bisect/"))
{
return (git_dir.to_path_buf(), refname.to_owned());
}
(
crate::refs::common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf()),
refname.to_owned(),
)
}
fn reftable_resolve_ref_depth(git_dir: &Path, refname: &str, depth: usize) -> Result<ObjectId> {
if depth >= crate::refs::SYMREF_MAXDEPTH {
return Err(RefStoreError::SymrefLoop.into());
}
if refname == "HEAD" {
let head_path = git_dir.join("HEAD");
if head_path.exists() {
let content = fs::read_to_string(&head_path).map_err(Error::Io)?;
let content = content.trim();
if let Some(target) = content.strip_prefix("ref: ") {
if target.trim() != "refs/heads/.invalid" {
return reftable_resolve_ref_depth(git_dir, target.trim(), depth + 1);
}
}
if content.len() == 40 && content.chars().all(|c| c.is_ascii_hexdigit()) {
return content.parse();
}
}
}
let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
let stack = ReftableStack::open(&store_git_dir)?;
match stack.lookup_ref(&storage_refname)? {
Some(rec) => match rec.value {
RefValue::Val1(oid) => Ok(oid),
RefValue::Val2(oid, _) => Ok(oid),
RefValue::Symref(target) => {
reftable_resolve_ref_depth(&store_git_dir, &target, depth + 1)
}
RefValue::Deletion => Err(RefStoreError::not_found(refname).into()),
},
None => Err(RefStoreError::not_found(refname).into()),
}
}
pub fn reftable_write_ref(
git_dir: &Path,
refname: &str,
oid: &ObjectId,
log_identity: Option<&str>,
log_message: Option<&str>,
) -> Result<()> {
let (store_git_dir, _) = reftable_storage_location(git_dir, refname);
let opts = read_write_options(&store_git_dir);
reftable_write_ref_with_write_options(git_dir, refname, oid, log_identity, log_message, &opts)
}
pub fn reftable_write_ref_with_write_options(
git_dir: &Path,
refname: &str,
oid: &ObjectId,
log_identity: Option<&str>,
log_message: Option<&str>,
opts: &WriteOptions,
) -> Result<()> {
let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
let mut stack = ReftableStack::open(&store_git_dir)?;
let old_oid = match stack
.lookup_ref(&storage_refname)?
.and_then(|r| match r.value {
RefValue::Val1(oid) => Some(oid),
RefValue::Val2(oid, _) => Some(oid),
_ => None,
}) {
Some(oid) => oid,
None => ObjectId::from_bytes(&vec![0u8; reftable_hash_size_for_git_dir(&store_git_dir)])?,
};
let log = if let Some(identity) = log_identity {
let (name, email, time_secs, tz) = parse_identity_string(identity);
Some(LogRecord {
refname: storage_refname.clone(),
update_index: 0, old_id: old_oid,
new_id: *oid,
name,
email,
time_seconds: time_secs,
tz_offset: tz,
message: log_message.unwrap_or("").to_owned(),
})
} else {
None
};
stack.write_ref(&storage_refname, RefValue::Val1(*oid), log, opts)
}
pub fn reftable_log_record_for_commit(
git_dir: &Path,
refname: &str,
old_oid: &ObjectId,
new_oid: &ObjectId,
identity: &str,
message: &str,
) -> Result<LogRecord> {
let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
let opts = read_write_options(&store_git_dir);
let (name, email, time_secs, tz) = parse_identity_string(identity);
Ok(LogRecord {
refname: storage_refname,
update_index: 0,
old_id: widen_oid_to(*old_oid, opts.hash_size),
new_id: widen_oid_to(*new_oid, opts.hash_size),
name,
email,
time_seconds: time_secs,
tz_offset: tz,
message: message.to_owned(),
})
}
pub fn reftable_write_symref(
git_dir: &Path,
refname: &str,
target: &str,
log_identity: Option<&str>,
log_message: Option<&str>,
) -> Result<()> {
let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
let mut stack = ReftableStack::open(&store_git_dir)?;
let opts = read_write_options(&store_git_dir);
let log = if let Some(identity) = log_identity {
let (name, email, time_secs, tz) = parse_identity_string(identity);
let zero_oid =
ObjectId::from_bytes(&vec![0u8; reftable_hash_size_for_git_dir(&store_git_dir)])?;
Some(LogRecord {
refname: storage_refname.clone(),
update_index: 0,
old_id: zero_oid,
new_id: zero_oid,
name,
email,
time_seconds: time_secs,
tz_offset: tz,
message: log_message.unwrap_or("").to_owned(),
})
} else {
None
};
stack.write_ref(
&storage_refname,
RefValue::Symref(target.to_owned()),
log,
&opts,
)
}
pub fn verify_ref_transaction_batch_reftable(
git_dir: &Path,
items: &[crate::refs::RefBatchItem],
) -> std::result::Result<(), crate::refs::RefnameUnavailable> {
if items.iter().all(|i| i.new_oid.is_none()) {
return Ok(());
}
let (store_git_dir, _) = reftable_storage_location(git_dir, "HEAD");
let Ok(stack) = ReftableStack::open(&store_git_dir) else {
return Ok(());
};
let extras: BTreeSet<String> = items.iter().map(|i| i.name.clone()).collect();
let empty_skip: HashSet<String> = HashSet::new();
let mut checked_ancestors: HashSet<String> = HashSet::new();
for item in items {
let Some(_oid) = item.new_oid else {
continue;
};
let segments: Vec<&str> = item.name.split('/').filter(|s| !s.is_empty()).collect();
if segments.len() > 1 {
let mut dirname = String::new();
for part in &segments[..segments.len() - 1] {
if !dirname.is_empty() {
dirname.push('/');
}
dirname.push_str(part);
if !checked_ancestors.insert(dirname.clone()) {
continue;
}
if empty_skip.contains(&dirname) {
continue;
}
if extras.contains(&dirname) {
return Err(crate::refs::RefnameUnavailable::SameBatch {
refname: item.name.clone(),
other: dirname.clone(),
});
}
if stack.lookup_ref(&dirname).ok().flatten().is_some() {
return Err(crate::refs::RefnameUnavailable::AncestorExists {
blocking: dirname.clone(),
new_ref: item.name.clone(),
});
}
}
}
if stack.lookup_ref_oid(&item.name).ok().flatten().is_some() {
continue;
}
if let Ok(Some(blocking)) = stack.first_ref_name_with_prefix(&format!("{}/", item.name)) {
return Err(crate::refs::RefnameUnavailable::DescendantExists {
blocking,
new_ref: item.name.clone(),
});
}
}
Ok(())
}
pub fn reftable_write_transaction(
git_dir: &Path,
updates: Vec<ReftableTransactionUpdate>,
) -> Result<()> {
crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?
.write_reftable_updates(updates)
.map_err(Into::into)
}
#[cfg(test)]
mod reftable_tx_fail_inject {
use std::cell::RefCell;
thread_local! {
pub static INJECT: RefCell<bool> = const { RefCell::new(false) };
}
}
#[cfg(test)]
pub fn set_test_inject_reftable_transaction_fail(enabled: bool) {
reftable_tx_fail_inject::INJECT.with(|c| *c.borrow_mut() = enabled);
}
#[cfg(test)]
fn test_inject_reftable_transaction_fail() -> Result<()> {
reftable_tx_fail_inject::INJECT.with(|c| {
if *c.borrow() {
return Err(Error::Io(std::io::Error::other(
"injected reftable transaction failure",
)));
}
Ok(())
})
}
#[cfg(not(test))]
fn test_inject_reftable_transaction_fail() -> Result<()> {
Ok(())
}
pub fn reftable_delete_ref(git_dir: &Path, refname: &str) -> Result<()> {
let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
let mut stack = ReftableStack::open(&store_git_dir)?;
if stack.lookup_ref(&storage_refname)?.is_none() {
return Err(RefStoreError::not_found(refname).into());
}
let opts = read_write_options(&store_git_dir);
stack.write_ref(&storage_refname, RefValue::Deletion, None, &opts)
}
pub fn reftable_read_symbolic_ref(git_dir: &Path, refname: &str) -> Result<Option<String>> {
crate::refs::read_symbolic_ref(git_dir, refname)
}
fn push_list_ref_entry(git_dir: &Path, rec: &RefRecord, out: &mut Vec<(String, ObjectId)>) {
match &rec.value {
RefValue::Val1(oid) => out.push((rec.name.clone(), *oid)),
RefValue::Val2(oid, _) => out.push((rec.name.clone(), *oid)),
RefValue::Symref(target) => {
if let Ok(oid) = reftable_resolve_ref(git_dir, target) {
out.push((rec.name.clone(), oid));
}
}
RefValue::Deletion => {}
}
}
pub fn reftable_list_refs(git_dir: &Path, prefix: &str) -> Result<Vec<(String, ObjectId)>> {
let stack = ReftableStack::open(git_dir)?;
stack.list_refs_under_prefix(git_dir, prefix)
}
pub fn reftable_read_reflog(
git_dir: &Path,
refname: &str,
) -> Result<Vec<crate::reflog::ReflogEntry>> {
use crate::refs::store::RefStore as _;
let store = crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?;
let mut entries = Vec::new();
store.for_each_reflog_entry(refname, false, &mut |entry| {
entries.push(entry.clone());
std::ops::ControlFlow::Continue(())
})?;
Ok(entries)
}
pub fn reftable_replace_reflog(
git_dir: &Path,
refname: &str,
entries: &[crate::reflog::ReflogEntry],
) -> Result<()> {
use crate::refs::store::RefStore as _;
crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?
.replace_reflog(refname, entries.to_vec())
}
fn reftable_should_write_log(git_dir: &Path, storage_refname: &str) -> bool {
let mode = crate::refs::effective_log_refs_config(git_dir);
crate::refs::should_autocreate_reflog_for_mode(storage_refname, mode)
|| reftable_reflog_exists(git_dir, storage_refname)
}
pub fn reftable_append_reflog(
git_dir: &Path,
refname: &str,
old_oid: &ObjectId,
new_oid: &ObjectId,
identity: &str,
message: &str,
force_create: bool,
) -> Result<()> {
let (store_git_dir, storage_refname) = reftable_storage_location(git_dir, refname);
if !force_create && !reftable_should_write_log(&store_git_dir, &storage_refname) {
return Ok(());
}
let (name, email, time_secs, tz) = parse_identity_string(identity);
let mut stack = ReftableStack::open(&store_git_dir)?;
let update_index = stack.max_update_index()? + 1;
let opts = read_write_options(&store_git_dir);
let hash_size = opts.hash_size;
let mut writer = ReftableWriter::new(opts.clone(), update_index, update_index);
writer.add_log(LogRecord {
refname: storage_refname.clone(),
update_index,
old_id: widen_oid_to(*old_oid, hash_size),
new_id: widen_oid_to(*new_oid, hash_size),
name,
email,
time_seconds: time_secs,
tz_offset: tz,
message: message.to_owned(),
})?;
let data = writer.finish()?;
stack.add_table(&data, update_index, &opts)?;
if storage_refname.starts_with("refs/heads/branch-") {
stack.reload_table_names();
let has_locked = stack
.table_names
.iter()
.any(|name| stack.table_is_locked(name));
if !has_locked && stack.table_names.len() <= 2 {
stack.compact()?;
}
}
Ok(())
}
pub fn reftable_reflog_exists(git_dir: &Path, refname: &str) -> bool {
use crate::refs::store::RefStore as _;
crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())
.ok()
.and_then(|store| store.reflog_exists(refname).ok())
.unwrap_or(false)
}
pub fn reftable_list_reflog_refs(git_dir: &Path) -> Result<Vec<String>> {
use crate::refs::store::RefStore as _;
let store = crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?;
let mut refs = Vec::new();
store.for_each_reflog_ref(&mut |name| {
refs.push(name.to_owned());
std::ops::ControlFlow::Continue(())
})?;
refs.sort();
Ok(refs)
}
pub fn reftable_create_reflog(git_dir: &Path, refname: &str) -> Result<()> {
use crate::refs::store::RefStore as _;
crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?.create_reflog(refname)
}
pub fn reftable_delete_reflog(git_dir: &Path, refname: &str) -> Result<()> {
use crate::refs::store::RefStore as _;
crate::refs::store::ReftableRefStore::open(git_dir.to_path_buf())?.delete_reflog(refname)
}
fn apply_geometric_factor_from_config(opts: &mut WriteOptions, raw: &str) {
if let Ok(v) = raw.trim().parse::<u64>() {
if (2..=255).contains(&v) {
opts.auto_compaction_factor = v as u8;
}
}
}
pub fn read_write_options(git_dir: &Path) -> WriteOptions {
read_write_options_with_environment(git_dir, &crate::environment::Environment::empty())
}
pub fn read_write_options_with_environment(
git_dir: &Path,
env: &crate::environment::Environment,
) -> WriteOptions {
let mut opts = WriteOptions {
hash_size: reftable_hash_size_for_git_dir(git_dir),
autocompaction_enabled: env.reftable_autocompaction_enabled(),
..Default::default()
};
let config = ConfigSet::load(env, Some(git_dir), true)
.unwrap_or_else(|_| local_config_best_effort(git_dir));
if let Some(Ok(v)) = config.get("reftable.blockSize").map(|v| v.parse::<u32>()) {
opts.block_size = v;
}
if let Some(Ok(v)) = config
.get("reftable.restartInterval")
.map(|v| v.parse::<usize>())
{
opts.restart_interval = v;
}
if let Some(Ok(false)) = config.get_bool("reftable.indexObjects") {
opts.skip_index_objects = true;
}
if let Some(value) = config.get("reftable.geometricFactor") {
apply_geometric_factor_from_config(&mut opts, &value);
}
if config.log_refs_config() == crate::refs::LogRefsConfig::None {
opts.write_log = false;
}
opts
}
fn local_config_best_effort(git_dir: &Path) -> ConfigSet {
let path = git_dir.join("config");
let Ok(content) = fs::read_to_string(&path) else {
return ConfigSet::new();
};
let (entries, _) = crate::config::ConfigFile::parse_gitmodules_best_effort(
&path,
&content,
crate::config::ConfigScope::Local,
);
ConfigSet::from_entries(entries)
}
pub fn dump_reftable_blocks(path: &Path) -> Result<String> {
let data = fs::read(path).map_err(Error::Io)?;
if data.len() < HEADER_SIZE {
return Err(ReftableError::TooSmall { len: data.len() }.into());
}
if &data[0..4] != REFTABLE_MAGIC {
return Err(ReftableError::BadMagic.into());
}
let version = data[4];
let header_size = if version == 2 { 28 } else { 24 };
let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
let block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
let table_size = data.len().saturating_sub(footer_size);
let mut out = String::new();
out.push_str("header:\n");
out.push_str(&format!(" block_size: {block_size}\n"));
let mut section_type: u8 = 0;
let mut block_off: u64 = 0;
let mut first = true;
loop {
if !first {
}
if block_off as usize >= table_size {
break;
}
let header_off = if block_off == 0 { header_size } else { 0 };
let pos = block_off as usize + header_off;
if pos + 1 > data.len() {
break;
}
let block_type = data[pos];
let Some(section) = block_section_name(block_type) else {
break;
};
if pos + 4 > data.len() {
break;
}
let blk_len =
((data[pos + 1] as u32) << 16) | ((data[pos + 2] as u32) << 8) | (data[pos + 3] as u32);
let blk_len = blk_len as usize;
let (restart_off, restart_count, full_block_size) = if block_type == BLOCK_TYPE_LOG {
let skip = 4 + header_off;
let comp = &data[block_off as usize + skip..];
let inflated_size = blk_len.saturating_sub(skip);
let (inflated, consumed) = decompress_log_payload(comp, inflated_size)?;
let mut full = vec![0u8; skip];
full.extend_from_slice(&inflated);
let rc = be16(&full, blk_len - 2) as usize;
let roff = blk_len - 2 - 3 * rc;
let fbs = skip + consumed;
(roff, rc, fbs)
} else {
let abs = block_off as usize;
if abs + blk_len < 2 {
break;
}
let rc = be16(&data, abs + blk_len - 2) as usize;
let roff = blk_len - 2 - 3 * rc;
let mut fbs = block_size as usize;
if fbs == 0 {
fbs = blk_len;
} else if blk_len < fbs
&& abs + blk_len < data.len()
&& data.get(abs + blk_len) == Some(&0u8)
{
} else if blk_len < fbs {
fbs = blk_len;
}
(roff, rc, fbs)
};
if block_type != section_type {
section_type = block_type;
out.push_str(&format!("{section}:\n"));
}
out.push_str(&format!(" - length: {restart_off}\n"));
out.push_str(&format!(" restarts: {restart_count}\n"));
block_off += full_block_size as u64;
first = false;
if full_block_size == 0 {
break;
}
}
Ok(out)
}
fn block_section_name(block_type: u8) -> Option<&'static str> {
match block_type {
BLOCK_TYPE_LOG => Some("log"),
BLOCK_TYPE_REF => Some("ref"),
BLOCK_TYPE_OBJ => Some("obj"),
BLOCK_TYPE_INDEX => Some("idx"),
_ => None,
}
}
fn be16(data: &[u8], off: usize) -> u16 {
((data[off] as u16) << 8) | (data[off + 1] as u16)
}
fn decompress_log_payload(compressed: &[u8], inflated_size: usize) -> Result<(Vec<u8>, usize)> {
use flate2::read::{DeflateDecoder, ZlibDecoder};
use std::io::Read;
let mut inflated = vec![0u8; inflated_size];
if compressed.len() >= 2 && compressed[0] == 0x78 {
let mut decoder = ZlibDecoder::new(compressed);
decoder
.read_exact(&mut inflated)
.map_err(|e| Error::Zlib(e.to_string()))?;
Ok((inflated, decoder.total_in() as usize))
} else {
let mut decoder = DeflateDecoder::new(compressed);
decoder
.read_exact(&mut inflated)
.map_err(|e| Error::Zlib(e.to_string()))?;
Ok((inflated, decoder.total_in() as usize))
}
}
fn crc32(data: &[u8]) -> u32 {
let mut crc: u32 = 0xffffffff;
for &byte in data {
crc ^= byte as u32;
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0xedb88320;
} else {
crc >>= 1;
}
}
}
!crc
}
fn common_prefix_len(a: &[u8], b: &[u8]) -> usize {
a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count()
}
fn read_u24(data: &[u8], pos: usize) -> usize {
((data[pos] as usize) << 16) | ((data[pos + 1] as usize) << 8) | (data[pos + 2] as usize)
}
fn read_u16(data: &[u8], pos: usize) -> usize {
((data[pos] as usize) << 8) | (data[pos + 1] as usize)
}
fn restart_table_start_for_block(
data: &[u8],
records_end: usize,
records_data_start: usize,
) -> Result<usize> {
if records_end < 2 || records_end > data.len() {
return Err(corrupt(ReftableError::InvalidBlockLength));
}
let rc = read_u16(data, records_end - 2);
if rc > MAX_RESTARTS {
return Err(corrupt(ReftableError::InvalidRestartTable));
}
let restart_bytes = rc * 3;
let Some(restart_table_start) = records_end
.checked_sub(2)
.and_then(|without_count| without_count.checked_sub(restart_bytes))
else {
return Err(corrupt(ReftableError::InvalidRestartTable));
};
if restart_table_start < records_data_start {
return Err(corrupt(ReftableError::InvalidRestartTable));
}
Ok(restart_table_start)
}
fn parse_footer(data: &[u8], version: u8) -> Result<Footer> {
let footer_size = if version == 2 { 72 } else { FOOTER_V1_SIZE };
if data.len() < footer_size {
return Err(ReftableError::TooSmall { len: data.len() }.into());
}
if &data[0..4] != REFTABLE_MAGIC {
return Err(ReftableError::BadMagic.into());
}
let fver = data[4];
if fver != version {
return Err(ReftableError::FooterVersionMismatch {
header: version,
footer: fver,
}
.into());
}
let read_u64 = |slice: &[u8]| -> Result<u64> {
let bytes: [u8; 8] = slice
.try_into()
.map_err(|_| truncated(ReftableField::Footer))?;
Ok(u64::from_be_bytes(bytes))
};
let block_size = ((data[5] as u32) << 16) | ((data[6] as u32) << 8) | (data[7] as u32);
let min_update_index = read_u64(&data[8..16])?;
let max_update_index = read_u64(&data[16..24])?;
let off = if version == 2 { 28 } else { 24 };
let ref_index_position = read_u64(&data[off..off + 8])?;
let log_position = read_u64(&data[off + 24..off + 32])?;
let crc_bytes: [u8; 4] = data[footer_size - 4..footer_size]
.try_into()
.map_err(|_| truncated(ReftableField::Footer))?;
let crc_stored = u32::from_be_bytes(crc_bytes);
let crc_computed = crc32(&data[..footer_size - 4]);
if crc_stored != crc_computed {
return Err(ReftableError::FooterChecksumMismatch {
stored: crc_stored,
computed: crc_computed,
}
.into());
}
Ok(Footer {
block_size,
min_update_index,
max_update_index,
ref_index_position,
log_position,
})
}
pub(crate) fn parse_identity_string(identity: &str) -> (String, String, u64, i16) {
let parts: Vec<&str> = identity.rsplitn(3, ' ').collect();
if parts.len() < 3 {
return (identity.to_owned(), String::new(), 0, 0);
}
let tz_str = parts[0]; let time_str = parts[1]; let name_email = parts[2];
let time_secs = time_str.parse::<u64>().unwrap_or(0);
let tz_minutes = if tz_str.len() >= 5 {
let sign = if tz_str.starts_with('-') { -1i16 } else { 1 };
let hours = tz_str[1..3].parse::<i16>().unwrap_or(0);
let mins = tz_str[3..5].parse::<i16>().unwrap_or(0);
sign * (hours * 60 + mins)
} else {
0
};
let (name, email) = if let Some(lt_pos) = name_email.find('<') {
let name = name_email[..lt_pos].trim().to_owned();
let email = if let Some(gt_pos) = name_email.find('>') {
name_email[lt_pos + 1..gt_pos].to_owned()
} else {
name_email[lt_pos + 1..].to_owned()
};
(name, email)
} else {
(name_email.to_owned(), String::new())
};
(name, email, time_secs, tz_minutes)
}
#[cfg(test)]
mod tests {
use super::*;
fn reftable_repo_with_config(extra: &str) -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().expect("tempdir");
crate::repo::init_repository(
dir.path(),
false,
"main",
None,
crate::RefStorageFormat::Reftable,
)
.expect("init");
let git_dir = dir.path().join(".git");
let mut cfg = fs::read_to_string(git_dir.join("config")).expect("read config");
cfg.push_str(extra);
fs::write(git_dir.join("config"), cfg).expect("write config");
(dir, git_dir)
}
#[test]
fn write_options_log_blocks_follow_log_all_ref_updates() {
for (value, write_log) in [
("true", true),
("always", true),
("yes", true),
("on", true),
("1", true),
("false", false),
("no", false),
("0", false),
("never", false),
] {
let (_dir, git_dir) =
reftable_repo_with_config(&format!("[core]\n\tlogAllRefUpdates = {value}\n"));
assert_eq!(
read_write_options(&git_dir).write_log,
write_log,
"core.logAllRefUpdates = {value}"
);
}
let (_dir, git_dir) = reftable_repo_with_config("");
assert!(read_write_options(&git_dir).write_log);
}
#[test]
fn write_options_broken_config_keeps_entries_before_bad_line() {
let (_dir, git_dir) = reftable_repo_with_config(
"[reftable]\n\tgeometricFactor = 5\n\tindexObjects = off\n[core]\n\tlogAllRefUpdates = no\n[broken\nblockSize = 1\n",
);
let opts = read_write_options(&git_dir);
assert_eq!(opts.auto_compaction_factor, 5);
assert!(opts.skip_index_objects);
assert!(!opts.write_log);
assert_eq!(opts.block_size, WriteOptions::default().block_size);
}
#[test]
fn should_write_log_follows_mode_and_existing_reflog() {
let (_dir, git_dir) = reftable_repo_with_config("[core]\n\tlogAllRefUpdates = true\n");
assert!(reftable_should_write_log(&git_dir, "refs/heads/main"));
assert!(!reftable_should_write_log(&git_dir, "refs/tags/v1"));
let (_dir, git_dir) = reftable_repo_with_config("[core]\n\tlogAllRefUpdates = always\n");
assert!(reftable_should_write_log(&git_dir, "refs/tags/v1"));
let (_dir, git_dir) = reftable_repo_with_config("[core]\n\tlogAllRefUpdates = false\n");
assert!(!reftable_should_write_log(&git_dir, "refs/heads/topic"));
reftable_create_reflog(&git_dir, "refs/heads/topic").expect("create reflog");
assert!(reftable_should_write_log(&git_dir, "refs/heads/topic"));
}
#[test]
fn read_refs_on_empty_reftable_without_tables_list() {
let dir = tempfile::tempdir().expect("tempdir");
let git_dir = dir.path().join(".git");
fs::create_dir_all(git_dir.join("reftable")).expect("reftable dir");
let stack = ReftableStack::open(&git_dir).expect("open empty stack");
assert!(stack.read_refs().expect("read").is_empty());
}
#[test]
fn read_refs_tracks_external_compaction_on_public_stack() {
use crate::objects::ObjectId;
use crate::repo::init_repository;
let dir = tempfile::tempdir().expect("tempdir");
init_repository(
dir.path(),
false,
"main",
None,
crate::RefStorageFormat::Reftable,
)
.expect("init");
let git_dir = dir.path().join(".git");
let mut opts = read_write_options(&git_dir);
opts.autocompaction_enabled = false;
let oid_a = ObjectId::from_bytes(&[0x31; 20]).expect("oid");
let oid_b = ObjectId::from_bytes(&[0x32; 20]).expect("oid");
let identity = "Compact Ext <ext@example.com> 1700000000 +0000";
reftable_write_ref_with_write_options(
&git_dir,
"refs/heads/keep-a",
&oid_a,
Some(identity),
None,
&opts,
)
.expect("write a");
reftable_write_ref_with_write_options(
&git_dir,
"refs/heads/keep-b",
&oid_b,
Some(identity),
None,
&opts,
)
.expect("write b");
let reader = ReftableStack::open(&git_dir).expect("reader stack");
let mut writer = ReftableStack::open(&git_dir).expect("writer stack");
writer.compact().expect("compact via second handle");
let refs = reader.read_refs().expect("read after external compact");
let names: Vec<&str> = refs.iter().map(|r| r.name.as_str()).collect();
assert!(names.iter().any(|n| *n == "refs/heads/keep-a"));
assert!(names.iter().any(|n| *n == "refs/heads/keep-b"));
}
#[test]
fn subset_stack_read_refs_respects_table_names() {
use crate::objects::ObjectId;
use crate::repo::init_repository;
let dir = tempfile::tempdir().expect("tempdir");
init_repository(
dir.path(),
false,
"main",
None,
crate::RefStorageFormat::Reftable,
)
.expect("init");
let git_dir = dir.path().join(".git");
let mut opts = read_write_options(&git_dir);
opts.autocompaction_enabled = false;
let oid_a = ObjectId::from_bytes(&[0x11; 20]).expect("oid");
let oid_b = ObjectId::from_bytes(&[0x22; 20]).expect("oid");
let identity = "Subset Stack <subset@example.com> 1700000000 +0000";
reftable_write_ref_with_write_options(
&git_dir,
"refs/heads/only-in-first-table",
&oid_a,
Some(identity),
None,
&opts,
)
.expect("first ref");
reftable_write_ref_with_write_options(
&git_dir,
"refs/heads/only-in-second-table",
&oid_b,
Some(identity),
None,
&opts,
)
.expect("second ref");
let stack = ReftableStack::open(&git_dir).expect("open");
assert!(
stack.table_names.len() >= 2,
"expected multiple tables without autocompaction, got {}",
stack.table_names.len()
);
let subset = ReftableStack::subset_stack(
stack.reftable_dir.clone(),
vec![stack.table_names[0].clone()],
);
let refs = subset.read_refs().expect("subset read");
let names: Vec<&str> = refs.iter().map(|r| r.name.as_str()).collect();
assert!(names.iter().any(|n| *n == "refs/heads/only-in-first-table"));
assert!(!names
.iter()
.any(|n| *n == "refs/heads/only-in-second-table"));
let full = stack.read_refs().expect("full read");
assert_eq!(full.len(), 2);
}
#[test]
fn suggest_compaction_segment_matches_geometric_break() {
assert_eq!(suggest_compaction_segment(&[10]), (0, 0));
assert_eq!(suggest_compaction_segment(&[64, 32, 16]), (0, 0));
let (start, end) = suggest_compaction_segment(&[64, 32, 16, 8, 4, 3, 1]);
assert!(
end > start + 1,
"expected compaction segment, got {start}..{end}"
);
}
#[test]
fn prefix_upper_exclusive_for_seek_end() {
assert_eq!(
prefix_upper_exclusive("refs/heads/b05").as_deref(),
Some("refs/heads/b06")
);
assert!(prefix_upper_exclusive("refs/heads/").is_none());
}
#[test]
fn test_varint_roundtrip() {
for val in [0u64, 1, 127, 128, 255, 256, 16383, 16384, u64::MAX] {
let mut buf = Vec::new();
put_varint(val, &mut buf);
let (decoded, end) = get_varint(&buf, 0).unwrap();
assert_eq!(decoded, val, "varint roundtrip failed for {val}");
assert_eq!(end, buf.len());
}
}
#[test]
fn test_crc32() {
assert_eq!(crc32(b"123456789"), 0xCBF43926);
}
#[test]
fn test_empty_table() {
let writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
let data = writer.finish().unwrap();
let reader = ReftableReader::new(data).unwrap();
let refs = reader.read_refs().unwrap();
assert!(refs.is_empty());
}
#[test]
fn test_write_read_single_ref() {
let oid = ObjectId::from_bytes(&[0xab; 20]).unwrap();
let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
writer
.add_ref(RefRecord {
name: "refs/heads/main".to_owned(),
update_index: 1,
value: RefValue::Val1(oid),
})
.unwrap();
let data = writer.finish().unwrap();
let reader = ReftableReader::new(data).unwrap();
let refs = reader.read_refs().unwrap();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].name, "refs/heads/main");
assert_eq!(refs[0].value, RefValue::Val1(oid));
assert_eq!(refs[0].update_index, 1);
}
#[test]
fn test_write_read_multiple_refs() {
let oid1 = ObjectId::from_bytes(&[0x11; 20]).unwrap();
let oid2 = ObjectId::from_bytes(&[0x22; 20]).unwrap();
let oid3 = ObjectId::from_bytes(&[0x33; 20]).unwrap();
let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
writer
.add_ref(RefRecord {
name: "refs/heads/a".to_owned(),
update_index: 1,
value: RefValue::Val1(oid1),
})
.unwrap();
writer
.add_ref(RefRecord {
name: "refs/heads/b".to_owned(),
update_index: 1,
value: RefValue::Val1(oid2),
})
.unwrap();
writer
.add_ref(RefRecord {
name: "refs/tags/v1.0".to_owned(),
update_index: 1,
value: RefValue::Val2(oid3, oid1),
})
.unwrap();
let data = writer.finish().unwrap();
let reader = ReftableReader::new(data).unwrap();
let refs = reader.read_refs().unwrap();
assert_eq!(refs.len(), 3);
assert_eq!(refs[0].name, "refs/heads/a");
assert_eq!(refs[1].name, "refs/heads/b");
assert_eq!(refs[2].name, "refs/tags/v1.0");
assert_eq!(refs[2].value, RefValue::Val2(oid3, oid1));
}
#[test]
fn test_symref_roundtrip() {
let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
writer
.add_ref(RefRecord {
name: "refs/heads/sym".to_owned(),
update_index: 1,
value: RefValue::Symref("refs/heads/main".to_owned()),
})
.unwrap();
let data = writer.finish().unwrap();
let reader = ReftableReader::new(data).unwrap();
let refs = reader.read_refs().unwrap();
assert_eq!(refs.len(), 1);
assert_eq!(
refs[0].value,
RefValue::Symref("refs/heads/main".to_owned())
);
}
#[test]
fn test_log_roundtrip() {
let old_oid = ObjectId::from_bytes(&[0; 20]).unwrap();
let new_oid = ObjectId::from_bytes(&[0xaa; 20]).unwrap();
let opts = WriteOptions {
write_log: true,
..Default::default()
};
let mut writer = ReftableWriter::new(opts, 1, 1);
writer
.add_log(LogRecord {
refname: "refs/heads/main".to_owned(),
update_index: 1,
old_id: old_oid,
new_id: new_oid,
name: "Test User".to_owned(),
email: "test@example.com".to_owned(),
time_seconds: 1700000000,
tz_offset: -480,
message: "initial commit".to_owned(),
})
.unwrap();
let data = writer.finish().unwrap();
let reader = ReftableReader::new(data).unwrap();
let logs = reader.read_logs().unwrap();
assert_eq!(logs.len(), 1);
assert_eq!(logs[0].refname, "refs/heads/main");
assert_eq!(logs[0].old_id, old_oid);
assert_eq!(logs[0].new_id, new_oid);
assert_eq!(logs[0].name, "Test User");
assert_eq!(logs[0].email, "test@example.com");
assert_eq!(logs[0].time_seconds, 1700000000);
assert_eq!(logs[0].tz_offset, -480);
assert_eq!(logs[0].message, "initial commit\n");
}
#[test]
fn test_unaligned_table() {
let oid = ObjectId::from_bytes(&[0xcc; 20]).unwrap();
let opts = WriteOptions {
unpadded: true,
restart_interval: 16,
write_log: false,
..WriteOptions::default()
};
let mut writer = ReftableWriter::new(opts, 1, 1);
writer
.add_ref(RefRecord {
name: "refs/heads/main".to_owned(),
update_index: 1,
value: RefValue::Val1(oid),
})
.unwrap();
let data = writer.finish().unwrap();
assert!(data.len() < DEFAULT_BLOCK_SIZE as usize);
let reader = ReftableReader::new(data).unwrap();
let refs = reader.read_refs().unwrap();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].value, RefValue::Val1(oid));
}
#[test]
fn test_parse_identity() {
let (name, email, ts, tz) =
parse_identity_string("Test User <test@example.com> 1700000000 -0800");
assert_eq!(name, "Test User");
assert_eq!(email, "test@example.com");
assert_eq!(ts, 1700000000);
assert_eq!(tz, -480);
}
#[test]
fn test_deletion_record() {
let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
writer
.add_ref(RefRecord {
name: "refs/heads/gone".to_owned(),
update_index: 1,
value: RefValue::Deletion,
})
.unwrap();
let data = writer.finish().unwrap();
let reader = ReftableReader::new(data).unwrap();
let refs = reader.read_refs().unwrap();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].value, RefValue::Deletion);
}
#[test]
fn verify_batch_reftable_symref_ancestor_blocks_child_create() {
use crate::objects::ObjectId;
use crate::refs::{verify_ref_transaction_batch, RefBatchItem, RefnameUnavailable};
use crate::repo::init_repository;
let dir = tempfile::tempdir().expect("tempdir");
init_repository(
dir.path(),
false,
"main",
None,
crate::RefStorageFormat::Reftable,
)
.expect("init");
let git_dir = dir.path().join(".git");
reftable_write_symref(
&git_dir,
"refs/heads/parent",
"refs/heads/missing-target",
None,
None,
)
.expect("symref parent");
let child_oid = ObjectId::from_bytes(&[0x44; 20]).expect("oid");
let items = vec![RefBatchItem {
name: "refs/heads/parent/child".to_owned(),
new_oid: Some(child_oid),
}];
let err = verify_ref_transaction_batch(&git_dir, &items).unwrap_err();
assert!(
matches!(
err,
RefnameUnavailable::AncestorExists {
ref blocking,
ref new_ref,
} if blocking == "refs/heads/parent" && new_ref == "refs/heads/parent/child"
),
"unexpected {err:?}"
);
}
#[test]
fn update_refs_reftable_symref_ancestor_blocks_child_create() {
use crate::error::Error;
use crate::objects::ObjectId;
use crate::refs::gc::{update_refs, RefTransactionItem};
use crate::refs::store::RefStoreError;
use crate::refs::RefnameUnavailable;
use crate::repo::init_repository;
let dir = tempfile::tempdir().expect("tempdir");
init_repository(
dir.path(),
false,
"main",
None,
crate::RefStorageFormat::Reftable,
)
.expect("init");
let git_dir = dir.path().join(".git");
reftable_write_symref(
&git_dir,
"refs/heads/parent",
"refs/heads/missing-target",
None,
None,
)
.expect("symref parent");
let child_oid = ObjectId::from_bytes(&[0x55; 20]).expect("oid");
let err = update_refs(
&git_dir,
&[RefTransactionItem {
name: "refs/heads/parent/child".to_owned(),
new_oid: Some(child_oid),
expected_old: None,
}],
)
.unwrap_err();
match err {
Error::RefStore(RefStoreError::NameUnavailable {
reason: RefnameUnavailable::AncestorExists { blocking, new_ref },
}) => {
assert_eq!(blocking, "refs/heads/parent");
assert_eq!(new_ref, "refs/heads/parent/child");
}
other => panic!("unexpected {other:?}"),
}
}
#[test]
fn write_transaction_inject_failure_leaves_tables_list_unchanged() {
use crate::objects::ObjectId;
use crate::repo::init_repository;
use tempfile::TempDir;
let tmp = TempDir::new().unwrap();
let repo = init_repository(
tmp.path(),
false,
"main",
None,
crate::RefStorageFormat::Reftable,
)
.unwrap();
let git_dir = repo.git_dir();
let tables_list = git_dir.join("reftable/tables.list");
let before = fs::read_to_string(&tables_list).unwrap_or_default();
let oid = ObjectId::from_bytes(&[0xcd; 20]).unwrap();
set_test_inject_reftable_transaction_fail(true);
let mut stack = ReftableStack::open(&git_dir).unwrap();
let opts = read_write_options(&git_dir);
let err = stack.write_transaction(
vec![ReftableTransactionUpdate {
refname: "refs/heads/inject-fail".to_owned(),
value: Some(RefValue::Val1(oid)),
log: None,
expected_old: None,
}],
&opts,
);
set_test_inject_reftable_transaction_fail(false);
assert!(err.is_err());
let after = fs::read_to_string(&tables_list).unwrap_or_default();
assert_eq!(after, before);
}
#[test]
fn truncated_varint_is_typed() {
let err = get_varint(&[0x80], 0).expect_err("continuation byte without a successor");
assert!(matches!(
err,
Error::Reftable(ReftableError::Truncated {
field: ReftableField::Varint
})
));
assert!(matches!(
get_varint(&[], 0),
Err(Error::Reftable(ReftableError::Truncated {
field: ReftableField::Varint
}))
));
}
#[test]
fn writer_rejects_unsorted_refs_with_both_names() {
let oid = ObjectId::from_bytes(&[0x11; 20]).expect("oid");
let mut writer = ReftableWriter::new(WriteOptions::default(), 1, 1);
writer
.add_ref(RefRecord {
name: "refs/heads/b".to_owned(),
update_index: 1,
value: RefValue::Val1(oid),
})
.expect("first ref");
let err = writer
.add_ref(RefRecord {
name: "refs/heads/a".to_owned(),
update_index: 1,
value: RefValue::Val1(oid),
})
.expect_err("out of order");
match err {
Error::Reftable(ReftableError::UnsortedRefs { previous, next }) => {
assert_eq!(previous, "refs/heads/b");
assert_eq!(next, "refs/heads/a");
}
other => panic!("unexpected {other:?}"),
}
}
#[test]
fn reader_reports_footer_version_mismatch_and_short_files() {
let data = ReftableWriter::new(WriteOptions::default(), 1, 1)
.finish()
.expect("empty table");
let footer_start = data.len() - FOOTER_V1_SIZE;
let mut mismatched = data.clone();
mismatched[footer_start + 4] = 2;
assert!(matches!(
ReftableReader::new(mismatched),
Err(Error::Reftable(ReftableError::FooterVersionMismatch {
header: 1,
footer: 2
}))
));
assert!(matches!(
ReftableReader::new(data[..10].to_vec()),
Err(Error::Reftable(ReftableError::TooSmall { len: 10 }))
));
assert!(matches!(
ReftableReader::new(data[..HEADER_SIZE + 4].to_vec()),
Err(Error::Reftable(ReftableError::TooSmall { .. }))
));
}
#[test]
fn write_transaction_reports_expected_mismatch() {
use crate::repo::init_repository;
let tmp = tempfile::tempdir().expect("tempdir");
let repo = init_repository(
tmp.path(),
false,
"main",
None,
crate::RefStorageFormat::Reftable,
)
.expect("init");
let git_dir = repo.git_dir();
let opts = read_write_options(&git_dir);
let current = ObjectId::from_bytes(&[0x21; 20]).expect("oid");
let stale = ObjectId::from_bytes(&[0x22; 20]).expect("oid");
let mut stack = ReftableStack::open(&git_dir).expect("open");
stack
.write_transaction(
vec![ReftableTransactionUpdate {
refname: "refs/heads/topic".to_owned(),
value: Some(RefValue::Val1(current)),
log: None,
expected_old: None,
}],
&opts,
)
.expect("seed topic");
let update = |refname: &str| ReftableTransactionUpdate {
refname: refname.to_owned(),
value: Some(RefValue::Val1(stale)),
log: None,
expected_old: Some(stale),
};
match stack.write_transaction(vec![update("refs/heads/topic")], &opts) {
Err(Error::Reftable(ReftableError::ExpectedMismatch {
refname,
expected,
actual,
})) => {
assert_eq!(refname, "refs/heads/topic");
assert_eq!(expected, stale);
assert_eq!(actual, Some(current));
}
other => panic!("unexpected {other:?}"),
}
let err = stack
.write_transaction(vec![update("refs/heads/absent")], &opts)
.expect_err("absent ref");
assert!(matches!(
&err,
Error::Reftable(ReftableError::ExpectedMismatch { actual: None, .. })
));
assert!(err.to_string().ends_with("but found <absent>"), "{err}");
}
#[test]
fn error_display_names_the_field() {
assert_eq!(
ReftableError::Truncated {
field: ReftableField::LogEmail
}
.to_string(),
"reftable: truncated log email"
);
assert_eq!(
Error::from(ReftableError::InvalidUtf8 {
field: ReftableField::SymrefTarget
})
.to_string(),
"reftable: symref target is not valid UTF-8"
);
}
}