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