use crate::cmd::{Utf8CmdOutput, Utf8CmdOutputLossy};
use std::convert::From;
use std::path::PathBuf;
use std::process::Output;
use thiserror::Error;
pub type QueryDir = PathBuf;
pub const ERROR_REPO_NOT_CLEAN: &str = "repo not clean, references not hermetic";
pub const ERROR_REPO_NOT_DIRTY: &str = "repo not dirty";
pub const ERROR_REPO_NONEMPTY_OUTPUT: &str = "unexpectedly returned no lines";
#[derive(Error, Debug)]
pub enum DriverError {
#[error("directory access issue: {0}")]
Directory(String),
#[error("vcs call failed: {:?}: {:?}", .context, .source)]
Command {
context: String,
source: std::io::Error,
},
#[error("vcs stderr: {:?}: {:?}", .context, .stderr)]
Stderr { context: String, stderr: String },
#[error("vcs returned a problematic root name")]
RootName(#[from] std::string::FromUtf8Error),
#[error("{0}")]
Unknown(String),
}
impl DriverError {
pub fn unwrap_cmd(
context: String,
cmd_output: std::io::Result<Output>,
) -> Result<Utf8CmdOutput, Self> {
let output = cmd_output.map_err(|e| Self::Command { context, source: e })?;
Ok(Utf8CmdOutput::from(output))
}
pub fn expect_cmd(
context: String,
cmd_output: std::io::Result<Output>,
) -> Result<Utf8CmdOutput, Self> {
let utf8_output = Self::unwrap_cmd(context.clone(), cmd_output)?;
if !utf8_output.status.success() {
return Err(Self::Stderr {
context,
stderr: utf8_output.stderr.map_err(|e| {
format!(
"bad utf8 from stderr: {}; lossy conversion: {}",
e, utf8_output.stderr_lossy
)
})?,
});
}
Ok(utf8_output)
}
pub fn unwrap_cmd_lossy(
context: String,
cmd_output: std::io::Result<Output>,
) -> Result<Utf8CmdOutputLossy, Self> {
let output = cmd_output.map_err(|e| Self::Command { context, source: e })?;
Ok(Utf8CmdOutputLossy::from(output))
}
pub fn expect_cmd_lossy(
context: String,
cmd_output: std::io::Result<Output>,
) -> Result<Utf8CmdOutputLossy, Self> {
let utf8_output = Self::unwrap_cmd_lossy(context.clone(), cmd_output)?;
if !utf8_output.status.success() {
return Err(Self::Stderr {
context,
stderr: utf8_output.stderr,
});
}
Ok(utf8_output)
}
pub fn expect_cmd_line(context: &str, output: &Utf8CmdOutputLossy) -> Result<String, Self> {
let lines = output.stdout_strings();
if lines.len() > 1 {
return Err(Self::Unknown(format!(
"unexpectedly got multiple ({}) lines: {}:\n'''\n{:?}\n'''\n'''",
lines.len(),
context,
lines
)));
}
Ok(lines
.last()
.ok_or_else(|| Self::Unknown(format!("unexpectedly returned empty output: {context}")))?
.to_string())
}
pub fn expect_cmd_lines(
output: std::io::Result<Output>,
min_lines: u8,
context: &str,
expect_msg: Option<String>,
) -> Result<Vec<String>, Self> {
let lines = Self::expect_cmd_lossy(context.to_string(), output)?.stdout_strings();
if lines.len() < min_lines.into() {
return Err(Self::Unknown(format!(
"{}: {}",
context,
expect_msg.unwrap_or(ERROR_REPO_NONEMPTY_OUTPUT.to_string()),
)));
}
Ok(lines)
}
}
impl From<String> for DriverError {
fn from(item: String) -> Self {
DriverError::Unknown(item)
}
}
pub type HistoryRefId = String;
pub type HistoryRefName = String;
pub struct HistoryRef {
pub id: HistoryRefId,
pub name: Option<HistoryRefName>,
pub dirty: bool,
}
pub struct AncestorRef {
pub id: HistoryRefId,
pub name: HistoryRefName,
pub distance: u64,
}
pub type VcsAvailable = Utf8CmdOutputLossy;
pub trait Validator
where
Self: std::fmt::Debug,
{
fn new_driver(&self, dir: QueryDir) -> Result<Option<Box<dyn Driver>>, DriverError>;
fn check_health(&self) -> Result<VcsAvailable, DriverError>;
}
pub trait Driver
where
Self: std::fmt::Debug,
{
fn root(&self) -> Result<QueryDir, DriverError>;
fn is_clean(&self) -> Result<bool, DriverError> {
let dirty_files = self.dirty_files(true )?;
Ok(dirty_files.is_empty())
}
fn dirty_files(&self, clean_ok: bool) -> Result<Vec<QueryDir>, DriverError>;
fn tracked_files(&self) -> Result<Vec<QueryDir>, DriverError>;
fn parent_ref(&self) -> Result<HistoryRef, DriverError> {
todo!(); }
fn parent_ref_id(&self) -> Result<HistoryRefId, DriverError> {
todo!(); }
fn parent_ref_name(&self) -> Result<Option<HistoryRefName>, DriverError> {
todo!(); }
fn first_ancestor_ref_name(
&self,
_limit: Option<u64>,
) -> Result<Option<AncestorRef>, DriverError> {
todo!(); }
fn current_ref(&self, dirty_ok: bool) -> Result<HistoryRef, DriverError> {
let is_dirty = !self.is_clean()?;
if !dirty_ok && is_dirty {
return Err(ERROR_REPO_NOT_CLEAN.to_string().into());
}
Ok(HistoryRef {
id: self.current_ref_id(dirty_ok)?,
name: self.current_ref_name(dirty_ok)?,
dirty: is_dirty,
})
}
fn current_ref_id(&self, _dirty_ok: bool) -> Result<HistoryRefId, DriverError>;
fn current_ref_name(&self, _dirty_ok: bool) -> Result<Option<HistoryRefName>, DriverError>;
}