use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use prikk_error::Result;
use prikk_object::{BlobKind, CanonicalEncode, ObjectId};
use crate::fsutil::{
RootFileStat, read_file_if_exists, stat_file_state_if_exists, write_file_atomically,
};
use crate::layout::RepositoryLayout;
const INDEX_FILE_NAME: &str = "commit-index.v1";
const INDEX_MAGIC: &str = "PRIKK-COMMIT-INDEX-V1";
const FIELD_COUNT: usize = 7;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CommitIndexEntry {
pub(crate) size: u64,
pub(crate) mtime_secs: i64,
pub(crate) mtime_nanos: u32,
pub(crate) mode: u32,
pub(crate) kind: BlobKind,
pub(crate) content_hash: ObjectId,
}
impl CommitIndexEntry {
pub(crate) fn matches_stat(&self, stat: &RootFileStat) -> bool {
self.size == stat.size
&& self.mtime_secs == stat.mtime_secs
&& self.mtime_nanos == stat.mtime_nanos
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct CommitIndex {
entries: BTreeMap<String, CommitIndexEntry>,
}
impl CommitIndex {
pub(crate) fn load(layout: &RepositoryLayout) -> Result<Self> {
let relative = layout.repository_relative(&index_path(layout))?;
let Some(bytes) = read_file_if_exists(layout.repository_mutation_root(), &relative)? else {
return Ok(Self::default());
};
Ok(parse(&bytes).unwrap_or_default())
}
pub(crate) fn save(&self, layout: &RepositoryLayout) -> Result<()> {
let relative = layout.repository_relative(&index_path(layout))?;
write_file_atomically(
layout.repository_mutation_root(),
&relative,
&serialize(self),
)
}
pub(crate) fn get(&self, path: &str) -> Option<&CommitIndexEntry> {
self.entries.get(path)
}
pub(crate) fn record(&mut self, path: String, entry: CommitIndexEntry) {
self.entries.insert(path, entry);
}
pub(crate) fn retain_paths(&mut self, live_paths: &BTreeSet<String>) {
self.entries.retain(|path, _| live_paths.contains(path));
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.entries.len()
}
pub(crate) fn entries(&self) -> &BTreeMap<String, CommitIndexEntry> {
&self.entries
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommitIndexDivergence {
pub path: String,
pub recorded_hash: ObjectId,
pub actual_hash: ObjectId,
}
pub(crate) fn verify_divergence(layout: &RepositoryLayout) -> Result<Vec<CommitIndexDivergence>> {
let index = CommitIndex::load(layout)?;
let mut divergences = Vec::new();
for (path, entry) in index.entries() {
let relative = Path::new(path);
let Some(stat) = stat_file_state_if_exists(layout.worktree_mutation_root(), relative)?
else {
continue;
};
if !entry.matches_stat(&stat) {
continue;
}
let Some(bytes) = read_file_if_exists(layout.worktree_mutation_root(), relative)? else {
continue;
};
let actual_hash = content_hash(entry.kind, &bytes)?;
if actual_hash != entry.content_hash {
divergences.push(CommitIndexDivergence {
path: path.clone(),
recorded_hash: entry.content_hash,
actual_hash,
});
}
}
Ok(divergences)
}
fn index_path(layout: &RepositoryLayout) -> std::path::PathBuf {
layout.cache_dir().join(INDEX_FILE_NAME)
}
fn serialize(index: &CommitIndex) -> Vec<u8> {
let mut out = String::new();
out.push_str(INDEX_MAGIC);
out.push('\n');
for (path, entry) in &index.entries {
out.push_str(path);
out.push('\t');
out.push_str(&entry.size.to_string());
out.push('\t');
out.push_str(&entry.mtime_secs.to_string());
out.push('\t');
out.push_str(&entry.mtime_nanos.to_string());
out.push('\t');
out.push_str(&entry.mode.to_string());
out.push('\t');
out.push_str(&entry.kind.code().to_string());
out.push('\t');
out.push_str(&entry.content_hash.to_hex());
out.push('\n');
}
out.into_bytes()
}
fn parse(bytes: &[u8]) -> Option<CommitIndex> {
let text = std::str::from_utf8(bytes).ok()?;
let mut lines = text.lines();
if lines.next()? != INDEX_MAGIC {
return None;
}
let mut entries = BTreeMap::new();
for line in lines {
if line.is_empty() {
continue;
}
let fields: Vec<&str> = line.split('\t').collect();
if fields.len() != FIELD_COUNT {
return None;
}
let path = fields.first()?.to_string();
let size: u64 = fields.get(1)?.parse().ok()?;
let mtime_secs: i64 = fields.get(2)?.parse().ok()?;
let mtime_nanos: u32 = fields.get(3)?.parse().ok()?;
let mode: u32 = fields.get(4)?.parse().ok()?;
let kind_code: u16 = fields.get(5)?.parse().ok()?;
let kind = BlobKind::from_code(kind_code).ok()?;
let content_hash: ObjectId = fields.get(6)?.parse().ok()?;
entries.insert(
path,
CommitIndexEntry {
size,
mtime_secs,
mtime_nanos,
mode,
kind,
content_hash,
},
);
}
Some(CommitIndex { entries })
}
pub(crate) fn content_hash(kind: BlobKind, bytes: &[u8]) -> Result<ObjectId> {
let payload = prikk_object::BlobPayload::new(kind, bytes.to_vec());
let canonical = payload.to_canonical_bytes()?;
Ok(ObjectId::from_canonical_payload(
prikk_object::ObjectType::Blob,
1,
&canonical,
))
}
#[cfg(test)]
mod tests;