use std::path::{Path, PathBuf};
use crate::{
ModifyGuardError,
repository::{Repository, RepositoryChanges},
};
#[cfg(test)]
mod tests;
#[expect(
missing_copy_implementations,
reason = "Copy is intentionally not part of the API contract"
)]
#[expect(
clippy::struct_excessive_bools,
reason = "This struct represents independent `--allow-*` and scope configuration flags whose combinations are meaningful, not a state machine"
)]
#[derive(Debug, Clone)]
pub struct AllowOptions {
allow_no_vcs: bool,
allow_dirty: bool,
allow_staged: bool,
check_entire_repository: bool,
}
impl Default for AllowOptions {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl AllowOptions {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
allow_no_vcs: false,
allow_dirty: false,
allow_staged: false,
check_entire_repository: false,
}
}
#[inline]
#[must_use]
pub const fn allow_no_vcs(mut self, enabled: bool) -> Self {
self.allow_no_vcs = enabled;
self
}
#[inline]
#[must_use]
pub const fn allow_dirty(mut self, enabled: bool) -> Self {
self.allow_dirty = enabled;
self
}
#[inline]
#[must_use]
pub const fn allow_staged(mut self, enabled: bool) -> Self {
self.allow_staged = enabled;
self
}
#[inline]
#[must_use]
pub const fn check_entire_repository(mut self, enabled: bool) -> Self {
self.check_entire_repository = enabled;
self
}
fn find_changes<R>(
&self,
repo: &R,
path: &Path,
) -> Result<Option<RepositoryChanges>, ModifyGuardError>
where
R: AllowOptionsRepository,
{
if self.check_entire_repository {
repo.repository_changes()
} else {
let wt_path = repo.resolve_path(path)?;
repo.path_changes(&wt_path)
}
}
#[inline]
pub fn check_safe_to_modify<P>(&self, path: P) -> Result<ModificationSafety, ModifyGuardError>
where
P: AsRef<Path>,
{
self.check_safe_to_modify_with_backend(path, &RealBackend)
}
fn check_safe_to_modify_with_backend<P, B>(
&self,
path: P,
backend: &B,
) -> Result<ModificationSafety, ModifyGuardError>
where
P: AsRef<Path>,
B: AllowOptionsBackend,
{
let path = path.as_ref();
if self.allow_no_vcs {
return Ok(ModificationSafety::Safe);
}
let Some(repo) = backend.discover(path)? else {
return Ok(UnsafeModificationReason::NoVcs.into());
};
if self.allow_dirty {
return Ok(ModificationSafety::Safe);
}
let Some(changes) = self.find_changes(&repo, path)? else {
return Ok(ModificationSafety::Safe);
};
let dirty_files = changes
.files()
.filter(|f| f.is_dirty())
.map(|f| f.wt_path().to_owned())
.collect::<Vec<_>>();
if self.allow_staged {
if !dirty_files.is_empty() {
return Ok(UnsafeModificationReason::Dirty {
worktree: repo.worktree().to_owned(),
dirty_files,
staged_files: vec![],
}
.into());
}
return Ok(ModificationSafety::Safe);
}
let staged_files = changes
.files()
.filter(|f| f.is_staged())
.map(|f| f.wt_path().to_owned())
.collect::<Vec<_>>();
if dirty_files.is_empty() {
return Ok(UnsafeModificationReason::Staged {
worktree: repo.worktree().to_owned(),
staged_files,
}
.into());
}
Ok(UnsafeModificationReason::Dirty {
worktree: repo.worktree().to_owned(),
dirty_files,
staged_files,
}
.into())
}
}
trait AllowOptionsBackend {
type Repo: AllowOptionsRepository;
fn discover(&self, path: &Path) -> Result<Option<Self::Repo>, ModifyGuardError>;
}
trait AllowOptionsRepository {
fn worktree(&self) -> &Path;
fn resolve_path(&self, path: &Path) -> Result<PathBuf, ModifyGuardError>;
fn path_changes(&self, wt_path: &Path) -> Result<Option<RepositoryChanges>, ModifyGuardError>;
fn repository_changes(&self) -> Result<Option<RepositoryChanges>, ModifyGuardError>;
}
struct RealBackend;
impl AllowOptionsBackend for RealBackend {
type Repo = Repository;
fn discover(&self, path: &Path) -> Result<Option<Self::Repo>, ModifyGuardError> {
Repository::discover(path)
}
}
impl AllowOptionsRepository for Repository {
fn worktree(&self) -> &Path {
Repository::worktree(self)
}
fn resolve_path(&self, path: &Path) -> Result<PathBuf, ModifyGuardError> {
Repository::resolve_path(self, path)
}
fn path_changes(&self, wt_path: &Path) -> Result<Option<RepositoryChanges>, ModifyGuardError> {
Repository::path_changes(self, wt_path)
}
fn repository_changes(&self) -> Result<Option<RepositoryChanges>, ModifyGuardError> {
Repository::repository_changes(self)
}
}
#[expect(
clippy::exhaustive_enums,
reason = "Callers should exhaustively match the current outcomes; adding a new variant is an intentional breaking API change"
)]
#[derive(Debug)]
pub enum ModificationSafety {
Safe,
Unsafe(UnsafeModificationReason),
}
#[doc = "This type explains why [`ModificationSafety::Unsafe`] was returned."]
#[derive(Debug)]
#[non_exhaustive]
pub enum UnsafeModificationReason {
NoVcs,
Dirty {
worktree: PathBuf,
dirty_files: Vec<PathBuf>,
staged_files: Vec<PathBuf>,
},
Staged {
worktree: PathBuf,
staged_files: Vec<PathBuf>,
},
}
impl From<UnsafeModificationReason> for ModificationSafety {
#[inline]
fn from(reason: UnsafeModificationReason) -> Self {
Self::Unsafe(reason)
}
}