use std::collections::{HashMap, VecDeque};
use crate::{Commit, CommitError, ObjectId, ObjectKind, ObjectReadError, Objects, ReadLimits};
#[derive(Clone, Copy, Debug)]
pub struct HistoryLimits {
pub max_commits: usize,
pub max_parents: usize,
pub read: ReadLimits,
}
impl Default for HistoryLimits {
fn default() -> Self {
Self {
max_commits: 100_000,
max_parents: 1_000_000,
read: ReadLimits::default(),
}
}
}
impl Objects {
pub fn walk(
&self,
roots: &[ObjectId],
limits: HistoryLimits,
) -> Result<Vec<ObjectId>, HistoryError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"history.walk",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
);
let operation = || Ok(Graph::read(self, roots, limits)?.ids);
#[cfg(feature = "tracing")]
let result = span.in_scope(operation);
#[cfg(not(feature = "tracing"))]
let result = { operation }();
#[cfg(feature = "tracing")]
crate::trace::finish(&span, &result, crate::trace::history);
result
}
pub fn is_ancestor(
&self,
ancestor: ObjectId,
descendant: ObjectId,
limits: HistoryLimits,
) -> Result<bool, HistoryError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"history.is_ancestor",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
);
let operation = || {
let graph = Graph::read(self, &[descendant, ancestor], limits)?;
let marks = graph.reachable(0);
Ok(marks[graph.positions[&ancestor]])
};
#[cfg(feature = "tracing")]
let result = span.in_scope(operation);
#[cfg(not(feature = "tracing"))]
let result = { operation }();
#[cfg(feature = "tracing")]
crate::trace::finish(&span, &result, crate::trace::history);
result
}
pub fn merge_bases(
&self,
left: ObjectId,
right: ObjectId,
limits: HistoryLimits,
) -> Result<Vec<ObjectId>, HistoryError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"history.merge_bases",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
);
let operation = || {
let graph = Graph::read(self, &[left, right], limits)?;
let left_marks = graph.reachable(0);
let right_marks = graph.reachable(graph.positions[&right]);
let common: Vec<_> = left_marks
.iter()
.zip(&right_marks)
.map(|(a, b)| *a && *b)
.collect();
let mut dominated = vec![false; graph.ids.len()];
for (index, parents) in graph.parents.iter().enumerate() {
if common[index] {
for &parent in parents {
dominated[parent] = true;
}
}
}
let mut bases: Vec<_> = graph
.ids
.iter()
.enumerate()
.filter_map(|(i, id)| (common[i] && !dominated[i]).then_some(*id))
.collect();
bases.sort();
Ok(bases)
};
#[cfg(feature = "tracing")]
let result = span.in_scope(operation);
#[cfg(not(feature = "tracing"))]
let result = { operation }();
#[cfg(feature = "tracing")]
crate::trace::finish(&span, &result, crate::trace::history);
result
}
}
struct Graph {
ids: Vec<ObjectId>,
positions: HashMap<ObjectId, usize>,
parents: Vec<Vec<usize>>,
}
impl Graph {
fn read(
objects: &Objects,
roots: &[ObjectId],
limits: HistoryLimits,
) -> Result<Self, HistoryError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"history.graph",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
roots = roots.len(),
commits = tracing::field::Empty,
);
let operation = || {
let mut graph = Self {
ids: vec![],
positions: HashMap::new(),
parents: vec![],
};
for &root in roots {
graph.discover(root, limits.max_commits)?;
}
let mut remaining = limits.max_parents;
let mut index = 0;
while index < graph.ids.len() {
let id = graph.ids[index];
let object = objects
.read(id, limits.read)
.map_err(|source| HistoryError::Read { id, source })?
.ok_or(HistoryError::Missing(id))?;
if object.kind() != ObjectKind::Commit {
return Err(HistoryError::NotCommit(id));
}
let commit = Commit::parse(object.object_format(), object.data())
.map_err(|source| HistoryError::Parse { id, source })?;
let parents = if objects.shallow_roots().contains(id) {
&[]
} else {
commit.parents()
};
remaining = remaining
.checked_sub(parents.len())
.ok_or(HistoryError::Limit("parent occurrences"))?;
let mut edges = Vec::with_capacity(parents.len());
for &parent in parents {
edges.push(graph.discover(parent, limits.max_commits)?);
}
graph.parents.push(edges);
index += 1;
}
graph.check_acyclic()?;
Ok(graph)
};
#[cfg(feature = "tracing")]
let result = span.in_scope(operation);
#[cfg(not(feature = "tracing"))]
let result = { operation }();
#[cfg(feature = "tracing")]
crate::trace::finish(&span, &result, crate::trace::history);
#[cfg(feature = "tracing")]
if let Ok(graph) = &result {
span.record("commits", graph.ids.len());
}
result
}
fn discover(&mut self, id: ObjectId, limit: usize) -> Result<usize, HistoryError> {
if let Some(&index) = self.positions.get(&id) {
return Ok(index);
}
if self.ids.len() == limit {
return Err(HistoryError::Limit("commits"));
}
let index = self.ids.len();
self.ids.push(id);
self.positions.insert(id, index);
Ok(index)
}
fn check_acyclic(&self) -> Result<(), HistoryError> {
let mut children = vec![0usize; self.ids.len()];
for parents in &self.parents {
for &parent in parents {
children[parent] += 1;
}
}
let mut ready: VecDeque<_> = children
.iter()
.enumerate()
.filter_map(|(i, &n)| (n == 0).then_some(i))
.collect();
let mut visited = 0;
while let Some(index) = ready.pop_front() {
visited += 1;
for &parent in &self.parents[index] {
children[parent] -= 1;
if children[parent] == 0 {
ready.push_back(parent);
}
}
}
if visited != self.ids.len() {
return Err(HistoryError::Cycle);
}
Ok(())
}
fn reachable(&self, root: usize) -> Vec<bool> {
let mut marks = vec![false; self.ids.len()];
marks[root] = true;
let mut pending = vec![root];
while let Some(index) = pending.pop() {
for &parent in &self.parents[index] {
if !marks[parent] {
marks[parent] = true;
pending.push(parent);
}
}
}
marks
}
}
#[derive(Debug, thiserror::Error)]
pub enum HistoryError {
#[error("missing commit {0}")]
Missing(ObjectId),
#[error("object {0} is not a commit")]
NotCommit(ObjectId),
#[error("reading commit {id}: {source}")]
Read {
id: ObjectId,
#[source]
source: ObjectReadError,
},
#[error("parsing commit {id}: {source}")]
Parse {
id: ObjectId,
#[source]
source: CommitError,
},
#[error("history limit exceeded: {0}")]
Limit(&'static str),
#[error("cyclic commit ancestry")]
Cycle,
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
fn graph(parents: Vec<Vec<usize>>) -> Graph {
let ids: Vec<_> = (0..parents.len())
.map(|i| ObjectId::for_blob(crate::ObjectFormat::Sha1, &i.to_le_bytes()))
.collect();
let positions = ids.iter().enumerate().map(|(i, &id)| (id, i)).collect();
Graph {
ids,
positions,
parents,
}
}
#[rstest]
#[case::self_cycle(vec![vec![0]])]
#[case::two_node_cycle(vec![vec![1],vec![0]])]
fn rejects_cycles(#[case] parents: Vec<Vec<usize>>) {
assert!(matches!(
graph(parents).check_acyclic(),
Err(HistoryError::Cycle)
));
}
#[test]
fn duplicate_edges_are_acyclic() {
assert!(graph(vec![vec![1, 1], vec![]]).check_acyclic().is_ok());
}
#[test]
fn deep_history_uses_no_recursion() {
let mut parents: Vec<_> = (1..100_000).map(|i| vec![i]).collect();
parents.push(vec![]);
let graph = graph(parents);
assert!(graph.check_acyclic().is_ok());
assert!(graph.reachable(0).into_iter().all(|marked| marked));
}
#[test]
fn discovery_enforces_limit_before_queue_growth() {
let mut graph = graph(vec![]);
let id = ObjectId::for_blob(crate::ObjectFormat::Sha1, b"root");
assert!(matches!(
graph.discover(id, 0),
Err(HistoryError::Limit("commits"))
));
assert!(graph.ids.is_empty());
}
}