Skip to content

Commit d53504a

Browse files
committed
Merge branch 'status'
2 parents 221bce4 + aa7c190 commit d53504a

File tree

11 files changed

+597
-166
lines changed

11 files changed

+597
-166
lines changed

Diff for: gitoxide-core/src/repository/clean.rs

+71-23
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub struct Options {
1515
pub precious: bool,
1616
pub directories: bool,
1717
pub repositories: bool,
18+
pub pathspec_matches_result: bool,
1819
pub skip_hidden_repositories: Option<FindRepository>,
1920
pub find_untracked_repositories: FindRepository,
2021
}
@@ -46,6 +47,7 @@ pub(crate) mod function {
4647
repositories,
4748
skip_hidden_repositories,
4849
find_untracked_repositories,
50+
pathspec_matches_result,
4951
}: Options,
5052
) -> anyhow::Result<()> {
5153
if format != OutputFormat::Human {
@@ -56,6 +58,7 @@ pub(crate) mod function {
5658
};
5759

5860
let index = repo.index_or_empty()?;
61+
let pathspec_for_dirwalk = !pathspec_matches_result;
5962
let has_patterns = !patterns.is_empty();
6063
let mut collect = InterruptableCollect::default();
6164
let collapse_directories = CollapseDirectory;
@@ -66,19 +69,39 @@ pub(crate) mod function {
6669
match skip_hidden_repositories {
6770
Some(FindRepository::NonBare) => Some(FindNonBareRepositoriesInIgnoredDirectories),
6871
Some(FindRepository::All) => Some(FindRepositoriesInIgnoredDirectories),
69-
None => None,
72+
None => Some(Default::default()),
7073
}
7174
} else {
72-
Some(IgnoredDirectoriesCanHideNestedRepositories)
75+
Some(Default::default())
7376
})
7477
.classify_untracked_bare_repositories(matches!(find_untracked_repositories, FindRepository::All))
7578
.emit_untracked(collapse_directories)
7679
.emit_ignored(Some(collapse_directories))
7780
.empty_patterns_match_prefix(true)
7881
.emit_empty_directories(true);
79-
repo.dirwalk(&index, patterns, options, &mut collect)?;
80-
let prefix = repo.prefix()?.unwrap_or(Path::new(""));
82+
repo.dirwalk(
83+
&index,
84+
if pathspec_for_dirwalk {
85+
patterns.clone()
86+
} else {
87+
Vec::new()
88+
},
89+
options,
90+
&mut collect,
91+
)?;
8192

93+
let mut pathspec = pathspec_matches_result
94+
.then(|| {
95+
repo.pathspec(
96+
true,
97+
patterns,
98+
true,
99+
&index,
100+
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping,
101+
)
102+
})
103+
.transpose()?;
104+
let prefix = repo.prefix()?.unwrap_or(Path::new(""));
82105
let entries = collect.inner.into_entries_by_path();
83106
let mut entries_to_clean = 0;
84107
let mut skipped_directories = 0;
@@ -88,7 +111,7 @@ pub(crate) mod function {
88111
let mut pruned_entries = 0;
89112
let mut saw_ignored_directory = false;
90113
let mut saw_untracked_directory = false;
91-
for (entry, dir_status) in entries.into_iter() {
114+
for (mut entry, dir_status) in entries.into_iter() {
92115
if dir_status.is_some() {
93116
if debug {
94117
writeln!(
@@ -101,21 +124,25 @@ pub(crate) mod function {
101124
continue;
102125
}
103126

104-
let pathspec_includes_entry = entry
105-
.pathspec_match
106-
.map_or(false, |m| m != gix::dir::entry::PathspecMatch::Excluded);
127+
let pathspec_includes_entry = match pathspec.as_mut() {
128+
None => entry
129+
.pathspec_match
130+
.map_or(false, |m| m != gix::dir::entry::PathspecMatch::Excluded),
131+
Some(pathspec) => pathspec
132+
.pattern_matching_relative_path(entry.rela_path.as_bstr(), entry.disk_kind.map(|k| k.is_dir()))
133+
.map_or(false, |m| !m.is_excluded()),
134+
};
107135
pruned_entries += usize::from(!pathspec_includes_entry);
108136
if !pathspec_includes_entry && debug {
109-
writeln!(err, "DBG: prune '{}' as it is excluded by pathspec", entry.rela_path).ok();
137+
writeln!(err, "DBG: prune '{}'", entry.rela_path).ok();
110138
}
111139
if entry.status.is_pruned() || !pathspec_includes_entry {
112140
continue;
113141
}
114142

115-
let mut disk_kind = entry.disk_kind.expect("present if not pruned");
116143
let keep = match entry.status {
117-
Status::DotGit | Status::Pruned | Status::TrackedExcluded => {
118-
unreachable!("BUG: Pruned are skipped already as their pathspec is always None")
144+
Status::Pruned => {
145+
unreachable!("BUG: we skipped these above")
119146
}
120147
Status::Tracked => {
121148
unreachable!("BUG: tracked aren't emitted")
@@ -130,6 +157,14 @@ pub(crate) mod function {
130157
}
131158
Status::Untracked => true,
132159
};
160+
if entry.disk_kind.is_none() {
161+
entry.disk_kind = workdir
162+
.join(gix::path::from_bstr(entry.rela_path.as_bstr()))
163+
.metadata()
164+
.ok()
165+
.map(|e| e.file_type().into());
166+
}
167+
let mut disk_kind = entry.disk_kind.expect("present if not pruned");
133168
if !keep {
134169
if debug {
135170
writeln!(err, "DBG: prune '{}' as -x or -p is missing", entry.rela_path).ok();
@@ -148,7 +183,7 @@ pub(crate) mod function {
148183

149184
match disk_kind {
150185
Kind::File | Kind::Symlink => {}
151-
Kind::EmptyDirectory | Kind::Directory => {
186+
Kind::Directory => {
152187
if !directories {
153188
skipped_directories += 1;
154189
if debug {
@@ -175,6 +210,11 @@ pub(crate) mod function {
175210
saw_ignored_directory |= is_ignored;
176211
saw_untracked_directory |= entry.status == gix::dir::entry::Status::Untracked;
177212
}
213+
214+
if gix::interrupt::is_triggered() {
215+
execute = false;
216+
}
217+
let mut may_remove_this_entry = execute;
178218
writeln!(
179219
out,
180220
"{maybe}{suffix} {}{} {status}",
@@ -200,24 +240,32 @@ pub(crate) mod function {
200240
"".into()
201241
},
202242
},
203-
maybe = if execute { "removing" } else { "WOULD remove" },
204-
suffix = match disk_kind {
205-
Kind::File | Kind::Symlink | Kind::Directory => {
206-
""
243+
maybe = if entry.property == Some(gix::dir::entry::Property::EmptyDirectoryAndCWD) {
244+
may_remove_this_entry = false;
245+
if execute {
246+
"Refusing to remove empty current working directory"
247+
} else {
248+
"Would refuse to remove empty current working directory"
207249
}
208-
Kind::EmptyDirectory => {
250+
} else if execute {
251+
"removing"
252+
} else {
253+
"WOULD remove"
254+
},
255+
suffix = match disk_kind {
256+
Kind::Directory if entry.property == Some(gix::dir::entry::Property::EmptyDirectory) => {
209257
" empty"
210258
}
211259
Kind::Repository => {
212260
" repository"
213261
}
262+
Kind::File | Kind::Symlink | Kind::Directory => {
263+
""
264+
}
214265
},
215266
)?;
216267

217-
if gix::interrupt::is_triggered() {
218-
execute = false;
219-
}
220-
if execute {
268+
if may_remove_this_entry {
221269
let path = workdir.join(entry_path);
222270
if disk_kind.is_dir() {
223271
std::fs::remove_dir_all(path)?;
@@ -256,7 +304,7 @@ pub(crate) mod function {
256304
}));
257305
messages.extend((pruned_entries > 0 && has_patterns).then(|| {
258306
format!(
259-
"try to adjust your pathspec to reveal some of the {pruned_entries} pruned {entries}",
307+
"try to adjust your pathspec to reveal some of the {pruned_entries} pruned {entries} - show with --debug",
260308
entries = plural("entry", "entries", pruned_entries)
261309
)
262310
}));

Diff for: gix-dir/src/entry.rs

+47-24
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,51 @@ 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] .
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+
/// 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.
626
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
727
pub enum Kind {
828
/// The entry is a blob, executable or not.
929
File,
1030
/// The entry is a symlink.
1131
Symlink,
12-
/// A directory that contains no file or directory.
13-
EmptyDirectory,
1432
/// The entry is an ordinary directory.
1533
///
1634
/// Note that since we don't check for bare repositories, this could in fact be a collapsed
1735
/// bare repository. To be sure, check it again with [`gix_discover::is_git()`] and act accordingly.
1836
Directory,
19-
/// The entry is a directory which *contains* a `.git` folder.
37+
/// The entry is a directory which *contains* a `.git` folder, or a submodule entry in the index.
2038
Repository,
2139
}
2240

2341
/// 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.
2942
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
3043
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`).
44+
/// The entry was removed from the walk due to its other properties, like [Property] or [PathspecMatch]
45+
///
46+
/// Note that entries flagged as `DotGit` directory will always be considered `Pruned`, but if they are
47+
/// also ignored, in delete mode, they will be considered `Ignored` instead. This way, it's easier to remove them
48+
/// while they will not be available for any interactions in read-only mode.
3549
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,
3950
/// The entry is tracked in Git.
4051
Tracked,
4152
/// The entry is ignored as per `.gitignore` files and their rules.
@@ -52,7 +63,7 @@ pub enum Status {
5263
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
5364
pub enum PathspecMatch {
5465
/// 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.
66+
/// Thus, this is not a match by merit.
5667
Always,
5768
/// A match happened, but the pattern excludes everything it matches, which means this entry was excluded.
5869
Excluded,
@@ -84,12 +95,24 @@ impl From<gix_pathspec::search::MatchKind> for PathspecMatch {
8495
}
8596
}
8697

98+
impl From<gix_pathspec::search::Match<'_>> for PathspecMatch {
99+
fn from(m: gix_pathspec::search::Match<'_>) -> Self {
100+
if m.is_excluded() {
101+
PathspecMatch::Excluded
102+
} else {
103+
m.kind.into()
104+
}
105+
}
106+
}
107+
108+
/// Conversion
87109
impl EntryRef<'_> {
88110
/// Strip the lifetime to obtain a fully owned copy.
89111
pub fn to_owned(&self) -> Entry {
90112
Entry {
91113
rela_path: self.rela_path.clone().into_owned(),
92114
status: self.status,
115+
property: self.property,
93116
disk_kind: self.disk_kind,
94117
index_kind: self.index_kind,
95118
pathspec_match: self.pathspec_match,
@@ -101,19 +124,22 @@ impl EntryRef<'_> {
101124
Entry {
102125
rela_path: self.rela_path.into_owned(),
103126
status: self.status,
127+
property: self.property,
104128
disk_kind: self.disk_kind,
105129
index_kind: self.index_kind,
106130
pathspec_match: self.pathspec_match,
107131
}
108132
}
109133
}
110134

135+
/// Conversion
111136
impl Entry {
112137
/// Obtain an [`EntryRef`] from this instance.
113138
pub fn to_ref(&self) -> EntryRef<'_> {
114139
EntryRef {
115140
rela_path: Cow::Borrowed(self.rela_path.as_ref()),
116141
status: self.status,
142+
property: self.property,
117143
disk_kind: self.disk_kind,
118144
index_kind: self.index_kind,
119145
pathspec_match: self.pathspec_match,
@@ -136,10 +162,7 @@ impl From<std::fs::FileType> for Kind {
136162
impl Status {
137163
/// Return true if this status is considered pruned. A pruned entry is typically hidden from view due to a pathspec.
138164
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-
}
165+
matches!(&self, Status::Pruned)
143166
}
144167
/// Return `true` if `file_type` is a directory on disk and isn't ignored, and is not a repository.
145168
/// This implements the default rules of `git status`, which is good for a minimal traversal through
@@ -158,7 +181,7 @@ impl Status {
158181
return false;
159182
}
160183
match self {
161-
Status::DotGit | Status::TrackedExcluded | Status::Pruned => false,
184+
Status::Pruned => false,
162185
Status::Ignored(_) => {
163186
for_deletion.map_or(false, |fd| {
164187
matches!(
@@ -174,12 +197,12 @@ impl Status {
174197
}
175198

176199
impl Kind {
177-
fn is_recursable_dir(&self) -> bool {
200+
pub(super) fn is_recursable_dir(&self) -> bool {
178201
matches!(self, Kind::Directory)
179202
}
180203

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

Diff for: gix-dir/src/lib.rs

+7-5
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>,

0 commit comments

Comments
 (0)