use std::collections::HashSet;
use crate::error::Error;
use crate::merge_base::{branch_tips_merged_into, RefContainsBatch, RefContainsMemo};
use crate::objects::{oid_hash_map, oid_hash_set, OidHashMap, OidHashSet};
use crate::objects::{parse_tag, ObjectId, ObjectKind};
use crate::odb::Odb;
use crate::repo::Repository;
use crate::rev_parse::peel_to_commit_for_merge_base;
use super::listing::{RefQuery, RefQueryError};
pub fn resolve_commit_filter_target(
repo: &Repository,
oid: ObjectId,
) -> std::result::Result<ObjectId, RefQueryError> {
match repo.odb().read_info(&oid) {
Err(Error::ObjectNotFound(_)) => Err(RefQueryError::MissingObject(oid)),
Err(e) => Err(RefQueryError::Store(e)),
Ok(info) => match info.kind {
ObjectKind::Commit => Ok(oid),
ObjectKind::Tag | ObjectKind::Tree | ObjectKind::Blob => {
peel_to_commit_for_merge_base(repo, oid).map_err(|_| RefQueryError::NotCommit(oid))
}
},
}
}
pub fn ref_points_at(
odb: &Odb,
ref_oid: ObjectId,
target: &ObjectId,
) -> crate::error::Result<bool> {
if &ref_oid == target {
return Ok(true);
}
let mut current = ref_oid;
let mut seen = HashSet::new();
loop {
if !seen.insert(current) {
return Err(Error::CorruptObject(format!(
"annotated tag peel cycle at {current}"
)));
}
let obj = match odb.read(¤t) {
Ok(o) => o,
Err(Error::ObjectNotFound(_)) => return Ok(false),
Err(e) => return Err(e),
};
if obj.kind != ObjectKind::Tag {
return Ok(false);
}
let tag = parse_tag(&obj.data)?;
if &tag.object == target {
return Ok(true);
}
current = tag.object;
}
}
pub fn ref_points_at_any(
odb: &Odb,
ref_oid: ObjectId,
points_at: &[ObjectId],
) -> crate::error::Result<bool> {
for target in points_at {
if ref_points_at(odb, ref_oid, target)? {
return Ok(true);
}
}
Ok(false)
}
fn resolve_targets(
repo: &Repository,
oids: &[ObjectId],
) -> std::result::Result<Vec<ObjectId>, RefQueryError> {
oids.iter()
.map(|&oid| resolve_commit_filter_target(repo, oid))
.collect()
}
#[derive(Debug, Clone, Copy)]
pub(super) struct ReachRow {
pub(super) oid: ObjectId,
pub(super) peeled: Option<Option<ObjectId>>,
}
fn peel_row_tip(
repo: &Repository,
row: &mut ReachRow,
) -> std::result::Result<Option<ObjectId>, RefQueryError> {
let odb = repo.odb();
if let Some(Some(p)) = row.peeled {
if matches!(odb.read_info(&p), Ok(info) if info.kind == ObjectKind::Commit) {
return Ok(Some(p));
}
}
if row.peeled.is_none() {
let (target, kind) = odb.peel_tags(row.oid).map_err(RefQueryError::Store)?;
row.peeled = Some((target != row.oid).then_some(target));
if kind == ObjectKind::Commit {
return Ok(Some(target));
}
} else if let Some(commit) = odb.peel_to_commit(row.oid).map_err(RefQueryError::Store)? {
return Ok(Some(commit));
}
Ok(peel_to_commit_for_merge_base(repo, row.oid).ok())
}
struct TipResolver {
resolved: OidHashMap<(Option<ObjectId>, Option<ObjectId>)>,
}
impl TipResolver {
fn new() -> Self {
Self {
resolved: oid_hash_map(),
}
}
fn tip(
&mut self,
repo: &Repository,
row: &mut ReachRow,
) -> std::result::Result<Option<ObjectId>, RefQueryError> {
if let Some(&(tip, peeled)) = self.resolved.get(&row.oid) {
row.peeled.get_or_insert(peeled);
return Ok(tip);
}
let tip = peel_row_tip(repo, row)?;
self.resolved
.insert(row.oid, (tip, row.peeled.unwrap_or_default()));
Ok(tip)
}
}
struct ReachabilityFilterCtx<'a> {
contains: &'a [ObjectId],
no_contains: &'a [ObjectId],
merged: &'a [ObjectId],
no_merged: &'a [ObjectId],
merged_tips: &'a OidHashSet,
no_merged_tips: &'a OidHashSet,
}
fn row_passes_reachability(
query: &RefQuery,
odb: &Odb,
ref_oid: &ObjectId,
tip: Option<ObjectId>,
ctx: &ReachabilityFilterCtx<'_>,
tip_idx: Option<usize>,
batch: Option<&RefContainsBatch<'_>>,
) -> std::result::Result<bool, RefQueryError> {
let needs_commit = !ctx.contains.is_empty()
|| !ctx.no_contains.is_empty()
|| !ctx.merged.is_empty()
|| !ctx.no_merged.is_empty();
if !needs_commit {
return ref_points_at_any(odb, *ref_oid, &query.points_at).map_err(RefQueryError::Store);
}
let Some(tip) = tip else {
return Ok(false);
};
if !ctx.contains.is_empty() {
let Some(idx) = tip_idx else {
return Ok(false);
};
let Some(batch) = batch else {
return Ok(false);
};
if !batch.tip_state(idx).contains_match {
return Ok(false);
}
}
if !ctx.no_contains.is_empty() {
let Some(idx) = tip_idx else {
return Ok(false);
};
let Some(batch) = batch else {
return Ok(false);
};
if batch.tip_state(idx).no_contains_match {
return Ok(false);
}
}
if !ctx.merged.is_empty() && !ctx.merged_tips.contains(&tip) {
return Ok(false);
}
if !ctx.no_merged.is_empty() && ctx.no_merged_tips.contains(&tip) {
return Ok(false);
}
if !query.points_at.is_empty()
&& !ref_points_at_any(odb, *ref_oid, &query.points_at).map_err(RefQueryError::Store)?
{
return Ok(false);
}
Ok(true)
}
pub(super) fn can_bound_contains(query: &RefQuery) -> bool {
query.limit.is_some()
&& query.sort.key == super::listing::SortKey::Name
&& !query.contains.is_empty()
&& query.no_contains.is_empty()
&& query.merged.is_empty()
&& query.no_merged.is_empty()
&& query.points_at.is_empty()
}
fn filter_bounded_contains(
repo: &Repository,
query: &RefQuery,
rows: &mut [ReachRow],
mut evaluator: RefContainsMemo<'_>,
limit: usize,
) -> std::result::Result<Vec<bool>, RefQueryError> {
let len = rows.len();
let mut keep = vec![false; len];
let mut kept = 0usize;
let mut tips = TipResolver::new();
for step in 0..len {
if kept == limit {
break;
}
let idx = if query.sort.reverse {
len - 1 - step
} else {
step
};
if let Some(tip) = tips.tip(repo, &mut rows[idx])? {
if evaluator.contains(tip)? {
keep[idx] = true;
kept += 1;
}
}
}
Ok(keep)
}
pub(super) fn filter_listings_by_reachability(
repo: &Repository,
query: &RefQuery,
rows: &mut [ReachRow],
) -> std::result::Result<Vec<bool>, RefQueryError> {
let needs_commit = !query.contains.is_empty()
|| !query.no_contains.is_empty()
|| !query.merged.is_empty()
|| !query.no_merged.is_empty();
if !needs_commit && query.points_at.is_empty() {
return Ok(vec![true; rows.len()]);
}
let contains = resolve_targets(repo, &query.contains)?;
let no_contains = resolve_targets(repo, &query.no_contains)?;
let merged = resolve_targets(repo, &query.merged)?;
let no_merged = resolve_targets(repo, &query.no_merged)?;
if can_bound_contains(query) {
if let Some(limit) = query.limit {
if limit == 0 || rows.is_empty() {
return Ok(vec![false; rows.len()]);
}
if let Some(evaluator) = RefContainsMemo::new(repo, &contains)? {
return filter_bounded_contains(repo, query, rows, evaluator, limit);
}
}
}
let odb = repo.odb();
let row_tips = if needs_commit {
let mut tips = TipResolver::new();
rows.iter_mut()
.map(|row| tips.tip(repo, row))
.collect::<std::result::Result<Vec<_>, _>>()?
} else {
vec![None; rows.len()]
};
let tips_for_merged: Vec<ObjectId> = if merged.is_empty() && no_merged.is_empty() {
Vec::new()
} else {
let set: OidHashSet = row_tips.iter().flatten().copied().collect();
set.into_iter().collect()
};
let empty_merged = oid_hash_set();
let merged_owned = if merged.is_empty() {
None
} else {
Some(
branch_tips_merged_into(repo, &merged, &tips_for_merged)
.map_err(RefQueryError::Store)?,
)
};
let no_merged_owned = if no_merged.is_empty() {
None
} else {
Some(
branch_tips_merged_into(repo, &no_merged, &tips_for_merged)
.map_err(RefQueryError::Store)?,
)
};
let merged_tips = merged_owned.as_ref().unwrap_or(&empty_merged);
let no_merged_tips = no_merged_owned.as_ref().unwrap_or(&empty_merged);
let ctx = ReachabilityFilterCtx {
contains: &contains,
no_contains: &no_contains,
merged: &merged,
no_merged: &no_merged,
merged_tips,
no_merged_tips,
};
let needs_contains = !contains.is_empty() || !no_contains.is_empty();
let mut batch = None;
let row_tip_idx: Vec<Option<usize>> = if needs_contains {
let batch = batch.insert(RefContainsBatch::new(repo));
let idx = row_tips
.iter()
.map(|tip| tip.map(|t| batch.register_tip(t)))
.collect();
batch.run(&contains, &no_contains)?;
idx
} else {
vec![None; rows.len()]
};
let batch_ref = batch.as_ref();
rows.iter()
.zip(row_tips)
.zip(row_tip_idx)
.map(|((row, tip), tip_idx)| {
row_passes_reachability(query, odb, &row.oid, tip, &ctx, tip_idx, batch_ref)
})
.collect()
}