use crate::candidate_reader::OBJECT_MISSING_PREFIX;
use crate::diagnostic_log::warn;
use crate::diagnostics::EngineFailureCode;
use crate::policy_events::PolicyEvent;
use crate::repo_read_layout::ObjectFormat;
use crate::repo_read_object::{ObjectKind, from_hex, name_length};
use crate::repo_read_odb::ObjectDatabase;
use crate::repo_read_tree::{MODE_GITLINK, is_tree_mode, parse_tree};
use crate::service_protocol::CommitRequest;
use crate::transaction_phase::{Phase, PhaseMarkerError, reach_transaction_phase};
use std::collections::HashSet;
use std::path::Path;
use std::sync::atomic::{AtomicU64, Ordering};
const TAG: &str = "cli-git-service";
static STAGED_REACHED: AtomicU64 = AtomicU64::new(0);
static PREPARED_REACHED: AtomicU64 = AtomicU64::new(0);
pub fn content_was_removed(events: &[PolicyEvent]) -> bool {
return events.iter().any(|event| {
return matches!(
event,
PolicyEvent::EngineFailure { code: EngineFailureCode::ContentUnavailable, message, .. } if message.starts_with(OBJECT_MISSING_PREFIX)
);
});
}
pub const CLOSURE_ENTRY_BUDGET: usize = 5_000_000;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ClosureEnd {
Intact,
Missing,
OutOfBudget,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct ClosureWalk {
end: ClosureEnd,
examined: usize,
}
fn walk_tree_closures(database: &ObjectDatabase, roots: &[&str], format: ObjectFormat, budget: usize) -> ClosureWalk {
let mut pending: Vec<Vec<u8>> = roots.iter().filter_map(|root| { return from_hex(root.as_bytes(), format); }).collect();
let mut visited: HashSet<Vec<u8>> = HashSet::new();
let mut examined: usize = 0;
while let Some(name) = pending.pop() {
if !visited.insert(name.clone()) {
continue;
}
if examined >= budget {
return ClosureWalk { end: ClosureEnd::OutOfBudget, examined };
}
examined += 1;
let Some(object) = database.read(name.as_slice()) else {
if database.contains(name.as_slice()) {
continue;
}
return ClosureWalk { end: ClosureEnd::Missing, examined };
};
if object.kind != ObjectKind::Tree {
continue;
}
let Some(entries) = parse_tree(object.bytes.as_slice(), name_length(format)) else { continue };
if entries.len() > budget - examined {
return ClosureWalk { end: ClosureEnd::OutOfBudget, examined };
}
examined += entries.len();
for entry in entries {
if entry.mode == MODE_GITLINK {
continue;
}
if is_tree_mode(entry.mode) {
pending.push(entry.id);
} else if visited.insert(entry.id.clone()) && !database.contains(entry.id.as_slice()) {
return ClosureWalk { end: ClosureEnd::Missing, examined };
}
}
}
return ClosureWalk { end: ClosureEnd::Intact, examined };
}
fn tree_closures_missing(database: &ObjectDatabase, roots: &[&str], format: ObjectFormat) -> bool {
let walk: ClosureWalk = walk_tree_closures(database, roots, format, CLOSURE_ENTRY_BUDGET);
if walk.end == ClosureEnd::OutOfBudget {
warn(TAG, &format!("the trees of the batch hold more than {CLOSURE_ENTRY_BUDGET} entries; their objects are not checked here and are left to Git"));
}
return walk.end == ClosureEnd::Missing;
}
pub fn prepared_content_missing(common_dir: &Path, objects_dir: &Path, object_format: &str, request: &CommitRequest) -> bool {
let format: ObjectFormat = if object_format == ObjectFormat::Sha256.name() { ObjectFormat::Sha256 } else { ObjectFormat::Sha1 };
let Some(database) = ObjectDatabase::open(common_dir, Some(objects_dir), format) else { return false };
let named = request
.changes
.iter()
.filter_map(|change| { return change.blob.as_deref(); })
.chain(request.tree.as_deref())
.chain(request.commit.as_deref());
for name in named {
match from_hex(name.as_bytes(), format) {
Some(bytes) if !database.contains(bytes.as_slice()) => return true,
Some(_) | None => {}
}
}
return request.tree.as_deref().is_some_and(|tree| { return tree_closures_missing(&database, &[tree], format); });
}
pub fn batch_content_missing(common_dir: &Path, requests: &[&CommitRequest]) -> bool {
let names: Vec<&str> = requests
.iter()
.flat_map(|request| {
return request.changes.iter().filter_map(|change| { return change.blob.as_deref(); }).chain(request.tree.as_deref()).chain(request.commit.as_deref());
})
.collect();
let objects: std::path::PathBuf = common_dir.join("objects");
let is_loose = |name: &str| -> bool { return name.len() > 2 && objects.join(&name[..2]).join(&name[2..]).is_file(); };
let absent: Vec<&str> = names.into_iter().filter(|name| { return !is_loose(name); }).collect();
let trees: Vec<&str> = requests.iter().filter_map(|request| { return request.tree.as_deref(); }).collect();
let Some(sample) = absent.first().or(trees.first()) else { return false };
let format: ObjectFormat = if sample.len() == ObjectFormat::Sha256.hex_length() { ObjectFormat::Sha256 } else { ObjectFormat::Sha1 };
let Some(database) = ObjectDatabase::open(common_dir, None, format) else { return false };
if absent.iter().any(|name| { return from_hex(name.as_bytes(), format).is_some_and(|bytes| { return !database.contains(bytes.as_slice()); }); }) {
return true;
}
return tree_closures_missing(&database, &trees, format);
}
pub fn reach_service_phase(phase: Phase) -> Result<(), PhaseMarkerError> {
let counter: &AtomicU64 = if phase == Phase::ServiceStaged { &STAGED_REACHED } else { &PREPARED_REACHED };
let number: u64 = counter.fetch_add(1, Ordering::SeqCst) + 1;
return reach_transaction_phase(phase, Some(number));
}
#[cfg(test)]
#[path = "service_pruned_tests.rs"]
mod tests;