#![deny(unsafe_code)]
#![warn(missing_docs)]
#![warn(clippy::unwrap_used)]
use std::ffi::{OsStr, OsString};
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
const NO_COLOR: &str = "NO_COLOR";
const PATHSPEC_SEPARATOR: &str = "--";
pub trait Repo {
fn diff(&self, context_lines: usize, paths: &[&Path]) -> Result<Vec<u8>, RepoError>;
}
pub struct GitRepo {
cwd: PathBuf,
args: Vec<OsString>,
}
impl GitRepo {
pub fn new(cwd: impl Into<PathBuf>, args: impl Into<Vec<OsString>>) -> Self {
Self {
cwd: cwd.into(),
args: args.into(),
}
}
fn forwarded_args<'a>(&'a self, paths: &[&Path]) -> Vec<&'a OsStr> {
let args = self
.args
.iter()
.position(|arg| arg.as_os_str() == OsStr::new(PATHSPEC_SEPARATOR))
.map_or(self.args.as_slice(), |separator| &self.args[..separator]);
args.iter()
.map(OsString::as_os_str)
.filter(|arg| !is_refetched_path(arg, paths))
.collect()
}
}
impl Repo for GitRepo {
fn diff(&self, context_lines: usize, paths: &[&Path]) -> Result<Vec<u8>, RepoError> {
let forwarded = self.forwarded_args(paths);
let context = OsString::from(format!("-U{context_lines}"));
let output = Command::new("git")
.arg("diff")
.args(forwarded)
.arg(context)
.arg(PATHSPEC_SEPARATOR)
.args(paths)
.current_dir(&self.cwd)
.env(NO_COLOR, "1")
.output()?;
if !output.status.success() {
return Err(RepoError::Status(output.status.code().unwrap_or(1)));
}
Ok(output.stdout)
}
}
fn is_refetched_path(arg: &OsStr, paths: &[&Path]) -> bool {
let arg = Path::new(arg);
let normalized = arg.strip_prefix(".").unwrap_or(arg);
paths.iter().any(|path| {
let normalized_path = path.strip_prefix(".").unwrap_or(path);
normalized == normalized_path
})
}
#[derive(Debug)]
pub enum RepoError {
Io(io::Error),
Status(i32),
}
impl From<io::Error> for RepoError {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
impl std::fmt::Display for RepoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(error) => write!(f, "{error}"),
Self::Status(code) => write!(f, "git diff exited with status {code}"),
}
}
}
impl std::error::Error for RepoError {}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn forwarded_args_strip_refetched_pathspecs_without_separator() {
let repo = GitRepo::new(
".",
vec![
OsString::from("HEAD~14"),
OsString::from("HEAD"),
OsString::from("assets/css/style.css"),
],
);
let path = Path::new("assets/css/style.css");
let args = repo.forwarded_args(&[path]);
assert_eq!(args, vec![OsStr::new("HEAD~14"), OsStr::new("HEAD")]);
}
#[test]
fn forwarded_args_ignore_pathspecs_after_separator() {
let repo = GitRepo::new(
".",
vec![
OsString::from("HEAD~14"),
OsString::from("HEAD"),
OsString::from("--"),
OsString::from("assets/css/style.css"),
],
);
let path = Path::new("assets/css/style.css");
let args = repo.forwarded_args(&[path]);
assert_eq!(args, vec![OsStr::new("HEAD~14"), OsStr::new("HEAD")]);
}
}