///| Commit-graph binary file reader
///|
pub struct CommitGraphFile {
data : Bytes
hash_size : Int
num_commits : Int
oidf_offset : Int // OID fanout chunk
oidl_offset : Int // OID lookup chunk
cdat_offset : Int // commit data chunk
edge_offset : Int // extra edges chunk (0 if absent)
}
///|
pub(all) struct CommitGraphCommitInfo {
tree : @bit.ObjectId
parents : Array[@bit.ObjectId]
committer_timestamp : Int64
}
///|
pub fn CommitGraphFile::load(
rfs : &@bit.RepoFileSystem,
git_dir : String,
) -> CommitGraphFile? raise @bit.GitError {
let graph_path = join_path(git_dir, "objects/info/commit-graph")
if !rfs.is_file(graph_path) {
return None
}
let data = rfs.read_file(graph_path)
if data.length() < 8 {
return None
}
let sig = cgraph_read_u32(data, 0)
if sig != 0x43475048 {
return None
}
let hash_version = data[5].to_int()
let hash_size = if hash_version == 2 { 32 } else { 20 }
let num_chunks = data[6].to_int()
let toc_start = 8
let mut oidf_offset = 0
let mut oidl_offset = 0
let mut cdat_offset = 0
let mut edge_offset = 0
// Parse chunk TOC: each entry is 4 bytes chunk_id + 8 bytes offset
// After the last chunk entry there is a terminator entry.
let chunk_offsets : Array[(Int, Int)] = [] // (chunk_id, offset)
for i = 0; i <= num_chunks; i = i + 1 {
let entry = toc_start + i * 12
if entry + 12 > data.length() {
break
}
let chunk_id = cgraph_read_u32(data, entry)
let offset = cgraph_read_u32(data, entry + 8) // lower 32 bits
chunk_offsets.push((chunk_id, offset))
}
for i = 0; i < chunk_offsets.length() - 1; i = i + 1 {
let (cid, off) = chunk_offsets[i]
match cid {
0x4f494446 => oidf_offset = off // OIDF
0x4f49444c => oidl_offset = off // OIDL
0x43444154 => cdat_offset = off // CDAT
0x45444745 => edge_offset = off // EDGE
_ => ()
}
}
if oidf_offset == 0 || oidl_offset == 0 || cdat_offset == 0 {
return None
}
// num_commits from fanout[255]
let num_commits = cgraph_read_u32(data, oidf_offset + 255 * 4)
Some({
data,
hash_size,
num_commits,
oidf_offset,
oidl_offset,
cdat_offset,
edge_offset,
})
}
///|
/// Look up a commit OID in the graph. Returns its position index or -1.
pub fn CommitGraphFile::find_commit(
self : CommitGraphFile,
id : @bit.ObjectId,
) -> Int {
let hex_bytes = id.bytes
let first_byte = hex_bytes[0].to_int()
// Fanout: lo = fanout[first_byte - 1], hi = fanout[first_byte]
let lo = if first_byte == 0 {
0
} else {
cgraph_read_u32(self.data, self.oidf_offset + (first_byte - 1) * 4)
}
let hi = cgraph_read_u32(self.data, self.oidf_offset + first_byte * 4)
// Binary search in OIDL[lo..hi]
let mut left = lo
let mut right = hi
while left < right {
let mid = (left + right) / 2
let cmp = cgraph_compare_oid(
self.data,
self.oidl_offset + mid * self.hash_size,
hex_bytes,
self.hash_size,
)
if cmp < 0 {
left = mid + 1
} else if cmp > 0 {
right = mid
} else {
return mid
}
}
-1
}
///|
/// Get the OID at position `pos` in the OID lookup table.
pub fn CommitGraphFile::get_oid(
self : CommitGraphFile,
pos : Int,
) -> @bit.ObjectId {
let offset = self.oidl_offset + pos * self.hash_size
if offset + self.hash_size > self.data.length() {
return @bit.ObjectId::zero()
}
@bit.ObjectId::from_bytes_at(self.data, offset, hash_size=self.hash_size)
}
///|
/// Read commit data at position `pos`. Returns (tree_oid, parent_ids, timestamp).
pub fn CommitGraphFile::read_commit(
self : CommitGraphFile,
pos : Int,
) -> (@bit.ObjectId, Array[@bit.ObjectId], Int64) {
let entry = self.cdat_offset + pos * (self.hash_size + 16)
if entry + self.hash_size + 16 > self.data.length() {
return (@bit.ObjectId::zero(), [], 0L)
}
let tree_oid = @bit.ObjectId::from_bytes_at(
self.data,
entry,
hash_size=self.hash_size,
)
let parent1_raw = cgraph_read_u32(self.data, entry + self.hash_size)
let parent2_raw = cgraph_read_u32(self.data, entry + self.hash_size + 4)
let gen_and_date = cgraph_read_u32(self.data, entry + self.hash_size + 8)
let date_low = cgraph_read_u32(self.data, entry + self.hash_size + 12)
// Format: bits 31-2 = generation number, bits 1-0 = upper 2 bits of timestamp
let date_hi = gen_and_date & 0x3
let timestamp = (date_hi.to_int64() << 32) |
date_low.to_int64().land(0xffffffffL)
// Parents
let parents : Array[@bit.ObjectId] = []
if parent1_raw != 0x70000000 && parent1_raw < self.num_commits {
parents.push(self.get_oid(parent1_raw))
}
if parent2_raw == 0x70000000 {
()
} else if (parent2_raw & 0x80000000) != 0 {
let edge_pos = parent2_raw & 0x7fffffff
if self.edge_offset > 0 {
let mut ei = edge_pos
let max_edges = (self.data.length() - self.edge_offset) / 4
while ei < max_edges {
let edge_val = cgraph_read_u32(self.data, self.edge_offset + ei * 4)
let parent_pos = edge_val & 0x3fffffff
if parent_pos < self.num_commits {
parents.push(self.get_oid(parent_pos))
}
if (edge_val & 0x80000000) != 0 {
break
}
ei += 1
}
}
} else if parent2_raw < self.num_commits {
parents.push(self.get_oid(parent2_raw))
}
(tree_oid, parents, timestamp)
}
///|
pub fn CommitGraphFile::read_commit_info(
self : CommitGraphFile,
pos : Int,
) -> CommitGraphCommitInfo {
let (tree, parents, committer_timestamp) = self.read_commit(pos)
{ tree, parents, committer_timestamp }
}
///|
/// Synthesize a commit object from graph data (for fallback when loose/packed missing).
pub fn CommitGraphFile::synthesize_commit_object(
self : CommitGraphFile,
pos : Int,
id : @bit.ObjectId,
) -> @bit.PackObject {
let (tree_oid, parents, timestamp) = self.read_commit(pos)
// Build minimal commit content
let buf = StringBuilder::new()
buf.write_string("tree ")
buf.write_string(tree_oid.to_hex())
buf.write_string("\n")
for parent in parents {
buf.write_string("parent ")
buf.write_string(parent.to_hex())
buf.write_string("\n")
}
buf.write_string("author Unknown ")
buf.write_string(timestamp.to_string())
buf.write_string(" +0000\n")
buf.write_string("committer Unknown ")
buf.write_string(timestamp.to_string())
buf.write_string(" +0000\n")
buf.write_string("\ncommit-graph synthesized\n")
let content_str = buf.to_string()
let content_bytes = @utf8.encode(content_str)
@bit.PackObject::with_metadata(
@bit.ObjectType::Commit,
content_bytes,
id,
-1,
0U,
)
}
///|
fn cgraph_read_u32(data : Bytes, offset : Int) -> Int {
(data[offset].to_int() << 24) |
(data[offset + 1].to_int() << 16) |
(data[offset + 2].to_int() << 8) |
data[offset + 3].to_int()
}
///|
fn cgraph_compare_oid(
data : Bytes,
offset : Int,
target : FixedArray[Byte],
len : Int,
) -> Int {
for i = 0; i < len; i = i + 1 {
let a = data[offset + i].to_int()
let b = target[i].to_int()
if a < b {
return -1
}
if a > b {
return 1
}
}
0
}