Skip to content

Commit

Permalink
feat: gix index entries also prints attributes.
Browse files Browse the repository at this point in the history
  • Loading branch information
Byron committed Apr 25, 2023
1 parent bd1ae0d commit 08e8fc2
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 81 deletions.
286 changes: 214 additions & 72 deletions gitoxide-core/src/repository/index/entries.rs
@@ -1,86 +1,228 @@
pub fn entries(repo: gix::Repository, mut out: impl std::io::Write, format: crate::OutputFormat) -> anyhow::Result<()> {
use crate::OutputFormat::*;
let index = repo.index()?;
#[derive(Debug)]
pub struct Options {
pub format: crate::OutputFormat,
/// If true, also show attributes
pub attributes: Option<Attributes>,
pub statistics: bool,
}

#[cfg(feature = "serde")]
if let Json = format {
out.write_all(b"[\n")?;
}
#[derive(Debug)]
pub enum Attributes {
/// Look at worktree attributes and index as fallback.
WorktreeAndIndex,
/// Look at attributes from index files only.
Index,
}

pub(crate) mod function {
use crate::repository::index::entries::{Attributes, Options};
use gix::attrs::State;
use gix::bstr::ByteSlice;
use gix::odb::FindExt;
use std::borrow::Cow;
use std::io::{BufWriter, Write};

let mut entries = index.entries().iter().peekable();
while let Some(entry) = entries.next() {
match format {
Human => to_human(&mut out, &index, entry)?,
#[cfg(feature = "serde")]
Json => to_json(&mut out, &index, entry, entries.peek().is_none())?,
pub fn entries(
repo: gix::Repository,
out: impl std::io::Write,
mut err: impl std::io::Write,
Options {
format,
attributes,
statistics,
}: Options,
) -> anyhow::Result<()> {
use crate::OutputFormat::*;
let index = repo.index()?;
let mut cache = attributes
.map(|attrs| {
repo.attributes(
&index,
match attrs {
Attributes::WorktreeAndIndex => {
gix::worktree::cache::state::attributes::Source::WorktreeThenIdMapping
}
Attributes::Index => gix::worktree::cache::state::attributes::Source::IdMapping,
},
match attrs {
Attributes::WorktreeAndIndex => {
gix::worktree::cache::state::ignore::Source::WorktreeThenIdMappingIfNotSkipped
}
Attributes::Index => gix::worktree::cache::state::ignore::Source::IdMapping,
},
None,
)
.map(|cache| (cache.attribute_matches(), cache))
})
.transpose()?;
let mut stats = Statistics {
entries: index.entries().len(),
..Default::default()
};

let mut out = BufWriter::new(out);
#[cfg(feature = "serde")]
if let Json = format {
out.write_all(b"[\n")?;
}
let mut entries = index.entries().iter().peekable();
while let Some(entry) = entries.next() {
let attrs = cache
.as_mut()
.map(|(attrs, cache)| {
cache
.at_entry(entry.path(&index), None, |id, buf| repo.objects.find_blob(id, buf))
.map(|entry| {
let is_excluded = entry.is_excluded();
stats.excluded += usize::from(is_excluded);
let attributes: Vec<_> = {
entry.matching_attributes(attrs);
attrs.iter().map(|m| m.assignment.to_owned()).collect()
};
stats.with_attributes += usize::from(!attributes.is_empty());
Attrs {
is_excluded,
attributes,
}
})
})
.transpose()?;
match format {
Human => to_human(&mut out, &index, entry, attrs)?,
#[cfg(feature = "serde")]
Json => to_json(&mut out, &index, entry, attrs, entries.peek().is_none())?,
}
}
}

#[cfg(feature = "serde")]
if let Json = format {
out.write_all(b"]\n")?;
#[cfg(feature = "serde")]
if format == Json {
out.write_all(b"]\n")?;
out.flush()?;
if statistics {
serde_json::to_writer_pretty(&mut err, &stats)?;
}
}
if format == Human && statistics {
out.flush()?;
stats.cache = cache.map(|c| *c.1.statistics());
writeln!(err, "{:#?}", stats)?;
}
Ok(())
}
Ok(())
}

#[cfg(feature = "serde")]
pub(crate) fn to_json(
mut out: &mut impl std::io::Write,
index: &gix::index::File,
entry: &gix::index::Entry,
is_last: bool,
) -> anyhow::Result<()> {
use gix::bstr::ByteSlice;
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
struct Attrs {
is_excluded: bool,
attributes: Vec<gix::attrs::Assignment>,
}

#[cfg_attr(feature = "serde", derive(serde::Serialize))]
struct Entry<'a> {
stat: &'a gix::index::entry::Stat,
hex_id: String,
flags: u32,
mode: u32,
path: std::borrow::Cow<'a, str>,
#[derive(Default, Debug)]
struct Statistics {
#[allow(dead_code)] // Not really dead, but Debug doesn't count for it even though it's crucial.
pub entries: usize,
pub excluded: usize,
pub with_attributes: usize,
pub cache: Option<gix::worktree::cache::Statistics>,
}

serde_json::to_writer(
&mut out,
&Entry {
stat: &entry.stat,
hex_id: entry.id.to_hex().to_string(),
flags: entry.flags.bits(),
mode: entry.mode.bits(),
path: entry.path(index).to_str_lossy(),
},
)?;
#[cfg(feature = "serde")]
fn to_json(
mut out: &mut impl std::io::Write,
index: &gix::index::File,
entry: &gix::index::Entry,
attrs: Option<Attrs>,
is_last: bool,
) -> anyhow::Result<()> {
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
struct Entry<'a> {
stat: &'a gix::index::entry::Stat,
hex_id: String,
flags: u32,
mode: u32,
path: std::borrow::Cow<'a, str>,
meta: Option<Attrs>,
}

serde_json::to_writer(
&mut out,
&Entry {
stat: &entry.stat,
hex_id: entry.id.to_hex().to_string(),
flags: entry.flags.bits(),
mode: entry.mode.bits(),
path: entry.path(index).to_str_lossy(),
meta: attrs,
},
)?;

if is_last {
out.write_all(b"\n")?;
} else {
out.write_all(b",\n")?;
if is_last {
out.write_all(b"\n")?;
} else {
out.write_all(b",\n")?;
}
Ok(())
}
Ok(())
}

pub(crate) fn to_human(
out: &mut impl std::io::Write,
file: &gix::index::File,
entry: &gix::index::Entry,
) -> std::io::Result<()> {
writeln!(
out,
"{} {}{:?} {} {}",
match entry.flags.stage() {
0 => "BASE ",
1 => "OURS ",
2 => "THEIRS ",
_ => "UNKNOWN",
},
if entry.flags.is_empty() {
"".to_string()
} else {
format!("{:?} ", entry.flags)
},
entry.mode,
entry.id,
entry.path(file)
)
fn to_human(
out: &mut impl std::io::Write,
file: &gix::index::File,
entry: &gix::index::Entry,
attrs: Option<Attrs>,
) -> std::io::Result<()> {
writeln!(
out,
"{} {}{:?} {} {}{}",
match entry.flags.stage() {
0 => "BASE ",
1 => "OURS ",
2 => "THEIRS ",
_ => "UNKNOWN",
},
if entry.flags.is_empty() {
"".to_string()
} else {
format!("{:?} ", entry.flags)
},
entry.mode,
entry.id,
entry.path(file),
attrs
.map(|a| {
let mut buf = String::new();
if a.is_excluded {
buf.push_str(" ❌");
}
if !a.attributes.is_empty() {
buf.push_str(" (");
for assignment in a.attributes {
match assignment.state {
State::Set => {
buf.push_str(assignment.name.as_str());
}
State::Unset => {
buf.push('-');
buf.push_str(assignment.name.as_str());
}
State::Value(v) => {
buf.push_str(assignment.name.as_str());
buf.push('=');
buf.push_str(v.as_ref().as_bstr().to_str_lossy().as_ref());
}
State::Unspecified => {
buf.push('!');
buf.push_str(assignment.name.as_str());
}
}
buf.push_str(", ");
}
buf.pop();
buf.pop();
buf.push(')');
}
buf.into()
})
.unwrap_or(Cow::Borrowed(""))
)
}
}
4 changes: 2 additions & 2 deletions gitoxide-core/src/repository/index/mod.rs
Expand Up @@ -35,5 +35,5 @@ pub fn from_tree(
Ok(())
}

mod entries;
pub use entries::entries;
pub mod entries;
pub use entries::function::entries;
1 change: 1 addition & 0 deletions gix/Cargo.toml
Expand Up @@ -67,6 +67,7 @@ serde = [ "dep:serde",
"gix-attributes/serde",
"gix-ignore/serde",
"gix-revision/serde",
"gix-worktree/serde",
"gix-credentials/serde"]

## Re-export the progress tree root which allows to obtain progress from various functions which take `impl gix::Progress`.
Expand Down
38 changes: 35 additions & 3 deletions src/plumbing/main.rs
Expand Up @@ -855,14 +855,35 @@ pub fn main() -> Result<()> {
),
},
Subcommands::Index(cmd) => match cmd {
index::Subcommands::Entries => prepare_and_run(
index::Subcommands::Entries {
no_attributes,
attributes_from_index,
statistics,
} => prepare_and_run(
"index-entries",
verbose,
progress,
progress_keep_open,
None,
move |_progress, out, _err| {
core::repository::index::entries(repository(Mode::LenientWithGitInstallConfig)?, out, format)
move |_progress, out, err| {
core::repository::index::entries(
repository(Mode::LenientWithGitInstallConfig)?,
out,
err,
core::repository::index::entries::Options {
format,
attributes: if no_attributes {
None
} else {
Some(if attributes_from_index {
core::repository::index::entries::Attributes::Index
} else {
core::repository::index::entries::Attributes::WorktreeAndIndex
})
},
statistics,
},
)
},
),
index::Subcommands::FromTree {
Expand Down Expand Up @@ -899,3 +920,14 @@ fn verify_mode(decode: bool, re_encode: bool) -> verify::Mode {
(false, false) => verify::Mode::HashCrc32,
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn clap() {
use clap::CommandFactory;
Args::command().debug_assert();
}
}
2 changes: 1 addition & 1 deletion src/plumbing/options/free.rs
Expand Up @@ -105,7 +105,7 @@ pub mod pack {
/// Possible values are "none" and "tree-traversal". Default is "none".
expansion: Option<core::pack::create::ObjectExpansion>,

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

0 comments on commit 08e8fc2

Please sign in to comment.