Skip to content
This repository was archived by the owner on May 11, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,4 @@ missing_errors_doc = "allow"
similar_names = "allow"
missing_panics_doc = "allow"
module_name_repetitions = "allow"
struct-excessive-bools = "allow"
14 changes: 12 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down Expand Up @@ -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)>,
Expand Down Expand Up @@ -101,7 +109,8 @@ pub trait Partial {
&self,
) -> Option<Result<ReplacePair<Alias, FilenameLowercase>, ReplacePairCompilationError>>;
fn fix(&self) -> Option<bool>;
fn allow_dirty(&self) -> Option<bool>;
fn allow_dirty(&self) -> Result<Option<bool>, NewConfigError>;
fn allow_staged(&self) -> Result<Option<bool>, NewConfigError>;
fn ignore_word_pairs(&self) -> Option<Vec<(String, String)>>;
fn ignore_remaining(&self) -> Option<bool>;
}
Expand Down Expand Up @@ -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()
Expand Down
33 changes: 30 additions & 3 deletions src/config/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use clap::Parser;
use std::path::PathBuf;

use crate::{
config::NewConfigError,
file::{
content::wikilink::Alias,
name::{Filename, FilenameLowercase},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -110,8 +120,25 @@ impl Partial for Config {
fn fix(&self) -> Option<bool> {
Some(self.fix)
}
fn allow_dirty(&self) -> Option<bool> {
Some(self.allow_dirty)
fn allow_dirty(&self) -> Result<Option<bool>, 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<Option<bool>, 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<Vec<(String, String)>> {
None
Expand Down
7 changes: 5 additions & 2 deletions src/config/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,11 @@ impl Partial for Config {
fn fix(&self) -> Option<bool> {
None
}
fn allow_dirty(&self) -> Option<bool> {
None
fn allow_dirty(&self) -> Result<Option<bool>, NewConfigError> {
Ok(None)
}
fn allow_staged(&self) -> Result<Option<bool>, NewConfigError> {
Ok(None)
}
fn ignore_word_pairs(&self) -> Option<Vec<(String, String)>> {
if self.ignore_word_pairs.is_empty() {
Expand Down
67 changes: 49 additions & 18 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,38 +100,69 @@ pub enum OutputErrors {

use git2::{Error, Repository, StatusOptions};

fn is_repo_dirty(repo: &Repository) -> Result<bool, Error> {
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<RepoStatus, Error> {
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
#[allow(clippy::result_large_err)]
fn fix(config: &config::Config) -> Result<OutputReport, OutputErrors> {
// 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 {
Expand Down
6 changes: 6 additions & 0 deletions src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down