use std::marker::PhantomData;
use serde::Deserialize;
use crate::cob::{Op, TypeName};
use crate::git;
use crate::git::Oid;
use crate::git::fmt::refspec::PatternString;
use super::CobRange;
use super::error;
/// A `Walk` specifies a range to construct a [`WalkIter`].
#[derive(Clone, Debug)]
pub(super) struct Walk {
from: Oid,
until: Until,
}
/// Specify the end of a range by either providing an [`Oid`] tip, or a
/// reference glob via a [`PatternString`].
#[derive(Clone, Debug)]
pub enum Until {
Tip(Oid),
Glob(PatternString),
}
impl From<Oid> for Until {
fn from(tip: Oid) -> Self {
Self::Tip(tip)
}
}
impl From<PatternString> for Until {
fn from(glob: PatternString) -> Self {
Self::Glob(glob)
}
}
/// A revwalk over a set of commits, including the commit that is being walked
/// from.
pub(super) struct WalkIter<'a> {
/// Git repository for looking up the commit object during the revwalk.
repo: &'a git::raw::Repository,
/// The root commit that is being walked from.
///
/// N.b. This is required since ranges are non-inclusive in Git, and if the
/// `^` notation is used with a root commit, then it will result in an
/// error.
from: Option<Oid>,
/// The revwalk that is being iterated over.
inner: git::raw::Revwalk<'a>,
}
impl From<CobRange> for Walk {
fn from(history: CobRange) -> Self {
Self::new(history.root, history.until)
}
}
impl Walk {
/// Construct a new `Walk`, `from` the given commit, `until` the end of a
/// given range.
pub(super) fn new(from: Oid, until: Until) -> Self {
Self { from, until }
}
/// Change the `Oid` that the walk starts from.
pub(super) fn since(mut self, from: Oid) -> Self {
self.from = from;
self
}
/// Change the `Until` that the walk finishes on.
pub(super) fn until(mut self, until: impl Into<Until>) -> Self {
self.until = until.into();
self
}
/// Get the iterator for the walk.
pub(super) fn iter(self, repo: &git::raw::Repository) -> Result<WalkIter<'_>, git::raw::Error> {
let mut walk = repo.revwalk()?;
// N.b. ensure that we start from the `self.from` commit.
walk.set_sorting(git::raw::Sort::TOPOLOGICAL.union(git::raw::Sort::REVERSE))?;
match self.until {
Until::Tip(tip) => walk.push_range(&format!("{}..{}", self.from, tip))?,
Until::Glob(glob) => {
walk.push(self.from.into())?;
walk.push_glob(glob.as_str())?
}
}
Ok(WalkIter {
repo,
from: Some(self.from),
inner: walk,
})
}
}
impl<'a> Iterator for WalkIter<'a> {
type Item = Result<git::raw::Commit<'a>, git::raw::Error>;
fn next(&mut self) -> Option<Self::Item> {
// N.b. ensure that we start using the `from` commit and use the revwalk
// after that.
if let Some(from) = self.from.take() {
return Some(self.repo.find_commit(from.into()));
}
let oid = self.inner.next()?;
Some(oid.and_then(|oid| self.repo.find_commit(oid)))
}
}
/// Iterate over all operations for a given range of commits.
pub struct OpsIter<'a, A> {
/// The [`WalkIter`] provides each commit that it is being walked over for a
/// given range.
walk: WalkIter<'a>,
/// The walk can iterate over other COBs, e.g. an Identity COB, so this is
/// used to filter for the correct type.
typename: TypeName,
/// Marker for the type of action that is associated with the Op
action: PhantomData<A>,
}
impl<A> Iterator for OpsIter<'_, A>
where
A: for<'de> Deserialize<'de>,
{
type Item = Result<Op<A>, error::Ops>;
fn next(&mut self) -> Option<Self::Item> {
let commit = self.walk.next()?;
match commit {
Ok(commit) => {
let entry = crate::git::Oid::from(commit.id());
// N.b. mark this commit as seen, so that it is not walked again
self.walk.inner.hide(commit.id()).ok();
// Skip any Op that do not match the manifest
match self.load(entry) {
Ok(entry) => entry.map(Ok).or_else(|| self.next()),
Err(err) => match err {
// This is a parent commit that is not an Op
error::Ops::Manifest { source: _ } => self.next(),
err => Some(Err(err)),
},
}
}
// Something was wrong with the commit
Err(err) => Some(Err(error::Ops::Commit { source: err })),
}
}
}
impl<'a, A> OpsIter<'a, A> {
pub(super) fn new(walk: WalkIter<'a>, typename: TypeName) -> Self {
Self {
walk,
typename,
action: PhantomData,
}
}
/// Load the `Op` for the given `entry`, ensuring that manifest matches with
/// the expected manifest.
fn load(&self, entry: crate::git::Oid) -> Result<Option<Op<A>>, error::Ops>
where
A: for<'de> Deserialize<'de>,
{
let manifest = Op::<A>::manifest_of(self.walk.repo, &entry)
.map_err(|err| error::Ops::Manifest { source: err })?;
if manifest.type_name == self.typename {
let op =
Op::load(self.walk.repo, entry).map_err(|err| error::Ops::Load { source: err })?;
Ok(Some(op))
} else {
Ok(None)
}
}
}