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