use std::collections::{BTreeMap, BTreeSet};
use prikk_error::{PrikkError, Result};
use prikk_object::{ObjectType, RefKind, RefStatePayload};
use crate::byte_cursor::ByteCursor;
use crate::file_codec::{push_string_u16, push_u64};
use crate::fsutil::len_to_u64;
use crate::layout::RepositoryLayout;
use crate::object_store::{ObjectReadSnapshot, ObjectReader};
use crate::patch_set_digest::{
PatchSetDigest, compute_patch_set_digest, compute_patch_set_digest_for_ref,
patch_ids_reachable_from_block,
};
use crate::refs::RefStore;
const SYNC_SUMMARY_MAGIC: &[u8; 8] = b"PSYNCSU1";
pub const DEFAULT_SYNC_SUMMARY_MAX_REF_COUNT: usize = 100_000;
pub const DEFAULT_SYNC_SUMMARY_MAX_TOTAL_BYTES: usize = 16 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyncSummaryRefEntry {
pub ref_name: String,
pub digest: PatchSetDigest,
pub patch_count: u64,
}
pub fn build_sync_summary(layout: &RepositoryLayout) -> Result<Vec<u8>> {
let ref_store = RefStore::new(layout.clone());
let object_store = ObjectReadSnapshot::open(layout)?;
let mut entries: Vec<(String, PatchSetDigest, u64)> = Vec::new();
for pointer in ref_store.list_ref_pointers()? {
if !pointer.ref_name.starts_with("heads/") {
continue;
}
let envelope = object_store
.read_typed(pointer.ref_state_id, ObjectType::RefState)?
.ok_or_else(|| {
PrikkError::Integrity(format!(
"ref {} names missing RefState {}",
pointer.ref_name, pointer.ref_state_id
))
})?;
let payload = RefStatePayload::decode_canonical(
&envelope.canonical_payload,
envelope.schema_version,
)?;
if payload.kind != RefKind::Branch {
return Err(PrikkError::Integrity(format!(
"ref {} is under heads/ but its RefState kind is not Branch",
pointer.ref_name
)));
}
let patch_ids = patch_ids_reachable_from_block(&object_store, payload.target_object_id)?;
let digest = compute_patch_set_digest(&patch_ids)?;
let patch_count = len_to_u64(patch_ids.len())?;
entries.push((pointer.ref_name, digest, patch_count));
}
let mut out = Vec::new();
out.extend_from_slice(SYNC_SUMMARY_MAGIC);
push_u64(&mut out, len_to_u64(entries.len())?);
for (ref_name, digest, patch_count) in &entries {
push_string_u16(&mut out, ref_name)?;
out.extend_from_slice(&digest.0);
push_u64(&mut out, *patch_count);
}
Ok(out)
}
pub fn decode_sync_summary(
bytes: &[u8],
max_total_bytes: usize,
max_ref_count: usize,
) -> Result<Vec<SyncSummaryRefEntry>> {
if bytes.len() > max_total_bytes {
return Err(PrikkError::MalformedData(format!(
"sync summary is {} bytes, over the configured limit of {max_total_bytes} bytes",
bytes.len()
)));
}
let mut cursor = ByteCursor::new(bytes);
let magic = cursor.read_array::<8>()?;
if &magic != SYNC_SUMMARY_MAGIC {
return Err(PrikkError::MalformedData(
"invalid sync summary magic".to_string(),
));
}
let ref_count = cursor.read_u64()?;
if ref_count > len_to_u64(max_ref_count)? {
return Err(PrikkError::MalformedData(format!(
"sync summary declares {ref_count} refs, over the configured limit of {max_ref_count}"
)));
}
let mut entries = Vec::new();
for _ in 0..ref_count {
let ref_name = cursor.read_string_u16()?;
let digest = PatchSetDigest(cursor.read_array::<32>()?);
let patch_count = cursor.read_u64()?;
entries.push(SyncSummaryRefEntry {
ref_name,
digest,
patch_count,
});
}
if !cursor.is_finished() {
return Err(PrikkError::MalformedData(
"trailing bytes in sync summary".to_string(),
));
}
Ok(entries)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SyncRefComparisonState {
InSync,
Differs,
RemoteOnly,
LocalOnly,
}
impl SyncRefComparisonState {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::InSync => "in-sync",
Self::Differs => "differs",
Self::RemoteOnly => "remote-only",
Self::LocalOnly => "local-only",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyncRefComparison {
pub ref_name: String,
pub state: SyncRefComparisonState,
}
pub fn compare_sync_summary(
layout: &RepositoryLayout,
remote: &[SyncSummaryRefEntry],
) -> Result<Vec<SyncRefComparison>> {
let ref_store = RefStore::new(layout.clone());
let mut local_ref_names: BTreeSet<String> = BTreeSet::new();
for pointer in ref_store.list_ref_pointers()? {
if pointer.ref_name.starts_with("heads/") {
local_ref_names.insert(pointer.ref_name);
}
}
let remote_digests: BTreeMap<&str, PatchSetDigest> = remote
.iter()
.map(|entry| (entry.ref_name.as_str(), entry.digest))
.collect();
let mut all_ref_names: BTreeSet<&str> = local_ref_names.iter().map(String::as_str).collect();
all_ref_names.extend(remote_digests.keys().copied());
let mut comparisons = Vec::with_capacity(all_ref_names.len());
for ref_name in all_ref_names {
let is_local = local_ref_names.contains(ref_name);
let state = match (is_local, remote_digests.get(ref_name)) {
(true, Some(remote_digest)) => {
let local_digest = compute_patch_set_digest_for_ref(layout, ref_name)?;
if &local_digest == remote_digest {
SyncRefComparisonState::InSync
} else {
SyncRefComparisonState::Differs
}
}
(true, None) => SyncRefComparisonState::LocalOnly,
(false, Some(_)) => SyncRefComparisonState::RemoteOnly,
(false, None) => unreachable!(
"ref_name is drawn from local_ref_names or remote_digests, so at least one holds it"
),
};
comparisons.push(SyncRefComparison {
ref_name: ref_name.to_string(),
state,
});
}
Ok(comparisons)
}
#[cfg(test)]
mod tests;