3

I am working with some code that uses the libgit2 bindings for Rust to perform operations on a Git repository.

I have a section of code that transforms a "committish" reference (something that either is a commit or ultimately references a commit, like a tag) that currently looks like this:

        let mut target_commit = target_object
            .peel(git2::ObjectType::Commit)
            .map_err(|_| anyhow!("Target `{commitish}` cannot be evaluated as a commit"))?
            .into_commit()
            .map_err(|_| anyhow!("Target `{commitish}` cannot be evaluated as a commit"))?;

I'd like to avoid having two identical calls to map_err in that chain. Based on the description of the and_then combinator...

Calls op if the result is Ok, otherwise returns the Err value of self.

...I thought maybe this would work:

        let mut target_commit = target_object
            .peel(git2::ObjectType::Commit)
            .and_then(|c| c.into_commit())
            .map_err(|_| anyhow!("Target `{commitish}` cannot be evaluated as a commit"))?;

But that fails with:

106 |             .and_then(|c| c.into_commit())
    |                           ^^^^^^^^^^^^^^^ expected struct `git2::Error`, found struct `git2::Object`

What's the correct way to simplify this expression?

larsks
  • 277,717
  • 41
  • 399
  • 399
  • 2
    Remember `map_err` can take a `fn` as an argument, so you should be able to define `fn target_cannot(_e: impl Error)` and then `map_err(target_cannot)`, at least presuming you can make the argument sufficiently generic. – tadman Aug 20 '22 at 23:06
  • @tadman: That is nice, but [`into_commit()`](https://docs.rs/git2/latest/git2/struct.Object.html#method.into_commit) returns the original object in case of error, so it does not `impl Error`, I think. A fully generic `fn target_cannot(_: E) { anyhow!(...) }` should work, although I usually prefer `let target_cannot = |_| anyhow!(...);` because it is shorter, more like the original code and can capture local variables for context. – rodrigo Aug 21 '22 at 01:22

1 Answers1

3

Simplification could mean multiple things. One way to simplify is to remove the duplication of the anyhow!() invocation.

Since you don't actually use the error information, you can use Result::ok to discard the error information by converting the Result<T, E> to Option<T>, then use Option::ok_or_else at the end of a chain to turn the final Option back into a Result.

let mut target_commit = target_object
    .peel(git2::ObjectType::Commit).ok()
    .and_then(|c| c.into_commit().ok())
    .ok_or_else(|| anyhow!("Target `{commitish}` cannot be evaluated as a commit"))?;
cdhowie
  • 158,093
  • 24
  • 286
  • 300
  • Just to make sure I follow: Calling `.ok()` gets us an `Option`, which will either wrap the result value from the previous method call or will be `None` in the event of an error. `ok_or_else` transforms an `Option` into a `Result`; if it's an `Err` then that is propagated upwards by `?`, otherwise it's unwrapped and the value is assigned to `target_commit`? – larsks Aug 21 '22 at 01:59
  • @larsks Yep, that's it. – cdhowie Aug 21 '22 at 03:01