Skip to content

Commit 08e8fc2

Browse files
committed
feat: gix index entries also prints attributes.
1 parent bd1ae0d commit 08e8fc2

File tree

6 files changed

+268
-81
lines changed

6 files changed

+268
-81
lines changed

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

+214-72
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,228 @@
1-
pub fn entries(repo: gix::Repository, mut out: impl std::io::Write, format: crate::OutputFormat) -> anyhow::Result<()> {
2-
use crate::OutputFormat::*;
3-
let index = repo.index()?;
1+
#[derive(Debug)]
2+
pub struct Options {
3+
pub format: crate::OutputFormat,
4+
/// If true, also show attributes
5+
pub attributes: Option<Attributes>,
6+
pub statistics: bool,
7+
}
48

5-
#[cfg(feature = "serde")]
6-
if let Json = format {
7-
out.write_all(b"[\n")?;
8-
}
9+
#[derive(Debug)]
10+
pub enum Attributes {
11+
/// Look at worktree attributes and index as fallback.
12+
WorktreeAndIndex,
13+
/// Look at attributes from index files only.
14+
Index,
15+
}
16+
17+
pub(crate) mod function {
18+
use crate::repository::index::entries::{Attributes, Options};
19+
use gix::attrs::State;
20+
use gix::bstr::ByteSlice;
21+
use gix::odb::FindExt;
22+
use std::borrow::Cow;
23+
use std::io::{BufWriter, Write};
924

10-
let mut entries = index.entries().iter().peekable();
11-
while let Some(entry) = entries.next() {
12-
match format {
13-
Human => to_human(&mut out, &index, entry)?,
14-
#[cfg(feature = "serde")]
15-
Json => to_json(&mut out, &index, entry, entries.peek().is_none())?,
25+
pub fn entries(
26+
repo: gix::Repository,
27+
out: impl std::io::Write,
28+
mut err: impl std::io::Write,
29+
Options {
30+
format,
31+
attributes,
32+
statistics,
33+
}: Options,
34+
) -> anyhow::Result<()> {
35+
use crate::OutputFormat::*;
36+
let index = repo.index()?;
37+
let mut cache = attributes
38+
.map(|attrs| {
39+
repo.attributes(
40+
&index,
41+
match attrs {
42+
Attributes::WorktreeAndIndex => {
43+
gix::worktree::cache::state::attributes::Source::WorktreeThenIdMapping
44+
}
45+
Attributes::Index => gix::worktree::cache::state::attributes::Source::IdMapping,
46+
},
47+
match attrs {
48+
Attributes::WorktreeAndIndex => {
49+
gix::worktree::cache::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped
50+
}
51+
Attributes::Index => gix::worktree::cache::state::ignore::Source::IdMapping,
52+
},
53+
None,
54+
)
55+
.map(|cache| (cache.attribute_matches(), cache))
56+
})
57+
.transpose()?;
58+
let mut stats = Statistics {
59+
entries: index.entries().len(),
60+
..Default::default()
61+
};
62+
63+
let mut out = BufWriter::new(out);
64+
#[cfg(feature = "serde")]
65+
if let Json = format {
66+
out.write_all(b"[\n")?;
67+
}
68+
let mut entries = index.entries().iter().peekable();
69+
while let Some(entry) = entries.next() {
70+
let attrs = cache
71+
.as_mut()
72+
.map(|(attrs, cache)| {
73+
cache
74+
.at_entry(entry.path(&index), None, |id, buf| repo.objects.find_blob(id, buf))
75+
.map(|entry| {
76+
let is_excluded = entry.is_excluded();
77+
stats.excluded += usize::from(is_excluded);
78+
let attributes: Vec<_> = {
79+
entry.matching_attributes(attrs);
80+
attrs.iter().map(|m| m.assignment.to_owned()).collect()
81+
};
82+
stats.with_attributes += usize::from(!attributes.is_empty());
83+
Attrs {
84+
is_excluded,
85+
attributes,
86+
}
87+
})
88+
})
89+
.transpose()?;
90+
match format {
91+
Human => to_human(&mut out, &index, entry, attrs)?,
92+
#[cfg(feature = "serde")]
93+
Json => to_json(&mut out, &index, entry, attrs, entries.peek().is_none())?,
94+
}
1695
}
17-
}
1896

19-
#[cfg(feature = "serde")]
20-
if let Json = format {
21-
out.write_all(b"]\n")?;
97+
#[cfg(feature = "serde")]
98+
if format == Json {
99+
out.write_all(b"]\n")?;
100+
out.flush()?;
101+
if statistics {
102+
serde_json::to_writer_pretty(&mut err, &stats)?;
103+
}
104+
}
105+
if format == Human && statistics {
106+
out.flush()?;
107+
stats.cache = cache.map(|c| *c.1.statistics());
108+
writeln!(err, "{:#?}", stats)?;
109+
}
110+
Ok(())
22111
}
23-
Ok(())
24-
}
25112

26-
#[cfg(feature = "serde")]
27-
pub(crate) fn to_json(
28-
mut out: &mut impl std::io::Write,
29-
index: &gix::index::File,
30-
entry: &gix::index::Entry,
31-
is_last: bool,
32-
) -> anyhow::Result<()> {
33-
use gix::bstr::ByteSlice;
113+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
114+
struct Attrs {
115+
is_excluded: bool,
116+
attributes: Vec<gix::attrs::Assignment>,
117+
}
34118

35119
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
36-
struct Entry<'a> {
37-
stat: &'a gix::index::entry::Stat,
38-
hex_id: String,
39-
flags: u32,
40-
mode: u32,
41-
path: std::borrow::Cow<'a, str>,
120+
#[derive(Default, Debug)]
121+
struct Statistics {
122+
#[allow(dead_code)] // Not really dead, but Debug doesn't count for it even though it's crucial.
123+
pub entries: usize,
124+
pub excluded: usize,
125+
pub with_attributes: usize,
126+
pub cache: Option<gix::worktree::cache::Statistics>,
42127
}
43128

44-
serde_json::to_writer(
45-
&mut out,
46-
&Entry {
47-
stat: &entry.stat,
48-
hex_id: entry.id.to_hex().to_string(),
49-
flags: entry.flags.bits(),
50-
mode: entry.mode.bits(),
51-
path: entry.path(index).to_str_lossy(),
52-
},
53-
)?;
129+
#[cfg(feature = "serde")]
130+
fn to_json(
131+
mut out: &mut impl std::io::Write,
132+
index: &gix::index::File,
133+
entry: &gix::index::Entry,
134+
attrs: Option<Attrs>,
135+
is_last: bool,
136+
) -> anyhow::Result<()> {
137+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
138+
struct Entry<'a> {
139+
stat: &'a gix::index::entry::Stat,
140+
hex_id: String,
141+
flags: u32,
142+
mode: u32,
143+
path: std::borrow::Cow<'a, str>,
144+
meta: Option<Attrs>,
145+
}
146+
147+
serde_json::to_writer(
148+
&mut out,
149+
&Entry {
150+
stat: &entry.stat,
151+
hex_id: entry.id.to_hex().to_string(),
152+
flags: entry.flags.bits(),
153+
mode: entry.mode.bits(),
154+
path: entry.path(index).to_str_lossy(),
155+
meta: attrs,
156+
},
157+
)?;
54158

55-
if is_last {
56-
out.write_all(b"\n")?;
57-
} else {
58-
out.write_all(b",\n")?;
159+
if is_last {
160+
out.write_all(b"\n")?;
161+
} else {
162+
out.write_all(b",\n")?;
163+
}
164+
Ok(())
59165
}
60-
Ok(())
61-
}
62166

63-
pub(crate) fn to_human(
64-
out: &mut impl std::io::Write,
65-
file: &gix::index::File,
66-
entry: &gix::index::Entry,
67-
) -> std::io::Result<()> {
68-
writeln!(
69-
out,
70-
"{} {}{:?} {} {}",
71-
match entry.flags.stage() {
72-
0 => "BASE ",
73-
1 => "OURS ",
74-
2 => "THEIRS ",
75-
_ => "UNKNOWN",
76-
},
77-
if entry.flags.is_empty() {
78-
"".to_string()
79-
} else {
80-
format!("{:?} ", entry.flags)
81-
},
82-
entry.mode,
83-
entry.id,
84-
entry.path(file)
85-
)
167+
fn to_human(
168+
out: &mut impl std::io::Write,
169+
file: &gix::index::File,
170+
entry: &gix::index::Entry,
171+
attrs: Option<Attrs>,
172+
) -> std::io::Result<()> {
173+
writeln!(
174+
out,
175+
"{} {}{:?} {} {}{}",
176+
match entry.flags.stage() {
177+
0 => "BASE ",
178+
1 => "OURS ",
179+
2 => "THEIRS ",
180+
_ => "UNKNOWN",
181+
},
182+
if entry.flags.is_empty() {
183+
"".to_string()
184+
} else {
185+
format!("{:?} ", entry.flags)
186+
},
187+
entry.mode,
188+
entry.id,
189+
entry.path(file),
190+
attrs
191+
.map(|a| {
192+
let mut buf = String::new();
193+
if a.is_excluded {
194+
buf.push_str(" ❌");
195+
}
196+
if !a.attributes.is_empty() {
197+
buf.push_str(" (");
198+
for assignment in a.attributes {
199+
match assignment.state {
200+
State::Set => {
201+
buf.push_str(assignment.name.as_str());
202+
}
203+
State::Unset => {
204+
buf.push('-');
205+
buf.push_str(assignment.name.as_str());
206+
}
207+
State::Value(v) => {
208+
buf.push_str(assignment.name.as_str());
209+
buf.push('=');
210+
buf.push_str(v.as_ref().as_bstr().to_str_lossy().as_ref());
211+
}
212+
State::Unspecified => {
213+
buf.push('!');
214+
buf.push_str(assignment.name.as_str());
215+
}
216+
}
217+
buf.push_str(", ");
218+
}
219+
buf.pop();
220+
buf.pop();
221+
buf.push(')');
222+
}
223+
buf.into()
224+
})
225+
.unwrap_or(Cow::Borrowed(""))
226+
)
227+
}
86228
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,5 @@ pub fn from_tree(
3535
Ok(())
3636
}
3737

38-
mod entries;
39-
pub use entries::entries;
38+
pub mod entries;
39+
pub use entries::function::entries;

Diff for: gix/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ serde = [ "dep:serde",
6767
"gix-attributes/serde",
6868
"gix-ignore/serde",
6969
"gix-revision/serde",
70+
"gix-worktree/serde",
7071
"gix-credentials/serde"]
7172

7273
## Re-export the progress tree root which allows to obtain progress from various functions which take `impl gix::Progress`.

Diff for: src/plumbing/main.rs

+35-3
Original file line numberDiff line numberDiff line change
@@ -855,14 +855,35 @@ pub fn main() -> Result<()> {
855855
),
856856
},
857857
Subcommands::Index(cmd) => match cmd {
858-
index::Subcommands::Entries => prepare_and_run(
858+
index::Subcommands::Entries {
859+
no_attributes,
860+
attributes_from_index,
861+
statistics,
862+
} => prepare_and_run(
859863
"index-entries",
860864
verbose,
861865
progress,
862866
progress_keep_open,
863867
None,
864-
move |_progress, out, _err| {
865-
core::repository::index::entries(repository(Mode::LenientWithGitInstallConfig)?, out, format)
868+
move |_progress, out, err| {
869+
core::repository::index::entries(
870+
repository(Mode::LenientWithGitInstallConfig)?,
871+
out,
872+
err,
873+
core::repository::index::entries::Options {
874+
format,
875+
attributes: if no_attributes {
876+
None
877+
} else {
878+
Some(if attributes_from_index {
879+
core::repository::index::entries::Attributes::Index
880+
} else {
881+
core::repository::index::entries::Attributes::WorktreeAndIndex
882+
})
883+
},
884+
statistics,
885+
},
886+
)
866887
},
867888
),
868889
index::Subcommands::FromTree {
@@ -899,3 +920,14 @@ fn verify_mode(decode: bool, re_encode: bool) -> verify::Mode {
899920
(false, false) => verify::Mode::HashCrc32,
900921
}
901922
}
923+
924+
#[cfg(test)]
925+
mod tests {
926+
use super::*;
927+
928+
#[test]
929+
fn clap() {
930+
use clap::CommandFactory;
931+
Args::command().debug_assert();
932+
}
933+
}

Diff for: src/plumbing/options/free.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ pub mod pack {
105105
/// Possible values are "none" and "tree-traversal". Default is "none".
106106
expansion: Option<core::pack::create::ObjectExpansion>,
107107

108-
#[clap(long, default_value_t = 3, requires = "nondeterministic-count")]
108+
#[clap(long, default_value_t = 3, requires = "nondeterministic_count")]
109109
/// The amount of threads to use when counting and the `--nondeterminisitc-count` flag is set, defaulting
110110
/// to the globally configured threads.
111111
///

0 commit comments

Comments
 (0)