use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::object::{Commit, ObjectType};
use crate::odb::ObjectDatabase;
use crate::oid::{HashAlgo, ObjectId};
const SIGNATURE: &[u8; 4] = b"CGPH";
const CHUNK_OIDF: u32 = 0x4f49_4446; const CHUNK_OIDL: u32 = 0x4f49_444c; const CHUNK_CDAT: u32 = 0x4344_4154;
const PARENT_NONE: u32 = 0x7000_0000;
pub fn write_commit_graph<D: ObjectDatabase>(odb: &D, commits: &[ObjectId]) -> Result<Vec<u8>> {
let algo = odb.algo();
let hlen = algo.raw_len();
let mut ids: Vec<ObjectId> = commits.to_vec();
ids.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
ids.dedup();
let pos_of: BTreeMap<&[u8], u32> = ids
.iter()
.enumerate()
.map(|(i, id)| (id.as_bytes(), i as u32))
.collect();
let mut parsed: Vec<Commit> = Vec::with_capacity(ids.len());
for id in &ids {
let (ty, payload) = odb.read(id)?;
if ty != ObjectType::Commit {
return Err(Error::Pack("commit-graph: non-commit id supplied".into()));
}
parsed.push(Commit::parse(algo, &payload)?);
}
let n = ids.len();
let mut gens = alloc::vec![0u32; n];
let mut changed = true;
let mut guard = 0usize;
while changed {
changed = false;
guard += 1;
if guard > n + 1 {
return Err(Error::Pack("commit-graph: parent cycle".into()));
}
for (i, c) in parsed.iter().enumerate() {
let mut g = 1u32;
for p in &c.parents {
let pp = *pos_of
.get(p.as_bytes())
.ok_or_else(|| Error::Pack("commit-graph: parent not in the set".into()))?;
g = g.max(gens[pp as usize] + 1);
}
if g != gens[i] {
gens[i] = g;
changed = true;
}
}
}
let mut oidf = Vec::with_capacity(256 * 4);
let mut acc = 0u32;
for b in 0..=255u8 {
acc += ids.iter().filter(|id| id.as_bytes()[0] == b).count() as u32;
oidf.extend_from_slice(&acc.to_be_bytes());
}
let mut oidl = Vec::with_capacity(n * hlen);
for id in &ids {
oidl.extend_from_slice(&id.as_bytes()[..hlen]);
}
let mut cdat = Vec::with_capacity(n * (hlen + 16));
for (i, c) in parsed.iter().enumerate() {
cdat.extend_from_slice(&c.tree.as_bytes()[..hlen]);
let parent_pos = |k: usize| -> Result<u32> {
match c.parents.get(k) {
None => Ok(PARENT_NONE),
Some(p) => Ok(*pos_of
.get(p.as_bytes())
.ok_or_else(|| Error::Pack("commit-graph: parent not in the set".into()))?),
}
};
if c.parents.len() > 2 {
return Err(Error::Unsupported(
"commit-graph: octopus merges (EDGE chunk) not yet supported".into(),
));
}
cdat.extend_from_slice(&parent_pos(0)?.to_be_bytes());
cdat.extend_from_slice(&parent_pos(1)?.to_be_bytes());
let date = c.committer.time.max(0) as u64;
let hi2 = ((date >> 32) & 0x3) as u32;
let word0 = (hi2 << 30) | (gens[i] & 0x3fff_ffff);
cdat.extend_from_slice(&word0.to_be_bytes());
cdat.extend_from_slice(&((date & 0xffff_ffff) as u32).to_be_bytes());
}
let num_chunks = 3u8;
let header_len = 8;
let table_len = (num_chunks as usize + 1) * 12;
let mut offset = (header_len + table_len) as u64;
let oidf_off = offset;
offset += oidf.len() as u64;
let oidl_off = offset;
offset += oidl.len() as u64;
let cdat_off = offset;
offset += cdat.len() as u64;
let end_off = offset;
let mut out = Vec::with_capacity(end_off as usize + hlen);
out.extend_from_slice(SIGNATURE);
out.push(1); out.push(if algo == HashAlgo::Sha256 { 2 } else { 1 });
out.push(num_chunks);
out.push(0); for (id, off) in [
(CHUNK_OIDF, oidf_off),
(CHUNK_OIDL, oidl_off),
(CHUNK_CDAT, cdat_off),
(0, end_off),
] {
out.extend_from_slice(&id.to_be_bytes());
out.extend_from_slice(&off.to_be_bytes());
}
out.extend_from_slice(&oidf);
out.extend_from_slice(&oidl);
out.extend_from_slice(&cdat);
let checksum = crate::hash::hash_bytes(algo, &out);
out.extend_from_slice(&checksum);
Ok(out)
}
pub struct CommitGraph {
algo: HashAlgo,
oids: Vec<u8>,
cdat: Vec<u8>,
count: usize,
}
impl CommitGraph {
pub fn parse(algo: HashAlgo, data: &[u8]) -> Result<Self> {
let hlen = algo.raw_len();
if data.len() < 8 + hlen || &data[..4] != SIGNATURE {
return Err(Error::Pack("commit-graph: bad header".into()));
}
if data[4] != 1 {
return Err(Error::Unsupported(
"commit-graph: unsupported version".into(),
));
}
let body = &data[..data.len() - hlen];
if crate::hash::hash_bytes(algo, body) != data[data.len() - hlen..] {
return Err(Error::Pack("commit-graph: checksum mismatch".into()));
}
let num_chunks = data[6] as usize;
let table = 8;
let mut chunks: BTreeMap<u32, (usize, usize)> = BTreeMap::new();
let mut entries: Vec<(u32, u64)> = Vec::with_capacity(num_chunks + 1);
for i in 0..=num_chunks {
let base = table + i * 12;
if base + 12 > body.len() {
return Err(Error::Pack("commit-graph: truncated chunk table".into()));
}
let id =
u32::from_be_bytes([data[base], data[base + 1], data[base + 2], data[base + 3]]);
let off = u64::from_be_bytes(data[base + 4..base + 12].try_into().expect("8 bytes"));
entries.push((id, off));
}
for w in entries.windows(2) {
let (id, start) = w[0];
let (_, end) = w[1];
if (start as usize) > body.len() || (end as usize) > body.len() || end < start {
return Err(Error::Pack("commit-graph: bad chunk offsets".into()));
}
chunks.insert(id, (start as usize, end as usize));
}
let (ol_start, ol_end) = *chunks
.get(&CHUNK_OIDL)
.ok_or_else(|| Error::Pack("commit-graph: no OIDL chunk".into()))?;
let (cd_start, cd_end) = *chunks
.get(&CHUNK_CDAT)
.ok_or_else(|| Error::Pack("commit-graph: no CDAT chunk".into()))?;
let oids = data[ol_start..ol_end].to_vec();
let cdat = data[cd_start..cd_end].to_vec();
let count = oids.len() / hlen;
if count * (hlen + 16) != cdat.len() {
return Err(Error::Pack("commit-graph: OIDL/CDAT size mismatch".into()));
}
Ok(CommitGraph {
algo,
oids,
cdat,
count,
})
}
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn position_of(&self, id: &ObjectId) -> Option<usize> {
if id.algo() != self.algo {
return None;
}
let hlen = self.algo.raw_len();
let needle = &id.as_bytes()[..hlen];
let (mut lo, mut hi) = (0usize, self.count);
while lo < hi {
let mid = (lo + hi) / 2;
match self.oids[mid * hlen..mid * hlen + hlen].cmp(needle) {
core::cmp::Ordering::Less => lo = mid + 1,
core::cmp::Ordering::Greater => hi = mid,
core::cmp::Ordering::Equal => return Some(mid),
}
}
None
}
pub fn id_at(&self, pos: usize) -> Option<ObjectId> {
let hlen = self.algo.raw_len();
let row = self.oids.get(pos * hlen..pos * hlen + hlen)?;
ObjectId::from_bytes(self.algo, row).ok()
}
fn record(&self, pos: usize) -> &[u8] {
let hlen = self.algo.raw_len();
let sz = hlen + 16;
&self.cdat[pos * sz..pos * sz + sz]
}
pub fn tree_at(&self, pos: usize) -> Option<ObjectId> {
let hlen = self.algo.raw_len();
ObjectId::from_bytes(self.algo, &self.record(pos)[..hlen]).ok()
}
pub fn parents_at(&self, pos: usize) -> Vec<ObjectId> {
let hlen = self.algo.raw_len();
let rec = self.record(pos);
let mut out = Vec::new();
for k in 0..2 {
let off = hlen + k * 4;
let v = u32::from_be_bytes([rec[off], rec[off + 1], rec[off + 2], rec[off + 3]]);
if v == PARENT_NONE || v >= 0x8000_0000 {
continue;
}
if let Some(id) = self.id_at(v as usize) {
out.push(id);
}
}
out
}
pub fn generation_at(&self, pos: usize) -> u32 {
let hlen = self.algo.raw_len();
let rec = self.record(pos);
let off = hlen + 8;
let word0 = u32::from_be_bytes([rec[off], rec[off + 1], rec[off + 2], rec[off + 3]]);
word0 & 0x3fff_ffff
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::object::Signature;
use crate::odb::MemoryOdb;
fn sig(t: i64) -> Signature {
Signature {
name: b"T".to_vec(),
email: b"t@e".to_vec(),
time: t,
tz: b"+0000".to_vec(),
}
}
#[test]
fn write_parse_round_trip() {
let odb = MemoryOdb::new(HashAlgo::Sha1);
let tree = odb.write(ObjectType::Tree, b"").unwrap();
let mk = |parents: Vec<ObjectId>, t: i64| {
let c = Commit {
tree,
parents,
author: sig(t),
committer: sig(t),
extra_headers: Vec::new(),
message: b"m\n".to_vec(),
};
odb.write(ObjectType::Commit, &c.serialize()).unwrap()
};
let c1 = mk(alloc::vec![], 1000);
let c2 = mk(alloc::vec![c1], 2000);
let c3 = mk(alloc::vec![c2, c1], 3000);
let bytes = write_commit_graph(&odb, &[c1, c2, c3]).unwrap();
let g = CommitGraph::parse(HashAlgo::Sha1, &bytes).unwrap();
assert_eq!(g.len(), 3);
let p3 = g.position_of(&c3).unwrap();
assert_eq!(g.generation_at(p3), 3);
assert_eq!(g.generation_at(g.position_of(&c1).unwrap()), 1);
assert_eq!(g.generation_at(g.position_of(&c2).unwrap()), 2);
let mut ps = g.parents_at(p3);
ps.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
let mut want = alloc::vec![c1, c2];
want.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
assert_eq!(ps, want);
assert_eq!(g.tree_at(p3), Some(tree));
let mut bad = bytes.clone();
*bad.last_mut().unwrap() ^= 0xff;
assert!(CommitGraph::parse(HashAlgo::Sha1, &bad).is_err());
}
}