1#[derive(Debug)]
2pub struct Options {
3 pub format: crate::OutputFormat,
4 pub attributes: Option<Attributes>,
6 pub statistics: bool,
7 pub simple: bool,
8 pub recurse_submodules: bool,
9}
10
11#[derive(Debug, Copy, Clone)]
12pub enum Attributes {
13 WorktreeAndIndex,
15 Index,
17}
18
19pub(crate) mod function {
20 use std::{
21 borrow::Cow,
22 collections::BTreeSet,
23 io::{BufWriter, Write},
24 };
25
26 use gix::{
27 Repository,
28 bstr::{BStr, BString, ByteSlice},
29 index::entry::Stage,
30 worktree::IndexPersistedOrInMemory,
31 };
32
33 use crate::{
34 OutputFormat, is_dir_to_mode,
35 repository::index::entries::{Attributes, Options},
36 };
37
38 pub fn entries(
39 repo: gix::Repository,
40 pathspecs: Vec<BString>,
41 out: impl std::io::Write,
42 mut err: impl std::io::Write,
43 Options {
44 simple,
45 format,
46 attributes,
47 statistics,
48 recurse_submodules,
49 }: Options,
50 ) -> anyhow::Result<()> {
51 let mut out = BufWriter::with_capacity(64 * 1024, out);
52 let mut all_attrs = statistics.then(BTreeSet::new);
53
54 #[cfg(feature = "serde")]
55 if let OutputFormat::Json = format {
56 out.write_all(b"[\n")?;
57 }
58
59 let stats = print_entries(
60 &repo,
61 attributes,
62 pathspecs.iter(),
63 format,
64 all_attrs.as_mut(),
65 simple,
66 "".into(),
67 recurse_submodules,
68 &mut out,
69 )?;
70
71 #[cfg(feature = "serde")]
72 if format == OutputFormat::Json {
73 out.write_all(b"]\n")?;
74 out.flush()?;
75 if statistics {
76 serde_json::to_writer_pretty(&mut err, &stats)?;
77 }
78 }
79 if format == OutputFormat::Human && statistics {
80 out.flush()?;
81 writeln!(err, "{stats:#?}")?;
82 if let Some(attrs) = all_attrs.filter(|a| !a.is_empty()) {
83 writeln!(err, "All encountered attributes:")?;
84 for attr in attrs {
85 writeln!(err, "\t{attr}", attr = attr.as_ref())?;
86 }
87 }
88 }
89 Ok(())
90 }
91
92 #[expect(clippy::too_many_arguments)]
93 fn print_entries(
94 repo: &Repository,
95 attributes: Option<Attributes>,
96 pathspecs: impl IntoIterator<Item = impl AsRef<BStr>> + Clone,
97 format: OutputFormat,
98 mut all_attrs: Option<&mut BTreeSet<gix::attrs::Assignment>>,
99 simple: bool,
100 prefix: &BStr,
101 recurse_submodules: bool,
102 out: &mut impl std::io::Write,
103 ) -> anyhow::Result<Statistics> {
104 let _span = gix::trace::coarse!("print_entries()", git_dir = ?repo.git_dir());
105 let (mut pathspec, index, mut cache) = init_cache(repo, attributes, pathspecs.clone())?;
106 let mut repo_attrs = all_attrs.is_some().then(BTreeSet::default);
107 let submodules_by_path = recurse_submodules
108 .then(|| {
109 repo.submodules()
110 .map(|opt| {
111 opt.map(|submodules| {
112 submodules
113 .map(|sm| sm.path().map(move |path| (path, sm)))
114 .collect::<Result<Vec<_>, _>>()
115 })
116 })
117 .transpose()
118 })
119 .flatten()
120 .transpose()?
121 .transpose()?;
122 let mut stats = Statistics {
123 entries: index.entries().len(),
124 ..Default::default()
125 };
126 if let Some(entries) = index.prefixed_entries(pathspec.common_prefix()) {
127 stats.entries_after_prune = entries.len();
128 let mut entries = entries.iter().peekable();
129 let mut path = prefix.to_owned();
130 let prefix_len = path.len();
131 let mut buf = Vec::new();
132 while let Some(entry) = entries.next() {
133 let mut last_match = None;
134 let attrs = cache
135 .as_mut()
136 .and_then(|(attrs, cache)| {
137 attributes.is_some().then(|| {
139 cache.at_entry(entry.path(&index), None).map(|entry| {
140 let is_excluded = entry.is_excluded();
141 stats.excluded += usize::from(is_excluded);
142 let attributes: Vec<_> = {
143 last_match = Some(entry.matching_attributes(attrs));
144 attrs.iter().map(|m| m.assignment.to_owned()).collect()
145 };
146 stats.with_attributes += usize::from(!attributes.is_empty());
147 stats.max_attributes_per_path = stats.max_attributes_per_path.max(attributes.len());
148 if let Some(attrs) = repo_attrs.as_mut() {
149 attributes.iter().for_each(|attr| {
150 attrs.insert(attr.clone());
151 });
152 }
153 Attrs {
154 is_excluded,
155 attributes,
156 }
157 })
158 })
159 })
160 .transpose()?;
161
162 let entry_is_excluded = pathspec
165 .pattern_matching_relative_path(
166 entry.path(&index),
167 Some(false),
168 &mut |rela_path, _case, is_dir, out| {
169 cache
170 .as_mut()
171 .map(|(attrs, cache)| {
172 match last_match {
173 Some(matched) => {
175 attrs.copy_into(cache.attributes_collection(), out);
176 matched
177 }
178 None => cache
180 .at_entry(rela_path, Some(is_dir_to_mode(is_dir)))
181 .ok()
182 .map(|platform| platform.matching_attributes(out))
183 .unwrap_or_default(),
184 }
185 })
186 .unwrap_or_default()
187 },
188 )
189 .is_none_or(|m| m.is_excluded());
190
191 let entry_is_submodule = entry.mode.is_submodule();
192 if entry_is_excluded && (!entry_is_submodule || !recurse_submodules) {
193 continue;
194 }
195 if let Some(sm) = submodules_by_path
196 .as_ref()
197 .filter(|_| entry_is_submodule)
198 .and_then(|sms_by_path| {
199 let entry_path = entry.path(&index);
200 sms_by_path
201 .iter()
202 .find_map(|(path, sm)| (path == entry_path).then_some(sm))
203 .filter(|sm| sm.git_dir_try_old_form().is_ok_and(|dot_git| dot_git.exists()))
204 })
205 {
206 let sm_path = gix::path::to_unix_separators_on_windows(sm.path()?);
207 let sm_repo = sm.open()?.expect("we checked it exists");
208 let mut prefix = prefix.to_owned();
209 prefix.extend_from_slice(sm_path.as_ref());
210 if !sm_path.ends_with(b"/") {
211 prefix.push(b'/');
212 }
213 let sm_stats = print_entries(
214 &sm_repo,
215 attributes,
216 pathspecs.clone(),
217 format,
218 all_attrs.as_deref_mut(),
219 simple,
220 prefix.as_ref(),
221 recurse_submodules,
222 out,
223 )?;
224 stats.submodule.push((sm_path.into_owned(), sm_stats));
225 } else {
226 let entry_path = entry.path(&index);
227 let path = if prefix.is_empty() {
228 entry_path
229 } else {
230 path.truncate(prefix_len);
231 path.extend_from_slice(entry_path);
232 path.as_bstr()
233 };
234 match format {
235 OutputFormat::Human => {
236 if simple {
237 to_human_simple(out, entry, attrs, path, &mut buf)
238 } else {
239 to_human(out, entry, attrs, path, &mut buf)
240 }?;
241 }
242 #[cfg(feature = "serde")]
243 OutputFormat::Json => to_json(out, &index, entry, attrs, entries.peek().is_none(), prefix)?,
244 }
245 }
246 }
247 }
248
249 stats.cache = cache.map(|c| *c.1.statistics());
250 if let Some((attrs, all_attrs)) = repo_attrs.zip(all_attrs) {
251 stats
252 .attributes
253 .extend(attrs.iter().map(|attr| attr.as_ref().to_string()));
254 all_attrs.extend(attrs);
255 }
256 Ok(stats)
257 }
258
259 #[expect(clippy::type_complexity)]
260 fn init_cache(
261 repo: &Repository,
262 attributes: Option<Attributes>,
263 pathspecs: impl IntoIterator<Item = impl AsRef<BStr>>,
264 ) -> anyhow::Result<(
265 gix::pathspec::Search,
266 IndexPersistedOrInMemory,
267 Option<(gix::attrs::search::Outcome, gix::AttributeStack<'_>)>,
268 )> {
269 let index = repo.index_or_load_from_head()?;
270 let pathspec = repo.pathspec(
271 true,
272 pathspecs,
273 false,
274 &index,
275 gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping.adjust_for_bare(repo.is_bare()),
276 )?;
277 let cache = attributes
278 .or_else(|| {
279 pathspec
280 .search()
281 .patterns()
282 .any(|spec| !spec.attributes.is_empty())
283 .then_some(Attributes::Index)
284 })
285 .map(|attrs| {
286 repo.attributes(
287 &index,
288 match attrs {
289 Attributes::WorktreeAndIndex => {
290 gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping
291 .adjust_for_bare(repo.is_bare())
292 }
293 Attributes::Index => gix::worktree::stack::state::attributes::Source::IdMapping,
294 },
295 match attrs {
296 Attributes::WorktreeAndIndex => {
297 gix::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped
298 .adjust_for_bare(repo.is_bare())
299 }
300 Attributes::Index => gix::worktree::stack::state::ignore::Source::IdMapping,
301 },
302 None,
303 )
304 .map(|cache| (cache.attribute_matches(), cache))
305 })
306 .transpose()?;
307 Ok((pathspec.into_parts().0, index, cache))
308 }
309
310 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
311 struct Attrs {
312 is_excluded: bool,
313 attributes: Vec<gix::attrs::Assignment>,
314 }
315
316 #[cfg_attr(feature = "serde", derive(serde::Serialize))]
317 #[derive(Default, Debug)]
318 struct Statistics {
319 pub entries: usize,
321 pub entries_after_prune: usize,
322 pub excluded: usize,
323 pub with_attributes: usize,
324 pub max_attributes_per_path: usize,
325 pub cache: Option<gix::worktree::stack::Statistics>,
326 pub attributes: Vec<String>,
327 pub submodule: Vec<(BString, Statistics)>,
328 }
329
330 #[cfg(feature = "serde")]
331 fn to_json(
332 out: &mut impl std::io::Write,
333 index: &gix::index::File,
334 entry: &gix::index::Entry,
335 attrs: Option<Attrs>,
336 is_last: bool,
337 prefix: &BStr,
338 ) -> anyhow::Result<()> {
339 use gix::bstr::ByteSlice;
340 #[derive(serde::Serialize)]
341 struct Entry<'a> {
342 stat: &'a gix::index::entry::Stat,
343 hex_id: String,
344 flags: u32,
345 mode: u32,
346 path: std::borrow::Cow<'a, str>,
347 meta: Option<Attrs>,
348 }
349
350 serde_json::to_writer(
351 &mut *out,
352 &Entry {
353 stat: &entry.stat,
354 hex_id: entry.id.to_hex().to_string(),
355 flags: entry.flags.bits(),
356 mode: entry.mode.bits(),
357 path: if prefix.is_empty() {
358 entry.path(index).to_str_lossy()
359 } else {
360 let mut path = prefix.to_owned();
361 path.extend_from_slice(entry.path(index));
362 path.to_string().into()
363 },
364 meta: attrs,
365 },
366 )?;
367
368 if is_last {
369 out.write_all(b"\n")?;
370 } else {
371 out.write_all(b",\n")?;
372 }
373 Ok(())
374 }
375
376 fn to_human_simple(
377 out: &mut impl std::io::Write,
378 entry: &gix::index::Entry,
379 attrs: Option<Attrs>,
380 path: &BStr,
381 buf: &mut Vec<u8>,
382 ) -> std::io::Result<()> {
383 crate::output::write_bstr(&mut *out, path, buf)?;
384 match attrs {
385 Some(attrs) => out.write_all(print_attrs(Some(attrs), entry.mode).as_bytes()),
386 None => Ok(()),
387 }?;
388 out.write_all(b"\n")
389 }
390
391 fn to_human(
392 out: &mut impl std::io::Write,
393 entry: &gix::index::Entry,
394 attrs: Option<Attrs>,
395 path: &BStr,
396 buf: &mut Vec<u8>,
397 ) -> std::io::Result<()> {
398 write!(
399 out,
400 "{} {}{:?} {} ",
401 match entry.flags.stage() {
402 Stage::Unconflicted => " ",
403 Stage::Base => "BASE ",
404 Stage::Ours => "OURS ",
405 Stage::Theirs => "THEIRS ",
406 },
407 if entry.flags.is_empty() {
408 "".to_string()
409 } else {
410 format!("{:?} ", entry.flags)
411 },
412 entry.mode,
413 entry.id,
414 )?;
415 crate::output::write_bstr(&mut *out, path, buf)?;
416 out.write_all(print_attrs(attrs, entry.mode).as_bytes())?;
417 out.write_all(b"\n")
418 }
419
420 fn print_attrs(attrs: Option<Attrs>, mode: gix::index::entry::Mode) -> Cow<'static, str> {
421 attrs.map_or(Cow::Borrowed(""), |a| {
422 let mut buf = String::new();
423 if mode.is_sparse() {
424 buf.push_str(" 📁 ");
425 } else if mode.is_submodule() {
426 buf.push_str(" ➡ ");
427 }
428 if a.is_excluded {
429 buf.push_str(" 🗑️");
430 }
431 if !a.attributes.is_empty() {
432 buf.push_str(" (");
433 for assignment in a.attributes {
434 use std::fmt::Write;
435 write!(&mut buf, "{}", assignment.as_ref()).ok();
436 buf.push_str(", ");
437 }
438 buf.pop();
439 buf.pop();
440 buf.push(')');
441 }
442 buf.into()
443 })
444 }
445}