From e854766fa7a1fabbf58ab03818f8228fbf860f2f Mon Sep 17 00:00:00 2001 From: Ryan Peach Date: Thu, 27 Feb 2025 10:55:19 -0500 Subject: [PATCH] Added an allow_staged command. This makes pre-commit a lot more usable. --- .pre-commit-hooks.yaml | 2 ++ Cargo.toml | 1 + src/config.rs | 11 +++++-- src/config/cli.rs | 33 +++++++++++++++++++-- src/config/file.rs | 7 +++-- src/lib.rs | 67 ++++++++++++++++++++++++++++++------------ src/rules.rs | 6 ++++ 7 files changed, 102 insertions(+), 25 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index f9dd68c..97db7f4 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -4,6 +4,8 @@ entry: mdlinker language: rust pass_filenames: false + args: + - --allow-staged - id: enforce-ascii name: Enforce ASCII Compliance description: Detects and replaces non-ASCII characters in specified files. diff --git a/Cargo.toml b/Cargo.toml index 3f9891d..e5f34a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,3 +77,4 @@ missing_errors_doc = "allow" similar_names = "allow" missing_panics_doc = "allow" module_name_repetitions = "allow" +struct-excessive-bools = "allow" diff --git a/src/config.rs b/src/config.rs index e5da7c0..adeb6a7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,6 +31,8 @@ pub enum NewConfigError { #[error("Pages directory missing")] #[help("Please provide a pages directory argument in either your cli or config file")] PagesDirectoryMissing, + #[error("Mutually exclusive options: {0} and {1}")] + MutuallyExclusiveError(String, String), } /// Config which contains both the cli and the config file @@ -71,6 +73,9 @@ pub struct Config { /// See [`self::cli::Config::allow_dirty`] #[builder(default = false)] pub allow_dirty: bool, + /// See [`self::cli::Config::allow_staged`] + #[builder(default = false)] + pub allow_staged: bool, /// See [`self::file::Config::ignore_word_pairs`] #[builder(default = vec![])] pub ignore_word_pairs: Vec<(String, String)>, @@ -98,7 +103,8 @@ pub trait Partial { &self, ) -> Option, ReplacePairCompilationError>>; fn fix(&self) -> Option; - fn allow_dirty(&self) -> Option; + fn allow_dirty(&self) -> Result, NewConfigError>; + fn allow_staged(&self) -> Result, NewConfigError>; fn ignore_word_pairs(&self) -> Option>; fn ignore_remaining(&self) -> Option; } @@ -161,7 +167,8 @@ fn combine_partials( } }) .maybe_fix(cli_config.fix().or(file_config.fix())) - .maybe_allow_dirty(cli_config.allow_dirty().or(file_config.allow_dirty())) + .maybe_allow_dirty(cli_config.allow_dirty()?.or(file_config.allow_dirty()?)) + .maybe_allow_staged(cli_config.allow_staged()?.or(file_config.allow_staged()?)) .pages_directory( cli_config .pages_directory() diff --git a/src/config/cli.rs b/src/config/cli.rs index ab5372c..17abe39 100644 --- a/src/config/cli.rs +++ b/src/config/cli.rs @@ -2,6 +2,7 @@ use clap::Parser; use std::path::PathBuf; use crate::{ + config::NewConfigError, file::{ content::wikilink::Alias, name::{Filename, FilenameLowercase}, @@ -57,10 +58,19 @@ pub struct Config { pub fix: bool, /// Whether or not to allow fixing in a "dirty" git repo, meaning - /// the git repo has uncommitted changes + /// the git repo has uncommitted changes and those changes will be overwritten by this apps + /// changes without user involvement. WARNING: Dangerous + /// Mutually exclusive with --allow-staged #[clap(long = "allow-dirty")] pub allow_dirty: bool, + /// Whether or not to allow fixing in a "dirty" git repo where everything is "staged", meaning + /// the git repo has uncommitted changes, but they still won't be changed by edits unless the + /// user stages the changes. + /// Mutually exclusive with --allow-dirty + #[clap(long = "allow-staged")] + pub allow_staged: bool, + /// Ignore remaining errors by adding them to the config #[clap(long = "ignore-remaining")] pub ignore_remaining: bool, @@ -111,8 +121,25 @@ impl Partial for Config { fn fix(&self) -> Option { Some(self.fix) } - fn allow_dirty(&self) -> Option { - Some(self.allow_dirty) + fn allow_dirty(&self) -> Result, NewConfigError> { + if self.allow_staged && self.allow_dirty { + Err(NewConfigError::MutuallyExclusiveError( + "allow_staged".to_string(), + "allow_dirty".to_string(), + )) + } else { + Ok(Some(self.allow_dirty)) + } + } + fn allow_staged(&self) -> Result, NewConfigError> { + if self.allow_staged && self.allow_dirty { + Err(NewConfigError::MutuallyExclusiveError( + "allow_staged".to_string(), + "allow_dirty".to_string(), + )) + } else { + Ok(Some(self.allow_staged)) + } } fn ignore_word_pairs(&self) -> Option> { None diff --git a/src/config/file.rs b/src/config/file.rs index d3b2309..9337fb0 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -154,8 +154,11 @@ impl Partial for Config { fn fix(&self) -> Option { None } - fn allow_dirty(&self) -> Option { - None + fn allow_dirty(&self) -> Result, NewConfigError> { + Ok(None) + } + fn allow_staged(&self) -> Result, NewConfigError> { + Ok(None) } fn ignore_word_pairs(&self) -> Option> { if self.ignore_word_pairs.is_empty() { diff --git a/src/lib.rs b/src/lib.rs index feb7fdc..ba1d60a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,37 +100,68 @@ pub enum OutputErrors { use git2::{Error, Repository, StatusOptions}; -fn is_repo_dirty(repo: &Repository) -> Result { - let mut options = StatusOptions::new(); - options - .include_untracked(true) - .recurse_untracked_dirs(true) - .exclude_submodules(true) - .include_unmodified(false) - .include_ignored(false); +pub enum RepoStatus { + Clean, + AllStaged, + Dirty, +} + +impl RepoStatus { + fn get_repo_status(repo: &Repository) -> Result { + let mut options = StatusOptions::new(); + options + .include_untracked(true) + .recurse_untracked_dirs(true) + .exclude_submodules(true) + .include_unmodified(false) + .include_ignored(false); - let statuses = repo.statuses(Some(&mut options))?; - Ok(!statuses.is_empty()) + let statuses = repo.statuses(Some(&mut options))?; + let mut staged = false; + let mut dirty = false; + for entry in statuses.iter() { + let status = entry.status(); + if status.is_wt_new() || status.is_wt_modified() || status.is_wt_deleted() { + dirty = true; + } + if status.is_index_new() || status.is_index_modified() || status.is_index_deleted() { + staged = true; + } + } + match (dirty, staged) { + (true, true) => Ok(RepoStatus::AllStaged), + (true, false) => Ok(RepoStatus::Dirty), + (false, _) => Ok(RepoStatus::Clean), + } + } } /// Runs [`check`] in a loop until no more fixes can be made fn fix(config: &config::Config) -> Result { // Check if the git repo is dirty match git2::Repository::open_from_env() { - Ok(git) => match is_repo_dirty(&git) { - Ok(is_dirty) => { - if !config.allow_dirty && is_dirty { - return Err(OutputErrors::FixError(rules::FixError::DirtyRepo { - backtrace: Backtrace::force_capture(), - })); - } + Ok(git) => match ( + RepoStatus::get_repo_status(&git), + config.allow_dirty, + config.allow_staged, + ) { + (Ok(RepoStatus::Dirty), false, _) => { + return Err(OutputErrors::FixError(rules::FixError::DirtyRepo { + backtrace: Backtrace::force_capture(), + })); + } + (Ok(RepoStatus::AllStaged), _, false) => { + return Err(OutputErrors::FixError(rules::FixError::UnstagedChanges { + backtrace: Backtrace::force_capture(), + })); } - Err(e) => { + (Err(e), _, _) => { return Err(OutputErrors::FixError(rules::FixError::GitError { source: e, backtrace: Backtrace::force_capture(), })); } + _ => {} }, Err(e) => { return Err(OutputErrors::FixError(rules::FixError::GitError { diff --git a/src/rules.rs b/src/rules.rs index 76c9250..d11e74b 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -95,6 +95,12 @@ pub enum FixError { #[backtrace] backtrace: Backtrace, }, + #[error("The git repo has unstaged changes")] + #[help("Please stage your changes")] + UnstagedChanges { + #[backtrace] + backtrace: Backtrace, + }, #[error("There was an error checking the git status: {source}")] GitError { source: git2::Error,