Skip to content

Commit c30ac0c

Browse files
committed
feat: gix index entries with styles and pathspecs.
This adds support for more simple git style, which is faster and thus allows for more direct comparisons to `git ls-files`.
1 parent 05ed965 commit c30ac0c

File tree

7 files changed

+185
-69
lines changed

7 files changed

+185
-69
lines changed

.gitattributes

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
**/generated-archives/*.tar.xz filter=lfs diff=lfs merge=lfs -text
22

33
# assure line feeds don't interfere with our working copy hash
4-
**/tests/fixtures/*.sh text crlf=input eol=lf
4+
**/tests/fixtures/**/*.sh text crlf=input eol=lf
55
/justfile text crlf=input eol=lf

gitoxide-core/src/query/engine/command.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ impl query::Engine {
2121
let is_excluded = spec.is_excluded();
2222
let relpath = spec
2323
.normalize(
24-
self.repo.prefix().transpose()?.unwrap_or_default().as_ref(),
24+
self.repo.prefix()?.unwrap_or_default().as_ref(),
2525
self.repo.work_dir().unwrap_or_else(|| self.repo.git_dir()),
2626
)?
2727
.path();

gitoxide-core/src/repository/attributes/query.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub(crate) mod function {
3232
// TODO(pathspec): The search is just used as a shortcut to normalization, but one day should be used for an actual search.
3333
let search = gix::pathspec::Search::from_specs(
3434
pathspecs,
35-
repo.prefix().transpose()?.as_deref(),
35+
repo.prefix()?.as_deref(),
3636
repo.work_dir().unwrap_or_else(|| repo.git_dir()),
3737
)?;
3838

gitoxide-core/src/repository/exclude.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ pub fn query(
4444
// TODO(pathspec): actually use the search to find items. This looks like `gix` capabilities to put it all together.
4545
let search = gix::pathspec::Search::from_specs(
4646
pathspecs,
47-
repo.prefix().transpose()?.as_deref(),
47+
repo.prefix()?.as_deref(),
4848
repo.work_dir().unwrap_or_else(|| repo.git_dir()),
4949
)?;
5050

gitoxide-core/src/repository/index/entries.rs

Lines changed: 148 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ pub struct Options {
44
/// If true, also show attributes
55
pub attributes: Option<Attributes>,
66
pub statistics: bool,
7+
pub simple: bool,
78
}
89

9-
#[derive(Debug)]
10+
#[derive(Debug, Copy, Clone)]
1011
pub enum Attributes {
1112
/// Look at worktree attributes and index as fallback.
1213
WorktreeAndIndex,
@@ -15,6 +16,7 @@ pub enum Attributes {
1516
}
1617

1718
pub(crate) mod function {
19+
use std::collections::BTreeSet;
1820
use std::{
1921
borrow::Cow,
2022
io::{BufWriter, Write},
@@ -26,9 +28,11 @@ pub(crate) mod function {
2628

2729
pub fn entries(
2830
repo: gix::Repository,
31+
pathspecs: Vec<gix::pathspec::Pattern>,
2932
out: impl std::io::Write,
3033
mut err: impl std::io::Write,
3134
Options {
35+
simple,
3236
format,
3337
attributes,
3438
statistics,
@@ -37,6 +41,12 @@ pub(crate) mod function {
3741
use crate::OutputFormat::*;
3842
let index = repo.index_or_load_from_head()?;
3943
let mut cache = attributes
44+
.or_else(|| {
45+
pathspecs
46+
.iter()
47+
.any(|spec| !spec.attributes.is_empty())
48+
.then_some(Attributes::Index)
49+
})
4050
.map(|attrs| {
4151
repo.attributes(
4252
&index,
@@ -70,52 +80,111 @@ pub(crate) mod function {
7080
..Default::default()
7181
};
7282

73-
let mut out = BufWriter::new(out);
83+
let mut out = BufWriter::with_capacity(64 * 1024, out);
7484
#[cfg(feature = "serde")]
7585
if let Json = format {
7686
out.write_all(b"[\n")?;
7787
}
78-
let mut entries = index.entries().iter().peekable();
79-
while let Some(entry) = entries.next() {
80-
let attrs = cache
81-
.as_mut()
82-
.map(|(attrs, cache)| {
83-
cache
84-
.at_entry(entry.path(&index), None, |id, buf| repo.objects.find_blob(id, buf))
85-
.map(|entry| {
86-
let is_excluded = entry.is_excluded();
87-
stats.excluded += usize::from(is_excluded);
88-
let attributes: Vec<_> = {
89-
entry.matching_attributes(attrs);
90-
attrs.iter().map(|m| m.assignment.to_owned()).collect()
91-
};
92-
stats.with_attributes += usize::from(!attributes.is_empty());
93-
Attrs {
94-
is_excluded,
95-
attributes,
96-
}
88+
let mut search = gix::pathspec::Search::from_specs(
89+
pathspecs,
90+
repo.prefix()?.as_deref(),
91+
gix::path::realpath(repo.work_dir().unwrap_or_else(|| repo.git_dir()))?.as_ref(), // TODO(pathspec): this setup needs `gix`.
92+
)?;
93+
let mut all_attrs = statistics.then(BTreeSet::new);
94+
if let Some(entries) = index.prefixed_entries(search.common_prefix()) {
95+
stats.entries_after_prune = entries.len();
96+
let mut entries = entries.iter().peekable();
97+
while let Some(entry) = entries.next() {
98+
let mut last_match = None;
99+
let attrs = cache
100+
.as_mut()
101+
.and_then(|(attrs, cache)| {
102+
// If the user wants to see assigned attributes, we always have to match.
103+
attributes.is_some().then(|| {
104+
cache
105+
.at_entry(entry.path(&index), None, |id, buf| repo.objects.find_blob(id, buf))
106+
.map(|entry| {
107+
let is_excluded = entry.is_excluded();
108+
stats.excluded += usize::from(is_excluded);
109+
let attributes: Vec<_> = {
110+
last_match = Some(entry.matching_attributes(attrs));
111+
attrs.iter().map(|m| m.assignment.to_owned()).collect()
112+
};
113+
stats.with_attributes += usize::from(!attributes.is_empty());
114+
stats.max_attributes_per_path = stats.max_attributes_per_path.max(attributes.len());
115+
if let Some(attrs) = all_attrs.as_mut() {
116+
attributes.iter().for_each(|attr| {
117+
attrs.insert(attr.clone());
118+
});
119+
}
120+
Attrs {
121+
is_excluded,
122+
attributes,
123+
}
124+
})
97125
})
98-
})
99-
.transpose()?;
100-
match format {
101-
Human => to_human(&mut out, &index, entry, attrs)?,
102-
#[cfg(feature = "serde")]
103-
Json => to_json(&mut out, &index, entry, attrs, entries.peek().is_none())?,
126+
})
127+
.transpose()?;
128+
129+
// Note that we intentionally ignore `_case` so that we act like git does, attribute matching case is determined
130+
// by the repository, not the pathspec.
131+
if search
132+
.pattern_matching_relative_path(entry.path(&index), Some(false), |rela_path, _case, is_dir, out| {
133+
cache
134+
.as_mut()
135+
.map(|(attrs, cache)| {
136+
match last_match {
137+
// The user wants the attributes for display, so the match happened already.
138+
Some(matched) => {
139+
attrs.copy_into(cache.attributes_collection(), out);
140+
matched
141+
}
142+
// The user doesn't want attributes, so we set the cache position on demand only
143+
None => cache
144+
.at_entry(rela_path, Some(is_dir), |id, buf| repo.objects.find_blob(id, buf))
145+
.ok()
146+
.map(|platform| platform.matching_attributes(out))
147+
.unwrap_or_default(),
148+
}
149+
})
150+
.unwrap_or_default()
151+
})
152+
.map_or(true, |m| m.is_excluded())
153+
{
154+
continue;
155+
}
156+
match format {
157+
Human => {
158+
if simple {
159+
to_human_simple(&mut out, &index, entry, attrs)
160+
} else {
161+
to_human(&mut out, &index, entry, attrs)
162+
}?
163+
}
164+
#[cfg(feature = "serde")]
165+
Json => to_json(&mut out, &index, entry, attrs, entries.peek().is_none())?,
166+
}
104167
}
105-
}
106168

107-
#[cfg(feature = "serde")]
108-
if format == Json {
109-
out.write_all(b"]\n")?;
110-
out.flush()?;
111-
if statistics {
112-
serde_json::to_writer_pretty(&mut err, &stats)?;
169+
#[cfg(feature = "serde")]
170+
if format == Json {
171+
out.write_all(b"]\n")?;
172+
out.flush()?;
173+
if statistics {
174+
serde_json::to_writer_pretty(&mut err, &stats)?;
175+
}
176+
}
177+
if format == Human && statistics {
178+
out.flush()?;
179+
stats.cache = cache.map(|c| *c.1.statistics());
180+
writeln!(err, "{stats:#?}")?;
181+
if let Some(attrs) = all_attrs.filter(|a| !a.is_empty()) {
182+
writeln!(err, "All encountered attributes:")?;
183+
for attr in attrs {
184+
writeln!(err, "\t{attr}", attr = attr.as_ref())?;
185+
}
186+
}
113187
}
114-
}
115-
if format == Human && statistics {
116-
out.flush()?;
117-
stats.cache = cache.map(|c| *c.1.statistics());
118-
writeln!(err, "{stats:#?}")?;
119188
}
120189
Ok(())
121190
}
@@ -131,8 +200,10 @@ pub(crate) mod function {
131200
struct Statistics {
132201
#[allow(dead_code)] // Not really dead, but Debug doesn't count for it even though it's crucial.
133202
pub entries: usize,
203+
pub entries_after_prune: usize,
134204
pub excluded: usize,
135205
pub with_attributes: usize,
206+
pub max_attributes_per_path: usize,
136207
pub cache: Option<gix::worktree::cache::Statistics>,
137208
}
138209

@@ -175,6 +246,22 @@ pub(crate) mod function {
175246
Ok(())
176247
}
177248

249+
fn to_human_simple(
250+
out: &mut impl std::io::Write,
251+
file: &gix::index::File,
252+
entry: &gix::index::Entry,
253+
attrs: Option<Attrs>,
254+
) -> std::io::Result<()> {
255+
match attrs {
256+
Some(attrs) => {
257+
out.write_all(entry.path(file))?;
258+
out.write_all(print_attrs(Some(attrs)).as_bytes())
259+
}
260+
None => out.write_all(entry.path(file)),
261+
}?;
262+
out.write_all(b"\n")
263+
}
264+
178265
fn to_human(
179266
out: &mut impl std::io::Write,
180267
file: &gix::index::File,
@@ -198,24 +285,28 @@ pub(crate) mod function {
198285
entry.mode,
199286
entry.id,
200287
entry.path(file),
201-
attrs.map_or(Cow::Borrowed(""), |a| {
202-
let mut buf = String::new();
203-
if a.is_excluded {
204-
buf.push_str(" ❌");
205-
}
206-
if !a.attributes.is_empty() {
207-
buf.push_str(" (");
208-
for assignment in a.attributes {
209-
use std::fmt::Write;
210-
write!(&mut buf, "{}", assignment.as_ref()).ok();
211-
buf.push_str(", ");
212-
}
213-
buf.pop();
214-
buf.pop();
215-
buf.push(')');
216-
}
217-
buf.into()
218-
})
288+
print_attrs(attrs)
219289
)
220290
}
291+
292+
fn print_attrs(attrs: Option<Attrs>) -> Cow<'static, str> {
293+
attrs.map_or(Cow::Borrowed(""), |a| {
294+
let mut buf = String::new();
295+
if a.is_excluded {
296+
buf.push_str(" ❌");
297+
}
298+
if !a.attributes.is_empty() {
299+
buf.push_str(" (");
300+
for assignment in a.attributes {
301+
use std::fmt::Write;
302+
write!(&mut buf, "{}", assignment.as_ref()).ok();
303+
buf.push_str(", ");
304+
}
305+
buf.pop();
306+
buf.pop();
307+
buf.push(')');
308+
}
309+
buf.into()
310+
})
311+
}
221312
}

src/plumbing/main.rs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,7 +1018,7 @@ pub fn main() -> Result<()> {
10181018
),
10191019
},
10201020
Subcommands::Attributes(cmd) => match cmd {
1021-
attributes::Subcommands::Query { statistics, pathspecs } => prepare_and_run(
1021+
attributes::Subcommands::Query { statistics, pathspec } => prepare_and_run(
10221022
"attributes-query",
10231023
trace,
10241024
verbose,
@@ -1029,7 +1029,7 @@ pub fn main() -> Result<()> {
10291029
use gix::bstr::ByteSlice;
10301030
core::repository::attributes::query(
10311031
repository(Mode::Strict)?,
1032-
if pathspecs.is_empty() {
1032+
if pathspec.is_empty() {
10331033
Box::new(stdin_or_bail()?.byte_lines().filter_map(Result::ok).filter_map(|line| {
10341034
gix::pathspec::parse(
10351035
line.as_bstr(),
@@ -1039,7 +1039,7 @@ pub fn main() -> Result<()> {
10391039
.ok()
10401040
})) as Box<dyn Iterator<Item = gix::pathspec::Pattern>>
10411041
} else {
1042-
Box::new(pathspecs.into_iter())
1042+
Box::new(pathspec.into_iter())
10431043
},
10441044
out,
10451045
err,
@@ -1076,7 +1076,7 @@ pub fn main() -> Result<()> {
10761076
exclude::Subcommands::Query {
10771077
statistics,
10781078
patterns,
1079-
pathspecs,
1079+
pathspec,
10801080
show_ignore_patterns,
10811081
} => prepare_and_run(
10821082
"exclude-query",
@@ -1088,15 +1088,15 @@ pub fn main() -> Result<()> {
10881088
move |_progress, out, err| {
10891089
core::repository::exclude::query(
10901090
repository(Mode::Strict)?,
1091-
if pathspecs.is_empty() {
1091+
if pathspec.is_empty() {
10921092
Box::new(
10931093
stdin_or_bail()?
10941094
.byte_lines()
10951095
.filter_map(Result::ok)
10961096
.filter_map(|line| gix::pathspec::parse(&line, Default::default()).ok()),
10971097
) as Box<dyn Iterator<Item = gix::pathspec::Pattern>>
10981098
} else {
1099-
Box::new(pathspecs.into_iter())
1099+
Box::new(pathspec.into_iter())
11001100
},
11011101
out,
11021102
err,
@@ -1112,9 +1112,11 @@ pub fn main() -> Result<()> {
11121112
},
11131113
Subcommands::Index(cmd) => match cmd {
11141114
index::Subcommands::Entries {
1115+
format: entry_format,
11151116
no_attributes,
11161117
attributes_from_index,
11171118
statistics,
1119+
pathspec,
11181120
} => prepare_and_run(
11191121
"index-entries",
11201122
trace,
@@ -1125,10 +1127,15 @@ pub fn main() -> Result<()> {
11251127
move |_progress, out, err| {
11261128
core::repository::index::entries(
11271129
repository(Mode::LenientWithGitInstallConfig)?,
1130+
pathspec,
11281131
out,
11291132
err,
11301133
core::repository::index::entries::Options {
11311134
format,
1135+
simple: match entry_format {
1136+
index::entries::Format::Simple => true,
1137+
index::entries::Format::Rich => false,
1138+
},
11321139
attributes: if no_attributes {
11331140
None
11341141
} else {

0 commit comments

Comments
 (0)