Skip to content

Commit 4d5767c

Browse files
committed
Make it even harder to remove your own CWD
1 parent 8959b21 commit 4d5767c

File tree

4 files changed

+103
-6
lines changed

4 files changed

+103
-6
lines changed

gix-dir/src/entry.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ pub enum Property {
99
DotGit,
1010
/// The entry is a directory, and that directory is empty.
1111
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,
1218
/// Always in conjunction with a directory on disk that is also known as cone-mode sparse-checkout exclude marker
1319
/// - i.e. a directory that is excluded, so its whole content is excluded and not checked out nor is part of the index.
1420
///

gix-dir/src/walk/readdir.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ pub(super) fn recursive(
9494
num_entries,
9595
state,
9696
&mut prevent_collapse,
97+
current,
9798
current_bstr.as_bstr(),
9899
current_info,
99100
opts,
@@ -157,7 +158,7 @@ impl State {
157158

158159
pub(super) fn emit_remaining(
159160
&mut self,
160-
is_top_level: bool,
161+
may_collapse: bool,
161162
opts: Options,
162163
out: &mut walk::Outcome,
163164
delegate: &mut dyn walk::Delegate,
@@ -168,7 +169,7 @@ impl State {
168169

169170
_ = Mark {
170171
start_index: 0,
171-
may_collapse: is_top_level,
172+
may_collapse,
172173
}
173174
.emit_all_held(self, opts, out, delegate);
174175
}
@@ -186,6 +187,7 @@ impl Mark {
186187
num_entries: usize,
187188
state: &mut State,
188189
prevent_collapse: &mut bool,
190+
dir_path: &Path,
189191
dir_rela_path: &BStr,
190192
dir_info: classify::Outcome,
191193
opts: Options,
@@ -195,19 +197,20 @@ impl Mark {
195197
) -> walk::Action {
196198
if num_entries == 0 {
197199
let empty_info = classify::Outcome {
198-
property: if num_entries == 0 {
200+
property: {
199201
assert_ne!(
200202
dir_info.disk_kind,
201203
Some(entry::Kind::Repository),
202204
"BUG: it shouldn't be possible to classify an empty dir as repository"
203205
);
204-
if dir_info.property.is_none() {
206+
if !state.may_collapse(dir_path) {
207+
*prevent_collapse = true;
208+
entry::Property::EmptyDirectoryAndCWD.into()
209+
} else if dir_info.property.is_none() {
205210
entry::Property::EmptyDirectory.into()
206211
} else {
207212
dir_info.property
208213
}
209-
} else {
210-
dir_info.property
211214
},
212215
pathspec_match: ctx
213216
.pathspec

gix-dir/tests/fixtures/many.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,14 @@ git init only-untracked
178178
>c
179179
)
180180

181+
git init ignored-with-empty
182+
(cd ignored-with-empty
183+
echo "/target/" >> .gitignore
184+
git add .gitignore && git commit -m "init"
185+
mkdir -p target/empty target/debug target/release
186+
touch target/debug/a target/release/b
187+
)
188+
181189
cp -R only-untracked subdir-untracked
182190
(cd subdir-untracked
183191
git add .

gix-dir/tests/walk/mod.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,86 @@ fn ignored_dir_with_cwd_handling() -> crate::Result {
357357
Ok(())
358358
}
359359

360+
#[test]
361+
fn ignored_with_cwd_handling() -> crate::Result {
362+
let root = gix_path::realpath(fixture("ignored-with-empty"))?;
363+
let ((out, _root), entries) = collect_filtered_with_cwd(
364+
&root,
365+
None,
366+
None,
367+
|keep, ctx| {
368+
walk(
369+
&root,
370+
ctx,
371+
walk::Options {
372+
for_deletion: Some(Default::default()),
373+
emit_ignored: Some(CollapseDirectory),
374+
emit_empty_directories: true,
375+
..options()
376+
},
377+
keep,
378+
)
379+
},
380+
None::<&str>,
381+
);
382+
383+
assert_eq!(
384+
out,
385+
walk::Outcome {
386+
read_dir_calls: 1,
387+
returned_entries: entries.len(),
388+
seen_entries: 3,
389+
}
390+
);
391+
392+
assert_eq!(
393+
entries,
394+
[entry("target", Ignored(Expendable), Directory),],
395+
"the baseline shows the content"
396+
);
397+
398+
let ((out, _root), entries) = collect_filtered_with_cwd(
399+
&root,
400+
Some(&root),
401+
Some("target/empty"),
402+
|keep, ctx| {
403+
walk(
404+
&root,
405+
ctx,
406+
walk::Options {
407+
for_deletion: Some(Default::default()),
408+
emit_ignored: Some(CollapseDirectory),
409+
emit_empty_directories: true,
410+
..options()
411+
},
412+
keep,
413+
)
414+
},
415+
Some("target"),
416+
);
417+
418+
assert_eq!(
419+
out,
420+
walk::Outcome {
421+
read_dir_calls: 5,
422+
returned_entries: entries.len(),
423+
seen_entries: 7,
424+
}
425+
);
426+
427+
assert_eq!(
428+
entries,
429+
[
430+
entryps("target/debug", Ignored(Expendable), Directory, Prefix),
431+
entryps("target/empty", Ignored(Expendable), Directory, Prefix).with_property(EmptyDirectoryAndCWD),
432+
entryps("target/release", Ignored(Expendable), Directory, Prefix),
433+
],
434+
"it detects empty as CWD (very special case) and lists it as usual, while also preventing collapse to assure \
435+
to not accidentally end up trying to delete a parent directory"
436+
);
437+
Ok(())
438+
}
439+
360440
#[test]
361441
fn only_untracked_with_cwd_handling() -> crate::Result {
362442
let root = fixture("only-untracked");

0 commit comments

Comments
 (0)