Skip to content

Commit 94d2170

Browse files
committed
feat!: Represent DotGit as ExtendedKind
This cleans up the model despite also making it harder to detect whether something is a DotGit.
1 parent 221bce4 commit 94d2170

File tree

7 files changed

+369
-130
lines changed

7 files changed

+369
-130
lines changed

gix-dir/src/entry.rs

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,45 @@ use crate::walk::ForDeletionMode;
22
use crate::{Entry, EntryRef};
33
use std::borrow::Cow;
44

5-
/// The kind of the entry.
5+
/// A way of attaching additional information to an [Entry](crate::Entry) .
6+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
7+
pub 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+
/// Always in conjunction with a directory on disk that is also known as cone-mode sparse-checkout exclude marker
13+
/// - i.e. a directory that is excluded, so its whole content is excluded and not checked out nor is part of the index.
14+
///
15+
/// Note that evne if the directory is empty, it will only have this state, not `EmptyDirectory`.
16+
TrackedExcluded,
17+
}
18+
19+
/// The kind of the entry, seated in their kinds available on disk.
620
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
721
pub enum Kind {
822
/// The entry is a blob, executable or not.
923
File,
1024
/// The entry is a symlink.
1125
Symlink,
12-
/// A directory that contains no file or directory.
13-
EmptyDirectory,
1426
/// The entry is an ordinary directory.
1527
///
1628
/// Note that since we don't check for bare repositories, this could in fact be a collapsed
1729
/// bare repository. To be sure, check it again with [`gix_discover::is_git()`] and act accordingly.
1830
Directory,
19-
/// The entry is a directory which *contains* a `.git` folder.
31+
/// The entry is a directory which *contains* a `.git` folder, or a submodule entry in the index.
2032
Repository,
2133
}
2234

2335
/// The kind of entry as obtained from a directory.
24-
///
25-
/// The order of variants roughly relates from cheap-to-compute to most expensive, as each level needs more tests to assert.
26-
/// Thus, `DotGit` is the cheapest, while `Untracked` is among the most expensive and one of the major outcomes of any
27-
/// [`walk`](crate::walk()) run.
28-
/// For example, if an entry was `Pruned`, we effectively don't know if it would have been `Untracked` as well as we stopped looking.
2936
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
3037
pub enum Status {
31-
/// The filename of an entry was `.git`, which is generally pruned.
32-
DotGit,
33-
/// The provided pathspec prevented further processing as the path didn't match.
34-
/// If this happens, no further checks are done so we wouldn't know if the path is also ignored for example (by mention in `.gitignore`).
38+
/// The entry was removed from the walk due to its other properties, like [Flags] or [PathspecMatch]
39+
///
40+
/// Note that entries flagged as `DotGit` directory will always be considered `Pruned`, but if they are
41+
/// also ignored, in delete mode, they will be considered `Ignored` instead. This way, it's easier to remove them
42+
/// while they will not be available for any interactions in read-only mode.
3543
Pruned,
36-
/// Always in conjunction with a directory on disk that is also known as cone-mode sparse-checkout exclude marker - i.e. a directory
37-
/// that is excluded, so its whole content is excluded and not checked out nor is part of the index.
38-
TrackedExcluded,
3944
/// The entry is tracked in Git.
4045
Tracked,
4146
/// The entry is ignored as per `.gitignore` files and their rules.
@@ -52,7 +57,7 @@ pub enum Status {
5257
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
5358
pub enum PathspecMatch {
5459
/// 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.
55-
/// Thus this is not a match by merit.
60+
/// Thus, this is not a match by merit.
5661
Always,
5762
/// A match happened, but the pattern excludes everything it matches, which means this entry was excluded.
5863
Excluded,
@@ -84,12 +89,24 @@ impl From<gix_pathspec::search::MatchKind> for PathspecMatch {
8489
}
8590
}
8691

92+
impl From<gix_pathspec::search::Match<'_>> for PathspecMatch {
93+
fn from(m: gix_pathspec::search::Match<'_>) -> Self {
94+
if m.is_excluded() {
95+
PathspecMatch::Excluded
96+
} else {
97+
m.kind.into()
98+
}
99+
}
100+
}
101+
102+
/// Conversion
87103
impl EntryRef<'_> {
88104
/// Strip the lifetime to obtain a fully owned copy.
89105
pub fn to_owned(&self) -> Entry {
90106
Entry {
91107
rela_path: self.rela_path.clone().into_owned(),
92108
status: self.status,
109+
property: self.property,
93110
disk_kind: self.disk_kind,
94111
index_kind: self.index_kind,
95112
pathspec_match: self.pathspec_match,
@@ -101,19 +118,22 @@ impl EntryRef<'_> {
101118
Entry {
102119
rela_path: self.rela_path.into_owned(),
103120
status: self.status,
121+
property: self.property,
104122
disk_kind: self.disk_kind,
105123
index_kind: self.index_kind,
106124
pathspec_match: self.pathspec_match,
107125
}
108126
}
109127
}
110128

129+
/// Conversion
111130
impl Entry {
112131
/// Obtain an [`EntryRef`] from this instance.
113132
pub fn to_ref(&self) -> EntryRef<'_> {
114133
EntryRef {
115134
rela_path: Cow::Borrowed(self.rela_path.as_ref()),
116135
status: self.status,
136+
property: self.property,
117137
disk_kind: self.disk_kind,
118138
index_kind: self.index_kind,
119139
pathspec_match: self.pathspec_match,
@@ -136,10 +156,7 @@ impl From<std::fs::FileType> for Kind {
136156
impl Status {
137157
/// Return true if this status is considered pruned. A pruned entry is typically hidden from view due to a pathspec.
138158
pub fn is_pruned(&self) -> bool {
139-
match self {
140-
Status::DotGit | Status::TrackedExcluded | Status::Pruned => true,
141-
Status::Ignored(_) | Status::Untracked | Status::Tracked => false,
142-
}
159+
matches!(&self, Status::Pruned)
143160
}
144161
/// Return `true` if `file_type` is a directory on disk and isn't ignored, and is not a repository.
145162
/// This implements the default rules of `git status`, which is good for a minimal traversal through
@@ -158,7 +175,7 @@ impl Status {
158175
return false;
159176
}
160177
match self {
161-
Status::DotGit | Status::TrackedExcluded | Status::Pruned => false,
178+
Status::Pruned => false,
162179
Status::Ignored(_) => {
163180
for_deletion.map_or(false, |fd| {
164181
matches!(
@@ -180,6 +197,6 @@ impl Kind {
180197

181198
/// Return `true` if this is a directory on disk. Note that this is true for repositories as well.
182199
pub fn is_dir(&self) -> bool {
183-
matches!(self, Kind::EmptyDirectory | Kind::Directory | Kind::Repository)
200+
matches!(self, Kind::Directory | Kind::Repository)
184201
}
185202
}

gix-dir/src/lib.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ pub struct EntryRef<'a> {
2929
/// Note that many entries with status `Pruned` will not show up as their kind hasn't yet been determined when they were
3030
/// pruned very early on.
3131
pub status: entry::Status,
32-
/// Further specify the what the entry is on disk, similar to a file mode.
33-
/// This is `None` if the entry was pruned by a pathspec that could not match, as we then won't invest the time to obtain
34-
/// the kind of the entry on disk.
32+
/// Additional properties of the entry.
33+
pub property: Option<entry::Property>,
34+
/// Further specify what the entry is on disk, similar to a file mode.
35+
/// This is `None` if we decided it's not worth it to exit early and avoid trying to obtain this information.
3536
pub disk_kind: Option<entry::Kind>,
3637
/// The kind of entry according to the index, if tracked. *Usually* the same as `disk_kind`.
3738
pub index_kind: Option<entry::Kind>,
3839
/// Determines how the pathspec matched.
39-
/// Can also be `None` if no pathspec matched, or if the status check stopped prior to checking for pathspec matches which is the case for [`entry::Status::DotGit`].
4040
/// Note that it can also be `Some(PathspecMatch::Excluded)` if a negative pathspec matched.
4141
pub pathspec_match: Option<entry::PathspecMatch>,
4242
}
@@ -48,7 +48,9 @@ pub struct Entry {
4848
pub rela_path: BString,
4949
/// The status of entry, most closely related to what we know from `git status`, but not the same.
5050
pub status: entry::Status,
51-
/// Further specify the what the entry is on disk, similar to a file mode.
51+
/// Additional flags that further clarify properties of the entry.
52+
pub property: Option<entry::Property>,
53+
/// Further specify what the entry is on disk, similar to a file mode.
5254
pub disk_kind: Option<entry::Kind>,
5355
/// The kind of entry according to the index, if tracked. *Usually* the same as `disk_kind`.
5456
pub index_kind: Option<entry::Kind>,

gix-dir/src/walk/classify.rs

Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use crate::{entry, Entry};
1+
use crate::{entry, Entry, EntryRef};
2+
use std::borrow::Cow;
23

34
use crate::entry::PathspecMatch;
45
use crate::walk::{Context, Error, ForDeletionMode, Options};
@@ -61,7 +62,10 @@ pub fn root(
6162
pub struct Outcome {
6263
/// The computed status of an entry. It can be seen as aggregate of things we know about an entry.
6364
pub status: entry::Status,
64-
/// What the entry is on disk, or `None` if we aborted the classification early.
65+
/// An additional property.
66+
pub property: Option<entry::Property>,
67+
/// What the entry is on disk, or `None` if we aborted the classification early or an IO-error occurred
68+
/// when querying the disk.
6569
///
6670
/// Note that the index is used to avoid disk access provided its entries are marked uptodate
6771
/// (possibly by a prior call to update the status).
@@ -89,13 +93,27 @@ impl From<&Entry> for Outcome {
8993
fn from(e: &Entry) -> Self {
9094
Outcome {
9195
status: e.status,
96+
property: e.property,
9297
disk_kind: e.disk_kind,
9398
index_kind: e.index_kind,
9499
pathspec_match: e.pathspec_match,
95100
}
96101
}
97102
}
98103

104+
impl<'a> EntryRef<'a> {
105+
pub(super) fn from_outcome(rela_path: Cow<'a, BStr>, info: crate::walk::classify::Outcome) -> Self {
106+
EntryRef {
107+
rela_path,
108+
property: info.property,
109+
status: info.status,
110+
disk_kind: info.disk_kind,
111+
index_kind: info.index_kind,
112+
pathspec_match: info.pathspec_match,
113+
}
114+
}
115+
}
116+
99117
/// Figure out what to do with `rela_path`, provided as worktree-relative path, with `disk_file_type` if it is known already
100118
/// as it helps to match pathspecs correctly, which can be different for directories.
101119
/// `path` is a disk-accessible variant of `rela_path` which is within the `worktree_root`, and will be modified temporarily but remain unchanged.
@@ -123,12 +141,36 @@ pub fn path(
123141
ctx: &mut Context<'_>,
124142
) -> Result<Outcome, Error> {
125143
let mut out = Outcome {
126-
status: entry::Status::DotGit,
144+
status: entry::Status::Pruned,
145+
property: None,
127146
disk_kind,
128147
index_kind: None,
129148
pathspec_match: None,
130149
};
131150
if is_eq(rela_path[filename_start_idx..].as_bstr(), ".git", ignore_case) {
151+
out.pathspec_match = ctx
152+
.pathspec
153+
.pattern_matching_relative_path(
154+
rela_path.as_bstr(),
155+
disk_kind.map(|ft| ft.is_dir()),
156+
ctx.pathspec_attributes,
157+
)
158+
.map(Into::into);
159+
if for_deletion.is_some() {
160+
if let Some(excluded) = ctx
161+
.excludes
162+
.as_mut()
163+
.map_or(Ok(None), |stack| {
164+
stack
165+
.at_entry(rela_path.as_bstr(), disk_kind.map(|ft| ft.is_dir()), ctx.objects)
166+
.map(|platform| platform.excluded_kind())
167+
})
168+
.map_err(Error::ExcludesAccess)?
169+
{
170+
out.status = entry::Status::Ignored(excluded);
171+
}
172+
}
173+
out.property = entry::Property::DotGit.into();
132174
return Ok(out);
133175
}
134176
let pathspec_could_match = rela_path.is_empty()
@@ -139,31 +181,25 @@ pub fn path(
139181
return Ok(out.with_status(entry::Status::Pruned));
140182
}
141183

142-
let (uptodate_index_kind, index_kind, mut maybe_status) = resolve_file_type_with_index(
184+
let (uptodate_index_kind, index_kind, property) = resolve_file_type_with_index(
143185
rela_path,
144186
ctx.index,
145187
ctx.ignore_case_index_lookup.filter(|_| ignore_case),
146188
);
147189
let mut kind = uptodate_index_kind.or(disk_kind).or_else(on_demand_disk_kind);
148190

149-
maybe_status = maybe_status
150-
.or_else(|| (index_kind.map(|k| k.is_dir()) == kind.map(|k| k.is_dir())).then_some(entry::Status::Tracked));
191+
let maybe_status = if property.is_none() {
192+
(index_kind.map(|k| k.is_dir()) == kind.map(|k| k.is_dir())).then_some(entry::Status::Tracked)
193+
} else {
194+
out.property = property;
195+
Some(entry::Status::Pruned)
196+
};
151197

152198
// We always check the pathspec to have the value filled in reliably.
153199
out.pathspec_match = ctx
154200
.pathspec
155-
.pattern_matching_relative_path(
156-
rela_path.as_bstr(),
157-
disk_kind.map(|ft| ft.is_dir()),
158-
ctx.pathspec_attributes,
159-
)
160-
.map(|m| {
161-
if m.is_excluded() {
162-
PathspecMatch::Excluded
163-
} else {
164-
m.kind.into()
165-
}
166-
});
201+
.pattern_matching_relative_path(rela_path.as_bstr(), kind.map(|ft| ft.is_dir()), ctx.pathspec_attributes)
202+
.map(Into::into);
167203

168204
let mut maybe_upgrade_to_repository = |current_kind, find_harder: bool| {
169205
if recurse_repositories {
@@ -258,8 +294,7 @@ pub fn path(
258294

259295
/// Note that `rela_path` is used as buffer for convenience, but will be left as is when this function returns.
260296
/// Also note `maybe_file_type` will be `None` for entries that aren't up-to-date and files, for directories at least one entry must be uptodate.
261-
/// Returns `(maybe_file_type, Option<index_file_type>, Option(TrackedExcluded)`, with the last option being set only for sparse directories.
262-
/// `tracked_exclued` indicates it's a sparse directory was found.
297+
/// Returns `(maybe_file_type, Option<index_file_type>, flags)`, with the last option being a flag set only for sparse directories in the index.
263298
/// `index_file_type` is the type of `rela_path` as available in the index.
264299
///
265300
/// ### Shortcoming
@@ -271,9 +306,9 @@ fn resolve_file_type_with_index(
271306
rela_path: &mut BString,
272307
index: &gix_index::State,
273308
ignore_case: Option<&gix_index::AccelerateLookup<'_>>,
274-
) -> (Option<entry::Kind>, Option<entry::Kind>, Option<entry::Status>) {
309+
) -> (Option<entry::Kind>, Option<entry::Kind>, Option<entry::Property>) {
275310
// TODO: either get this to work for icase as well, or remove the need for it. Logic is different in both branches.
276-
let mut special_status = None;
311+
let mut special_property = None;
277312

278313
fn entry_to_kinds(entry: &gix_index::Entry) -> (Option<entry::Kind>, Option<entry::Kind>) {
279314
let kind = if entry.mode.is_submodule() {
@@ -352,14 +387,14 @@ fn resolve_file_type_with_index(
352387
.filter(|_| kind.is_none())
353388
.map_or(false, |idx| index.entries()[idx].mode.is_sparse())
354389
{
355-
special_status = Some(entry::Status::TrackedExcluded);
390+
special_property = Some(entry::Property::TrackedExcluded);
356391
}
357392
(kind, is_tracked.then_some(entry::Kind::Directory))
358393
}
359394
Some(entry) => entry_to_kinds(entry),
360395
}
361396
};
362-
(uptodate_kind, index_kind, special_status)
397+
(uptodate_kind, index_kind, special_property)
363398
}
364399

365400
fn is_eq(lhs: &BStr, rhs: impl AsRef<BStr>, ignore_case: bool) -> bool {

gix-dir/src/walk/function.rs

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,7 @@ pub(super) fn can_recurse(
146146
if info.disk_kind.map_or(true, |k| !k.is_dir()) {
147147
return false;
148148
}
149-
let entry = EntryRef {
150-
rela_path: Cow::Borrowed(rela_path),
151-
status: info.status,
152-
disk_kind: info.disk_kind,
153-
index_kind: info.index_kind,
154-
pathspec_match: info.pathspec_match,
155-
};
156-
delegate.can_recurse(entry, for_deletion)
149+
delegate.can_recurse(EntryRef::from_outcome(Cow::Borrowed(rela_path), info), for_deletion)
157150
}
158151

159152
/// Possibly emit an entry to `for_each` in case the provided information makes that possible.
@@ -174,7 +167,7 @@ pub(super) fn emit_entry(
174167
) -> Action {
175168
out.seen_entries += 1;
176169

177-
if (!emit_empty_directories && info.disk_kind == Some(entry::Kind::EmptyDirectory)
170+
if (!emit_empty_directories && info.property == Some(entry::Property::EmptyDirectory)
178171
|| !emit_tracked && info.status == entry::Status::Tracked)
179172
|| emit_ignored.is_none() && matches!(info.status, entry::Status::Ignored(_))
180173
|| !emit_pruned
@@ -187,14 +180,5 @@ pub(super) fn emit_entry(
187180
}
188181

189182
out.returned_entries += 1;
190-
delegate.emit(
191-
EntryRef {
192-
rela_path,
193-
status: info.status,
194-
disk_kind: info.disk_kind,
195-
index_kind: info.index_kind,
196-
pathspec_match: info.pathspec_match,
197-
},
198-
dir_status,
199-
)
183+
delegate.emit(EntryRef::from_outcome(rela_path, info), dir_status)
200184
}

0 commit comments

Comments
 (0)