Skip to content

Commit 594c838

Browse files
committed
fix: make it possible to use a submodule root for a full walk.
Previously, it would not allow to enter the repository, making a walk impossible.
1 parent 37eac54 commit 594c838

File tree

7 files changed

+199
-30
lines changed

7 files changed

+199
-30
lines changed

gix-dir/src/entry.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,15 +168,25 @@ impl Status {
168168
/// This implements the default rules of `git status`, which is good for a minimal traversal through
169169
/// tracked and non-ignored portions of a worktree.
170170
/// `for_deletion` is used to determine if recursion into a directory is allowed even though it otherwise wouldn't be.
171+
/// If `worktree_root_is_repository` is `true`, then this status is part of the root of an iteration, and the corresponding
172+
/// worktree root is a repository itself. This typically happens for submodules. In this case, recursion rules are relaxed
173+
/// to allow traversing submodule worktrees.
171174
///
172175
/// Use `pathspec_match` to determine if a pathspec matches in any way, affecting the decision to recurse.
173176
pub fn can_recurse(
174177
&self,
175178
file_type: Option<Kind>,
176179
pathspec_match: Option<PathspecMatch>,
177180
for_deletion: Option<ForDeletionMode>,
181+
worktree_root_is_repository: bool,
178182
) -> bool {
179-
let is_dir_on_disk = file_type.map_or(false, |ft| ft.is_recursable_dir());
183+
let is_dir_on_disk = file_type.map_or(false, |ft| {
184+
if worktree_root_is_repository {
185+
ft.is_dir()
186+
} else {
187+
ft.is_recursable_dir()
188+
}
189+
});
180190
if !is_dir_on_disk {
181191
return false;
182192
}

gix-dir/src/walk/classify.rs

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ use std::borrow::Cow;
44
use crate::entry::PathspecMatch;
55
use crate::walk::{Context, Error, ForDeletionMode, Options};
66
use bstr::{BStr, BString, ByteSlice};
7-
use std::ffi::OsStr;
8-
use std::path::{Component, Path, PathBuf};
7+
use std::path::{Path, PathBuf};
98

109
/// Classify the `worktree_relative_root` path and return the first `PathKind` that indicates that
1110
/// it isn't a directory, leaving `buf` with the path matching the returned `PathKind`,
@@ -16,28 +15,26 @@ pub fn root(
1615
worktree_relative_root: &Path,
1716
options: Options,
1817
ctx: &mut Context<'_>,
19-
) -> Result<Outcome, Error> {
18+
) -> Result<(Outcome, bool), Error> {
2019
buf.clear();
2120
let mut last_length = None;
2221
let mut path_buf = worktree_root.to_owned();
2322
// These initial values kick in if worktree_relative_root.is_empty();
24-
let mut out = None;
25-
for component in worktree_relative_root
26-
.components()
27-
.chain(if worktree_relative_root.as_os_str().is_empty() {
28-
Some(Component::Normal(OsStr::new("")))
29-
} else {
30-
None
31-
})
32-
{
23+
let file_kind = path_buf.symlink_metadata().map(|m| m.file_type().into()).ok();
24+
let mut out = path(&mut path_buf, buf, 0, file_kind, || None, options, ctx)?;
25+
let worktree_root_is_repository = out
26+
.disk_kind
27+
.map_or(false, |kind| matches!(kind, entry::Kind::Repository));
28+
29+
for component in worktree_relative_root.components() {
3330
if last_length.is_some() {
3431
buf.push(b'/');
3532
}
3633
path_buf.push(component);
3734
buf.extend_from_slice(gix_path::os_str_into_bstr(component.as_os_str()).expect("no illformed UTF8"));
3835
let file_kind = path_buf.symlink_metadata().map(|m| m.file_type().into()).ok();
3936

40-
let res = path(
37+
out = path(
4138
&mut path_buf,
4239
buf,
4340
last_length.map(|l| l + 1 /* slash */).unwrap_or_default(),
@@ -46,16 +43,17 @@ pub fn root(
4643
options,
4744
ctx,
4845
)?;
49-
out = Some(res);
50-
if !res
51-
.status
52-
.can_recurse(res.disk_kind, res.pathspec_match, options.for_deletion)
53-
{
46+
if !out.status.can_recurse(
47+
out.disk_kind,
48+
out.pathspec_match,
49+
options.for_deletion,
50+
worktree_root_is_repository,
51+
) {
5452
break;
5553
}
5654
last_length = Some(buf.len());
5755
}
58-
Ok(out.expect("One iteration of the loop at least"))
56+
Ok((out, worktree_root_is_repository))
5957
}
6058
/// The product of [`path()`] calls.
6159
#[derive(Debug, Copy, Clone)]

gix-dir/src/walk/function.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,21 @@ pub fn walk(
6060
let (mut current, worktree_root_relative) = assure_no_symlink_in_root(worktree_root, &root)?;
6161
let mut out = Outcome::default();
6262
let mut buf = BString::default();
63-
let root_info = classify::root(
63+
let (root_info, worktree_root_is_repository) = classify::root(
6464
worktree_root,
6565
&mut buf,
6666
worktree_root_relative.as_ref(),
6767
options,
6868
&mut ctx,
6969
)?;
70-
if !can_recurse(buf.as_bstr(), root_info, options.for_deletion, delegate) {
71-
if buf.is_empty() && !matches!(root_info.disk_kind, Some(entry::Kind::Directory { .. })) {
70+
if !can_recurse(
71+
buf.as_bstr(),
72+
root_info,
73+
options.for_deletion,
74+
worktree_root_is_repository, /* is root */
75+
delegate,
76+
) {
77+
if buf.is_empty() && !root_info.disk_kind.map_or(false, |kind| kind.is_dir()) {
7278
return Err(Error::WorktreeRootIsFile { root: root.to_owned() });
7379
}
7480
if options.precompose_unicode {
@@ -141,12 +147,17 @@ pub(super) fn can_recurse(
141147
rela_path: &BStr,
142148
info: classify::Outcome,
143149
for_deletion: Option<ForDeletionMode>,
150+
is_root: bool,
144151
delegate: &mut dyn Delegate,
145152
) -> bool {
146153
if info.disk_kind.map_or(true, |k| !k.is_dir()) {
147154
return false;
148155
}
149-
delegate.can_recurse(EntryRef::from_outcome(Cow::Borrowed(rela_path), info), for_deletion)
156+
delegate.can_recurse(
157+
EntryRef::from_outcome(Cow::Borrowed(rela_path), info),
158+
for_deletion,
159+
is_root,
160+
)
150161
}
151162

152163
/// Possibly emit an entry to `for_each` in case the provided information makes that possible.

gix-dir/src/walk/mod.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,24 @@ pub trait Delegate {
6969
/// Use `for_deletion` to specify if the seen entries should ultimately be deleted, which may affect the decision
7070
/// of whether to resource or not.
7171
///
72+
/// If `worktree_root_is_repository` is `true`, then this status is part of the root of an iteration, and the corresponding
73+
/// worktree root is a repository itself. This typically happens for submodules. In this case, recursion rules are relaxed
74+
/// to allow traversing submodule worktrees.
75+
///
7276
/// Note that this method will see all directories, even though not all of them may end up being [emitted](Self::emit()).
7377
/// If this method returns `false`, the `entry` will always be emitted.
74-
fn can_recurse(&mut self, entry: EntryRef<'_>, for_deletion: Option<ForDeletionMode>) -> bool {
75-
entry
76-
.status
77-
.can_recurse(entry.disk_kind, entry.pathspec_match, for_deletion)
78+
fn can_recurse(
79+
&mut self,
80+
entry: EntryRef<'_>,
81+
for_deletion: Option<ForDeletionMode>,
82+
worktree_root_is_repository: bool,
83+
) -> bool {
84+
entry.status.can_recurse(
85+
entry.disk_kind,
86+
entry.pathspec_match,
87+
for_deletion,
88+
worktree_root_is_repository,
89+
)
7890
}
7991
}
8092

gix-dir/src/walk/readdir.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,13 @@ pub(super) fn recursive(
6363
ctx,
6464
)?;
6565

66-
if can_recurse(current_bstr.as_bstr(), info, opts.for_deletion, delegate) {
66+
if can_recurse(
67+
current_bstr.as_bstr(),
68+
info,
69+
opts.for_deletion,
70+
false, /* is root */
71+
delegate,
72+
) {
6773
let subdir_may_collapse = state.may_collapse(current);
6874
let (action, subdir_prevent_collapse) = recursive(
6975
subdir_may_collapse,

gix-dir/tests/fixtures/many.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ git init dir-with-tracked-file
2929
git commit -m "init"
3030
)
3131

32+
git init repo-with-submodule
33+
(cd repo-with-submodule
34+
git submodule add ../dir-with-tracked-file submodule
35+
git commit -m "add submodule"
36+
touch submodule/untracked
37+
)
38+
3239
git init ignored-dir
3340
(cd ignored-dir
3441
mkdir dir

gix-dir/tests/walk/mod.rs

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2631,6 +2631,130 @@ fn root_may_not_go_through_dot_git() -> crate::Result {
26312631
Ok(())
26322632
}
26332633

2634+
#[test]
2635+
fn root_at_submodule_repository_allows_walk() -> crate::Result {
2636+
let root = fixture("repo-with-submodule");
2637+
let troot = root.join("submodule");
2638+
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
2639+
&troot,
2640+
None,
2641+
Some(&troot),
2642+
|keep, ctx| {
2643+
walk(
2644+
&troot,
2645+
ctx,
2646+
walk::Options {
2647+
emit_tracked: true,
2648+
emit_untracked: Matching,
2649+
..options()
2650+
},
2651+
keep,
2652+
)
2653+
},
2654+
None::<&str>,
2655+
Options::git_dir("../.git/modules/submodule"),
2656+
)?;
2657+
2658+
assert_eq!(
2659+
out,
2660+
walk::Outcome {
2661+
read_dir_calls: 2,
2662+
returned_entries: entries.len(),
2663+
seen_entries: 3,
2664+
}
2665+
);
2666+
2667+
assert_eq!(
2668+
entries,
2669+
[entry("dir/file", Tracked, File), entry("untracked", Untracked, File)],
2670+
"this is a special case to allow walking submodules specifically, like a normal repository"
2671+
);
2672+
Ok(())
2673+
}
2674+
2675+
#[test]
2676+
fn root_in_submodule_repository_allows_walk() -> crate::Result {
2677+
let root = fixture("repo-with-submodule");
2678+
let troot = root.join("submodule");
2679+
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
2680+
&troot,
2681+
None,
2682+
Some(&troot.join("dir")),
2683+
|keep, ctx| {
2684+
walk(
2685+
&troot,
2686+
ctx,
2687+
walk::Options {
2688+
emit_tracked: true,
2689+
emit_untracked: Matching,
2690+
..options()
2691+
},
2692+
keep,
2693+
)
2694+
},
2695+
None::<&str>,
2696+
Options::git_dir("../.git/modules/submodule"),
2697+
)?;
2698+
2699+
assert_eq!(
2700+
out,
2701+
walk::Outcome {
2702+
read_dir_calls: 1,
2703+
returned_entries: entries.len(),
2704+
seen_entries: 1,
2705+
}
2706+
);
2707+
2708+
assert_eq!(
2709+
entries,
2710+
[entry("dir/file", Tracked, File)],
2711+
"it's also working if the traversal root is inside the subdmodule"
2712+
);
2713+
Ok(())
2714+
}
2715+
2716+
#[test]
2717+
fn root_in_submodule_from_superproject_repository_allows_walk() -> crate::Result {
2718+
let root = fixture("repo-with-submodule");
2719+
let troot = root.join("submodule").join("dir");
2720+
let ((out, _root), entries) = try_collect_filtered_opts_collect_with_root(
2721+
&root,
2722+
None,
2723+
Some(&troot),
2724+
|keep, ctx| {
2725+
walk(
2726+
&troot,
2727+
ctx,
2728+
walk::Options {
2729+
emit_tracked: true,
2730+
emit_untracked: Matching,
2731+
..options()
2732+
},
2733+
keep,
2734+
)
2735+
},
2736+
None::<&str>,
2737+
Default::default(),
2738+
)?;
2739+
2740+
assert_eq!(
2741+
out,
2742+
walk::Outcome {
2743+
read_dir_calls: 1,
2744+
returned_entries: entries.len(),
2745+
seen_entries: 1,
2746+
}
2747+
);
2748+
2749+
assert_eq!(
2750+
entries,
2751+
[entry("file", Untracked, File)],
2752+
"there is no index that has 'file' in it (it's 'dir/file'), hence it's untracked.\
2753+
But the traversal is possible, even though it might not make the most sense."
2754+
);
2755+
Ok(())
2756+
}
2757+
26342758
#[test]
26352759
fn root_enters_directory_with_dot_git_in_reconfigured_worktree_tracked() -> crate::Result {
26362760
let root = fixture("nonstandard-worktree");
@@ -2797,7 +2921,8 @@ fn root_may_not_go_through_submodule() -> crate::Result {
27972921
assert_eq!(
27982922
entries,
27992923
[entry("submodule", Tracked, Repository)],
2800-
"it refuses to start traversal in a submodule, thus it ends in the directory that is the submodule"
2924+
"it refuses to start traversal in a submodule, thus it ends in the directory that is the submodule, \
2925+
if the root is another repository"
28012926
);
28022927
Ok(())
28032928
}

0 commit comments

Comments
 (0)