Skip to content

Commit c9cd2d2

Browse files
committed
feat!: make it possible to consider worktrees to be 'tracked' (#1464)
That way it's possibel for them to be equivalent to submodules, which would never be deleted by accident due to their 'tracked' status. This works by passing repository-relative paths of worktree locations that are within this repository.
1 parent c7213bc commit c9cd2d2

File tree

8 files changed

+107
-23
lines changed

8 files changed

+107
-23
lines changed

gix-dir/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ pub struct Entry {
5353
/// Further specify what the entry is on disk, similar to a file mode.
5454
pub disk_kind: Option<entry::Kind>,
5555
/// The kind of entry according to the index, if tracked. *Usually* the same as `disk_kind`.
56+
/// Note that even if tracked, this might be `None` which indicates this is a worktree placed
57+
/// within the parent repository.
5658
pub index_kind: Option<entry::Kind>,
5759
/// Indicate how the pathspec matches the entry. See more in [`EntryRef::pathspec_match`].
5860
pub pathspec_match: Option<entry::PathspecMatch>,

gix-dir/src/walk/classify.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ pub fn root(
1313
worktree_root: &Path,
1414
buf: &mut BString,
1515
worktree_relative_root: &Path,
16-
options: Options,
16+
options: Options<'_>,
1717
ctx: &mut Context<'_>,
1818
) -> Result<(Outcome, bool), Error> {
1919
buf.clear();
@@ -135,8 +135,9 @@ pub fn path(
135135
for_deletion,
136136
classify_untracked_bare_repositories,
137137
symlinks_to_directories_are_ignored_like_directories,
138+
worktree_relative_worktree_dirs,
138139
..
139-
}: Options,
140+
}: Options<'_>,
140141
ctx: &mut Context<'_>,
141142
) -> Result<Outcome, Error> {
142143
let mut out = Outcome {
@@ -191,19 +192,25 @@ pub fn path(
191192
);
192193
let mut kind = uptodate_index_kind.or(disk_kind).or_else(on_demand_disk_kind);
193194

195+
// We always check the pathspec to have the value filled in reliably.
196+
out.pathspec_match = ctx
197+
.pathspec
198+
.pattern_matching_relative_path(rela_path.as_bstr(), kind.map(|ft| ft.is_dir()), ctx.pathspec_attributes)
199+
.map(Into::into);
200+
201+
if worktree_relative_worktree_dirs.map_or(false, |worktrees| worktrees.contains(&*rela_path)) {
202+
return Ok(out
203+
.with_kind(Some(entry::Kind::Repository), None)
204+
.with_status(entry::Status::Tracked));
205+
}
206+
194207
let maybe_status = if property.is_none() {
195208
(index_kind.map(|k| k.is_dir()) == kind.map(|k| k.is_dir())).then_some(entry::Status::Tracked)
196209
} else {
197210
out.property = property;
198211
Some(entry::Status::Pruned)
199212
};
200213

201-
// We always check the pathspec to have the value filled in reliably.
202-
out.pathspec_match = ctx
203-
.pathspec
204-
.pattern_matching_relative_path(rela_path.as_bstr(), kind.map(|ft| ft.is_dir()), ctx.pathspec_attributes)
205-
.map(Into::into);
206-
207214
let is_dir = if symlinks_to_directories_are_ignored_like_directories
208215
&& ctx.excludes.is_some()
209216
&& kind.map_or(false, |ft| ft == entry::Kind::Symlink)

gix-dir/src/walk/function.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ use crate::{entry, EntryRef};
4242
pub fn walk(
4343
worktree_root: &Path,
4444
mut ctx: Context<'_>,
45-
options: Options,
45+
options: Options<'_>,
4646
delegate: &mut dyn Delegate,
4747
) -> Result<(Outcome, PathBuf), Error> {
4848
let root = match ctx.explicit_traversal_root {
@@ -182,7 +182,7 @@ pub(super) fn emit_entry(
182182
emit_ignored,
183183
emit_empty_directories,
184184
..
185-
}: Options,
185+
}: Options<'_>,
186186
out: &mut Outcome,
187187
delegate: &mut dyn Delegate,
188188
) -> Action {

gix-dir/src/walk/mod.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::{entry, EntryRef};
2-
use bstr::BStr;
2+
use bstr::{BStr, BString};
3+
use std::collections::BTreeSet;
34
use std::path::PathBuf;
45
use std::sync::atomic::AtomicBool;
56

@@ -143,12 +144,12 @@ pub enum ForDeletionMode {
143144

144145
/// Options for use in [`walk()`](function::walk()) function.
145146
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
146-
pub struct Options {
147-
/// If true, the filesystem will store paths as decomposed unicode, i.e. `ä` becomes `"a\u{308}"`, which means that
147+
pub struct Options<'a> {
148+
/// If `true`, the filesystem will store paths as decomposed unicode, i.e. `ä` becomes `"a\u{308}"`, which means that
148149
/// we have to turn these forms back from decomposed to precomposed unicode before storing it in the index or generally
149150
/// using it. This also applies to input received from the command-line, so callers may have to be aware of this and
150151
/// perform conversions accordingly.
151-
/// If false, no conversions will be performed.
152+
/// If `false`, no conversions will be performed.
152153
pub precompose_unicode: bool,
153154
/// If true, the filesystem ignores the case of input, which makes `A` the same file as `a`.
154155
/// This is also called case-folding.
@@ -192,6 +193,11 @@ pub struct Options {
192193
///
193194
/// In other words, for Git compatibility this flag should be `false`, the default, for `git2` compatibility it should be `true`.
194195
pub symlinks_to_directories_are_ignored_like_directories: bool,
196+
/// A set of all git worktree checkouts that are located within the main worktree directory.
197+
///
198+
/// They will automatically be detected as 'tracked', but without providing index information (as there is no actual index entry).
199+
/// Note that the unicode composition must match the `precompose_unicode` field so that paths will match verbatim.
200+
pub worktree_relative_worktree_dirs: Option<&'a BTreeSet<BString>>,
195201
}
196202

197203
/// All information that is required to perform a dirwalk, and classify paths properly.

gix-dir/src/walk/readdir.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ pub(super) fn recursive(
2121
current_bstr: &mut BString,
2222
current_info: classify::Outcome,
2323
ctx: &mut Context<'_>,
24-
opts: Options,
24+
opts: Options<'_>,
2525
delegate: &mut dyn Delegate,
2626
out: &mut Outcome,
2727
state: &mut State,
@@ -141,7 +141,7 @@ pub(super) struct State {
141141

142142
impl State {
143143
/// Hold the entry with the given `status` if it's a candidate for collapsing the containing directory.
144-
fn held_for_directory_collapse(&mut self, rela_path: &BStr, info: classify::Outcome, opts: &Options) -> bool {
144+
fn held_for_directory_collapse(&mut self, rela_path: &BStr, info: classify::Outcome, opts: &Options<'_>) -> bool {
145145
if opts.should_hold(info.status) {
146146
self.on_hold
147147
.push(EntryRef::from_outcome(Cow::Borrowed(rela_path), info).into_owned());
@@ -186,7 +186,7 @@ impl State {
186186
pub(super) fn emit_remaining(
187187
&mut self,
188188
may_collapse: bool,
189-
opts: Options,
189+
opts: Options<'_>,
190190
out: &mut walk::Outcome,
191191
delegate: &mut dyn walk::Delegate,
192192
) {
@@ -217,7 +217,7 @@ impl Mark {
217217
dir_path: &Path,
218218
dir_rela_path: &BStr,
219219
dir_info: classify::Outcome,
220-
opts: Options,
220+
opts: Options<'_>,
221221
out: &mut walk::Outcome,
222222
ctx: &mut Context<'_>,
223223
delegate: &mut dyn walk::Delegate,
@@ -266,7 +266,7 @@ impl Mark {
266266
fn emit_all_held(
267267
&mut self,
268268
state: &mut State,
269-
opts: Options,
269+
opts: Options<'_>,
270270
out: &mut walk::Outcome,
271271
delegate: &mut dyn walk::Delegate,
272272
) -> Action {
@@ -287,7 +287,7 @@ impl Mark {
287287
dir_info: classify::Outcome,
288288
state: &mut State,
289289
out: &mut walk::Outcome,
290-
opts: Options,
290+
opts: Options<'_>,
291291
ctx: &mut Context<'_>,
292292
delegate: &mut dyn walk::Delegate,
293293
) -> Option<Action> {
@@ -408,7 +408,7 @@ fn filter_dir_pathspec(current: Option<PathspecMatch>) -> Option<PathspecMatch>
408408
})
409409
}
410410

411-
impl Options {
411+
impl Options<'_> {
412412
fn should_hold(&self, status: entry::Status) -> bool {
413413
if status.is_pruned() {
414414
return false;

gix-dir/tests/fixtures/many.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,3 +438,9 @@ git init with-sub-repo
438438
git add -f .gitignore
439439
git clone ../dir-with-file sub-repo
440440
)
441+
442+
git clone dir-with-tracked-file in-repo-worktree
443+
(cd in-repo-worktree
444+
git worktree add worktree
445+
git worktree add -b other-worktree dir/worktree
446+
)

gix-dir/tests/walk/mod.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use gix_dir::{walk, EntryRef};
22
use pretty_assertions::assert_eq;
3+
use std::collections::BTreeSet;
34
use std::sync::atomic::AtomicBool;
45

56
use crate::walk_utils::{
@@ -4493,3 +4494,59 @@ fn ignored_sub_repo() -> crate::Result {
44934494
}
44944495
Ok(())
44954496
}
4497+
4498+
#[test]
4499+
fn in_repo_worktree() -> crate::Result {
4500+
let root = fixture("in-repo-worktree");
4501+
let ((out, _root), entries) = collect(&root, None, |keep, ctx| walk(&root, ctx, options_emit_all(), keep));
4502+
assert_eq!(
4503+
out,
4504+
walk::Outcome {
4505+
read_dir_calls: 2,
4506+
returned_entries: entries.len(),
4507+
seen_entries: 4,
4508+
}
4509+
);
4510+
assert_eq!(
4511+
entries,
4512+
&[
4513+
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
4514+
entry("dir/file", Tracked, File),
4515+
entry("dir/worktree", Untracked, Repository),
4516+
entry("worktree", Untracked, Repository),
4517+
],
4518+
"without passing worktree information, they count as untracked repositories, making them vulnerable"
4519+
);
4520+
4521+
let ((out, _root), entries) = collect(&root, None, |keep, ctx| {
4522+
walk(
4523+
&root,
4524+
ctx,
4525+
walk::Options {
4526+
worktree_relative_worktree_dirs: Some(&BTreeSet::from(["worktree".into(), "dir/worktree".into()])),
4527+
..options_emit_all()
4528+
},
4529+
keep,
4530+
)
4531+
});
4532+
assert_eq!(
4533+
out,
4534+
walk::Outcome {
4535+
read_dir_calls: 2,
4536+
returned_entries: entries.len(),
4537+
seen_entries: 4,
4538+
}
4539+
);
4540+
assert_eq!(
4541+
entries,
4542+
&[
4543+
entry_nokind(".git", Pruned).with_property(DotGit).with_match(Always),
4544+
entry("dir/file", Tracked, File),
4545+
entry("dir/worktree", Tracked, Repository).no_index_kind(),
4546+
entry("worktree", Tracked, Repository).no_index_kind(),
4547+
],
4548+
"But when worktree information is passed, it is identified as tracked to look similarly to a submodule.\
4549+
What gives it away is that the index-kind is None, which is unusual for a tracked file."
4550+
);
4551+
Ok(())
4552+
}

gix-dir/tests/walk_utils/mod.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ pub fn fixture(name: &str) -> PathBuf {
1414
}
1515

1616
/// Default options
17-
pub fn options() -> walk::Options {
17+
pub fn options() -> walk::Options<'static> {
1818
walk::Options::default()
1919
}
2020

2121
/// Default options
22-
pub fn options_emit_all() -> walk::Options {
22+
pub fn options_emit_all() -> walk::Options<'static> {
2323
walk::Options {
2424
precompose_unicode: false,
2525
ignore_case: false,
@@ -33,6 +33,7 @@ pub fn options_emit_all() -> walk::Options {
3333
emit_empty_directories: true,
3434
emit_collapsed: None,
3535
symlinks_to_directories_are_ignored_like_directories: false,
36+
worktree_relative_worktree_dirs: None,
3637
}
3738
}
3839

@@ -145,6 +146,7 @@ pub trait EntryExt {
145146
fn with_match(self, m: entry::PathspecMatch) -> Self;
146147
fn no_match(self) -> Self;
147148
fn no_kind(self) -> Self;
149+
fn no_index_kind(self) -> Self;
148150
}
149151

150152
impl EntryExt for (Entry, Option<entry::Status>) {
@@ -169,6 +171,10 @@ impl EntryExt for (Entry, Option<entry::Status>) {
169171
self.0.disk_kind = None;
170172
self
171173
}
174+
fn no_index_kind(mut self) -> Self {
175+
self.0.index_kind = None;
176+
self
177+
}
172178
}
173179

174180
pub fn collect(

0 commit comments

Comments
 (0)