Skip to content

Commit c4bfff3

Browse files
committed
feat!: Represent DotGit as file kind
The problem here is just that `index_kind` now can also be DotGit even though that's never the case.
1 parent 221bce4 commit c4bfff3

File tree

9 files changed

+320
-125
lines changed

9 files changed

+320
-125
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-dir/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ gix-utils = { version = "^0.1.9", path = "../gix-utils", features = ["bstr"] }
2525

2626
bstr = { version = "1.5.0", default-features = false }
2727
thiserror = "1.0.56"
28+
bitflags = "2"
2829

2930
[dev-dependencies]
3031
gix-testtools = { path = "../tests/tools" }

gix-dir/src/entry.rs

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

5-
/// The kind of the entry.
5+
bitflags::bitflags! {
6+
/// A way of attaching additional information to an [Entry](crate::Entry) .
7+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
8+
pub struct Flags: u8 {
9+
/// The entry was named `.git`, matched according to the case-sensitivity rules of the repository.
10+
const DOT_GIT = 1 << 0;
11+
/// The entry is a directory, and that directory is empty.
12+
const EMPTY_DIRECTORY = 1 << 1;
13+
/// Always in conjunction with a directory on disk that is also known as cone-mode sparse-checkout exclude marker
14+
/// - i.e. a directory that is excluded, so its whole content is excluded and not checked out nor is part of the index.
15+
const TRACKED_EXCLUDED = 1 << 1;
16+
}
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]
3539
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,
3940
/// The entry is tracked in Git.
4041
Tracked,
4142
/// The entry is ignored as per `.gitignore` files and their rules.
@@ -52,7 +53,7 @@ pub enum Status {
5253
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
5354
pub enum PathspecMatch {
5455
/// 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.
56+
/// Thus, this is not a match by merit.
5657
Always,
5758
/// A match happened, but the pattern excludes everything it matches, which means this entry was excluded.
5859
Excluded,
@@ -84,12 +85,23 @@ impl From<gix_pathspec::search::MatchKind> for PathspecMatch {
8485
}
8586
}
8687

88+
impl From<gix_pathspec::search::Match<'_>> for PathspecMatch {
89+
fn from(m: gix_pathspec::search::Match<'_>) -> Self {
90+
if m.is_excluded() {
91+
PathspecMatch::Excluded
92+
} else {
93+
m.kind.into()
94+
}
95+
}
96+
}
97+
8798
impl EntryRef<'_> {
8899
/// Strip the lifetime to obtain a fully owned copy.
89100
pub fn to_owned(&self) -> Entry {
90101
Entry {
91102
rela_path: self.rela_path.clone().into_owned(),
92103
status: self.status,
104+
flags: self.flags,
93105
disk_kind: self.disk_kind,
94106
index_kind: self.index_kind,
95107
pathspec_match: self.pathspec_match,
@@ -101,6 +113,7 @@ impl EntryRef<'_> {
101113
Entry {
102114
rela_path: self.rela_path.into_owned(),
103115
status: self.status,
116+
flags: self.flags,
104117
disk_kind: self.disk_kind,
105118
index_kind: self.index_kind,
106119
pathspec_match: self.pathspec_match,
@@ -114,6 +127,7 @@ impl Entry {
114127
EntryRef {
115128
rela_path: Cow::Borrowed(self.rela_path.as_ref()),
116129
status: self.status,
130+
flags: self.flags,
117131
disk_kind: self.disk_kind,
118132
index_kind: self.index_kind,
119133
pathspec_match: self.pathspec_match,
@@ -137,7 +151,7 @@ impl Status {
137151
/// Return true if this status is considered pruned. A pruned entry is typically hidden from view due to a pathspec.
138152
pub fn is_pruned(&self) -> bool {
139153
match self {
140-
Status::DotGit | Status::TrackedExcluded | Status::Pruned => true,
154+
Status::Pruned => true,
141155
Status::Ignored(_) | Status::Untracked | Status::Tracked => false,
142156
}
143157
}
@@ -158,7 +172,7 @@ impl Status {
158172
return false;
159173
}
160174
match self {
161-
Status::DotGit | Status::TrackedExcluded | Status::Pruned => false,
175+
Status::Pruned => false,
162176
Status::Ignored(_) => {
163177
for_deletion.map_or(false, |fd| {
164178
matches!(
@@ -180,6 +194,6 @@ impl Kind {
180194

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

gix-dir/src/lib.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ 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 flags that further clarify properties of the entry.
33+
pub flags: entry::Flags,
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>,
@@ -48,7 +49,9 @@ pub struct Entry {
4849
pub rela_path: BString,
4950
/// The status of entry, most closely related to what we know from `git status`, but not the same.
5051
pub status: entry::Status,
51-
/// Further specify the what the entry is on disk, similar to a file mode.
52+
/// Additional flags that further clarify properties of the entry.
53+
pub flags: entry::Flags,
54+
/// Further specify what the entry is on disk, similar to a file mode.
5255
pub disk_kind: Option<entry::Kind>,
5356
/// The kind of entry according to the index, if tracked. *Usually* the same as `disk_kind`.
5457
pub index_kind: Option<entry::Kind>,

gix-dir/src/walk/classify.rs

Lines changed: 56 additions & 23 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+
/// Additional properties.
66+
pub flags: entry::Flags,
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+
flags: e.flags,
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+
flags: info.flags,
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,34 @@ 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+
flags: entry::Flags::empty(),
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 let Some(excluded) = ctx
160+
.excludes
161+
.as_mut()
162+
.map_or(Ok(None), |stack| {
163+
stack
164+
.at_entry(rela_path.as_bstr(), disk_kind.map(|ft| ft.is_dir()), ctx.objects)
165+
.map(|platform| platform.excluded_kind())
166+
})
167+
.map_err(Error::ExcludesAccess)?
168+
{
169+
out.status = entry::Status::Ignored(excluded);
170+
}
171+
out.flags = entry::Flags::DOT_GIT;
132172
return Ok(out);
133173
}
134174
let pathspec_could_match = rela_path.is_empty()
@@ -139,31 +179,25 @@ pub fn path(
139179
return Ok(out.with_status(entry::Status::Pruned));
140180
}
141181

142-
let (uptodate_index_kind, index_kind, mut maybe_status) = resolve_file_type_with_index(
182+
let (uptodate_index_kind, index_kind, extra_flags) = resolve_file_type_with_index(
143183
rela_path,
144184
ctx.index,
145185
ctx.ignore_case_index_lookup.filter(|_| ignore_case),
146186
);
147187
let mut kind = uptodate_index_kind.or(disk_kind).or_else(on_demand_disk_kind);
148188

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));
189+
let maybe_status = if extra_flags.is_empty() {
190+
(index_kind.map(|k| k.is_dir()) == kind.map(|k| k.is_dir())).then_some(entry::Status::Tracked)
191+
} else {
192+
out.flags |= extra_flags;
193+
Some(entry::Status::Pruned)
194+
};
151195

152196
// We always check the pathspec to have the value filled in reliably.
153197
out.pathspec_match = ctx
154198
.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-
});
199+
.pattern_matching_relative_path(rela_path.as_bstr(), kind.map(|ft| ft.is_dir()), ctx.pathspec_attributes)
200+
.map(Into::into);
167201

168202
let mut maybe_upgrade_to_repository = |current_kind, find_harder: bool| {
169203
if recurse_repositories {
@@ -258,8 +292,7 @@ pub fn path(
258292

259293
/// Note that `rela_path` is used as buffer for convenience, but will be left as is when this function returns.
260294
/// 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.
295+
/// Returns `(maybe_file_type, Option<index_file_type>, flags)`, with the last option being a flag set only for sparse directories in the index.
263296
/// `index_file_type` is the type of `rela_path` as available in the index.
264297
///
265298
/// ### Shortcoming
@@ -271,9 +304,9 @@ fn resolve_file_type_with_index(
271304
rela_path: &mut BString,
272305
index: &gix_index::State,
273306
ignore_case: Option<&gix_index::AccelerateLookup<'_>>,
274-
) -> (Option<entry::Kind>, Option<entry::Kind>, Option<entry::Status>) {
307+
) -> (Option<entry::Kind>, Option<entry::Kind>, entry::Flags) {
275308
// 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;
309+
let mut special_status = entry::Flags::empty();
277310

278311
fn entry_to_kinds(entry: &gix_index::Entry) -> (Option<entry::Kind>, Option<entry::Kind>) {
279312
let kind = if entry.mode.is_submodule() {
@@ -352,7 +385,7 @@ fn resolve_file_type_with_index(
352385
.filter(|_| kind.is_none())
353386
.map_or(false, |idx| index.entries()[idx].mode.is_sparse())
354387
{
355-
special_status = Some(entry::Status::TrackedExcluded);
388+
special_status |= entry::Flags::TRACKED_EXCLUDED;
356389
}
357390
(kind, is_tracked.then_some(entry::Kind::Directory))
358391
}

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.flags.contains(entry::Flags::EMPTY_DIRECTORY)
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)