gix_dir/entry.rs
1use std::{borrow::Cow, fs::FileType};
2
3use crate::{walk::ForDeletionMode, Entry, EntryRef};
4
5/// A way of attaching additional information to an [Entry] .
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
7pub enum Property {
8 /// The entry was named `.git`, matched according to the case-sensitivity rules of the repository.
9 DotGit,
10 /// The entry is a directory, and that directory is empty.
11 EmptyDirectory,
12 /// The entry is a directory, it is empty and the current working directory.
13 ///
14 /// The caller should pay special attention to this very special case, as it is indeed only possible to run into it
15 /// while traversing the directory for deletion.
16 /// Non-empty directory will never be collapsed, hence if they are working directories, they naturally become unobservable.
17 EmptyDirectoryAndCWD,
18 /// Always in conjunction with a directory on disk that is also known as cone-mode sparse-checkout exclude marker
19 /// - i.e. a directory that is excluded, so its whole content is excluded and not checked out nor is part of the index.
20 ///
21 /// Note that evne if the directory is empty, it will only have this state, not `EmptyDirectory`.
22 TrackedExcluded,
23}
24
25/// The kind of the entry, seated in their kinds available on disk.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
27pub enum Kind {
28 /// Something that is not a regular file, directory, or symbolic link.
29 ///
30 /// These can only exist in the filesystem,
31 /// because Git repositories do not support them, thus they cannot be tracked.
32 /// Hence, they do not appear as blobs in a repository, and their type is not specifiable in a tree object.
33 /// Examples include named pipes (FIFOs), character devices, block devices, and sockets.
34 Untrackable,
35 /// The entry is a blob, representing a regular file, executable or not.
36 File,
37 /// The entry is a symlink.
38 Symlink,
39 /// The entry is an ordinary directory.
40 ///
41 /// Note that since we don't check for bare repositories, this could in fact be a collapsed
42 /// bare repository. To be sure, check it again with [`gix_discover::is_git()`] and act accordingly.
43 Directory,
44 /// The entry is a directory which *contains* a `.git` folder, or a submodule entry in the index.
45 Repository,
46}
47
48/// The kind of entry as obtained from a directory.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
50pub enum Status {
51 /// The entry was removed from the walk due to its other properties, like [Property] or [PathspecMatch]
52 ///
53 /// Note that entries flagged as `DotGit` directory will always be considered `Pruned`, but if they are
54 /// also ignored, in delete mode, they will be considered `Ignored` instead. This way, it's easier to remove them
55 /// while they will not be available for any interactions in read-only mode.
56 Pruned,
57 /// The entry is tracked in Git.
58 Tracked,
59 /// The entry is ignored as per `.gitignore` files and their rules.
60 ///
61 /// If this is a directory, then its entire contents is ignored. Otherwise, possibly due to configuration, individual ignored files are listed.
62 Ignored(gix_ignore::Kind),
63 /// The entry is not tracked by git yet, it was not found in the [index](gix_index::State).
64 ///
65 /// If it's a directory, the entire directory contents is untracked.
66 Untracked,
67}
68
69/// Describe how a pathspec pattern matched.
70#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
71pub enum PathspecMatch {
72 /// 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.
73 /// Thus, this is not a match by merit.
74 Always,
75 /// A match happened, but the pattern excludes everything it matches, which means this entry was excluded.
76 Excluded,
77 /// The first part of a pathspec matches, like `dir/` that matches `dir/a`.
78 Prefix,
79 /// The whole pathspec matched and used a wildcard match, like `a/*` matching `a/file`.
80 WildcardMatch,
81 /// The entire pathspec matched, letter by letter, e.g. `a/file` matching `a/file`.
82 Verbatim,
83}
84
85impl PathspecMatch {
86 pub(crate) fn should_ignore(&self) -> bool {
87 match self {
88 PathspecMatch::Always | PathspecMatch::Excluded => true,
89 PathspecMatch::Prefix | PathspecMatch::WildcardMatch | PathspecMatch::Verbatim => false,
90 }
91 }
92}
93
94impl From<gix_pathspec::search::MatchKind> for PathspecMatch {
95 fn from(kind: gix_pathspec::search::MatchKind) -> Self {
96 match kind {
97 gix_pathspec::search::MatchKind::Always => Self::Always,
98 gix_pathspec::search::MatchKind::Prefix => Self::Prefix,
99 gix_pathspec::search::MatchKind::WildcardMatch => Self::WildcardMatch,
100 gix_pathspec::search::MatchKind::Verbatim => Self::Verbatim,
101 }
102 }
103}
104
105impl From<gix_pathspec::search::Match<'_>> for PathspecMatch {
106 fn from(m: gix_pathspec::search::Match<'_>) -> Self {
107 if m.is_excluded() {
108 PathspecMatch::Excluded
109 } else {
110 m.kind.into()
111 }
112 }
113}
114
115/// Conversion
116impl EntryRef<'_> {
117 /// Strip the lifetime to obtain a fully owned copy.
118 pub fn to_owned(&self) -> Entry {
119 Entry {
120 rela_path: self.rela_path.clone().into_owned(),
121 status: self.status,
122 property: self.property,
123 disk_kind: self.disk_kind,
124 index_kind: self.index_kind,
125 pathspec_match: self.pathspec_match,
126 }
127 }
128
129 /// Turn this instance into a fully owned copy.
130 pub fn into_owned(self) -> Entry {
131 Entry {
132 rela_path: self.rela_path.into_owned(),
133 status: self.status,
134 property: self.property,
135 disk_kind: self.disk_kind,
136 index_kind: self.index_kind,
137 pathspec_match: self.pathspec_match,
138 }
139 }
140}
141
142/// Conversion
143impl Entry {
144 /// Obtain an [`EntryRef`] from this instance.
145 pub fn to_ref(&self) -> EntryRef<'_> {
146 EntryRef {
147 rela_path: Cow::Borrowed(self.rela_path.as_ref()),
148 status: self.status,
149 property: self.property,
150 disk_kind: self.disk_kind,
151 index_kind: self.index_kind,
152 pathspec_match: self.pathspec_match,
153 }
154 }
155}
156
157impl From<std::fs::FileType> for Kind {
158 fn from(value: FileType) -> Self {
159 if value.is_dir() {
160 Kind::Directory
161 } else if value.is_symlink() {
162 Kind::Symlink
163 } else if value.is_file() {
164 Kind::File
165 } else {
166 Kind::Untrackable
167 }
168 }
169}
170
171impl Status {
172 /// Return true if this status is considered pruned. A pruned entry is typically hidden from view due to a pathspec.
173 pub fn is_pruned(&self) -> bool {
174 matches!(&self, Status::Pruned)
175 }
176 /// Return `true` if `file_type` is a directory on disk and isn't ignored, and is not a repository.
177 /// This implements the default rules of `git status`, which is good for a minimal traversal through
178 /// tracked and non-ignored portions of a worktree.
179 /// `for_deletion` is used to determine if recursion into a directory is allowed even though it otherwise wouldn't be.
180 /// If `worktree_root_is_repository` is `true`, then this status is part of the root of an iteration, and the corresponding
181 /// worktree root is a repository itself. This typically happens for submodules. In this case, recursion rules are relaxed
182 /// to allow traversing submodule worktrees.
183 ///
184 /// Use `pathspec_match` to determine if a pathspec matches in any way, affecting the decision to recurse.
185 pub fn can_recurse(
186 &self,
187 file_type: Option<Kind>,
188 pathspec_match: Option<PathspecMatch>,
189 for_deletion: Option<ForDeletionMode>,
190 worktree_root_is_repository: bool,
191 ) -> bool {
192 let is_dir_on_disk = file_type.is_some_and(|ft| {
193 if worktree_root_is_repository {
194 ft.is_dir()
195 } else {
196 ft.is_recursable_dir()
197 }
198 });
199 if !is_dir_on_disk {
200 return false;
201 }
202 match self {
203 Status::Pruned => false,
204 Status::Ignored(_) => {
205 for_deletion.is_some_and(|fd| {
206 matches!(
207 fd,
208 ForDeletionMode::FindNonBareRepositoriesInIgnoredDirectories
209 | ForDeletionMode::FindRepositoriesInIgnoredDirectories
210 )
211 }) || pathspec_match.is_some_and(|m| !m.should_ignore())
212 }
213 Status::Untracked | Status::Tracked => true,
214 }
215 }
216}
217
218impl Kind {
219 pub(super) fn is_recursable_dir(&self) -> bool {
220 matches!(self, Kind::Directory)
221 }
222
223 /// Return `true` if this is a directory on disk. Note that this is true for repositories as well.
224 pub fn is_dir(&self) -> bool {
225 matches!(self, Kind::Directory | Kind::Repository)
226 }
227}