diff --git a/gitoxide-core/src/repository/index/entries.rs b/gitoxide-core/src/repository/index/entries.rs index 05227dd88d2..07241e52f52 100644 --- a/gitoxide-core/src/repository/index/entries.rs +++ b/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, + 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, + } #[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, } - 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, + 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, + } + + 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, + ) -> 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("")) + ) + } } diff --git a/gitoxide-core/src/repository/index/mod.rs b/gitoxide-core/src/repository/index/mod.rs index 7e1b3c274da..bdaa61b5abf 100644 --- a/gitoxide-core/src/repository/index/mod.rs +++ b/gitoxide-core/src/repository/index/mod.rs @@ -35,5 +35,5 @@ pub fn from_tree( Ok(()) } -mod entries; -pub use entries::entries; +pub mod entries; +pub use entries::function::entries; diff --git a/gix/Cargo.toml b/gix/Cargo.toml index 6f557886087..3b6c6bca844 100644 --- a/gix/Cargo.toml +++ b/gix/Cargo.toml @@ -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`. diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index ac05d3964fc..6aafdb5f2ce 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -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 { @@ -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(); + } +} diff --git a/src/plumbing/options/free.rs b/src/plumbing/options/free.rs index 00641f33b28..2869fb0cedf 100644 --- a/src/plumbing/options/free.rs +++ b/src/plumbing/options/free.rs @@ -105,7 +105,7 @@ pub mod pack { /// Possible values are "none" and "tree-traversal". Default is "none". expansion: Option, - #[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. /// diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index d2b8d7805d2..107da1b414f 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -361,11 +361,11 @@ pub mod commit { /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry. Describe { /// Use annotated tag references only, not all tags. - #[clap(long, short = 't', conflicts_with("all-refs"))] + #[clap(long, short = 't', conflicts_with("all_refs"))] annotated_tags: bool, /// Use all references under the `ref/` namespaces, which includes tag references, local and remote branches. - #[clap(long, short = 'a', conflicts_with("annotated-tags"))] + #[clap(long, short = 'a', conflicts_with("annotated_tags"))] all_refs: bool, /// Only follow the first parent when traversing the commit graph. @@ -472,7 +472,19 @@ pub mod index { #[derive(Debug, clap::Subcommand)] pub enum Subcommands { /// Print all entries to standard output - Entries, + Entries { + /// Do not visualize excluded entries or attributes per path. + #[clap(long)] + no_attributes: bool, + /// Load attribute and ignore files from the index, don't look at the worktree. + /// + /// This is to see what IO for probing attribute/ignore files does to performance. + #[clap(long, short = 'i', conflicts_with = "no_attributes")] + attributes_from_index: bool, + /// Print various statistics to stderr + #[clap(long, short = 's')] + statistics: bool, + }, /// Create an index from a tree-ish. #[clap(visible_alias = "read-tree")] FromTree {