Skip to content

Commit 12924d7

Browse files
authored
Merge pull request GitoxideLabs#2382 from GitoxideLabs/improvements
improvements
2 parents 5c1bd03 + da53014 commit 12924d7

File tree

10 files changed

+183
-39
lines changed

10 files changed

+183
-39
lines changed

gix-dir/src/walk/classify.rs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -388,20 +388,27 @@ fn resolve_file_type_with_index(
388388
// TODO(perf): multi-threaded hash-table so it's always used, even for case-sensitive lookups, just like Git does it.
389389
let (uptodate_kind, index_kind) = if let Some(accelerate) = ignore_case {
390390
match index.entry_by_path_icase(rela_path.as_bstr(), true, accelerate) {
391-
None => {
392-
icase_directory_to_kinds(index.entry_closest_to_directory_icase(rela_path.as_bstr(), true, accelerate))
393-
}
391+
None => icase_directory_to_kinds(index.entry_closest_to_directory_or_directory_icase(
392+
rela_path.as_bstr(),
393+
true,
394+
accelerate,
395+
)),
394396
Some(entry) => {
395-
let icase_dir = index.entry_closest_to_directory_icase(rela_path.as_bstr(), true, accelerate);
396-
let directory_matches_exactly = icase_dir.is_some_and(|dir| {
397-
let path = dir.path(index);
398-
let slash_idx = path.rfind_byte(b'/').expect("dir");
399-
path[..slash_idx].as_bstr() == rela_path
400-
});
401-
if directory_matches_exactly {
402-
icase_directory_to_kinds(icase_dir)
403-
} else {
397+
if entry.mode.is_submodule() || entry.mode.is_sparse() {
404398
entry_to_kinds(entry)
399+
} else {
400+
let icase_dir =
401+
index.entry_closest_to_directory_or_directory_icase(rela_path.as_bstr(), true, accelerate);
402+
let directory_matches_exactly = icase_dir.is_some_and(|dir| {
403+
let path = dir.path(index);
404+
let slash_idx = path.rfind_byte(b'/').expect("dir");
405+
path[..slash_idx].as_bstr() == rela_path
406+
});
407+
if directory_matches_exactly {
408+
icase_directory_to_kinds(icase_dir)
409+
} else {
410+
entry_to_kinds(entry)
411+
}
405412
}
406413
}
407414
}

gix-index/src/access/mod.rs

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,16 @@ impl State {
149149
let hash = AccelerateLookup::icase_hash(entry_path);
150150
out.icase_entries
151151
.insert_unique(hash, entry, |e| AccelerateLookup::icase_hash(e.path(self)));
152+
if entry_is_dir(entry) {
153+
out.icase_dirs.insert_unique(
154+
hash,
155+
crate::DirEntry {
156+
entry,
157+
dir_end: entry.path.end,
158+
},
159+
|dir| AccelerateLookup::icase_hash(dir.path(self)),
160+
);
161+
}
152162

153163
let mut last_pos = entry_path.len();
154164
while let Some(slash_idx) = entry_path[..last_pos].rfind_byte(b'/') {
@@ -206,14 +216,11 @@ impl State {
206216
.copied()
207217
}
208218

209-
/// Return the entry (at any stage) that is inside of `directory`, or `None`,
210-
/// using `lookup` for acceleration.
211-
/// Note that submodules are not detected as directories and the user should
212-
/// make another call to [`entry_by_path_icase()`](Self::entry_by_path_icase) to check for this
213-
/// possibility. Doing so might also reveal a sparse directory.
219+
/// Return the entry (at any stage) that is inside `directory`, or `None`,
220+
/// or a directory itself like a submodule or sparse directory, using `lookup` for acceleration.
214221
///
215222
/// If `ignore_case` is set, a case-insensitive (ASCII-folding only) search will be performed.
216-
pub fn entry_closest_to_directory_icase<'a>(
223+
pub fn entry_closest_to_directory_or_directory_icase<'a>(
217224
&'a self,
218225
directory: &BStr,
219226
ignore_case: bool,
@@ -234,14 +241,18 @@ impl State {
234241
.map(|dir| dir.entry)
235242
}
236243

237-
/// Return the entry (at any stage) that is inside of `directory`, or `None`.
238-
/// Note that submodules are not detected as directories and the user should
239-
/// make another call to [`entry_by_path_icase()`](Self::entry_by_path_icase) to check for this
240-
/// possibility. Doing so might also reveal a sparse directory.
244+
/// Return the entry (at any stage) that is inside `directory`, or `None`,
245+
/// or that is a directory itself like a submodule or sparse directory.
241246
///
242-
/// Note that this is a case-sensitive search.
243-
pub fn entry_closest_to_directory(&self, directory: &BStr) -> Option<&Entry> {
244-
let idx = self.entry_index_by_path(directory).err()?;
247+
/// Note that this is a *case-sensitive* search.
248+
pub fn entry_closest_to_directory_or_directory(&self, directory: &BStr) -> Option<&Entry> {
249+
let idx = match self.entry_index_by_path(directory) {
250+
Ok(idx) => {
251+
let entry = &self.entries[idx];
252+
return entry_is_dir(entry).then_some(entry);
253+
}
254+
Err(closest_idx) => closest_idx,
255+
};
245256
for entry in &self.entries[idx..] {
246257
let path = entry.path(self);
247258
if path.get(..directory.len())? != directory {
@@ -259,7 +270,7 @@ impl State {
259270
None
260271
}
261272

262-
/// Check if `path` is a directory that contains entries in the index.
273+
/// Check if `path` is a directory that contains entries in the index, or is a submodule.
263274
///
264275
/// Returns `true` if there is at least one entry in the index whose path starts with `path/`,
265276
/// indicating that `path` is a directory containing indexed files.
@@ -269,10 +280,11 @@ impl State {
269280
///
270281
/// Note that this is a case-sensitive search.
271282
pub fn path_is_directory(&self, path: &BStr) -> bool {
272-
self.entry_closest_to_directory(path).is_some()
283+
self.entry_closest_to_directory_or_directory(path).is_some()
273284
}
274285

275-
/// Check if `path` is a directory that contains entries in the index, with optional case-insensitive matching.
286+
/// Check if `path` is a directory that contains entries in the index or is a submodule,
287+
/// with optional case-insensitive matching.
276288
///
277289
/// Returns `true` if there is at least one entry in the index whose path starts with `path/`,
278290
/// indicating that `path` is a directory containing indexed files.
@@ -288,7 +300,7 @@ impl State {
288300
ignore_case: bool,
289301
lookup: &AccelerateLookup<'a>,
290302
) -> bool {
291-
self.entry_closest_to_directory_icase(path, ignore_case, lookup)
303+
self.entry_closest_to_directory_or_directory_icase(path, ignore_case, lookup)
292304
.is_some()
293305
}
294306

@@ -623,6 +635,10 @@ impl State {
623635
}
624636
}
625637

638+
fn entry_is_dir(entry: &Entry) -> bool {
639+
entry.mode.is_sparse() || entry.mode.is_submodule()
640+
}
641+
626642
#[cfg(test)]
627643
mod tests {
628644
use std::path::{Path, PathBuf};
Binary file not shown.

gix-index/tests/fixtures/generated-archives/V2_empty.tar renamed to gix-index/tests/fixtures/generated-archives/v2_empty.tar

File renamed without changes.

gix-index/tests/fixtures/make_index/v2_all_file_kinds.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ ln -s a c
1919
mkdir d
2020
(cd d && touch a b c)
2121

22+
git submodule add ./sub sub-worktree
23+
2224
git add .
2325
git update-index --chmod=+x b # For Windows.
2426
git commit -m "init"
File renamed without changes.

gix-index/tests/index/access.rs

Lines changed: 125 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ fn dirwalk_api_and_icase_support() {
4848
last_pos += 1;
4949

5050
let entry = file
51-
.entry_closest_to_directory(dir)
51+
.entry_closest_to_directory_or_directory(dir)
5252
.unwrap_or_else(|| panic!("didn't find {dir}"));
5353
assert!(
5454
entry.path(&file).starts_with(dir),
@@ -57,13 +57,75 @@ fn dirwalk_api_and_icase_support() {
5757

5858
let dir_upper: BString = dir.to_ascii_uppercase().into();
5959
let other_entry = file
60-
.entry_closest_to_directory_icase(dir_upper.as_bstr(), true, &icase)
60+
.entry_closest_to_directory_or_directory_icase(dir_upper.as_bstr(), true, &icase)
6161
.unwrap_or_else(|| panic!("didn't find upper-cased {dir_upper}"));
6262
assert_eq!(other_entry, entry, "the first entry is always the same, no matter what kind of search is conducted (as there are no clashes/ambiguities here)");
6363
}
6464
}
6565
}
6666

67+
#[test]
68+
fn entry_closest_to_directory_or_directory_with_submodule() {
69+
let file = Fixture::Generated("v2_all_file_kinds").open();
70+
71+
assert!(
72+
file.entry_closest_to_directory_or_directory("d".into()).is_some(),
73+
"this is a directory"
74+
);
75+
assert!(
76+
file.entry_closest_to_directory_or_directory("sub".into()).is_some(),
77+
"this is a checked in repository, a directory itself"
78+
);
79+
assert!(
80+
file.entry_closest_to_directory_or_directory("sub-worktree".into())
81+
.is_some(),
82+
"a submodule that is officially registered, absolutely the same as 'sub' in the index."
83+
);
84+
assert!(
85+
file.entry_closest_to_directory_or_directory("a".into()).is_none(),
86+
"'a' is a file, and we ask for a directory"
87+
);
88+
}
89+
90+
#[test]
91+
fn entry_closest_to_directory_or_directory_icase_with_submodule() {
92+
let file = Fixture::Generated("v2_all_file_kinds").open();
93+
let icase = file.prepare_icase_backing();
94+
95+
assert!(
96+
file.entry_closest_to_directory_or_directory_icase("D".into(), true, &icase)
97+
.is_some(),
98+
"this is a directory"
99+
);
100+
assert!(file
101+
.entry_closest_to_directory_or_directory_icase("D".into(), false, &icase)
102+
.is_none());
103+
104+
assert!(
105+
file.entry_closest_to_directory_or_directory_icase("SuB".into(), true, &icase)
106+
.is_some(),
107+
"this is a checked in repository, a directory itself"
108+
);
109+
assert!(file
110+
.entry_closest_to_directory_or_directory_icase("SuB".into(), false, &icase)
111+
.is_none());
112+
113+
assert!(
114+
file.entry_closest_to_directory_or_directory_icase("SUB-worktree".into(), true, &icase)
115+
.is_some(),
116+
"a submodule that is officially registered, absolutely the same as 'sub' in the index."
117+
);
118+
assert!(file
119+
.entry_closest_to_directory_or_directory_icase("SUB-worktree".into(), false, &icase)
120+
.is_none());
121+
122+
assert!(
123+
file.entry_closest_to_directory_or_directory_icase("A".into(), true, &icase)
124+
.is_none(),
125+
"'a' is a file, and we ask for a directory"
126+
);
127+
}
128+
67129
#[test]
68130
fn ignorecase_clashes_and_order() {
69131
let file = icase_fixture();
@@ -85,7 +147,7 @@ fn ignorecase_clashes_and_order() {
85147
last_pos += 1;
86148

87149
let entry = file
88-
.entry_closest_to_directory(dir)
150+
.entry_closest_to_directory_or_directory(dir)
89151
.unwrap_or_else(|| panic!("didn't find {dir}"));
90152
assert!(
91153
entry.path(&file).starts_with(dir),
@@ -110,16 +172,16 @@ fn ignorecase_clashes_and_order() {
110172
);
111173

112174
assert!(
113-
file.entry_closest_to_directory("d".into()).is_none(),
175+
file.entry_closest_to_directory_or_directory("d".into()).is_none(),
114176
"this is a file, and this directory search isn't case-sensitive"
115177
);
116-
let entry = file.entry_closest_to_directory("D".into());
178+
let entry = file.entry_closest_to_directory_or_directory("D".into());
117179
assert_eq!(
118180
entry.map(|e| e.path(&file)).expect("present"),
119181
"D/B",
120182
"this is a directory, indeed, we find the first file in it"
121183
);
122-
let entry_icase = file.entry_closest_to_directory_icase("d".into(), true, &icase);
184+
let entry_icase = file.entry_closest_to_directory_or_directory_icase("d".into(), true, &icase);
123185
assert_eq!(
124186
entry_icase, entry,
125187
"case-insensitive searches don't confuse directories and files, so `d` finds `D`, the directory."
@@ -325,6 +387,26 @@ fn check_prefix(index: &gix_index::State, prefix: &str, expected: &[&str]) {
325387
);
326388
}
327389

390+
#[test]
391+
fn path_is_directory_with_submodule() {
392+
let file = Fixture::Generated("v2_all_file_kinds").open();
393+
394+
assert!(file.path_is_directory("sub-worktree".into()), "a submodule worktree");
395+
assert!(file.path_is_directory("d".into()), "a single-letter directory");
396+
assert!(
397+
file.path_is_directory("sub".into()),
398+
"this is the parent repository, and it was added as well"
399+
);
400+
assert!(
401+
!file.path_is_directory("su".into()),
402+
"just a sub-string of the directory which doesn't match"
403+
);
404+
assert!(
405+
!file.path_is_directory("a".into()),
406+
"a one-letter file isn't a directory"
407+
);
408+
}
409+
328410
#[test]
329411
fn path_is_directory() {
330412
let file = Fixture::Loose("ignore-case-realistic").open();
@@ -408,6 +490,43 @@ fn path_is_directory_icase() {
408490
);
409491
}
410492

493+
#[test]
494+
fn path_is_directory_icase_with_submodule() {
495+
let file = Fixture::Generated("v2_all_file_kinds").open();
496+
let icase = file.prepare_icase_backing();
497+
498+
assert!(
499+
file.path_is_directory_icase("SUB-worktree".into(), true, &icase),
500+
"a submodule worktree"
501+
);
502+
assert!(!file.path_is_directory_icase("SUB-worktree".into(), false, &icase));
503+
504+
assert!(
505+
file.path_is_directory_icase("D".into(), true, &icase),
506+
"a single-letter directory"
507+
);
508+
assert!(!file.path_is_directory_icase("D".into(), false, &icase));
509+
510+
assert!(
511+
file.path_is_directory_icase("SuB".into(), true, &icase),
512+
"this is the parent repository, and it was added as well"
513+
);
514+
assert!(!file.path_is_directory_icase("SuB".into(), false, &icase));
515+
516+
assert!(
517+
!file.path_is_directory_icase("Su".into(), true, &icase),
518+
"just a sub-string of the directory which doesn't match"
519+
);
520+
assert!(
521+
!file.path_is_directory_icase("A".into(), true, &icase),
522+
"a one-letter file isn't a directory"
523+
);
524+
assert!(
525+
!file.path_is_directory_icase("a".into(), true, &icase),
526+
"a one-letter file isn't a directory, even with correct case"
527+
);
528+
}
529+
411530
#[test]
412531
fn path_is_directory_icase_with_clashes() {
413532
let file = icase_fixture();

gix-index/tests/index/file/init.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ mod from_state {
3737
let fixtures = [
3838
(Loose("extended-flags"), V3),
3939
(Generated("v2"), V2),
40-
(Generated("V2_empty"), V2),
40+
(Generated("v2_empty"), V2),
4141
(Generated("v2_more_files"), V2),
4242
(Generated("v2_all_file_kinds"), V2),
4343
(Generated("v4_more_files_IEOT"), V2),

gix-index/tests/index/file/read.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ fn v2_with_single_entry_tree_and_eoie_ext() {
6666
}
6767
#[test]
6868
fn v2_empty() {
69-
let file = file("V2_empty");
69+
let file = file("v2_empty");
7070
assert_eq!(file.version(), Version::V2);
7171
assert_eq!(file.entries().len(), 0);
7272
let tree = file.tree().unwrap();

gix-index/tests/index/file/write.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ fn roundtrips() -> crate::Result {
1717
end_of_index_entry: true,
1818
}),
1919
),
20-
(Generated("V2_empty"), only_tree_ext()),
20+
(Generated("v2_empty"), only_tree_ext()),
2121
(Generated("v2_more_files"), only_tree_ext()),
2222
(Generated("v2_all_file_kinds"), only_tree_ext()),
2323
];
@@ -128,7 +128,7 @@ fn state_comparisons_with_various_extension_configurations() {
128128
Loose("REUC"),
129129
Loose("UNTR-with-oids"),
130130
Loose("UNTR"),
131-
Generated("V2_empty"),
131+
Generated("v2_empty"),
132132
Generated("v2"),
133133
Generated("v2_more_files"),
134134
Generated("v2_all_file_kinds"),

0 commit comments

Comments
 (0)