diff --git a/Cargo.lock b/Cargo.lock index 918c90b6698..6df2c8fbacd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1179,6 +1179,7 @@ dependencies = [ "gix", "gix-archive", "gix-pack", + "gix-status", "gix-transport", "gix-url", "itertools 0.11.0", diff --git a/gitoxide-core/Cargo.toml b/gitoxide-core/Cargo.toml index 5b0c97471df..abbc8623512 100644 --- a/gitoxide-core/Cargo.toml +++ b/gitoxide-core/Cargo.toml @@ -44,10 +44,11 @@ serde = ["gix/serde", "dep:serde_json", "dep:serde", "bytesize/serde"] [dependencies] # deselect everything else (like "performance") as this should be controllable by the parent application. -gix = { version = "^0.53.1", path = "../gix", default-features = false, features = ["blob-diff", "revision", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt"] } +gix = { version = "^0.53.1", path = "../gix", default-features = false, features = ["blob-diff", "revision", "mailmap", "excludes", "attributes", "worktree-mutation", "credentials", "interrupt", "status"] } gix-pack-for-configuration-only = { package = "gix-pack", version = "^0.42.0", path = "../gix-pack", default-features = false, features = ["pack-cache-lru-dynamic", "pack-cache-lru-static", "generate", "streaming-input"] } gix-transport-configuration-only = { package = "gix-transport", version = "^0.36.0", path = "../gix-transport", default-features = false } gix-archive-for-configuration-only = { package = "gix-archive", version = "^0.4.0", path = "../gix-archive", optional = true, features = ["tar", "tar_gz"] } +gix-status = { version = "0.1.0", path = "../gix-status" } serde = { version = "1.0.114", optional = true, default-features = false, features = ["derive"] } anyhow = "1.0.42" thiserror = "1.0.34" diff --git a/gitoxide-core/src/repository/mod.rs b/gitoxide-core/src/repository/mod.rs index be4d4dfd0bf..c78e82edb85 100644 --- a/gitoxide-core/src/repository/mod.rs +++ b/gitoxide-core/src/repository/mod.rs @@ -40,6 +40,7 @@ pub mod mailmap; pub mod odb; pub mod remote; pub mod revision; +pub mod status; pub mod submodule; pub mod tree; pub mod verify; diff --git a/gitoxide-core/src/repository/status.rs b/gitoxide-core/src/repository/status.rs new file mode 100644 index 00000000000..2b892d53190 --- /dev/null +++ b/gitoxide-core/src/repository/status.rs @@ -0,0 +1,120 @@ +use crate::OutputFormat; +use anyhow::{bail, Context}; +use gix::bstr::{BStr, BString}; +use gix::index::Entry; +use gix::prelude::FindExt; +use gix::Progress; +use gix_status::index_as_worktree::content::FastEq; +use gix_status::index_as_worktree::Change; + +pub enum Submodules { + /// display all information about submodules, including ref changes, modifications and untracked files. + All, + /// Compare only the configuration of the superprojects commit with the actually checked out `HEAD` commit. + RefChange, + /// See if there are worktree modifications compared to the index, but do not check for untracked files. + Modifications, +} + +pub struct Options { + pub format: OutputFormat, + pub submodules: Submodules, + pub thread_limit: Option, +} + +pub fn show( + repo: gix::Repository, + pathspecs: Vec, + out: impl std::io::Write, + mut err: impl std::io::Write, + mut progress: impl gix::NestedProgress, + Options { + format, + // TODO: implement this + submodules: _, + thread_limit, + }: Options, +) -> anyhow::Result<()> { + if format != OutputFormat::Human { + bail!("Only human format is supported right now"); + } + let mut index = repo.index()?; + let index = gix::threading::make_mut(&mut index); + let pathspec = repo.pathspec( + pathspecs, + true, + index, + gix::worktree::stack::state::attributes::Source::WorktreeThenIdMapping, + )?; + let mut progress = progress.add_child("traverse index"); + let start = std::time::Instant::now(); + gix_status::index_as_worktree( + index, + repo.work_dir() + .context("This operation cannot be run on a bare repository")?, + &mut Printer(out), + FastEq, + { + let odb = repo.objects.clone().into_arc()?; + move |id, buf| odb.find_blob(id, buf) + }, + &mut progress, + pathspec.detach()?, + gix_status::index_as_worktree::Options { + fs: repo.filesystem_options()?, + thread_limit, + stat: repo.stat_options()?, + }, + )?; + + writeln!(err, "\nhead -> index and untracked files aren't implemented yet")?; + progress.show_throughput(start); + Ok(()) +} + +struct Printer(W); + +impl<'index, W> gix_status::index_as_worktree::VisitEntry<'index> for Printer +where + W: std::io::Write, +{ + type ContentChange = (); + + fn visit_entry( + &mut self, + entry: &'index Entry, + rela_path: &'index BStr, + change: Option>, + conflict: bool, + ) { + self.visit_inner(entry, rela_path, change, conflict).ok(); + } +} + +impl Printer { + fn visit_inner( + &mut self, + _entry: &Entry, + rela_path: &BStr, + change: Option>, + conflict: bool, + ) -> anyhow::Result<()> { + if let Some(change) = conflict + .then_some('U') + .or_else(|| change.as_ref().and_then(change_to_char)) + { + writeln!(&mut self.0, "{change} {rela_path}")?; + } + Ok(()) + } +} + +fn change_to_char(change: &Change<()>) -> Option { + // Known status letters: https://github.com/git/git/blob/6807fcfedab84bc8cd0fbf721bc13c4e68cda9ae/diff.h#L613 + Some(match change { + Change::Removed => 'D', + Change::Type => 'T', + Change::Modification { .. } => 'M', + Change::IntentToAdd => return None, + }) +} diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 5ae9a9e3e78..04a4fa36bcd 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -133,6 +133,33 @@ pub fn main() -> Result<()> { })?; match cmd { + Subcommands::Status(crate::plumbing::options::status::Platform { submodules, pathspec }) => prepare_and_run( + "status", + trace, + auto_verbose, + progress, + progress_keep_open, + None, + move |progress, out, err| { + use crate::plumbing::options::status::Submodules; + core::repository::status::show( + repository(Mode::Lenient)?, + pathspec, + out, + err, + progress, + core::repository::status::Options { + format, + thread_limit: thread_limit.or(cfg!(target_os = "macos").then_some(3)), // TODO: make this a configurable when in `gix`, this seems to be optimal on MacOS, linux scales though! + submodules: match submodules { + Submodules::All => core::repository::status::Submodules::All, + Submodules::RefChange => core::repository::status::Submodules::RefChange, + Submodules::Modifications => core::repository::status::Submodules::Modifications, + }, + }, + ) + }, + ), Subcommands::Submodule(platform) => match platform .cmds .unwrap_or(crate::plumbing::options::submodule::Subcommands::List) diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index 54a6d8db3c8..300f1810e61 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -127,6 +127,7 @@ pub enum Subcommands { Submodule(submodule::Platform), /// Show which git configuration values are used or planned. ConfigTree, + Status(status::Platform), Config(config::Platform), #[cfg(feature = "gitoxide-core-tools-corpus")] Corpus(corpus::Platform), @@ -183,6 +184,33 @@ pub mod archive { } } +pub mod status { + use gitoxide::shared::CheckPathSpec; + use gix::bstr::BString; + + #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] + pub enum Submodules { + /// display all information about submodules, including ref changes, modifications and untracked files. + #[default] + All, + /// Compare only the configuration of the superprojects commit with the actually checked out `HEAD` commit. + RefChange, + /// See if there are worktree modifications compared to the index, but do not check for untracked files. + Modifications, + } + + #[derive(Debug, clap::Parser)] + #[command(about = "compute repository status similar to `git status`")] + pub struct Platform { + /// Define how to display submodule status. + #[clap(long, default_value = "all")] + pub submodules: Submodules, + /// The git path specifications to list attributes for, or unset to read from stdin one per line. + #[clap(value_parser = CheckPathSpec)] + pub pathspec: Vec, + } +} + #[cfg(feature = "gitoxide-core-tools-corpus")] pub mod corpus { use std::path::PathBuf;