diff --git a/Cargo.toml b/Cargo.toml index c8f806f..c60f097 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 3d69bd6..c419c2e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -28,6 +28,11 @@ pub enum NewConfigError { FileDoesNotParseError(#[from] toml::de::Error), #[error("ReplacePair compilation error")] ReplacePairCompilationError(#[from] ReplacePairCompilationError), + #[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), #[error("No files were passed to the config")] NoFilesPassedError, } @@ -74,6 +79,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)>, @@ -101,7 +109,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; } @@ -177,7 +186,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()?)) .maybe_ignore_word_pairs( cli_config .ignore_word_pairs() diff --git a/src/config/cli.rs b/src/config/cli.rs index 4c272ee..6c14b32 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, @@ -110,8 +120,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 4f57176..bf0c9fb 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -199,8 +199,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 d0f101d..1504806 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,17 +100,40 @@ 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 @@ -118,20 +141,28 @@ fn is_repo_dirty(repo: &Repository) -> Result { 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 591ced1..1f0f9ad 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -94,6 +94,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,