use crate::repo_read_layout::ObjectFormat;
use crate::repo_read_object::{ObjectKind, from_hex, name_length, to_hex};
use crate::repo_read_odb::ObjectDatabase;
use crate::repo_read_tree::{MODE_GITLINK, TreeEntry, is_tree_mode, parse_commit, parse_tree};
use crate::config_snapshot::ConfigSnapshot;
use crate::transaction_git::GitContext;
use std::collections::HashMap;
use std::path::Path;
const ENTRY_BUDGET: usize = 1_000_000;
const TREE_DEPTH_LIMIT: usize = 256;
thread_local! {
static NAME_STEPS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
#[cfg(test)]
pub(crate) fn name_steps() -> u64 {
return NAME_STEPS.with(std::cell::Cell::get);
}
#[derive(Debug, Eq, PartialEq)]
pub struct BuiltCommit {
name: Vec<u8>,
pub parents: Vec<String>,
tree: Vec<u8>,
}
fn format_of(hex: &str) -> ObjectFormat {
return if hex.len() == ObjectFormat::Sha256.hex_length() { ObjectFormat::Sha256 } else { ObjectFormat::Sha1 };
}
const HISTORY_VARIABLES: [&str; 6] = [
"GIT_SHALLOW_FILE",
"GIT_GRAFT_FILE",
"GIT_REPLACE_REF_BASE",
"GIT_NO_REPLACE_OBJECTS",
"GIT_OBJECT_DIRECTORY",
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
];
fn history_is_rewritten(common_dir: &Path) -> bool {
return common_dir.join("shallow").exists() || common_dir.join("info").join("grafts").exists();
}
pub fn history_is_plain(common_dir: &Path, git: &GitContext, snapshot: Option<&ConfigSnapshot>) -> bool {
let Some(read) = snapshot else { return false };
if history_is_rewritten(common_dir) {
return false;
}
let overlay_sets = |name: &str| -> bool { return git.overlay.iter().any(|(key, _)| { return key.to_string_lossy() == name; }); };
if HISTORY_VARIABLES.iter().any(|name| { return std::env::var_os(name).is_some() || overlay_sets(name); }) {
return false;
}
let prefix_changes_replacements = git.global_prefix.iter().any(|word| {
let text = word.to_string_lossy().to_ascii_lowercase();
return text == "--no-replace-objects" || text.starts_with("--replace-refs") || text.contains("core.usereplacerefs");
});
return !prefix_changes_replacements && !read.entries().iter().any(|entry| { return entry.key == "core.usereplacerefs"; });
}
pub fn read_commit(common_dir: &Path, built: &str) -> Option<BuiltCommit> {
if history_is_rewritten(common_dir) {
return None;
}
let format: ObjectFormat = format_of(built);
let database: ObjectDatabase = ObjectDatabase::open(common_dir, None, format)?;
let object = database.read(from_hex(built.as_bytes(), format)?.as_slice())?;
if object.kind != ObjectKind::Commit {
return None;
}
let fields = parse_commit(object.bytes.as_slice(), format)?;
return Some(BuiltCommit { name: from_hex(built.as_bytes(), format)?, parents: fields.parents.iter().map(|parent| { return to_hex(parent.as_slice()); }).collect(), tree: fields.tree });
}
#[derive(Debug, Eq, PartialEq)]
enum Found {
Present,
Unproven,
}
struct Proof<'a> {
database: &'a ObjectDatabase,
format: ObjectFormat,
budget: usize,
}
impl Proof<'_> {
fn entries(&self, name: &[u8]) -> Option<Vec<TreeEntry>> {
let object = self.database.read(name)?;
if object.kind != ObjectKind::Tree {
return None;
}
return parse_tree(object.bytes.as_slice(), name_length(self.format));
}
fn tree(&mut self, new: &[u8], old: Option<&[u8]>, depth: usize) -> Found {
if old == Some(new) {
return Found::Present;
}
if depth > TREE_DEPTH_LIMIT {
return Found::Unproven;
}
let Some(wanted) = self.entries(new) else { return Found::Unproven };
let before: Vec<TreeEntry> = old.and_then(|name| { return self.entries(name); }).unwrap_or_default();
let cost: usize = wanted.len().saturating_add(before.len());
if cost > self.budget {
return Found::Unproven;
}
self.budget -= cost;
let mut by_name: HashMap<&[u8], &TreeEntry> = HashMap::with_capacity(before.len());
for candidate in &before {
NAME_STEPS.with(|steps| { steps.set(steps.get() + 1); });
by_name.insert(candidate.name.as_slice(), candidate);
}
for entry in wanted {
NAME_STEPS.with(|steps| { steps.set(steps.get() + 1); });
let same_name: Option<&&TreeEntry> = by_name.get(entry.name.as_slice());
if same_name.is_some_and(|candidate| { return candidate.id == entry.id && candidate.mode == entry.mode; }) || entry.mode == MODE_GITLINK {
continue;
}
if is_tree_mode(entry.mode) {
let below: Option<&[u8]> = same_name.filter(|candidate| { return is_tree_mode(candidate.mode); }).map(|candidate| { return candidate.id.as_slice(); });
if self.tree(entry.id.as_slice(), below, depth + 1) == Found::Unproven {
return Found::Unproven;
}
} else if !self.database.contains(entry.id.as_slice()) {
return Found::Unproven;
}
}
return Found::Present;
}
}
pub fn adds_only_present_objects(common_dir: &Path, built: &BuiltCommit, tip: &str) -> bool {
if history_is_rewritten(common_dir) || built.parents.len() != 1 || built.parents[0] != tip {
return false;
}
let format: ObjectFormat = format_of(tip);
let Some(database) = ObjectDatabase::open(common_dir, None, format) else { return false };
let Some(tip_bytes) = from_hex(tip.as_bytes(), format) else { return false };
let Some(tip_object) = database.read(tip_bytes.as_slice()) else { return false };
let Some(tip_fields) = (tip_object.kind == ObjectKind::Commit).then(|| { return parse_commit(tip_object.bytes.as_slice(), format); }).flatten() else { return false };
if !database.contains(built.name.as_slice()) {
return false;
}
let mut proof: Proof<'_> = Proof { database: &database, format, budget: ENTRY_BUDGET };
return proof.tree(built.tree.as_slice(), Some(tip_fields.tree.as_slice()), 0) == Found::Present;
}