Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle/git/repository: implement git2 adapter for all traits
✗ CI failure Fintan Halpenny committed 24 days ago
commit e1867632fb03a1a75b73087e8ce5fb5d0846187c
parent 9bd68f02a3c274e73f4609a3a29c74c70dd94fee
1 failed (1 total) View logs
8 files changed +712 -4
modified crates/radicle/src/git/raw.rs
@@ -6,7 +6,8 @@

// Re-exports that are only used within this crate.
pub(crate) use git2::{
-
    AutotagOption, Blob, FetchOptions, FetchPrune, Object, Revwalk, Sort, message_trailers_strs,
+
    AutotagOption, Blob, FetchOptions, FetchPrune, Object, Revwalk, Sort, TreeEntry,
+
    message_trailers_strs,
};

#[cfg(unix)]
@@ -19,8 +20,8 @@ pub(crate) use git2::RemoteCallbacks;
// Re-exports that are used by other crates in the workspace, including this crate.
pub use git2::{
    Branch, BranchType, Commit, Direction, Error, ErrorClass, ErrorCode, FileMode, ObjectType, Oid,
-
    Reference, Remote, Repository, RepositoryInitOptions, RepositoryOpenFlags, Signature, Time,
-
    Tree,
+
    Reference, References, Remote, Repository, RepositoryInitOptions, RepositoryOpenFlags,
+
    Signature, Time, Tree,
};

// Re-exports that are used by other crates in the workspace, but *not* this crate.
@@ -28,9 +29,11 @@ pub use git2::{
    AnnotatedCommit, Diff, DiffFindOptions, DiffOptions, DiffStats, MergeAnalysis, MergeOptions,
};

-
// Re-exports for `radicle-cli`.
pub mod build {
+
    // Re-exports for `radicle-cli`.
    pub use git2::build::CheckoutBuilder;
+

+
    pub(crate) use git2::build::TreeUpdateBuilder;
}

pub(crate) mod transport {
modified crates/radicle/src/git/repository.rs
@@ -17,6 +17,8 @@ pub mod reference;
pub mod revwalk;
pub mod types;

+
mod adapter;
+

pub use ancestry::{AheadBehind, Ancestry};
pub use object::{ObjectReader, ObjectWriter};
pub use reference::{RefReader, RefTarget, RefWriter, SymbolicRefTarget, SymbolicRefWriter};
added crates/radicle/src/git/repository/adapter.rs
@@ -0,0 +1,5 @@
+
//! Adapters for the traits defined in [`git::repository`].
+
//!
+
//! [`git::repository`]: crate::git::repository
+

+
mod git2;
added crates/radicle/src/git/repository/adapter/git2.rs
@@ -0,0 +1,44 @@
+
//! The [`Repository`] adapters for the various repository traits.
+
//!
+
//! [`Repository`]: crate::git::raw::Repository
+

+
use crate::git;
+
use crate::git::raw;
+

+
use crate::git::repository::ObjectKind;
+

+
mod ancestry;
+
mod object;
+
mod reference;
+
mod revwalk;
+

+
/// Helper trait to enable method chaining to return `None` when the error
+
/// matches [`ErrorCode::NotFound`].
+
///
+
/// [`ErrorCode::NotFound`]: crate::git::raw::ErrorCode::NotFound
+
trait NotFound<T> {
+
    fn or_is_not_found(self) -> Result<Option<T>, raw::Error>;
+
}
+

+
impl<T> NotFound<T> for Result<T, raw::Error> {
+
    fn or_is_not_found(self) -> Result<Option<T>, git::raw::Error> {
+
        self.map(|t| Ok(Some(t))).unwrap_or_else(|e| {
+
            if matches!(e.code(), raw::ErrorCode::NotFound) {
+
                Ok(None)
+
            } else {
+
                Err(e)
+
            }
+
        })
+
    }
+
}
+

+
/// Map a [`raw::ObjectType`] to an [`ObjectKind`].
+
fn object_kind(kind: raw::ObjectType) -> ObjectKind {
+
    match kind {
+
        raw::ObjectType::Blob => ObjectKind::Blob,
+
        raw::ObjectType::Tree => ObjectKind::Tree,
+
        raw::ObjectType::Commit => ObjectKind::Commit,
+
        raw::ObjectType::Tag => ObjectKind::Tag,
+
        raw::ObjectType::Any => unreachable!("git2 does not expose other object types"),
+
    }
+
}
added crates/radicle/src/git/repository/adapter/git2/ancestry.rs
@@ -0,0 +1,62 @@
+
use radicle_oid::Oid;
+

+
use crate::git::raw;
+
use crate::git::repository::ancestry;
+
use crate::git::repository::ancestry::{AheadBehind, Ancestry};
+

+
use super::NotFound as _;
+

+
impl Ancestry for raw::Repository {
+
    fn merge_base(&self, a: Oid, b: Oid) -> Result<Option<Oid>, ancestry::MergeBaseError> {
+
        let odb = self.odb().map_err(ancestry::MergeBaseError::backend)?;
+

+
        if !odb.exists(a.into()) {
+
            return Err(ancestry::MergeBaseError::CommitNotFound { oid: a });
+
        }
+

+
        if !odb.exists(b.into()) {
+
            return Err(ancestry::MergeBaseError::CommitNotFound { oid: b });
+
        }
+

+
        self.merge_base(a.into(), b.into())
+
            .map(Oid::from)
+
            .or_is_not_found()
+
            .map_err(ancestry::MergeBaseError::backend)
+
    }
+

+
    fn is_ancestor(&self, ancestor: Oid, head: Oid) -> Result<bool, ancestry::IsAncestorError> {
+
        let odb = self.odb().map_err(ancestry::IsAncestorError::backend)?;
+

+
        if !odb.exists(ancestor.into()) {
+
            return Err(ancestry::IsAncestorError::CommitNotFound { oid: ancestor });
+
        }
+

+
        if !odb.exists(head.into()) {
+
            return Err(ancestry::IsAncestorError::CommitNotFound { oid: head });
+
        }
+

+
        self.graph_descendant_of(head.into(), ancestor.into())
+
            .map_err(ancestry::IsAncestorError::backend)
+
    }
+

+
    fn ahead_behind(
+
        &self,
+
        commit: Oid,
+
        upstream: Oid,
+
    ) -> Result<AheadBehind, ancestry::AheadBehindError> {
+
        let odb = self.odb().map_err(ancestry::AheadBehindError::backend)?;
+

+
        if !odb.exists(commit.into()) {
+
            return Err(ancestry::AheadBehindError::CommitNotFound { oid: commit });
+
        }
+

+
        if !odb.exists(upstream.into()) {
+
            return Err(ancestry::AheadBehindError::CommitNotFound { oid: upstream });
+
        }
+

+
        let (ahead, behind) = self
+
            .graph_ahead_behind(commit.into(), upstream.into())
+
            .map_err(ancestry::AheadBehindError::backend)?;
+
        Ok(AheadBehind { ahead, behind })
+
    }
+
}
added crates/radicle/src/git/repository/adapter/git2/object.rs
@@ -0,0 +1,174 @@
+
use std::path::Path;
+

+
use radicle_oid::Oid;
+

+
use crate::git;
+
use crate::git::raw;
+
use crate::git::repository::object::error::{read, write};
+
use crate::git::repository::object::{ObjectReader, ObjectWriter};
+
use crate::git::repository::types::{Blob, Commit, ObjectKind, TreeEntry};
+

+
use super::NotFound as _;
+
use super::object_kind;
+

+
impl ObjectReader for raw::Repository {
+
    fn blob(&self, oid: Oid) -> Result<Option<Blob>, read::BlobError> {
+
        self.find_blob(oid.into())
+
            .map(|blob| Blob {
+
                oid,
+
                content: blob.content().to_vec(),
+
            })
+
            .or_is_not_found()
+
            .map_err(read::BlobError::backend)
+
    }
+

+
    fn blob_at<P: AsRef<Path>>(
+
        &self,
+
        oid: Oid,
+
        path: &P,
+
    ) -> Result<Option<Blob>, read::BlobAtError> {
+
        let path = path.as_ref();
+
        let commit = find_commit(self, oid, &path)?;
+
        let tree = commit.tree().map_err(|e| read::BlobAtError::Tree {
+
            commit: oid,
+
            source: Box::new(e),
+
        })?;
+
        let entry =
+
            tree.get_path(path)
+
                .or_is_not_found()
+
                .map_err(|e| read::BlobAtError::TreeEntry {
+
                    commit: oid,
+
                    path: path.to_path_buf(),
+
                    source: Box::new(e),
+
                })?;
+
        entry
+
            .map(|entry| try_entry_to_blob(self, oid, path, entry))
+
            .transpose()
+
    }
+

+
    fn commit(&self, oid: Oid) -> Result<Option<Commit>, read::CommitError> {
+
        let odb = self.odb().map_err(read::CommitError::backend)?;
+
        let commit = odb
+
            .read(oid.into())
+
            .or_is_not_found()
+
            .map_err(read::CommitError::backend)?;
+
        commit
+
            .map(|obj| {
+
                Commit::from_bytes(obj.data())
+
                    .map_err(|e| read::CommitError::Parse { oid, source: e })
+
            })
+
            .transpose()
+
    }
+

+
    fn exists(&self, oid: Oid) -> Result<bool, read::ExistsError> {
+
        self.odb()
+
            .map(|odb| odb.exists(oid.into()))
+
            .map_err(read::ExistsError::backend)
+
    }
+
}
+

+
impl ObjectWriter for raw::Repository {
+
    fn write_blob(&self, content: &[u8]) -> Result<Oid, write::BlobError> {
+
        self.blob(content)
+
            .map(Oid::from)
+
            .map_err(write::BlobError::backend)
+
    }
+

+
    fn write_tree(&self, entries: &[TreeEntry]) -> Result<Oid, write::TreeError> {
+
        let empty_tree = empty_tree(self)?;
+
        let mut builder = raw::build::TreeUpdateBuilder::new();
+
        let odb = self.odb().map_err(write::TreeError::backend)?;
+
        for entry in entries {
+
            match entry {
+
                TreeEntry::Blob { path, content } => {
+
                    let oid = self
+
                        .blob(content)
+
                        .map_err(|e| write::TreeError::WriteBlob {
+
                            path: path.to_path_buf(),
+
                            source: Box::new(e),
+
                        })?;
+
                    builder.upsert(path, oid, raw::FileMode::Blob);
+
                }
+
                TreeEntry::BlobRef { path, oid } => {
+
                    if !odb.exists(oid.into()) {
+
                        return Err(write::TreeError::MissingBlob { oid: *oid });
+
                    }
+
                    builder.upsert(path, (*oid).into(), raw::FileMode::Blob);
+
                }
+
            }
+
        }
+

+
        builder
+
            .create_updated(self, &empty_tree)
+
            .map(Oid::from)
+
            .map_err(write::TreeError::backend)
+
    }
+

+
    fn write_commit(&self, bytes: &[u8]) -> Result<Oid, write::CommitError> {
+
        let odb = self.odb().map_err(write::CommitError::backend)?;
+
        odb.write(raw::ObjectType::Commit, bytes)
+
            .map(Oid::from)
+
            .map_err(write::CommitError::backend)
+
    }
+
}
+

+
/// Get or create the empty tree for use as a baseline.
+
fn empty_tree(repo: &raw::Repository) -> Result<git::raw::Tree<'_>, write::TreeError> {
+
    let empty_oid = repo
+
        .treebuilder(None)
+
        .map_err(write::TreeError::backend)?
+
        .write()
+
        .map_err(write::TreeError::backend)?;
+
    repo.find_tree(empty_oid).map_err(write::TreeError::backend)
+
}
+

+
fn find_commit<'a, P: AsRef<Path>>(
+
    repository: &'a git::raw::Repository,
+
    commit: Oid,
+
    path: &P,
+
) -> Result<git::raw::Commit<'a>, read::BlobAtError> {
+
    match repository.find_commit(commit.into()) {
+
        Ok(c) => Ok(c),
+
        Err(e) if matches!(e.code(), git::raw::ErrorCode::NotFound) => {
+
            Err(read::BlobAtError::CommitNotFound {
+
                commit,
+
                path: path.as_ref().to_path_buf(),
+
            })
+
        }
+
        Err(e) => Err(read::BlobAtError::backend(e)),
+
    }
+
}
+

+
fn try_object_to_blob(obj: git::raw::Object) -> Result<Blob, read::BlobAtError> {
+
    let blob = obj.into_blob().map_err(|obj| {
+
        let actual = obj
+
            .kind()
+
            .map(|k| object_kind(k).to_string())
+
            .unwrap_or_else(|| "unknown".to_string());
+
        read::BlobAtError::TypeMismatch {
+
            oid: Oid::from(obj.id()),
+
            expected: ObjectKind::Blob,
+
            actual,
+
        }
+
    })?;
+
    Ok(Blob {
+
        oid: Oid::from(blob.id()),
+
        content: blob.content().to_vec(),
+
    })
+
}
+

+
fn try_entry_to_blob(
+
    repository: &git::raw::Repository,
+
    oid: Oid,
+
    path: &Path,
+
    entry: raw::TreeEntry<'_>,
+
) -> Result<Blob, read::BlobAtError> {
+
    let obj = entry
+
        .to_object(repository)
+
        .map_err(|e| read::BlobAtError::Object {
+
            commit: oid,
+
            path: path.to_path_buf(),
+
            source: Box::new(e),
+
        })?;
+
    try_object_to_blob(obj)
+
}
added crates/radicle/src/git/repository/adapter/git2/reference.rs
@@ -0,0 +1,297 @@
+
use radicle_git_ref_format::{Qualified, RefStr, refspec};
+
use radicle_oid::Oid;
+

+
use crate::git;
+
use crate::git::raw;
+
use crate::git::repository::reference::error::{read, write};
+

+
use crate::git::repository::reference::{
+
    RefReader, RefTarget, RefWriter, SymbolicRefTarget, SymbolicRefWriter,
+
};
+

+
use super::NotFound as _;
+

+
/// Iterator adapter for [`RefReader::list_refs`].
+
pub struct References<'a> {
+
    inner: git::raw::References<'a>,
+
}
+

+
impl Iterator for References<'_> {
+
    type Item = Result<(Qualified<'static>, Oid), read::ListReferenceError>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        loop {
+
            let r = match self.inner.next()? {
+
                Ok(r) => r,
+
                Err(e) => {
+
                    return Some(Err(read::ListReferenceError::backend(e)));
+
                }
+
            };
+

+
            let name = match r.name() {
+
                Some(n) => n,
+
                None => continue,
+
            };
+

+
            let refstr = match RefStr::try_from_str(name) {
+
                Ok(r) => r,
+
                Err(e) => {
+
                    return Some(Err(read::ListReferenceError::Parse {
+
                        name: name.to_string(),
+
                        source: e,
+
                    }));
+
                }
+
            };
+

+
            let qualified = match Qualified::from_refstr(refstr) {
+
                Some(q) => q.to_owned(),
+
                None => continue,
+
            };
+

+
            let oid = match r.resolve().map(|r| r.target()) {
+
                Ok(Some(oid)) => Oid::from(oid),
+
                Ok(None) => continue,
+
                Err(e) => {
+
                    return Some(Err(read::ListReferenceError::Peel {
+
                        name: qualified,
+
                        source: Box::new(e),
+
                    }));
+
                }
+
            };
+

+
            return Some(Ok((qualified, oid)));
+
        }
+
    }
+
}
+

+
impl RefReader for raw::Repository {
+
    type References<'a> = References<'a>;
+

+
    fn ref_target<R: AsRef<RefStr>>(&self, name: &R) -> Result<Option<Oid>, read::RefTargetError> {
+
        self.refname_to_id(name.as_ref().as_str())
+
            .map(Oid::from)
+
            .or_is_not_found()
+
            .map_err(read::RefTargetError::backend)
+
    }
+

+
    fn list_refs<'a, P>(&'a self, pattern: &P) -> Result<Self::References<'a>, read::ListRefsError>
+
    where
+
        P: AsRef<refspec::PatternStr>,
+
    {
+
        let inner = self
+
            .references_glob(pattern.as_ref().as_str())
+
            .map_err(read::ListRefsError::backend)?;
+
        Ok(References { inner })
+
    }
+
}
+

+
impl RefWriter for raw::Repository {
+
    fn write_ref<R>(
+
        &self,
+
        name: &R,
+
        target: RefTarget,
+
        reflog: &str,
+
    ) -> Result<(), write::WriteRefError>
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        let name = name.as_ref();
+

+
        // Verify the target object exists.
+
        {
+
            let odb = self.odb().map_err(write::WriteRefError::backend)?;
+
            let target_oid = target.target();
+
            if !odb.exists(target_oid.into()) {
+
                return Err(write::WriteRefError::MissingTarget {
+
                    name: name.to_string(),
+
                    target: target_oid,
+
                });
+
            }
+
        }
+

+
        match target {
+
            RefTarget::Create { target } => {
+
                create_reference(self, reflog, name, target)?;
+
            }
+
            RefTarget::Upsert { target } => {
+
                upsert_reference(self, reflog, name, target)?;
+
            }
+
            RefTarget::Cas { target, expected } => {
+
                cas_reference(self, reflog, name, target, expected)?;
+
            }
+
        }
+

+
        Ok(())
+
    }
+

+
    fn delete_ref<R>(&self, name: &R) -> Result<(), write::DeleteRefError>
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        match self.find_reference(name.as_ref().as_str()) {
+
            Ok(mut r) => r.delete().map_err(write::DeleteRefError::backend),
+
            Err(e) if matches!(e.code(), git::raw::ErrorCode::NotFound) => Ok(()),
+
            Err(e) => Err(write::DeleteRefError::backend(e)),
+
        }
+
    }
+
}
+

+
fn create_reference(
+
    repository: &git::raw::Repository,
+
    reflog: &str,
+
    name: &RefStr,
+
    target: Oid,
+
) -> Result<(), write::WriteRefError> {
+
    repository
+
        .reference(name, target.into(), false, reflog)
+
        .map_err(|e| {
+
            if matches!(e.code(), raw::ErrorCode::Exists) {
+
                write::WriteRefError::ReferenceExists {
+
                    name: name.to_string(),
+
                }
+
            } else {
+
                write::WriteRefError::backend(e)
+
            }
+
        })?;
+
    Ok(())
+
}
+

+
fn upsert_reference(
+
    repository: &git::raw::Repository,
+
    reflog: &str,
+
    name: &RefStr,
+
    target: Oid,
+
) -> Result<(), write::WriteRefError> {
+
    repository
+
        .reference(name, target.into(), true, reflog)
+
        .map_err(write::WriteRefError::backend)?;
+
    Ok(())
+
}
+

+
fn cas_reference(
+
    repository: &git::raw::Repository,
+
    reflog: &str,
+
    name: &RefStr,
+
    target: Oid,
+
    expected: Oid,
+
) -> Result<(), write::WriteRefError> {
+
    // CAS requires `force=true` so that libgit2 skips the existence
+
    // check in `reference_path_available` and instead compares the
+
    // current value via `cmp_old_ref`.  With `force=false`, an existing
+
    // reference would always fail with `GIT_EEXISTS` before the old
+
    // value is ever compared.
+
    repository
+
        .reference_matching(name, target.into(), true, expected.into(), reflog)
+
        .map_err(|e| {
+
            if matches!(e.code(), raw::ErrorCode::Modified) {
+
                write::WriteRefError::CasFailed {
+
                    name: name.to_string(),
+
                    expected,
+
                }
+
            } else {
+
                write::WriteRefError::backend(e)
+
            }
+
        })?;
+
    Ok(())
+
}
+

+
impl SymbolicRefWriter for raw::Repository {
+
    fn write_symbolic_ref<R>(
+
        &self,
+
        name: &R,
+
        target: SymbolicRefTarget,
+
        reflog: &str,
+
    ) -> Result<(), write::SymbolicWriteError>
+
    where
+
        R: AsRef<RefStr>,
+
    {
+
        let name = name.as_ref();
+

+
        // Ensure the target reference exists.
+
        {
+
            let target = target.target();
+
            match self.find_reference(target) {
+
                Ok(_) => {}
+
                Err(e) if matches!(e.code(), git::raw::ErrorCode::NotFound) => {
+
                    return Err(write::SymbolicWriteError::MissingTarget {
+
                        name: name.to_ref_string(),
+
                        target: target.to_owned(),
+
                    });
+
                }
+
                Err(e) => {
+
                    return Err(write::SymbolicWriteError::backend(e));
+
                }
+
            }
+
        }
+

+
        match target {
+
            SymbolicRefTarget::Create { target } => {
+
                create_symbolic_reference(self, reflog, name, &target)?;
+
            }
+
            SymbolicRefTarget::Upsert { target } => {
+
                upsert_symbolic_reference(self, reflog, name, &target)?;
+
            }
+
            SymbolicRefTarget::Cas { target, expected } => {
+
                cas_symbolic_reference(self, reflog, name, &target, &expected)?;
+
            }
+
        }
+

+
        Ok(())
+
    }
+
}
+

+
fn create_symbolic_reference(
+
    repository: &git::raw::Repository,
+
    reflog: &str,
+
    name: &RefStr,
+
    target: &RefStr,
+
) -> Result<(), write::SymbolicWriteError> {
+
    repository
+
        .reference_symbolic(name, target, false, reflog)
+
        .map_err(|e| {
+
            if matches!(e.code(), raw::ErrorCode::Exists) {
+
                write::SymbolicWriteError::ReferenceExists {
+
                    name: name.to_ref_string(),
+
                    target: target.to_ref_string(),
+
                }
+
            } else {
+
                write::SymbolicWriteError::backend(e)
+
            }
+
        })?;
+
    Ok(())
+
}
+

+
fn upsert_symbolic_reference(
+
    repository: &git::raw::Repository,
+
    reflog: &str,
+
    name: &RefStr,
+
    target: &RefStr,
+
) -> Result<(), write::SymbolicWriteError> {
+
    repository
+
        .reference_symbolic(name, target, true, reflog)
+
        .map_err(write::SymbolicWriteError::backend)?;
+
    Ok(())
+
}
+

+
fn cas_symbolic_reference(
+
    repository: &git::raw::Repository,
+
    reflog: &str,
+
    name: &RefStr,
+
    target: &RefStr,
+
    expected: &RefStr,
+
) -> Result<(), write::SymbolicWriteError> {
+
    // See `cas_reference` for why `force=true` is required for CAS.
+
    repository
+
        .reference_symbolic_matching(name, target, true, expected, reflog)
+
        .map_err(|e| {
+
            if matches!(e.code(), raw::ErrorCode::Modified) {
+
                write::SymbolicWriteError::CasFailed {
+
                    name: name.to_ref_string(),
+
                    expected: expected.to_ref_string(),
+
                }
+
            } else {
+
                write::SymbolicWriteError::backend(e)
+
            }
+
        })?;
+
    Ok(())
+
}
added crates/radicle/src/git/repository/adapter/git2/revwalk.rs
@@ -0,0 +1,121 @@
+
use radicle_oid::Oid;
+

+
use crate::git;
+
use crate::git::raw;
+

+
use crate::git::repository::revwalk;
+
use crate::git::repository::revwalk::{Revwalk, RevwalkPlan, SortOrder};
+
use crate::git::repository::types::Commit;
+

+
/// [`Revwalk::RevwalkOids`] iterator using [`raw::Revwalk`].
+
pub struct RevwalkOids<'a> {
+
    inner: raw::Revwalk<'a>,
+
}
+

+
impl<'a> RevwalkOids<'a> {
+
    pub fn hide(&mut self, oid: Oid) -> Result<(), revwalk::OidError> {
+
        self.inner
+
            .hide(oid.into())
+
            .map_err(revwalk::OidError::backend)
+
    }
+
}
+

+
impl Iterator for RevwalkOids<'_> {
+
    type Item = Result<Oid, revwalk::OidError>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        self.inner
+
            .next()
+
            .map(|r| r.map(Oid::from).map_err(revwalk::OidError::backend))
+
    }
+
}
+

+
/// [`Revwalk::RevwalkCommits`] iterator using [`raw::Revwalk`].
+
pub struct RevwalkCommits<'a> {
+
    oids: RevwalkOids<'a>,
+
    backend: &'a raw::Repository,
+
}
+

+
impl<'a> RevwalkCommits<'a> {
+
    pub fn hide(&mut self, oid: Oid) -> Result<(), revwalk::OidError> {
+
        self.oids.hide(oid)
+
    }
+

+
    fn read(backend: &raw::Repository, oid: Oid) -> Result<Commit, revwalk::CommitError> {
+
        let odb = backend.odb().map_err(revwalk::CommitError::backend)?;
+
        let obj = odb
+
            .read(oid.into())
+
            .map_err(revwalk::CommitError::backend)?;
+
        Commit::from_bytes(obj.data()).map_err(|e| revwalk::CommitError::Parse { oid, source: e })
+
    }
+
}
+

+
impl Iterator for RevwalkCommits<'_> {
+
    type Item = Result<Commit, revwalk::CommitError>;
+

+
    fn next(&mut self) -> Option<Self::Item> {
+
        let oid = match self.oids.next()? {
+
            Ok(oid) => oid,
+
            Err(e) => return Some(Err(revwalk::CommitError::backend(e))),
+
        };
+
        Some(Self::read(self.backend, oid))
+
    }
+
}
+

+
/// Configure a [`raw::Revwalk`] from a [`RevwalkPlan`].
+
fn configure_revwalk(
+
    walk: &mut raw::Revwalk<'_>,
+
    plan: &RevwalkPlan,
+
) -> Result<(), revwalk::InitError> {
+
    let sort = match plan.sort_order() {
+
        SortOrder::Chronological => git::raw::Sort::TIME,
+
        SortOrder::Topological => git::raw::Sort::TOPOLOGICAL,
+
        SortOrder::Reverse => git::raw::Sort::REVERSE,
+
        SortOrder::TopologicalReverse => git::raw::Sort::TOPOLOGICAL | git::raw::Sort::REVERSE,
+
    };
+
    walk.set_sorting(sort)
+
        .map_err(revwalk::InitError::backend)?;
+

+
    if let Some((from, to)) = plan.range_bounds() {
+
        walk.push_range(&format!("{from}..{to}"))
+
            .map_err(revwalk::InitError::backend)?;
+
    }
+

+
    for oid in plan.starts() {
+
        walk.push((*oid).into())
+
            .map_err(revwalk::InitError::backend)?;
+
    }
+

+
    for oid in plan.hidden() {
+
        walk.hide((*oid).into())
+
            .map_err(revwalk::InitError::backend)?;
+
    }
+

+
    Ok(())
+
}
+

+
impl Revwalk for raw::Repository {
+
    type RevwalkOids<'a> = RevwalkOids<'a>;
+
    type RevwalkCommits<'a> = RevwalkCommits<'a>;
+

+
    fn revwalk_oids<'a>(
+
        &'a self,
+
        plan: &RevwalkPlan,
+
    ) -> Result<Self::RevwalkOids<'a>, revwalk::InitError> {
+
        let mut walk = self.revwalk().map_err(revwalk::InitError::backend)?;
+
        configure_revwalk(&mut walk, plan)?;
+
        Ok(RevwalkOids { inner: walk })
+
    }
+

+
    fn revwalk_commits<'a>(
+
        &'a self,
+
        plan: &RevwalkPlan,
+
    ) -> Result<Self::RevwalkCommits<'a>, revwalk::InitError> {
+
        let mut walk = self.revwalk().map_err(revwalk::InitError::backend)?;
+
        configure_revwalk(&mut walk, plan)?;
+
        Ok(RevwalkCommits {
+
            oids: RevwalkOids { inner: walk },
+
            backend: self,
+
        })
+
    }
+
}