hit 0.2.0

Utilities for working with git.
Documentation
use crate::Git;
use std::path::{Path, PathBuf};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum Error {
    #[error("Failed to fetch repo: {0}")]
    FetchFailed(#[source] bossy::Error),
    #[error("Failed to get checkout revision: {0}")]
    RevParseLocalFailed(#[source] bossy::Error),
    #[error("Failed to get upstream revision: {0}")]
    RevParseRemoteFailed(#[source] bossy::Error),
    #[error("Failed to get commit log: {0}")]
    LogFailed(#[source] bossy::Error),
    #[error("Failed to create parent directory {path:?}: {source}")]
    ParentDirCreationFailed {
        path: PathBuf,
        source: std::io::Error,
    },
    #[error("Failed to clone repo: {0}")]
    CloneFailed(#[source] bossy::Error),
    #[error("Failed to reset repo: {0}")]
    ResetFailed(#[source] bossy::Error),
    #[error("Failed to clean repo: {0}")]
    CleanFailed(#[source] bossy::Error),
}

#[derive(Clone, Copy, Debug)]
pub enum Status {
    Stale,
    Fresh,
}

impl Status {
    pub fn stale(self) -> bool {
        matches!(self, Self::Stale)
    }
}

#[derive(Clone, Debug)]
pub struct Repo {
    path: PathBuf,
}

impl Repo {
    pub fn from_path(path: impl Into<PathBuf>) -> Self {
        Self { path: path.into() }
    }

    pub fn path(&self) -> &Path {
        &self.path
    }

    pub fn git(&self) -> Git<'_> {
        Git::new(self.path())
    }

    pub fn status(&self) -> Result<Status, Error> {
        let status = if !self.path().is_dir() {
            Status::Stale
        } else {
            let git = self.git();
            git.command_parse("fetch origin")
                .run_and_wait()
                .map_err(Error::FetchFailed)?;
            let local = git
                .command_parse("rev-parse HEAD")
                .run_and_wait_for_output()
                .map_err(Error::RevParseLocalFailed)?;
            let remote = git
                .command_parse("rev-parse @{u}")
                .run_and_wait_for_output()
                .map_err(Error::RevParseRemoteFailed)?;
            if local.stdout() != remote.stdout() {
                Status::Stale
            } else {
                Status::Fresh
            }
        };
        Ok(status)
    }

    pub fn latest_commit(&self, format: impl AsRef<str>) -> Result<String, Error> {
        self.git()
            .command_parse(format!("log -1 --pretty={}", format.as_ref()))
            .run_and_wait_for_str(|s| s.trim().to_owned())
            .map_err(Error::LogFailed)
    }

    pub fn latest_subject(&self) -> Result<String, Error> {
        self.latest_commit("%s")
    }

    pub fn latest_body(&self) -> Result<String, Error> {
        self.latest_commit("%b")
    }

    pub fn update(&self, url: impl AsRef<std::ffi::OsStr>) -> Result<(), Error> {
        let path = self.path();
        if !path.is_dir() {
            let parent = self
                .path()
                .parent()
                .expect("developer error: `Repo` path was at root");
            if !parent.is_dir() {
                std::fs::create_dir_all(parent).map_err(|source| {
                    Error::ParentDirCreationFailed {
                        path: parent.to_owned(),
                        source,
                    }
                })?;
            }
            Git::new(parent)
                .command_parse("clone --depth 1 --single-branch")
                .with_arg(url)
                .with_arg(path)
                .run_and_wait()
                .map_err(Error::CloneFailed)?;
        } else {
            println!(
                "Updating `{}` repo...",
                Path::new(
                    self.path()
                        .file_name()
                        .expect("developer error: `Repo` path had no file name")
                )
                .display()
            );
            self.git()
                .command_parse("fetch --depth 1")
                .run_and_wait()
                .map_err(Error::FetchFailed)?;
            self.git()
                .command_parse("reset --hard origin/master")
                .run_and_wait()
                .map_err(Error::ResetFailed)?;
            self.git()
                .command_parse("clean -dfx --exclude /target")
                .run_and_wait()
                .map_err(Error::CleanFailed)?;
        }
        Ok(())
    }
}