blob: 4bfc02e02a4cff74f0947a0d8740db1b850c69b5 [file] [log] [blame] [edit]
use crate::walk::ForDeletionMode;
use crate::{Entry, EntryRef};
use std::borrow::Cow;
/// A way of attaching additional information to an [Entry] .
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum Property {
/// The entry was named `.git`, matched according to the case-sensitivity rules of the repository.
DotGit,
/// The entry is a directory, and that directory is empty.
EmptyDirectory,
/// The entry is a directory, it is empty and the current working directory.
///
/// The caller should pay special attention to this very special case, as it is indeed only possible to run into it
/// while traversing the directory for deletion.
/// Non-empty directory will never be collapsed, hence if they are working directories, they naturally become unobservable.
EmptyDirectoryAndCWD,
/// Always in conjunction with a directory on disk that is also known as cone-mode sparse-checkout exclude marker
/// - i.e. a directory that is excluded, so its whole content is excluded and not checked out nor is part of the index.
///
/// Note that evne if the directory is empty, it will only have this state, not `EmptyDirectory`.
TrackedExcluded,
}
/// The kind of the entry, seated in their kinds available on disk.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum Kind {
/// The entry is a blob, executable or not.
File,
/// The entry is a symlink.
Symlink,
/// The entry is an ordinary directory.
///
/// Note that since we don't check for bare repositories, this could in fact be a collapsed
/// bare repository. To be sure, check it again with [`gix_discover::is_git()`] and act accordingly.
Directory,
/// The entry is a directory which *contains* a `.git` folder, or a submodule entry in the index.
Repository,
}
/// The kind of entry as obtained from a directory.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum Status {
/// The entry was removed from the walk due to its other properties, like [Property] or [PathspecMatch]
///
/// Note that entries flagged as `DotGit` directory will always be considered `Pruned`, but if they are
/// also ignored, in delete mode, they will be considered `Ignored` instead. This way, it's easier to remove them
/// while they will not be available for any interactions in read-only mode.
Pruned,
/// The entry is tracked in Git.
Tracked,
/// The entry is ignored as per `.gitignore` files and their rules.
///
/// If this is a directory, then its entire contents is ignored. Otherwise, possibly due to configuration, individual ignored files are listed.
Ignored(gix_ignore::Kind),
/// The entry is not tracked by git yet, it was not found in the [index](gix_index::State).
///
/// If it's a directory, the entire directory contents is untracked.
Untracked,
}
/// Describe how a pathspec pattern matched.
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
pub enum PathspecMatch {
/// The match happened because there wasn't any pattern, which matches all, or because there was a nil pattern or one with an empty path.
/// Thus, this is not a match by merit.
Always,
/// A match happened, but the pattern excludes everything it matches, which means this entry was excluded.
Excluded,
/// The first part of a pathspec matches, like `dir/` that matches `dir/a`.
Prefix,
/// The whole pathspec matched and used a wildcard match, like `a/*` matching `a/file`.
WildcardMatch,
/// The entire pathspec matched, letter by letter, e.g. `a/file` matching `a/file`.
Verbatim,
}
impl PathspecMatch {
pub(crate) fn should_ignore(&self) -> bool {
match self {
PathspecMatch::Always | PathspecMatch::Excluded => true,
PathspecMatch::Prefix | PathspecMatch::WildcardMatch | PathspecMatch::Verbatim => false,
}
}
}
impl From<gix_pathspec::search::MatchKind> for PathspecMatch {
fn from(kind: gix_pathspec::search::MatchKind) -> Self {
match kind {
gix_pathspec::search::MatchKind::Always => Self::Always,
gix_pathspec::search::MatchKind::Prefix => Self::Prefix,
gix_pathspec::search::MatchKind::WildcardMatch => Self::WildcardMatch,
gix_pathspec::search::MatchKind::Verbatim => Self::Verbatim,
}
}
}
impl From<gix_pathspec::search::Match<'_>> for PathspecMatch {
fn from(m: gix_pathspec::search::Match<'_>) -> Self {
if m.is_excluded() {
PathspecMatch::Excluded
} else {
m.kind.into()
}
}
}
/// Conversion
impl EntryRef<'_> {
/// Strip the lifetime to obtain a fully owned copy.
pub fn to_owned(&self) -> Entry {
Entry {
rela_path: self.rela_path.clone().into_owned(),
status: self.status,
property: self.property,
disk_kind: self.disk_kind,
index_kind: self.index_kind,
pathspec_match: self.pathspec_match,
}
}
/// Turn this instance into a fully owned copy.
pub fn into_owned(self) -> Entry {
Entry {
rela_path: self.rela_path.into_owned(),
status: self.status,
property: self.property,
disk_kind: self.disk_kind,
index_kind: self.index_kind,
pathspec_match: self.pathspec_match,
}
}
}
/// Conversion
impl Entry {
/// Obtain an [`EntryRef`] from this instance.
pub fn to_ref(&self) -> EntryRef<'_> {
EntryRef {
rela_path: Cow::Borrowed(self.rela_path.as_ref()),
status: self.status,
property: self.property,
disk_kind: self.disk_kind,
index_kind: self.index_kind,
pathspec_match: self.pathspec_match,
}
}
}
impl From<std::fs::FileType> for Kind {
fn from(value: std::fs::FileType) -> Self {
if value.is_dir() {
Kind::Directory
} else if value.is_symlink() {
Kind::Symlink
} else {
Kind::File
}
}
}
impl Status {
/// Return true if this status is considered pruned. A pruned entry is typically hidden from view due to a pathspec.
pub fn is_pruned(&self) -> bool {
matches!(&self, Status::Pruned)
}
/// Return `true` if `file_type` is a directory on disk and isn't ignored, and is not a repository.
/// This implements the default rules of `git status`, which is good for a minimal traversal through
/// tracked and non-ignored portions of a worktree.
/// `for_deletion` is used to determine if recursion into a directory is allowed even though it otherwise wouldn't be.
/// If `worktree_root_is_repository` is `true`, then this status is part of the root of an iteration, and the corresponding
/// worktree root is a repository itself. This typically happens for submodules. In this case, recursion rules are relaxed
/// to allow traversing submodule worktrees.
///
/// Use `pathspec_match` to determine if a pathspec matches in any way, affecting the decision to recurse.
pub fn can_recurse(
&self,
file_type: Option<Kind>,
pathspec_match: Option<PathspecMatch>,
for_deletion: Option<ForDeletionMode>,
worktree_root_is_repository: bool,
) -> bool {
let is_dir_on_disk = file_type.map_or(false, |ft| {
if worktree_root_is_repository {
ft.is_dir()
} else {
ft.is_recursable_dir()
}
});
if !is_dir_on_disk {
return false;
}
match self {
Status::Pruned => false,
Status::Ignored(_) => {
for_deletion.map_or(false, |fd| {
matches!(
fd,
ForDeletionMode::FindNonBareRepositoriesInIgnoredDirectories
| ForDeletionMode::FindRepositoriesInIgnoredDirectories
)
}) || pathspec_match.map_or(false, |m| !m.should_ignore())
}
Status::Untracked | Status::Tracked => true,
}
}
}
impl Kind {
pub(super) fn is_recursable_dir(&self) -> bool {
matches!(self, Kind::Directory)
}
/// Return `true` if this is a directory on disk. Note that this is true for repositories as well.
pub fn is_dir(&self) -> bool {
matches!(self, Kind::Directory | Kind::Repository)
}
}