Skip to content

Commit 252e14a

Browse files
committed
feat: gix index entries also prints attributes.
1 parent 2e6107f commit 252e14a

File tree

5 files changed

+220
-78
lines changed

5 files changed

+220
-78
lines changed
Lines changed: 194 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,208 @@
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: bool,
6+
pub statistics: bool,
7+
}
8+
pub(crate) mod function {
9+
use crate::repository::index::entries::Options;
10+
use gix::attrs::State;
11+
use gix::bstr::ByteSlice;
12+
use gix::odb::FindExt;
13+
use std::borrow::Cow;
14+
use std::io::{BufWriter, Write};
415

5-
#[cfg(feature = "serde")]
6-
if let Json = format {
7-
out.write_all(b"[\n")?;
8-
}
16+
pub fn entries(
17+
repo: gix::Repository,
18+
out: impl std::io::Write,
19+
mut err: impl std::io::Write,
20+
Options {
21+
format,
22+
attributes,
23+
statistics,
24+
}: Options,
25+
) -> anyhow::Result<()> {
26+
use crate::OutputFormat::*;
27+
let index = repo.index()?;
28+
let mut cache = attributes
29+
.then(|| {
30+
repo.attributes(
31+
&index,
32+
gix::worktree::cache::state::attributes::Source::WorktreeThenIdMapping,
33+
None,
34+
)
35+
.map(|cache| (cache.attribute_matches(), cache))
36+
})
37+
.transpose()?;
38+
let mut stats = Statistics {
39+
entries: index.entries().len(),
40+
..Default::default()
41+
};
942

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())?,
43+
let mut out = BufWriter::new(out);
44+
#[cfg(feature = "serde")]
45+
if let Json = format {
46+
out.write_all(b"[\n")?;
47+
}
48+
let mut entries = index.entries().iter().peekable();
49+
while let Some(entry) = entries.next() {
50+
let attrs = cache
51+
.as_mut()
52+
.map(|(attrs, cache)| {
53+
cache
54+
.at_entry(entry.path(&index), None, |id, buf| repo.objects.find_blob(id, buf))
55+
.map(|entry| {
56+
let is_excluded = entry.is_excluded();
57+
stats.excluded += usize::from(is_excluded);
58+
let attributes: Vec<_> = {
59+
entry.matching_attributes(attrs);
60+
attrs.iter().map(|m| m.assignment.to_owned()).collect()
61+
};
62+
stats.with_attributes += usize::from(!attributes.is_empty());
63+
Attrs {
64+
is_excluded,
65+
attributes,
66+
}
67+
})
68+
})
69+
.transpose()?;
70+
match format {
71+
Human => to_human(&mut out, &index, entry, attrs)?,
72+
#[cfg(feature = "serde")]
73+
Json => to_json(&mut out, &index, entry, attrs, entries.peek().is_none())?,
74+
}
1675
}
17-
}
1876

19-
#[cfg(feature = "serde")]
20-
if let Json = format {
21-
out.write_all(b"]\n")?;
77+
#[cfg(feature = "serde")]
78+
if format == Json {
79+
out.write_all(b"]\n")?;
80+
out.flush()?;
81+
if statistics {
82+
serde_json::to_writer_pretty(&mut err, &stats)?;
83+
}
84+
}
85+
if format == Human && statistics {
86+
out.flush()?;
87+
stats.cache = cache.map(|c| *c.1.statistics());
88+
writeln!(err, "{:#?}", stats)?;
89+
}
90+
Ok(())
2291
}
23-
Ok(())
24-
}
2592

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;
93+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
94+
struct Attrs {
95+
is_excluded: bool,
96+
attributes: Vec<gix::attrs::Assignment>,
97+
}
3498

3599
#[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>,
100+
#[derive(Default, Debug)]
101+
struct Statistics {
102+
#[allow(dead_code)] // Not really dead, but Debug doesn't count for it even though it's crucial.
103+
pub entries: usize,
104+
pub excluded: usize,
105+
pub with_attributes: usize,
106+
pub cache: Option<gix::worktree::cache::Statistics>,
42107
}
43108

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-
)?;
109+
#[cfg(feature = "serde")]
110+
fn to_json(
111+
mut out: &mut impl std::io::Write,
112+
index: &gix::index::File,
113+
entry: &gix::index::Entry,
114+
attrs: Option<Attrs>,
115+
is_last: bool,
116+
) -> anyhow::Result<()> {
117+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
118+
struct Entry<'a> {
119+
stat: &'a gix::index::entry::Stat,
120+
hex_id: String,
121+
flags: u32,
122+
mode: u32,
123+
path: std::borrow::Cow<'a, str>,
124+
meta: Option<Attrs>,
125+
}
54126

55-
if is_last {
56-
out.write_all(b"\n")?;
57-
} else {
58-
out.write_all(b",\n")?;
59-
}
60-
Ok(())
61-
}
127+
serde_json::to_writer(
128+
&mut out,
129+
&Entry {
130+
stat: &entry.stat,
131+
hex_id: entry.id.to_hex().to_string(),
132+
flags: entry.flags.bits(),
133+
mode: entry.mode.bits(),
134+
path: entry.path(index).to_str_lossy(),
135+
meta: attrs,
136+
},
137+
)?;
62138

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()
139+
if is_last {
140+
out.write_all(b"\n")?;
79141
} else {
80-
format!("{:?} ", entry.flags)
81-
},
82-
entry.mode,
83-
entry.id,
84-
entry.path(file)
85-
)
142+
out.write_all(b",\n")?;
143+
}
144+
Ok(())
145+
}
146+
147+
fn to_human(
148+
out: &mut impl std::io::Write,
149+
file: &gix::index::File,
150+
entry: &gix::index::Entry,
151+
attrs: Option<Attrs>,
152+
) -> std::io::Result<()> {
153+
writeln!(
154+
out,
155+
"{} {}{:?} {} {}{}",
156+
match entry.flags.stage() {
157+
0 => "BASE ",
158+
1 => "OURS ",
159+
2 => "THEIRS ",
160+
_ => "UNKNOWN",
161+
},
162+
if entry.flags.is_empty() {
163+
"".to_string()
164+
} else {
165+
format!("{:?} ", entry.flags)
166+
},
167+
entry.mode,
168+
entry.id,
169+
entry.path(file),
170+
attrs
171+
.map(|a| {
172+
let mut buf = String::new();
173+
if a.is_excluded {
174+
buf.push_str(" ❌");
175+
}
176+
if !a.attributes.is_empty() {
177+
buf.push_str(" (");
178+
for assignment in a.attributes {
179+
match assignment.state {
180+
State::Set => {
181+
buf.push_str(assignment.name.as_str());
182+
}
183+
State::Unset => {
184+
buf.push('-');
185+
buf.push_str(assignment.name.as_str());
186+
}
187+
State::Value(v) => {
188+
buf.push_str(assignment.name.as_str());
189+
buf.push('=');
190+
buf.push_str(v.as_ref().as_bstr().to_str_lossy().as_ref());
191+
}
192+
State::Unspecified => {
193+
buf.push('!');
194+
buf.push_str(assignment.name.as_str());
195+
}
196+
}
197+
buf.push_str(", ");
198+
}
199+
buf.pop();
200+
buf.pop();
201+
buf.push(')');
202+
}
203+
buf.into()
204+
})
205+
.unwrap_or(Cow::Borrowed(""))
206+
)
207+
}
86208
}

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

Lines changed: 2 additions & 2 deletions
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;

gix/Cargo.toml

Lines changed: 1 addition & 0 deletions
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`.

src/plumbing/main.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -855,14 +855,26 @@ 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+
statistics,
861+
} => prepare_and_run(
859862
"index-entries",
860863
verbose,
861864
progress,
862865
progress_keep_open,
863866
None,
864-
move |_progress, out, _err| {
865-
core::repository::index::entries(repository(Mode::LenientWithGitInstallConfig)?, out, format)
867+
move |_progress, out, err| {
868+
core::repository::index::entries(
869+
repository(Mode::LenientWithGitInstallConfig)?,
870+
out,
871+
err,
872+
core::repository::index::entries::Options {
873+
format,
874+
attributes: !no_attributes,
875+
statistics,
876+
},
877+
)
866878
},
867879
),
868880
index::Subcommands::FromTree {

src/plumbing/options/mod.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,14 @@ pub mod index {
472472
#[derive(Debug, clap::Subcommand)]
473473
pub enum Subcommands {
474474
/// Print all entries to standard output
475-
Entries,
475+
Entries {
476+
/// Do not visualize excluded entries or attributes per path.
477+
#[clap(long)]
478+
no_attributes: bool,
479+
/// Print various statistics to stderr
480+
#[clap(long, short = 's')]
481+
statistics: bool,
482+
},
476483
/// Create an index from a tree-ish.
477484
#[clap(visible_alias = "read-tree")]
478485
FromTree {

0 commit comments

Comments
 (0)