1use std::borrow::Cow;
2use std::path::{Path, PathBuf};
3
4use bstr::{BStr, BString, ByteSlice};
5
6use crate::walk::{classify, readdir, Action, Context, Delegate, Error, ForDeletionMode, Options, Outcome};
7use crate::{entry, EntryRef};
8
9pub fn walk(
43 worktree_root: &Path,
44 mut ctx: Context<'_>,
45 options: Options<'_>,
46 delegate: &mut dyn Delegate,
47) -> Result<(Outcome, PathBuf), Error> {
48 let root = match ctx.explicit_traversal_root {
49 Some(root) => root.to_owned(),
50 None => ctx
51 .pathspec
52 .longest_common_directory()
53 .and_then(|candidate| {
54 let candidate = worktree_root.join(candidate);
55 candidate.is_dir().then_some(candidate)
56 })
57 .unwrap_or_else(|| worktree_root.join(ctx.pathspec.prefix_directory())),
58 };
59 let _span = gix_trace::coarse!("walk", root = ?root, worktree_root = ?worktree_root, options = ?options);
60 let (mut current, worktree_root_relative) = assure_no_symlink_in_root(worktree_root, &root)?;
61 let mut out = Outcome::default();
62 let mut buf = BString::default();
63 let (root_info, worktree_root_is_repository) = classify::root(
64 worktree_root,
65 &mut buf,
66 worktree_root_relative.as_ref(),
67 options,
68 &mut ctx,
69 )?;
70
71 let can_recurse = can_recurse(
72 buf.as_bstr(),
73 if root == worktree_root && root_info.disk_kind == Some(entry::Kind::Symlink) && current.is_dir() {
74 classify::Outcome {
75 disk_kind: Some(entry::Kind::Directory),
76 ..root_info
77 }
78 } else {
79 root_info
80 },
81 options.for_deletion,
82 worktree_root_is_repository,
83 delegate,
84 );
85 if !can_recurse {
86 if buf.is_empty() && !root_info.disk_kind.is_some_and(|kind| kind.is_dir()) {
87 return Err(Error::WorktreeRootIsFile { root: root.to_owned() });
88 }
89 if options.precompose_unicode {
90 buf = gix_utils::str::precompose_bstr(buf.into()).into_owned();
91 }
92 let _ = emit_entry(
93 Cow::Borrowed(buf.as_bstr()),
94 root_info,
95 None,
96 options,
97 &mut out,
98 delegate,
99 );
100 return Ok((out, root.to_owned()));
101 }
102
103 let mut state = readdir::State::new(worktree_root, ctx.current_dir, options.for_deletion.is_some());
104 let may_collapse = root != worktree_root && state.may_collapse(¤t);
105 let (action, _) = readdir::recursive(
106 may_collapse,
107 &mut current,
108 &mut buf,
109 root_info,
110 &mut ctx,
111 options,
112 delegate,
113 &mut out,
114 &mut state,
115 )?;
116 if action != Action::Cancel {
117 state.emit_remaining(may_collapse, options, &mut out, delegate);
118 assert_eq!(state.on_hold.len(), 0, "BUG: after emission, on hold must be empty");
119 }
120 gix_trace::debug!(statistics = ?out);
121 Ok((out, root.to_owned()))
122}
123
124fn assure_no_symlink_in_root<'root>(
128 worktree_root: &Path,
129 root: &'root Path,
130) -> Result<(PathBuf, Cow<'root, Path>), Error> {
131 let mut current = worktree_root.to_owned();
132 let worktree_relative = root
133 .strip_prefix(worktree_root)
134 .expect("BUG: root was created from worktree_root + prefix");
135 let worktree_relative = gix_path::normalize(worktree_relative.into(), Path::new(""))
136 .ok_or(Error::NormalizeRoot { root: root.to_owned() })?;
137
138 for (idx, component) in worktree_relative.components().enumerate() {
139 current.push(component);
140 let meta = current.symlink_metadata().map_err(|err| Error::SymlinkMetadata {
141 source: err,
142 path: current.to_owned(),
143 })?;
144 if meta.is_symlink() {
145 return Err(Error::SymlinkInRoot {
146 root: root.to_owned(),
147 worktree_root: worktree_root.to_owned(),
148 component_index: idx,
149 });
150 }
151 }
152 Ok((current, worktree_relative))
153}
154
155pub(super) fn can_recurse(
156 rela_path: &BStr,
157 info: classify::Outcome,
158 for_deletion: Option<ForDeletionMode>,
159 worktree_root_is_repository: bool,
160 delegate: &mut dyn Delegate,
161) -> bool {
162 let is_dir = info.disk_kind.is_some_and(|k| k.is_dir());
163 if !is_dir {
164 return false;
165 }
166 delegate.can_recurse(
167 EntryRef::from_outcome(Cow::Borrowed(rela_path), info),
168 for_deletion,
169 worktree_root_is_repository,
170 )
171}
172
173#[allow(clippy::too_many_arguments)]
175pub(super) fn emit_entry(
176 rela_path: Cow<'_, BStr>,
177 info: classify::Outcome,
178 dir_status: Option<entry::Status>,
179 Options {
180 emit_pruned,
181 emit_tracked,
182 emit_ignored,
183 emit_empty_directories,
184 ..
185 }: Options<'_>,
186 out: &mut Outcome,
187 delegate: &mut dyn Delegate,
188) -> Action {
189 out.seen_entries += 1;
190
191 if (!emit_empty_directories && info.property == Some(entry::Property::EmptyDirectory)
192 || !emit_tracked && info.status == entry::Status::Tracked)
193 || emit_ignored.is_none() && matches!(info.status, entry::Status::Ignored(_))
194 || !emit_pruned
195 && (info.status.is_pruned()
196 || info
197 .pathspec_match
198 .map_or(true, |m| m == entry::PathspecMatch::Excluded))
199 {
200 return Action::Continue;
201 }
202
203 out.returned_entries += 1;
204 delegate.emit(EntryRef::from_outcome(rela_path, info), dir_status)
205}