diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index edbace7..078ed83 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,12 @@ exclude: | )$ repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - repo: https://github.com/crate-ci/typos rev: v1.26.0 hooks: diff --git a/README.md b/README.md index 7b66077..80ba78b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A reimplementation of the [pre-commit](https://pre-commit.com/) tool in Rust, providing a faster and dependency-free alternative. It aims to be a drop-in replacement for the original tool while also providing some more advanced features. -> [!WARNING] +> [!WARNING] > This project is still in very early development, only a few of the original pre-commit features are implemented. ## Features @@ -39,8 +39,8 @@ Please refer to the [official documentation](https://pre-commit.com/) for more i ## Acknowledgements -This project is heavily inspired by the original [pre-commit](https://pre-commit.com/) tool, and it wouldn't be possible without the hard work +This project is heavily inspired by the original [pre-commit](https://pre-commit.com/) tool, and it wouldn't be possible without the hard work of the maintainers and contributors of that project. -And a special thanks to the [Astral](https://github.com/astral-sh) team for their remarkable projects, particularly [uv](https://github.com/astral-sh/uv), +And a special thanks to the [Astral](https://github.com/astral-sh) team for their remarkable projects, particularly [uv](https://github.com/astral-sh/uv), from which I've learned a lot on how to write efficient and idiomatic Rust code. diff --git a/src/cli/run.rs b/src/cli/run.rs index c944c31..69f5320 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -17,8 +17,8 @@ use unicode_width::UnicodeWidthStr; use crate::cli::ExitStatus; use crate::config::Stage; -use crate::fs::normalize_path; -use crate::git::{get_all_files, get_changed_files, get_diff, get_staged_files}; +use crate::fs::{normalize_path, Simplified}; +use crate::git::{get_all_files, get_changed_files, get_diff, get_staged_files, GIT}; use crate::hook::{Hook, Project}; use crate::identify::tags_from_path; use crate::printer::Printer; @@ -37,8 +37,18 @@ pub(crate) async fn run( verbose: bool, printer: Printer, ) -> Result { + let config_file = Project::find_config_file(config)?; + if config_not_staged(&config_file).await? { + writeln!( + printer.stderr(), + "Your pre-commit configuration is unstaged.\n`git add {}` to fix this.", + &config_file.user_display() + )?; + return Ok(ExitStatus::Failure); + } + + let mut project = Project::new(config_file)?; let store = Store::from_settings()?.init()?; - let mut project = Project::current(config)?; // TODO: check .pre-commit-config.yaml status and git status // TODO: fill env vars @@ -123,6 +133,18 @@ pub(crate) async fn run( Ok(ExitStatus::Success) } +async fn config_not_staged(config: &Path) -> Result { + let output = Command::new(GIT.as_ref()?) + .arg("diff") + .arg("--quiet") // Implies --exit-code + .arg("--no-ext-diff") + .arg(config) + .status() + .await?; + + Ok(!output.success()) +} + fn get_skips() -> Vec { match std::env::var_os("SKIP") { Some(s) if !s.is_empty() => s diff --git a/src/config.rs b/src/config.rs index 5f87037..b2df162 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,6 +7,8 @@ use anyhow::Result; use serde::{Deserialize, Deserializer, Serialize}; use url::Url; +use crate::fs::Simplified; + pub const CONFIG_FILE: &str = ".pre-commit-config.yaml"; pub const MANIFEST_FILE: &str = ".pre-commit-hooks.yaml"; @@ -505,6 +507,9 @@ pub struct ManifestWire { #[derive(Debug, thiserror::Error)] pub enum Error { + #[error("Config file not found: {0}")] + NotFound(String), + #[error(transparent)] Io(#[from] std::io::Error), @@ -517,9 +522,15 @@ pub enum Error { /// Read the configuration file from the given path. pub fn read_config(path: &Path) -> Result { - let content = fs_err::read_to_string(path)?; - let config = - serde_yaml::from_str(&content).map_err(|e| Error::Yaml(path.display().to_string(), e))?; + let content = match fs_err::read_to_string(path) { + Ok(content) => content, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + return Err(Error::NotFound(path.user_display().to_string())); + } + Err(e) => return Err(e.into()), + }; + let config = serde_yaml::from_str(&content) + .map_err(|e| Error::Yaml(path.user_display().to_string(), e))?; Ok(config) } @@ -527,8 +538,8 @@ pub fn read_config(path: &Path) -> Result { /// Read the manifest file from the given path. pub fn read_manifest(path: &Path) -> Result { let content = fs_err::read_to_string(path)?; - let manifest = - serde_yaml::from_str(&content).map_err(|e| Error::Yaml(path.display().to_string(), e))?; + let manifest = serde_yaml::from_str(&content) + .map_err(|e| Error::Yaml(path.user_display().to_string(), e))?; Ok(manifest) } diff --git a/src/fs.rs b/src/fs.rs index 33e48b8..856a0fe 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -244,3 +244,46 @@ pub fn relative_to( Ok(up.join(stripped)) } + +pub trait Simplified { + /// Simplify a [`Path`]. + /// + /// On Windows, this will strip the `\\?\` prefix from paths. On other platforms, it's a no-op. + fn simplified(&self) -> &Path; + + /// Render a [`Path`] for display. + /// + /// On Windows, this will strip the `\\?\` prefix from paths. On other platforms, it's + /// equivalent to [`std::path::Display`]. + fn simplified_display(&self) -> impl Display; + + /// Render a [`Path`] for user-facing display. + /// + /// Like [`simplified_display`], but relativizes the path against the current working directory. + fn user_display(&self) -> impl Display; +} + +impl> Simplified for T { + fn simplified(&self) -> &Path { + dunce::simplified(self.as_ref()) + } + + fn simplified_display(&self) -> impl Display { + dunce::simplified(self.as_ref()).display() + } + + fn user_display(&self) -> impl Display { + let path = dunce::simplified(self.as_ref()); + + // If current working directory is root, display the path as-is. + if CWD.ancestors().nth(1).is_none() { + return path.display(); + } + + // Attempt to strip the current working directory, then the canonicalized current working + // directory, in case they differ. + let path = path.strip_prefix(CWD.simplified()).unwrap_or(path); + + path.display() + } +} diff --git a/src/git.rs b/src/git.rs index 0865e07..43efac8 100644 --- a/src/git.rs +++ b/src/git.rs @@ -14,7 +14,7 @@ pub enum Error { GitNotFound(#[from] which::Error), } -static GIT: LazyLock> = LazyLock::new(|| which::which("git")); +pub static GIT: LazyLock> = LazyLock::new(|| which::which("git")); static GIT_ENV: LazyLock> = LazyLock::new(|| { let keep = &[ diff --git a/src/hook.rs b/src/hook.rs index 5085429..8991b4b 100644 --- a/src/hook.rs +++ b/src/hook.rs @@ -15,7 +15,7 @@ use crate::config::{ self, read_config, read_manifest, ConfigLocalHook, ConfigRemoteHook, ConfigRepo, ConfigWire, ManifestHook, Stage, CONFIG_FILE, MANIFEST_FILE, }; -use crate::fs::CWD; +use crate::fs::{Simplified, CWD}; use crate::languages::{Language, DEFAULT_VERSION}; use crate::printer::Printer; use crate::store::Store; @@ -26,7 +26,7 @@ pub enum Error { #[error("Failed to parse URL: {0}")] InvalidUrl(#[from] url::ParseError), #[error(transparent)] - ReadConfig(#[from] config::Error), + Config(#[from] config::Error), #[error("Hook {hook} in not present in repository {repo}")] HookNotFound { hook: String, repo: String }, #[error(transparent)] @@ -106,15 +106,23 @@ impl Display for Repo { } pub struct Project { - root: PathBuf, + config_path: PathBuf, config: ConfigWire, repos: Vec>, } impl Project { + pub fn find_config_file(config: Option) -> Result { + let file = config.unwrap_or_else(|| CWD.join(CONFIG_FILE)); + if file.try_exists()? { + return Ok(file); + } + let file = file.user_display().to_string(); + Err(Error::Config(config::Error::NotFound(file))) + } + /// Load a project configuration from a directory. - pub fn from_directory(root: PathBuf, config: Option) -> Result { - let config_path = config.unwrap_or_else(|| root.join(CONFIG_FILE)); + pub fn new(config_path: PathBuf) -> Result { debug!( "Loading project configuration from {}", config_path.display() @@ -122,17 +130,12 @@ impl Project { let config = read_config(&config_path)?; let size = config.repos.len(); Ok(Self { - root, config, + config_path, repos: Vec::with_capacity(size), }) } - /// Load project configuration from the current directory. - pub fn current(config: Option) -> Result { - Self::from_directory(CWD.clone(), config) - } - pub fn config(&self) -> &ConfigWire { &self.config }