use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::object::ObjectType;
use crate::odb::ObjectDatabase;
use crate::oid::ObjectId;
use super::PackIndex;
use super::ewah::Ewah;
const BITMAP_VERSION: u16 = 1;
const FLAG_FULL_DAG: u16 = 1;
pub fn write_bitmap<D: ObjectDatabase>(
odb: &D,
idx: &PackIndex,
pack_hash: &ObjectId,
commits: &[ObjectId],
) -> Result<Vec<u8>> {
let algo = odb.algo();
let count = idx.len();
let mut by_offset: Vec<(u64, ObjectId)> = (0..count)
.filter_map(|row| Some((idx.offset_at(row)?, idx.id_at(row)?)))
.collect();
if by_offset.len() != count {
return Err(Error::Pack("bitmap: idx row out of range".into()));
}
by_offset.sort_unstable_by_key(|(off, _)| *off);
let mut position: BTreeMap<ObjectId, usize> = BTreeMap::new();
for (pos, (_, id)) in by_offset.iter().enumerate() {
position.insert(*id, pos);
}
let mut idx_row: BTreeMap<ObjectId, usize> = BTreeMap::new();
for row in 0..count {
if let Some(id) = idx.id_at(row) {
idx_row.insert(id, row);
}
}
let (mut commits_b, mut trees_b, mut blobs_b, mut tags_b) =
(Vec::new(), Vec::new(), Vec::new(), Vec::new());
for (pos, (_, id)) in by_offset.iter().enumerate() {
let (ty, _) = odb.read(id)?;
match ty {
ObjectType::Commit => commits_b.push(pos),
ObjectType::Tree => trees_b.push(pos),
ObjectType::Blob => blobs_b.push(pos),
ObjectType::Tag => tags_b.push(pos),
}
}
let mut out = Vec::new();
out.extend_from_slice(b"BITM");
out.extend_from_slice(&BITMAP_VERSION.to_be_bytes());
out.extend_from_slice(&FLAG_FULL_DAG.to_be_bytes());
out.extend_from_slice(&(commits.len() as u32).to_be_bytes());
out.extend_from_slice(pack_hash.as_bytes());
for set in [&commits_b, &trees_b, &blobs_b, &tags_b] {
out.extend_from_slice(&type_bitmap(set).to_bytes());
}
let mut entries: Vec<(usize, ObjectId)> = commits
.iter()
.filter_map(|c| idx_row.get(c).map(|&p| (p, *c)))
.collect();
entries.sort_unstable_by_key(|(p, _)| *p);
for (row, commit) in entries {
let reachable = crate::walk::reachable_objects(odb, &[commit])?;
let mut positions: Vec<usize> = reachable
.iter()
.filter_map(|id| position.get(id).copied())
.collect();
positions.sort_unstable();
out.extend_from_slice(&(row as u32).to_be_bytes());
out.push(0); out.push(0); out.extend_from_slice(&type_bitmap(&positions).to_bytes());
}
let checksum = crate::hash::hash_bytes(algo, &out);
out.extend_from_slice(&checksum);
Ok(out)
}
pub fn write_reverse_index(idx: &PackIndex, pack_hash: &ObjectId) -> Result<Vec<u8>> {
let algo = idx.algo();
let count = idx.len();
let mut by_offset: Vec<(u64, u32)> = (0..count)
.filter_map(|row| Some((idx.offset_at(row)?, row as u32)))
.collect();
if by_offset.len() != count {
return Err(Error::Pack("rev-index: idx row out of range".into()));
}
by_offset.sort_unstable_by_key(|(off, _)| *off);
let hash_id: u32 = match algo {
crate::oid::HashAlgo::Sha1 => 1,
crate::oid::HashAlgo::Sha256 => 2,
};
let mut out = Vec::new();
out.extend_from_slice(b"RIDX");
out.extend_from_slice(&1u32.to_be_bytes());
out.extend_from_slice(&hash_id.to_be_bytes());
for (_, idx_row) in &by_offset {
out.extend_from_slice(&idx_row.to_be_bytes());
}
out.extend_from_slice(pack_hash.as_bytes());
let checksum = crate::hash::hash_bytes(algo, &out);
out.extend_from_slice(&checksum);
Ok(out)
}
fn type_bitmap(set: &[usize]) -> Ewah {
let bit_size = set.last().map_or(0, |&b| b + 1);
Ewah::from_set_bits(set, bit_size)
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::pack::PackWriter;
#[test]
fn writes_a_parseable_bitmap_header() {
let dir =
std::env::temp_dir().join(alloc::format!("puregit-bitmap-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let repo = crate::repository::Repository::init(&dir).unwrap();
std::fs::write(dir.join("a.txt"), b"hello\n").unwrap();
repo.add_path("a.txt").unwrap();
let tip = repo
.commit(
b"c\n",
crate::object::Signature {
name: b"T".to_vec(),
email: b"t@e".to_vec(),
time: 0,
tz: b"+0000".to_vec(),
},
crate::object::Signature {
name: b"T".to_vec(),
email: b"t@e".to_vec(),
time: 0,
tz: b"+0000".to_vec(),
},
)
.unwrap();
let objs = crate::walk::reachable_objects(repo.objects(), &[tip]).unwrap();
let mut writer = PackWriter::new(repo.algo());
for id in &objs {
let (ty, payload) = crate::odb::ObjectDatabase::read(repo.objects(), id).unwrap();
writer.add(ty, &payload);
}
let out = writer.finish().unwrap();
let idx = PackIndex::parse(repo.algo(), &out.idx).unwrap();
let bitmap = write_bitmap(repo.objects(), &idx, &out.hash, &[tip]).unwrap();
assert_eq!(&bitmap[..4], b"BITM");
assert_eq!(u16::from_be_bytes([bitmap[4], bitmap[5]]), 1);
assert_eq!(
u32::from_be_bytes([bitmap[8], bitmap[9], bitmap[10], bitmap[11]]),
1
);
let id_len = repo.algo().raw_len();
assert_eq!(&bitmap[12..12 + id_len], out.hash.as_bytes());
let mut off = 12 + id_len;
let (commits_ewah, n) = Ewah::from_bytes(&bitmap[off..]).unwrap();
assert_eq!(commits_ewah.set_bits().len(), 1);
off += n;
for _ in 0..3 {
let (_, n) = Ewah::from_bytes(&bitmap[off..]).unwrap();
off += n;
}
off += 4 + 1 + 1;
let (reach, _) = Ewah::from_bytes(&bitmap[off..]).unwrap();
assert_eq!(reach.set_bits().len(), objs.len());
let rev = write_reverse_index(&idx, &out.hash).unwrap();
assert_eq!(&rev[..4], b"RIDX");
assert_eq!(u32::from_be_bytes([rev[4], rev[5], rev[6], rev[7]]), 1);
assert_eq!(rev.len(), 12 + idx.len() * 4 + 2 * id_len);
let _ = std::fs::remove_dir_all(&dir);
}
}