Skip to content

Commit 6bc69e3

Browse files
committed
feat: gix index entries --recurse-subomdules to also list submodules.
1 parent 5fd6364 commit 6bc69e3

File tree

3 files changed

+200
-82
lines changed

3 files changed

+200
-82
lines changed

Diff for: gitoxide-core/src/repository/index/entries.rs

+195-82
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub struct Options {
55
pub attributes: Option<Attributes>,
66
pub statistics: bool,
77
pub simple: bool,
8+
pub recurse_submodules: bool,
89
}
910

1011
#[derive(Debug, Copy, Clone)]
@@ -16,14 +17,17 @@ pub enum Attributes {
1617
}
1718

1819
pub(crate) mod function {
19-
use gix::bstr::BString;
20+
use gix::bstr::{BStr, BString};
2021
use std::collections::BTreeSet;
2122
use std::{
2223
borrow::Cow,
2324
io::{BufWriter, Write},
2425
};
2526

27+
use crate::OutputFormat;
2628
use gix::odb::FindExt;
29+
use gix::repository::IndexPersistedOrInMemory;
30+
use gix::Repository;
2731

2832
use crate::repository::index::entries::{Attributes, Options};
2933

@@ -37,59 +41,83 @@ pub(crate) mod function {
3741
format,
3842
attributes,
3943
statistics,
44+
recurse_submodules,
4045
}: Options,
4146
) -> anyhow::Result<()> {
4247
use crate::OutputFormat::*;
43-
let index = repo.index_or_load_from_head()?;
44-
let pathspec = repo.pathspec(
45-
pathspecs,
46-
false,
47-
&index,
48-
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping.adjust_for_bare(repo.is_bare()),
48+
let mut out = BufWriter::with_capacity(64 * 1024, out);
49+
let mut all_attrs = statistics.then(BTreeSet::new);
50+
51+
#[cfg(feature = "serde")]
52+
if let Json = format {
53+
out.write_all(b"[\n")?;
54+
}
55+
56+
let stats = print_entries(
57+
&repo,
58+
attributes,
59+
pathspecs.iter(),
60+
format,
61+
all_attrs.as_mut(),
62+
simple,
63+
"".into(),
64+
recurse_submodules,
65+
&mut out,
4966
)?;
50-
let mut cache = attributes
51-
.or_else(|| {
52-
pathspec
53-
.search()
54-
.patterns()
55-
.any(|spec| !spec.attributes.is_empty())
56-
.then_some(Attributes::Index)
57-
})
58-
.map(|attrs| {
59-
repo.attributes(
60-
&index,
61-
match attrs {
62-
Attributes::WorktreeAndIndex => {
63-
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping
64-
.adjust_for_bare(repo.is_bare())
65-
}
66-
Attributes::Index => gix::worktree::stack::state::attributes::Source::IdMapping,
67-
},
68-
match attrs {
69-
Attributes::WorktreeAndIndex => {
70-
gix::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped
71-
.adjust_for_bare(repo.is_bare())
72-
}
73-
Attributes::Index => gix::worktree::stack::state::ignore::Source::IdMapping,
74-
},
75-
None,
76-
)
77-
.map(|cache| (cache.attribute_matches(), cache))
67+
68+
#[cfg(feature = "serde")]
69+
if format == Json {
70+
out.write_all(b"]\n")?;
71+
out.flush()?;
72+
if statistics {
73+
serde_json::to_writer_pretty(&mut err, &stats)?;
74+
}
75+
} else if format == Human && statistics {
76+
out.flush()?;
77+
writeln!(err, "{stats:#?}")?;
78+
if let Some(attrs) = all_attrs.filter(|a| !a.is_empty()) {
79+
writeln!(err, "All encountered attributes:")?;
80+
for attr in attrs {
81+
writeln!(err, "\t{attr}", attr = attr.as_ref())?;
82+
}
83+
}
84+
}
85+
Ok(())
86+
}
87+
88+
#[allow(clippy::too_many_arguments)]
89+
fn print_entries(
90+
repo: &Repository,
91+
attributes: Option<Attributes>,
92+
pathspecs: impl IntoIterator<Item = impl AsRef<BStr>> + Clone,
93+
format: OutputFormat,
94+
mut all_attrs: Option<&mut BTreeSet<gix::attrs::Assignment>>,
95+
simple: bool,
96+
prefix: &BStr,
97+
recurse_submodules: bool,
98+
out: &mut impl std::io::Write,
99+
) -> anyhow::Result<Statistics> {
100+
let (mut pathspec, index, mut cache) = init_cache(repo, attributes, pathspecs.clone())?;
101+
let submodules_by_path = recurse_submodules
102+
.then(|| {
103+
repo.submodules()
104+
.map(|opt| {
105+
opt.map(|submodules| {
106+
submodules
107+
.map(|sm| sm.path().map(Cow::into_owned).map(move |path| (path, sm)))
108+
.collect::<Result<Vec<_>, _>>()
109+
})
110+
})
111+
.transpose()
78112
})
113+
.flatten()
114+
.transpose()?
79115
.transpose()?;
80116
let mut stats = Statistics {
81117
entries: index.entries().len(),
82118
..Default::default()
83119
};
84-
85-
let mut out = BufWriter::with_capacity(64 * 1024, out);
86-
#[cfg(feature = "serde")]
87-
if let Json = format {
88-
out.write_all(b"[\n")?;
89-
}
90-
let (mut search, _cache) = pathspec.into_parts();
91-
let mut all_attrs = statistics.then(BTreeSet::new);
92-
if let Some(entries) = index.prefixed_entries(search.common_prefix()) {
120+
if let Some(entries) = index.prefixed_entries(pathspec.common_prefix()) {
93121
stats.entries_after_prune = entries.len();
94122
let mut entries = entries.iter().peekable();
95123
while let Some(entry) = entries.next() {
@@ -110,7 +138,7 @@ pub(crate) mod function {
110138
};
111139
stats.with_attributes += usize::from(!attributes.is_empty());
112140
stats.max_attributes_per_path = stats.max_attributes_per_path.max(attributes.len());
113-
if let Some(attrs) = all_attrs.as_mut() {
141+
if let Some(attrs) = all_attrs.as_deref_mut() {
114142
attributes.iter().for_each(|attr| {
115143
attrs.insert(attr.clone());
116144
});
@@ -126,7 +154,7 @@ pub(crate) mod function {
126154

127155
// Note that we intentionally ignore `_case` so that we act like git does, attribute matching case is determined
128156
// by the repository, not the pathspec.
129-
if search
157+
let entry_is_excluded = pathspec
130158
.pattern_matching_relative_path(entry.path(&index), Some(false), |rela_path, _case, is_dir, out| {
131159
cache
132160
.as_mut()
@@ -147,44 +175,110 @@ pub(crate) mod function {
147175
})
148176
.unwrap_or_default()
149177
})
150-
.map_or(true, |m| m.is_excluded())
151-
{
178+
.map_or(true, |m| m.is_excluded());
179+
180+
let entry_is_submodule = entry.mode.is_submodule();
181+
if entry_is_excluded && (!entry_is_submodule || !recurse_submodules) {
152182
continue;
153183
}
154-
match format {
155-
Human => {
156-
if simple {
157-
to_human_simple(&mut out, &index, entry, attrs)
158-
} else {
159-
to_human(&mut out, &index, entry, attrs)
160-
}?
184+
if let Some(sm) = submodules_by_path
185+
.as_ref()
186+
.filter(|_| entry_is_submodule)
187+
.and_then(|sms_by_path| {
188+
let entry_path = entry.path(&index);
189+
sms_by_path
190+
.iter()
191+
.find_map(|(path, sm)| (path == entry_path).then_some(sm))
192+
.filter(|sm| sm.git_dir_try_old_form().map_or(false, |dot_git| dot_git.exists()))
193+
})
194+
{
195+
let sm_path = gix::path::to_unix_separators_on_windows(sm.path()?);
196+
let sm_repo = sm.open()?.expect("we checked it exists");
197+
let mut prefix = prefix.to_owned();
198+
prefix.extend_from_slice(sm_path.as_ref());
199+
if !sm_path.ends_with(b"/") {
200+
prefix.push(b'/');
161201
}
162-
#[cfg(feature = "serde")]
163-
Json => to_json(&mut out, &index, entry, attrs, entries.peek().is_none())?,
164-
}
165-
}
166-
167-
#[cfg(feature = "serde")]
168-
if format == Json {
169-
out.write_all(b"]\n")?;
170-
out.flush()?;
171-
if statistics {
172-
serde_json::to_writer_pretty(&mut err, &stats)?;
173-
}
174-
}
175-
if format == Human && statistics {
176-
out.flush()?;
177-
stats.cache = cache.map(|c| *c.1.statistics());
178-
writeln!(err, "{stats:#?}")?;
179-
if let Some(attrs) = all_attrs.filter(|a| !a.is_empty()) {
180-
writeln!(err, "All encountered attributes:")?;
181-
for attr in attrs {
182-
writeln!(err, "\t{attr}", attr = attr.as_ref())?;
202+
let sm_stats = print_entries(
203+
&sm_repo,
204+
attributes,
205+
pathspecs.clone(),
206+
format,
207+
all_attrs.as_deref_mut(),
208+
simple,
209+
prefix.as_ref(),
210+
recurse_submodules,
211+
out,
212+
)?;
213+
stats.submodule.push((sm_path.into_owned(), sm_stats));
214+
} else {
215+
match format {
216+
OutputFormat::Human => {
217+
if simple {
218+
to_human_simple(out, &index, entry, attrs, prefix)
219+
} else {
220+
to_human(out, &index, entry, attrs, prefix)
221+
}?
222+
}
223+
#[cfg(feature = "serde")]
224+
OutputFormat::Json => to_json(out, &index, entry, attrs, entries.peek().is_none(), prefix)?,
183225
}
184226
}
185227
}
186228
}
187-
Ok(())
229+
230+
stats.cache = cache.map(|c| *c.1.statistics());
231+
Ok(stats)
232+
}
233+
234+
#[allow(clippy::type_complexity)]
235+
fn init_cache(
236+
repo: &Repository,
237+
attributes: Option<Attributes>,
238+
pathspecs: impl IntoIterator<Item = impl AsRef<BStr>>,
239+
) -> anyhow::Result<(
240+
gix::pathspec::Search,
241+
IndexPersistedOrInMemory,
242+
Option<(gix::attrs::search::Outcome, gix::worktree::Stack)>,
243+
)> {
244+
let index = repo.index_or_load_from_head()?;
245+
let pathspec = repo.pathspec(
246+
pathspecs,
247+
false,
248+
&index,
249+
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping.adjust_for_bare(repo.is_bare()),
250+
)?;
251+
let cache = attributes
252+
.or_else(|| {
253+
pathspec
254+
.search()
255+
.patterns()
256+
.any(|spec| !spec.attributes.is_empty())
257+
.then_some(Attributes::Index)
258+
})
259+
.map(|attrs| {
260+
repo.attributes(
261+
&index,
262+
match attrs {
263+
Attributes::WorktreeAndIndex => {
264+
gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping
265+
.adjust_for_bare(repo.is_bare())
266+
}
267+
Attributes::Index => gix::worktree::stack::state::attributes::Source::IdMapping,
268+
},
269+
match attrs {
270+
Attributes::WorktreeAndIndex => {
271+
gix::worktree::stack::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped
272+
.adjust_for_bare(repo.is_bare())
273+
}
274+
Attributes::Index => gix::worktree::stack::state::ignore::Source::IdMapping,
275+
},
276+
None,
277+
)
278+
.map(|cache| (cache.attribute_matches(), cache))
279+
})
280+
.transpose()?;
281+
Ok((pathspec.into_parts().0, index, cache))
188282
}
189283

190284
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
@@ -203,6 +297,7 @@ pub(crate) mod function {
203297
pub with_attributes: usize,
204298
pub max_attributes_per_path: usize,
205299
pub cache: Option<gix::worktree::stack::Statistics>,
300+
pub submodule: Vec<(BString, Statistics)>,
206301
}
207302

208303
#[cfg(feature = "serde")]
@@ -212,6 +307,7 @@ pub(crate) mod function {
212307
entry: &gix::index::Entry,
213308
attrs: Option<Attrs>,
214309
is_last: bool,
310+
prefix: &BStr,
215311
) -> anyhow::Result<()> {
216312
use gix::bstr::ByteSlice;
217313
#[derive(serde::Serialize)]
@@ -231,7 +327,13 @@ pub(crate) mod function {
231327
hex_id: entry.id.to_hex().to_string(),
232328
flags: entry.flags.bits(),
233329
mode: entry.mode.bits(),
234-
path: entry.path(index).to_str_lossy(),
330+
path: if prefix.is_empty() {
331+
entry.path(index).to_str_lossy()
332+
} else {
333+
let mut path = prefix.to_owned();
334+
path.extend_from_slice(entry.path(index));
335+
path.to_string().into()
336+
},
235337
meta: attrs,
236338
},
237339
)?;
@@ -249,11 +351,15 @@ pub(crate) mod function {
249351
file: &gix::index::File,
250352
entry: &gix::index::Entry,
251353
attrs: Option<Attrs>,
354+
prefix: &BStr,
252355
) -> std::io::Result<()> {
356+
if !prefix.is_empty() {
357+
out.write_all(prefix)?;
358+
}
253359
match attrs {
254360
Some(attrs) => {
255361
out.write_all(entry.path(file))?;
256-
out.write_all(print_attrs(Some(attrs)).as_bytes())
362+
out.write_all(print_attrs(Some(attrs), entry.mode).as_bytes())
257363
}
258364
None => out.write_all(entry.path(file)),
259365
}?;
@@ -265,10 +371,11 @@ pub(crate) mod function {
265371
file: &gix::index::File,
266372
entry: &gix::index::Entry,
267373
attrs: Option<Attrs>,
374+
prefix: &BStr,
268375
) -> std::io::Result<()> {
269376
writeln!(
270377
out,
271-
"{} {}{:?} {} {}{}",
378+
"{} {}{:?} {} {}{}{}",
272379
match entry.flags.stage() {
273380
0 => "BASE ",
274381
1 => "OURS ",
@@ -282,14 +389,20 @@ pub(crate) mod function {
282389
},
283390
entry.mode,
284391
entry.id,
392+
prefix,
285393
entry.path(file),
286-
print_attrs(attrs)
394+
print_attrs(attrs, entry.mode)
287395
)
288396
}
289397

290-
fn print_attrs(attrs: Option<Attrs>) -> Cow<'static, str> {
398+
fn print_attrs(attrs: Option<Attrs>, mode: gix::index::entry::Mode) -> Cow<'static, str> {
291399
attrs.map_or(Cow::Borrowed(""), |a| {
292400
let mut buf = String::new();
401+
if mode.is_sparse() {
402+
buf.push_str(" 📁 ");
403+
} else if mode.is_submodule() {
404+
buf.push_str(" ➡ ");
405+
}
293406
if a.is_excluded {
294407
buf.push_str(" ❌");
295408
}

0 commit comments

Comments
 (0)