use crate::{BloomFilterSettings, File, Graph, MAX_COMMITS};
use gix_error::{ErrorExt, Exn, Message, ResultExt, message};
use std::{
io::{BufRead, BufReader},
path::Path,
};
/// Instantiate a `Graph` from various sources.
impl Graph {
/// Instantiate a commit graph from `path` which may be a directory containing graph files or the graph file itself.
pub fn at(path: &Path) -> Result<Self, Exn<Message>> {
Self::try_from(path)
}
/// Instantiate a commit graph from the directory containing all of its files.
///
/// Loading stops at the first layer that can't be opened or parsed, whose checksum doesn't match its
/// name in the chain, or whose `BASE` chunk doesn't list the layers below it in chain order, keeping only the
/// layers loaded so far. It's an error if no layer could be loaded.
pub fn from_commit_graphs_dir(path: &Path) -> Result<Self, Exn<Message>> {
let commit_graphs_dir = path;
let chain_file_path = commit_graphs_dir.join("commit-graph-chain");
let chain_file = std::fs::File::open(&chain_file_path).or_raise(|| {
message!(
"Could not open commit-graph chain file at '{}'",
chain_file_path.display()
)
})?;
let mut files = Vec::new();
for line in BufReader::new(chain_file).lines() {
let hash = line.or_raise(|| {
message!(
"Could not read from commit-graph file at '{}'",
chain_file_path.display()
)
})?;
let Ok(id) = gix_hash::ObjectId::from_hex(hash.as_bytes()) else {
break;
};
let Ok(file) = File::at(commit_graphs_dir.join(format!("graph-{hash}.graph"))) else {
break;
};
if file.checksum() != id || !base_graphs_match(&file, &files) {
break;
}
files.push(file);
}
Ok(Self::new(files)?)
}
/// Instantiate a commit graph from a `.git/objects/info/commit-graph` or
/// `.git/objects/info/commit-graphs/graph-*.graph` file.
pub fn from_file(path: &Path) -> Result<Self, Exn<Message>> {
let file = File::at(path)
.or_raise(|| message!("Could not open commit-graph file at '{}'", path.display()))?;
Ok(Self::new(vec![file])?)
}
/// Instantiate a commit graph from an `.git/objects/info` directory.
pub fn from_info_dir(info_dir: &Path) -> Result<Self, Exn<Message>> {
Self::from_file(&info_dir.join("commit-graph"))
.or_else(|_| Self::from_commit_graphs_dir(&info_dir.join("commit-graphs")))
}
/// Create a new commit graph from a list of `files`.
pub fn new(files: Vec<File>) -> Result<Self, Message> {
let mut files = nonempty::NonEmpty::from_vec(files)
.ok_or_else(|| message!("Commit-graph must contain at least one file"))?;
let num_commits: u64 = files.iter().map(|f| u64::from(f.num_commits())).sum();
if num_commits > u64::from(MAX_COMMITS) {
return Err(message!(
"Commit-graph files contain {num_commits} commits altogether, but only {MAX_COMMITS} commits are allowed"
));
}
let mut f1 = files.first();
for f2 in files.tail() {
if f1.object_hash() != f2.object_hash() {
return Err(message!(
"Commit-graph files mismatch: '{path1}' uses hash {hash1:?}, but '{path2}' uses hash {hash2:?}",
path1 = f1.path().display(),
hash1 = f1.object_hash(),
path2 = f2.path().display(),
hash2 = f2.object_hash(),
));
}
f1 = f2;
}
disable_incompatible_bloom_layers(&mut files);
if !files.iter().all(File::has_generation_data) {
files.iter_mut().for_each(File::disable_generation_data);
}
Ok(Self { files })
}
}
/// Return `true` if `file`'s `BASE` chunk lists the layers `below` it in chain order.
fn base_graphs_match(file: &File, below: &[File]) -> bool {
usize::from(file.base_graph_count()) >= below.len()
&& file
.iter_base_graph_ids()
.zip(below)
.all(|(base, below)| base == below.checksum())
}
fn disable_incompatible_bloom_layers(files: &mut nonempty::NonEmpty<File>) {
let mut preferred: Option<BloomFilterSettings> = None;
for file in files.iter_mut().rev() {
let Some(settings) = file.bloom_filter_settings else {
continue;
};
if preferred.is_none() {
preferred = Some(settings);
continue;
}
if preferred != Some(settings) {
file.clear_bloom_filters();
}
}
}
impl TryFrom<&Path> for Graph {
type Error = Exn<Message>;
fn try_from(path: &Path) -> Result<Self, Self::Error> {
if path.is_file() {
// Assume we are looking at `.git/objects/info/commit-graph` or
// `.git/objects/info/commit-graphs/graph-*.graph`.
Self::from_file(path)
} else if path.is_dir() {
if path.join("commit-graph-chain").is_file() {
Self::from_commit_graphs_dir(path)
} else {
Self::from_info_dir(path)
}
} else {
Err(message!(
"Did not find any files that look like commit graphs at '{}'",
path.display()
)
.raise())
}
}
}