use std::collections::{HashMap, HashSet, VecDeque};
use crate::refs::RefSnapshot;
use gix_hash::ObjectId;
use crate::graph::{Graph, Missing};
use crate::{Error, Result};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ShallowRequest {
pub client_shallow: Vec<ObjectId>,
pub depth: Option<usize>,
pub relative: bool,
pub since: Option<i64>,
pub not: Vec<String>,
}
impl ShallowRequest {
pub fn is_active(&self) -> bool {
!self.client_shallow.is_empty()
|| self.depth.is_some()
|| self.since.is_some()
|| !self.not.is_empty()
}
pub fn is_deepening(&self) -> bool {
self.depth.is_some() || self.since.is_some() || !self.not.is_empty()
}
}
#[derive(Debug, Clone, Default)]
pub struct ShallowOutcome {
pub boundary: HashSet<ObjectId>,
pub client_boundary: HashSet<ObjectId>,
pub include: HashSet<ObjectId>,
pub newly_shallow: Vec<ObjectId>,
pub unshallow: Vec<ObjectId>,
pub unshallow_parents: Vec<ObjectId>,
}
impl ShallowOutcome {
pub fn has_info(&self) -> bool {
!self.newly_shallow.is_empty() || !self.unshallow.is_empty()
}
}
const DEEPEN_NOT_RULES: &[&str] = &[
"{}",
"refs/{}",
"refs/tags/{}",
"refs/heads/{}",
"refs/remotes/{}",
"refs/remotes/{}/HEAD",
];
fn resolve_deepen_not(refs: &RefSnapshot, name: &str) -> Result<ObjectId> {
let mut found: Vec<(String, ObjectId)> = Vec::new();
for rule in DEEPEN_NOT_RULES {
let candidate = rule.replace("{}", name);
if gunnar_store::refname::validate(candidate.as_bytes()).is_err() {
continue;
}
if found.iter().any(|(seen, _)| seen == &candidate) {
continue;
}
if let Some(id) = refs.resolve(&candidate)? {
found.push((candidate, id));
}
}
match found.len() {
1 => Ok(found[0].1),
0 => Err(crate::Error::protocol(format!(
"deepen-not {name:?} names no reference in this repository"
))),
n => Err(crate::Error::protocol(format!(
"ambiguous deepen-not {name:?}: it names {n} references ({})",
found
.iter()
.map(|(full, _)| full.clone())
.collect::<Vec<_>>()
.join(", ")
))),
}
}
pub(crate) fn compute(
graph: &Graph<'_>,
refs: &RefSnapshot,
want_commits: &[ObjectId],
request: &ShallowRequest,
) -> Result<ShallowOutcome> {
let client_shallow: HashSet<ObjectId> = request.client_shallow.iter().copied().collect();
if !request.is_deepening() {
return Ok(ShallowOutcome {
boundary: client_shallow.clone(),
client_boundary: client_shallow,
include: HashSet::new(),
newly_shallow: Vec::new(),
unshallow: Vec::new(),
unshallow_parents: Vec::new(),
});
}
let mut excluded: HashSet<ObjectId> = HashSet::new();
for name in &request.not {
let tip = resolve_deepen_not(refs, name)?;
let closure = graph.commit_closure(
&[tip],
&HashSet::new(),
&HashSet::new(),
None,
Missing::Skip,
)?;
excluded.extend(closure.ids);
}
let max_depth = request.depth;
let mut depth_of: HashMap<ObjectId, usize> = HashMap::new();
let mut queue: VecDeque<ObjectId> = VecDeque::new();
let mut boundary: HashSet<ObjectId> = HashSet::new();
let mut reached: HashSet<ObjectId> = HashSet::new();
let seed_depth = if request.relative { 0 } else { 1 };
for want in want_commits {
if excluded.contains(want) {
continue;
}
if let Some(since) = request.since {
match graph.commit(want) {
Ok(facts) if facts.committed_at < since => continue,
Ok(_) => {}
Err(crate::Error::ObjectMissing { .. }) => continue,
Err(err) => return Err(err),
}
}
if depth_of.insert(*want, seed_depth).is_none() {
queue.push_back(*want);
}
}
if queue.is_empty() && !want_commits.is_empty() {
return Err(Error::protocol(
"no commits selected for shallow requests: deepen-since and deepen-not between them \
exclude every requested tip",
));
}
while let Some(id) = queue.pop_front() {
reached.insert(id);
let depth = depth_of[&id];
if max_depth.is_some_and(|max| depth >= max) {
boundary.insert(id);
continue;
}
let facts = match graph.commit(&id) {
Ok(facts) => facts,
Err(crate::Error::ObjectMissing { .. }) => {
boundary.insert(id);
continue;
}
Err(err) => return Err(err),
};
let mut cut_here = false;
let child_depth = if request.relative && depth == 0 && !client_shallow.contains(&id) {
0
} else {
depth + 1
};
for parent in facts.parents {
if excluded.contains(&parent) {
cut_here = true;
continue;
}
if let Some(since) = request.since {
match graph.commit(&parent) {
Ok(parent_facts) if parent_facts.committed_at < since => {
cut_here = true;
continue;
}
Ok(_) => {}
Err(crate::Error::ObjectMissing { .. }) => {
cut_here = true;
continue;
}
Err(err) => return Err(err),
}
}
if !graph.contains(&parent)? {
cut_here = true;
continue;
}
if let std::collections::hash_map::Entry::Vacant(slot) = depth_of.entry(parent) {
slot.insert(child_depth);
queue.push_back(parent);
}
}
if cut_here {
boundary.insert(id);
}
}
let mut newly_shallow: Vec<ObjectId> = boundary
.iter()
.filter(|id| !client_shallow.contains(*id))
.filter(|id| {
graph
.commit(id)
.map(|facts| !facts.parents.is_empty())
.unwrap_or(false)
})
.copied()
.collect();
newly_shallow.sort();
let mut unshallow: Vec<ObjectId> = client_shallow
.iter()
.filter(|id| reached.contains(*id) && !boundary.contains(*id))
.copied()
.collect();
unshallow.sort();
let mut unshallow_parents: Vec<ObjectId> = Vec::new();
for id in &unshallow {
for parent in graph.commit(id)?.parents {
if reached.contains(&parent) && !unshallow_parents.contains(&parent) {
unshallow_parents.push(parent);
}
}
}
Ok(ShallowOutcome {
boundary: HashSet::new(),
client_boundary: client_shallow,
include: reached,
newly_shallow,
unshallow,
unshallow_parents,
})
}