diff --git a/Cargo.lock b/Cargo.lock index c82c1ef45d0..4dc241d1671 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1248,7 +1248,7 @@ dependencies = [ [[package]] name = "gitoxide-core" -version = "0.39.0" +version = "0.39.1" dependencies = [ "anyhow", "async-io 2.2.2", diff --git a/Cargo.toml b/Cargo.toml index 38ee898d9d4..4dc1037437a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -168,7 +168,7 @@ gitoxide-core-async-client = ["gitoxide-core/async-client", "futures-lite"] [dependencies] anyhow = "1.0.42" -gitoxide-core = { version = "^0.39.0", path = "gitoxide-core" } +gitoxide-core = { version = "^0.39.1", path = "gitoxide-core" } gix-features = { version = "^0.38.2", path = "gix-features" } gix = { version = "^0.64.0", path = "gix", default-features = false } time = "0.3.23" diff --git a/gitoxide-core/Cargo.toml b/gitoxide-core/Cargo.toml index 65dab5c037a..7f947de992f 100644 --- a/gitoxide-core/Cargo.toml +++ b/gitoxide-core/Cargo.toml @@ -2,7 +2,7 @@ name = "gitoxide-core" description = "The library implementing all capabilities of the gitoxide CLI" repository = "https://github.com/Byron/gitoxide" -version = "0.39.0" +version = "0.39.1" authors = ["Sebastian Thiel "] license = "MIT OR Apache-2.0" edition = "2021" diff --git a/gix-dir/src/walk/classify.rs b/gix-dir/src/walk/classify.rs index a93e7c0b16a..b87c893e7c7 100644 --- a/gix-dir/src/walk/classify.rs +++ b/gix-dir/src/walk/classify.rs @@ -265,8 +265,17 @@ pub fn path( ), ); } - if kind.map_or(false, |d| d.is_recursable_dir()) && out.pathspec_match.is_none() { - // we have patterns that didn't match at all, *yet*. We want to look inside. + if kind.map_or(false, |d| d.is_recursable_dir()) + && (out.pathspec_match.is_none() + || worktree_relative_worktree_dirs.map_or(false, |worktrees| { + for_deletion.is_some() + && worktrees + .iter() + .any(|dir| dir.starts_with_str(&*rela_path) && dir.get(rela_path.len()) == Some(&b'/')) + })) + { + // We have patterns that didn't match at all, *yet*, or there are contained worktrees. + // We want to look inside. out.pathspec_match = Some(PathspecMatch::Prefix); } } diff --git a/gix-dir/tests/fixtures/many.sh b/gix-dir/tests/fixtures/many.sh index a83e902100e..e5146933644 100755 --- a/gix-dir/tests/fixtures/many.sh +++ b/gix-dir/tests/fixtures/many.sh @@ -443,4 +443,12 @@ git clone dir-with-tracked-file in-repo-worktree (cd in-repo-worktree git worktree add worktree git worktree add -b other-worktree dir/worktree -) \ No newline at end of file +) + +git clone dir-with-tracked-file in-repo-hidden-worktree +(cd in-repo-hidden-worktree + echo '/hidden/' > .gitignore + mkdir -p hidden/subdir + touch hidden/file + git worktree add -b worktree-branch hidden/subdir/worktree +) diff --git a/gix-dir/tests/walk/mod.rs b/gix-dir/tests/walk/mod.rs index cb5412977f2..057271e0968 100644 --- a/gix-dir/tests/walk/mod.rs +++ b/gix-dir/tests/walk/mod.rs @@ -4550,3 +4550,100 @@ fn in_repo_worktree() -> crate::Result { ); Ok(()) } + +#[test] +fn in_repo_hidden_worktree() -> crate::Result { + let root = fixture("in-repo-hidden-worktree"); + let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep)); + assert_eq!( + out, + walk::Outcome { + read_dir_calls: 2, + returned_entries: entries.len(), + seen_entries: 4, + } + ); + assert_eq!( + entries, + &[ + entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always), + entry(".gitignore", Untracked, File), + entry("dir/file", Tracked, File), + entry("hidden", Ignored(Expendable), Directory), + ], + "if worktree information isn't provided, they would not be discovered in hidden directories" + ); + + let ((out, _root), entries) = collect(&root, None, |keep, ctx| { + walk( + &root, + ctx, + walk::Options { + for_deletion: None, + worktree_relative_worktree_dirs: Some(&BTreeSet::from(["hidden/subdir/worktree".into()])), + ..options_emit_all() + }, + keep, + ) + }); + assert_eq!( + out, + walk::Outcome { + read_dir_calls: 2, + returned_entries: entries.len(), + seen_entries: 4, + } + ); + assert_eq!( + entries, + &[ + entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always), + entry(".gitignore", Untracked, File), + entry("dir/file", Tracked, File), + entry("hidden", Ignored(Expendable), Directory), + ], + "Without the intend to delete, the worktree remains hidden, which is what we want to see in a `status` for example" + ); + + for ignored_emission_mode in [Matching, CollapseDirectory] { + for deletion_mode in [ + ForDeletionMode::IgnoredDirectoriesCanHideNestedRepositories, + ForDeletionMode::FindRepositoriesInIgnoredDirectories, + ForDeletionMode::FindNonBareRepositoriesInIgnoredDirectories, + ] { + let ((out, _root), entries) = collect(&root, None, |keep, ctx| { + walk( + &root, + ctx, + walk::Options { + emit_ignored: Some(ignored_emission_mode), + for_deletion: Some(deletion_mode), + worktree_relative_worktree_dirs: Some(&BTreeSet::from(["hidden/subdir/worktree".into()])), + ..options_emit_all() + }, + keep, + ) + }); + assert_eq!( + out, + walk::Outcome { + read_dir_calls: 4, + returned_entries: entries.len(), + seen_entries: 5, + } + ); + assert_eq!( + entries, + &[ + entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always), + entry(".gitignore", Untracked, File), + entry("dir/file", Tracked, File), + entry("hidden/file", Ignored(Expendable), File), + entry("hidden/subdir/worktree", Tracked, Repository).no_index_kind(), + ], + "Worktrees within hidden directories are also detected and protected by counting them as tracked (like submodules)" + ); + } + } + Ok(()) +}