#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
#![forbid(unsafe_code)]
use fs_err as fs;
use std::{
collections::HashSet,
env::{self, consts},
ffi::OsStr,
ops::Deref,
path::{Path, PathBuf},
};
use anyhow::Result;
use devx_cmd::{cmd, run};
pub struct PreCommitContext {
staged_files: Vec<PathBuf>,
project_root: PathBuf,
}
impl PreCommitContext {
pub fn from_git_diff(project_root: impl Into<PathBuf>) -> Result<Self> {
let project_root = project_root.into();
let diff = cmd!(
"git",
"diff",
"--diff-filter",
"MAR",
"--name-only",
"--cached"
)
.current_dir(&project_root)
.read()?;
Ok(Self {
staged_files: diff.lines().map(PathBuf::from).collect(),
project_root,
})
}
pub fn staged_files(&self) -> impl Iterator<Item = &Path> {
self.staged_files.iter().map(PathBuf::as_path)
}
pub fn retain_staged_files(&mut self, mut f: impl FnMut(&Path) -> bool) {
self.staged_files.retain(|it| f(it));
}
pub fn touched_crates(&self) -> HashSet<String> {
self.staged_rust_files()
.filter_map(|rust_file_path| {
rust_file_path.ancestors().find_map(|candidate| {
let cargo_toml = self.project_root.join(candidate).join("Cargo.toml");
let cargo_toml = fs::read_to_string(&cargo_toml).ok()?;
Self::parse_crate_name(&cargo_toml)
})
})
.collect()
}
pub fn staged_rust_files(&self) -> impl Iterator<Item = &Path> {
self.staged_files
.iter()
.filter(|path| path.extension() == Some(OsStr::new("rs")))
.map(PathBuf::as_path)
}
fn parse_crate_name(cargo_toml: &str) -> Option<String> {
let name_prefix = "\nname = \"";
let name = cargo_toml.find(name_prefix)? + name_prefix.len();
let len = cargo_toml[name..]
.find('"')
.expect("Invalid toml, couldn't find closing double quote");
Some(cargo_toml[name..name + len].to_owned())
}
pub fn rustfmt(&self) -> Result<()> {
let touched_crates = self.touched_crates();
if touched_crates.is_empty() {
return Ok(());
}
cmd!(std::env::var("CARGO")
.as_ref()
.map(Deref::deref)
.unwrap_or("cargo"))
.arg("fmt")
.arg("--package")
.args(touched_crates)
.run()?;
Ok(())
}
pub fn stage_new_changes(&self) -> Result<()> {
run!("git", "update-index", "--again")?;
Ok(())
}
}
pub fn install_self_as_hook(project_root: impl AsRef<Path>) -> Result<()> {
let hook_path = project_root
.as_ref()
.join(".git")
.join("hooks")
.join("pre-commit")
.with_extension(consts::EXE_EXTENSION);
let me = env::current_exe()?;
fs::copy(me, hook_path)?;
Ok(())
}
pub fn locate_project_root() -> Result<PathBuf> {
Ok(env::var("GIT_DIR").map(Into::into).or_else(|_| {
cmd!("git", "rev-parse", "--show-toplevel")
.read()
.map(|it| it.trim_end().into())
})?)
}