use std::{
cmp::{Ordering, Reverse},
collections::VecDeque,
};
use gix_date::SecondsSinceUnixEpoch;
use gix_hash::ObjectId;
use smallvec::SmallVec;
#[derive(Default, Debug, Copy, Clone)]
pub enum CommitTimeOrder {
#[default]
NewestFirst,
#[doc(alias = "Sort::REVERSE", alias = "git2")]
OldestFirst,
}
#[derive(Default, Debug, Copy, Clone)]
pub enum Sorting {
#[default]
BreadthFirst,
ByCommitTime(CommitTimeOrder),
ByCommitTimeCutoff {
order: CommitTimeOrder,
seconds: gix_date::SecondsSinceUnixEpoch,
},
}
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error(transparent)]
Find(#[from] gix_object::find::existing_iter::Error),
#[error(transparent)]
ObjectDecode(#[from] gix_object::decode::Error),
#[error(transparent)]
HiddenGraph(#[from] gix_revwalk::graph::get_or_insert_default::Error),
}
use Result as Either;
type QueueKey<T> = Either<T, Reverse<T>>;
type CommitDateQueue = gix_revwalk::PriorityQueue<QueueKey<SecondsSinceUnixEpoch>, ObjectId>;
bitflags::bitflags! {
#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
struct PaintFlags: u8 {
const VISIBLE = 1 << 0;
const HIDDEN = 1 << 1;
const STALE = 1 << 2;
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
struct GenThenTime {
generation: gix_revwalk::graph::Generation,
time: SecondsSinceUnixEpoch,
}
impl From<&gix_revwalk::graph::Commit<PaintFlags>> for GenThenTime {
fn from(commit: &gix_revwalk::graph::Commit<PaintFlags>) -> Self {
GenThenTime {
generation: commit.generation.unwrap_or(gix_commitgraph::GENERATION_NUMBER_INFINITY),
time: commit.commit_time,
}
}
}
impl Ord for GenThenTime {
fn cmp(&self, other: &Self) -> Ordering {
self.generation.cmp(&other.generation).then(self.time.cmp(&other.time))
}
}
impl PartialOrd<Self> for GenThenTime {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Clone)]
pub(super) struct State {
next: VecDeque<ObjectId>,
queue: CommitDateQueue,
buf: Vec<u8>,
object_hash: gix_hash::Kind,
seen: gix_hashtable::HashSet<ObjectId>,
hidden: gix_revwalk::graph::IdMap<()>,
hidden_tips: Vec<ObjectId>,
parents_buf: Vec<u8>,
parent_ids: SmallVec<[(ObjectId, SecondsSinceUnixEpoch); 2]>,
}
fn to_queue_key(i: i64, order: CommitTimeOrder) -> QueueKey<i64> {
match order {
CommitTimeOrder::NewestFirst => Ok(i),
CommitTimeOrder::OldestFirst => Err(Reverse(i)),
}
}
fn compute_hidden_frontier(
visible_tips: &[ObjectId],
hidden_tips: &[ObjectId],
objects: &impl gix_object::Find,
cache: Option<&gix_commitgraph::Graph>,
) -> Result<gix_revwalk::graph::IdMap<()>, Error> {
let mut graph = gix_revwalk::Graph::<gix_revwalk::graph::Commit<PaintFlags>>::new(objects, cache);
let mut queue = gix_revwalk::PriorityQueue::<GenThenTime, ObjectId>::new();
for &visible in visible_tips {
graph.get_or_insert_full_commit(visible, |commit| {
commit.data |= PaintFlags::VISIBLE;
queue.insert(GenThenTime::from(&*commit), visible);
})?;
}
for &hidden in hidden_tips {
graph.get_or_insert_full_commit(hidden, |commit| {
commit.data |= PaintFlags::HIDDEN;
queue.insert(GenThenTime::from(&*commit), hidden);
})?;
}
while queue.iter_unordered().any(|id| {
graph
.get(id)
.is_some_and(|commit| !commit.data.contains(PaintFlags::STALE))
}) {
let (_info, commit_id) = match queue.pop() {
Some(v) => v,
None => break,
};
let commit = graph.get_mut(&commit_id).expect("queued commits are in the graph");
let mut flags = commit.data;
if flags == (PaintFlags::VISIBLE | PaintFlags::HIDDEN) {
flags |= PaintFlags::STALE;
}
for parent_id in commit.parents.clone() {
graph.get_or_insert_full_commit(parent_id, |parent| {
if (parent.data & flags) != flags {
parent.data |= flags;
queue.insert(GenThenTime::from(&*parent), parent_id);
}
})?;
}
}
Ok(graph
.detach()
.into_iter()
.filter_map(|(id, commit)| {
commit
.data
.contains(PaintFlags::VISIBLE | PaintFlags::HIDDEN)
.then_some((id, ()))
})
.collect())
}
mod init {
use super::{
collect_parents, compute_hidden_frontier, to_queue_key, CommitDateQueue, CommitTimeOrder, Error, Sorting, State,
};
use crate::commit::{Either, Info, ParentIds, Parents, Simple};
use gix_date::SecondsSinceUnixEpoch;
use gix_hash::{oid, ObjectId};
use gix_object::{CommitRefIter, FindExt};
use std::{cmp::Reverse, collections::VecDeque};
impl Default for State {
fn default() -> Self {
State {
next: Default::default(),
queue: gix_revwalk::PriorityQueue::new(),
buf: vec![],
object_hash: gix_hash::Kind::Sha1,
seen: Default::default(),
hidden: Default::default(),
hidden_tips: Vec::new(),
parents_buf: vec![],
parent_ids: Default::default(),
}
}
}
impl State {
fn clear(&mut self) {
let Self {
next,
queue,
buf,
object_hash,
seen,
hidden,
hidden_tips,
parents_buf: _,
parent_ids: _,
} = self;
next.clear();
queue.clear();
buf.clear();
*object_hash = gix_hash::Kind::Sha1;
seen.clear();
hidden.clear();
hidden_tips.clear();
}
}
impl Sorting {
fn cutoff_time(&self) -> Option<SecondsSinceUnixEpoch> {
match self {
Sorting::ByCommitTimeCutoff { seconds, .. } => Some(*seconds),
_ => None,
}
}
}
impl<Find, Predicate> Simple<Find, Predicate>
where
Find: gix_object::Find,
{
pub fn sorting(mut self, sorting: Sorting) -> Result<Self, Error> {
self.sorting = sorting;
match self.sorting {
Sorting::BreadthFirst => self.queue_to_vecdeque(),
Sorting::ByCommitTime(order) | Sorting::ByCommitTimeCutoff { order, .. } => {
let state = &mut self.state;
for commit_id in state.next.drain(..) {
add_to_queue(
commit_id,
order,
sorting.cutoff_time(),
&mut state.queue,
&self.objects,
&mut state.buf,
)?;
}
}
}
Ok(self)
}
pub fn parents(mut self, mode: Parents) -> Self {
self.parents = mode;
if matches!(self.parents, Parents::First) {
self.queue_to_vecdeque();
}
self
}
pub fn hide(mut self, tips: impl IntoIterator<Item = ObjectId>) -> Result<Self, Error> {
self.state.hidden_tips = tips.into_iter().collect();
Ok(self)
}
pub fn commit_graph(mut self, cache: Option<gix_commitgraph::Graph>) -> Self {
self.cache = cache;
self
}
fn queue_to_vecdeque(&mut self) {
let state = &mut self.state;
state.next.extend(
std::mem::replace(&mut state.queue, gix_revwalk::PriorityQueue::new())
.into_iter_unordered()
.map(|(_time, id)| id),
);
}
fn visible_inputs_sorted(&self) -> Vec<ObjectId> {
let mut out: Vec<_> = self
.state
.next
.iter()
.copied()
.chain(self.state.queue.iter_unordered().copied())
.collect();
out.sort();
out.dedup();
out
}
fn compute_hidden_frontier(&mut self, hidden_tips: Vec<ObjectId>) -> Result<(), Error> {
self.state.hidden.clear();
if hidden_tips.is_empty() {
return Ok(());
}
let visible_tips = self.visible_inputs_sorted();
if visible_tips.is_empty() {
return Ok(());
}
self.state.hidden =
compute_hidden_frontier(&visible_tips, &hidden_tips, &self.objects, self.cache.as_ref())?;
self.state.next.retain(|id| !self.state.hidden.contains_key(id));
self.state.queue = std::mem::replace(&mut self.state.queue, gix_revwalk::PriorityQueue::new())
.into_iter_unordered()
.filter(|(_, id)| !self.state.hidden.contains_key(id))
.collect();
Ok(())
}
}
fn add_to_queue(
commit_id: ObjectId,
order: CommitTimeOrder,
cutoff_time: Option<SecondsSinceUnixEpoch>,
queue: &mut CommitDateQueue,
objects: &impl gix_object::Find,
buf: &mut Vec<u8>,
) -> Result<(), Error> {
let commit_iter = objects.find_commit_iter(&commit_id, buf)?;
let time = commit_iter.committer()?.seconds();
let key = to_queue_key(time, order);
match (cutoff_time, order) {
(Some(cutoff_time), _) if time >= cutoff_time => queue.insert(key, commit_id),
(Some(_), _) => {}
(None, _) => queue.insert(key, commit_id),
}
Ok(())
}
impl<Find> Simple<Find, fn(&oid) -> bool>
where
Find: gix_object::Find,
{
pub fn new(tips: impl IntoIterator<Item = impl Into<ObjectId>>, find: Find) -> Self {
Self::filtered(tips, find, |_| true)
}
}
impl<Find, Predicate> Simple<Find, Predicate>
where
Find: gix_object::Find,
Predicate: FnMut(&oid) -> bool,
{
pub fn filtered(
tips: impl IntoIterator<Item = impl Into<ObjectId>>,
find: Find,
mut predicate: Predicate,
) -> Self {
let tips = tips.into_iter();
let mut state = State::default();
{
state.clear();
state.next.reserve(tips.size_hint().0);
for tip in tips.map(Into::into) {
if state.seen.insert(tip) && predicate(&tip) {
state.next.push_back(tip);
}
}
}
Self {
objects: find,
cache: None,
predicate,
state,
parents: Default::default(),
sorting: Default::default(),
}
}
}
impl<Find, Predicate> Simple<Find, Predicate> {
pub fn commit_iter(&self) -> CommitRefIter<'_> {
CommitRefIter::from_bytes(self.commit_data(), self.state.object_hash)
}
pub fn commit_data(&self) -> &[u8] {
&self.state.buf
}
}
impl<Find, Predicate> Iterator for Simple<Find, Predicate>
where
Find: gix_object::Find,
Predicate: FnMut(&oid) -> bool,
{
type Item = Result<Info, Error>;
fn next(&mut self) -> Option<Self::Item> {
if !self.state.hidden_tips.is_empty() {
let hidden_tips = std::mem::take(&mut self.state.hidden_tips);
if let Err(err) = self.compute_hidden_frontier(hidden_tips) {
self.state.queue.clear();
self.state.next.clear();
return Some(Err(err));
}
}
if matches!(self.parents, Parents::First) {
self.next_by_topology()
} else {
match self.sorting {
Sorting::BreadthFirst => self.next_by_topology(),
Sorting::ByCommitTime(order) => self.next_by_commit_date(order, None),
Sorting::ByCommitTimeCutoff { seconds, order } => self.next_by_commit_date(order, seconds.into()),
}
}
}
}
impl<Find, Predicate> Simple<Find, Predicate>
where
Find: gix_object::Find,
Predicate: FnMut(&oid) -> bool,
{
fn next_by_commit_date(
&mut self,
order: CommitTimeOrder,
cutoff: Option<SecondsSinceUnixEpoch>,
) -> Option<Result<Info, Error>> {
let state = &mut self.state;
let next = &mut state.queue;
loop {
let (commit_time, oid) = match next.pop()? {
(Ok(t) | Err(Reverse(t)), o) => (t, o),
};
state.object_hash = oid.kind();
if state.hidden.contains_key(&oid) {
continue;
}
let mut parents: ParentIds = Default::default();
match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) {
Ok(Either::CachedCommit(commit)) => {
if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) {
self.cache = None;
return self.next_by_commit_date(order, cutoff);
}
for (id, parent_commit_time) in state.parent_ids.drain(..) {
parents.push(id);
insert_into_seen_and_queue(
&mut state.seen,
&state.hidden,
id,
&mut self.predicate,
next,
order,
cutoff,
|| parent_commit_time,
);
}
}
Ok(Either::CommitRefIter(commit_iter)) => {
for token in commit_iter {
match token {
Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue,
Ok(gix_object::commit::ref_iter::Token::Parent { id }) => {
parents.push(id);
insert_into_seen_and_queue(
&mut state.seen,
&state.hidden,
id,
&mut self.predicate,
next,
order,
cutoff,
|| {
let parent =
self.objects.find_commit_iter(id.as_ref(), &mut state.parents_buf).ok();
parent
.and_then(|parent| {
parent.committer().ok().map(|committer| committer.seconds())
})
.unwrap_or_default()
},
);
}
Ok(_unused_token) => break,
Err(err) => return Some(Err(err.into())),
}
}
}
Err(err) => return Some(Err(err.into())),
}
return Some(Ok(Info {
id: oid,
parent_ids: parents,
commit_time: Some(commit_time),
}));
}
}
fn next_by_topology(&mut self) -> Option<Result<Info, Error>> {
let state = &mut self.state;
let next = &mut state.next;
loop {
let oid = next.pop_front()?;
state.object_hash = oid.kind();
if state.hidden.contains_key(&oid) {
continue;
}
let mut parents: ParentIds = Default::default();
match super::super::find(self.cache.as_ref(), &self.objects, &oid, &mut state.buf) {
Ok(Either::CachedCommit(commit)) => {
if !collect_parents(&mut state.parent_ids, self.cache.as_ref(), commit.iter_parents()) {
self.cache = None;
return self.next_by_topology();
}
for (pid, _commit_time) in state.parent_ids.drain(..) {
parents.push(pid);
insert_into_seen_and_next(&mut state.seen, &state.hidden, pid, &mut self.predicate, next);
if matches!(self.parents, Parents::First) {
break;
}
}
}
Ok(Either::CommitRefIter(commit_iter)) => {
for token in commit_iter {
match token {
Ok(gix_object::commit::ref_iter::Token::Tree { .. }) => continue,
Ok(gix_object::commit::ref_iter::Token::Parent { id: pid }) => {
parents.push(pid);
insert_into_seen_and_next(
&mut state.seen,
&state.hidden,
pid,
&mut self.predicate,
next,
);
if matches!(self.parents, Parents::First) {
break;
}
}
Ok(_a_token_past_the_parents) => break,
Err(err) => return Some(Err(err.into())),
}
}
}
Err(err) => return Some(Err(err.into())),
}
return Some(Ok(Info {
id: oid,
parent_ids: parents,
commit_time: None,
}));
}
}
}
fn insert_into_seen_and_next(
seen: &mut gix_hashtable::HashSet<ObjectId>,
hidden: &gix_revwalk::graph::IdMap<()>,
parent_id: ObjectId,
predicate: &mut impl FnMut(&oid) -> bool,
next: &mut VecDeque<ObjectId>,
) {
if hidden.contains_key(&parent_id) {
return;
}
if seen.insert(parent_id) && predicate(&parent_id) {
next.push_back(parent_id);
}
}
#[allow(clippy::too_many_arguments)]
fn insert_into_seen_and_queue(
seen: &mut gix_hashtable::HashSet<ObjectId>,
hidden: &gix_revwalk::graph::IdMap<()>,
parent_id: ObjectId,
predicate: &mut impl FnMut(&oid) -> bool,
queue: &mut CommitDateQueue,
order: CommitTimeOrder,
cutoff: Option<SecondsSinceUnixEpoch>,
get_parent_commit_time: impl FnOnce() -> gix_date::SecondsSinceUnixEpoch,
) {
if hidden.contains_key(&parent_id) {
return;
}
if seen.insert(parent_id) && predicate(&parent_id) {
let parent_commit_time = get_parent_commit_time();
let key = to_queue_key(parent_commit_time, order);
match cutoff {
Some(cutoff_older_than) if parent_commit_time < cutoff_older_than => {}
Some(_) | None => queue.insert(key, parent_id),
}
}
}
}
fn collect_parents(
dest: &mut SmallVec<[(gix_hash::ObjectId, gix_date::SecondsSinceUnixEpoch); 2]>,
cache: Option<&gix_commitgraph::Graph>,
parents: gix_commitgraph::file::commit::Parents<'_>,
) -> bool {
dest.clear();
let cache = cache.as_ref().expect("parents iter is available, backed by `cache`");
for parent_id in parents {
match parent_id {
Ok(pos) => dest.push({
let parent = cache.commit_at(pos);
(
parent.id().to_owned(),
parent.committer_timestamp() as gix_date::SecondsSinceUnixEpoch,
)
}),
Err(_err) => return false,
}
}
true
}