diff --git a/gitoxide-core/src/repository/status.rs b/gitoxide-core/src/repository/status.rs index fe7ac0174a7..33a00166756 100644 --- a/gitoxide-core/src/repository/status.rs +++ b/gitoxide-core/src/repository/status.rs @@ -23,6 +23,7 @@ pub struct Options { pub thread_limit: Option, pub statistics: bool, pub allow_write: bool, + pub index_worktree_renames: Option, } pub fn show( @@ -37,6 +38,7 @@ pub fn show( thread_limit, allow_write, statistics, + index_worktree_renames, }: Options, ) -> anyhow::Result<()> { if format != OutputFormat::Human { @@ -50,6 +52,16 @@ pub fn show( .status(index_progress)? .should_interrupt_shared(&gix::interrupt::IS_INTERRUPTED) .index_worktree_options_mut(|opts| { + opts.rewrites = index_worktree_renames.map(|percentage| gix::diff::Rewrites { + copies: None, + percentage: Some(percentage), + limit: 0, + }); + if opts.rewrites.is_some() { + if let Some(opts) = opts.dirwalk_options.as_mut() { + opts.set_emit_untracked(gix::dir::walk::EmissionMode::Matching); + } + } opts.thread_limit = thread_limit; opts.sorting = Some(gix::status::plumbing::index_as_worktree_with_renames::Sorting::ByPathCaseSensitive); }) @@ -85,14 +97,38 @@ pub fn show( if collapsed_directory_status.is_none() { writeln!( out, - "{status: >3} {rela_path}", + "{status: >3} {rela_path}{slash}", status = "?", rela_path = - gix::path::relativize_with_prefix(&gix::path::from_bstr(entry.rela_path), prefix).display() + gix::path::relativize_with_prefix(&gix::path::from_bstr(entry.rela_path), prefix).display(), + slash = if entry.disk_kind.unwrap_or(gix::dir::entry::Kind::File).is_dir() { + "/" + } else { + "" + } )?; } } - Item::Rewrite { .. } => {} + Item::Rewrite { + source, + dirwalk_entry, + copy: _, // TODO: how to visualize copies? + .. + } => { + // TODO: handle multi-status characters, there can also be modifications at the same time as determined by their ID and potentially diffstats. + writeln!( + out, + "{status: >3} {source_rela_path} → {dest_rela_path}", + status = "R", + source_rela_path = + gix::path::relativize_with_prefix(&gix::path::from_bstr(source.rela_path()), prefix).display(), + dest_rela_path = gix::path::relativize_with_prefix( + &gix::path::from_bstr(dirwalk_entry.rela_path.as_ref()), + prefix + ) + .display(), + )?; + } } } if gix::interrupt::is_triggered() { diff --git a/gix/src/dirwalk.rs b/gix/src/dirwalk.rs index 59357662c63..ab159cff2f5 100644 --- a/gix/src/dirwalk.rs +++ b/gix/src/dirwalk.rs @@ -68,23 +68,43 @@ impl Options { self.empty_patterns_match_prefix = toggle; self } + /// Like [`empty_patterns_match_prefix()`](Self::empty_patterns_match_prefix), but only requires a mutably borrowed instance. + pub fn set_empty_patterns_match_prefix(&mut self, toggle: bool) -> &mut Self { + self.empty_patterns_match_prefix = toggle; + self + } /// If `toggle` is `true`, we will stop figuring out if any directory that is a candidate for recursion is also a nested repository, /// which saves time but leads to recurse into it. If `false`, nested repositories will not be traversed. pub fn recurse_repositories(mut self, toggle: bool) -> Self { self.recurse_repositories = toggle; self } + /// Like [`recurse_repositories()`](Self::recurse_repositories), but only requires a mutably borrowed instance. + pub fn set_recurse_repositories(&mut self, toggle: bool) -> &mut Self { + self.recurse_repositories = toggle; + self + } /// If `toggle` is `true`, entries that are pruned and whose [Kind](gix_dir::entry::Kind) is known will be emitted. pub fn emit_pruned(mut self, toggle: bool) -> Self { self.emit_pruned = toggle; self } + /// Like [`emit_pruned()`](Self::emit_pruned), but only requires a mutably borrowed instance. + pub fn set_emit_pruned(&mut self, toggle: bool) -> &mut Self { + self.emit_pruned = toggle; + self + } /// If `value` is `Some(mode)`, entries that are ignored will be emitted according to the given `mode`. /// If `None`, ignored entries will not be emitted at all. pub fn emit_ignored(mut self, value: Option) -> Self { self.emit_ignored = value; self } + /// Like [`emit_ignored()`](Self::emit_ignored), but only requires a mutably borrowed instance. + pub fn set_emit_ignored(&mut self, value: Option) -> &mut Self { + self.emit_ignored = value; + self + } /// When the walk is for deletion, `value` must be `Some(_)` to assure we don't collapse directories that have precious files in /// them, and otherwise assure that no entries are observable that shouldn't be deleted. /// If `None`, precious files are treated like expendable files, which is usually what you want when displaying them @@ -93,17 +113,32 @@ impl Options { self.for_deletion = value; self } + /// Like [`for_deletion()`](Self::for_deletion), but only requires a mutably borrowed instance. + pub fn set_for_deletion(&mut self, value: Option) -> &mut Self { + self.for_deletion = value; + self + } /// If `toggle` is `true`, we will also emit entries for tracked items. Otherwise these will remain 'hidden', /// even if a pathspec directly refers to it. pub fn emit_tracked(mut self, toggle: bool) -> Self { self.emit_tracked = toggle; self } + /// Like [`emit_tracked()`](Self::emit_tracked), but only requires a mutably borrowed instance. + pub fn set_emit_tracked(&mut self, toggle: bool) -> &mut Self { + self.emit_tracked = toggle; + self + } /// Controls the way untracked files are emitted. By default, this is happening immediately and without any simplification. pub fn emit_untracked(mut self, toggle: EmissionMode) -> Self { self.emit_untracked = toggle; self } + /// Like [`emit_untracked()`](Self::emit_untracked), but only requires a mutably borrowed instance. + pub fn set_emit_untracked(&mut self, toggle: EmissionMode) -> &mut Self { + self.emit_untracked = toggle; + self + } /// If `toggle` is `true`, emit empty directories as well. Note that a directory also counts as empty if it has any /// amount or depth of nested subdirectories, as long as none of them includes a file. /// Thus, this makes leaf-level empty directories visible, as those don't have any content. @@ -112,6 +147,12 @@ impl Options { self } + /// Like [`emit_empty_directories()`](Self::emit_empty_directories), but only requires a mutably borrowed instance. + pub fn set_emit_empty_directories(&mut self, toggle: bool) -> &mut Self { + self.emit_empty_directories = toggle; + self + } + /// If `toggle` is `true`, we will not only find non-bare repositories in untracked directories, but also bare ones. /// /// Note that this is very costly, but without it, bare repositories will appear like untracked directories when collapsed, @@ -121,10 +162,22 @@ impl Options { self } + /// Like [`classify_untracked_bare_repositories()`](Self::classify_untracked_bare_repositories), but only requires a mutably borrowed instance. + pub fn set_classify_untracked_bare_repositories(&mut self, toggle: bool) -> &mut Self { + self.classify_untracked_bare_repositories = toggle; + self + } + /// Control whether entries that are in an about-to-be collapsed directory will be emitted. The default is `None`, /// so entries in a collapsed directory are not observable. pub fn emit_collapsed(mut self, value: Option) -> Self { self.emit_collapsed = value; self } + + /// Like [`emit_collapsed()`](Self::emit_collapsed), but only requires a mutably borrowed instance. + pub fn set_emit_collapsed(&mut self, value: Option) -> &mut Self { + self.emit_collapsed = value; + self + } } diff --git a/gix/src/status/index_worktree.rs b/gix/src/status/index_worktree.rs index 7d7016c90c2..35342e3f575 100644 --- a/gix/src/status/index_worktree.rs +++ b/gix/src/status/index_worktree.rs @@ -310,7 +310,7 @@ pub struct Iter { /// #[allow(clippy::empty_docs)] pub mod iter { - use crate::bstr::BString; + use crate::bstr::{BStr, BString}; use crate::config::cache::util::ApplyLeniencyDefault; use crate::status::index_worktree::{iter, BuiltinSubmoduleStatus}; use crate::status::{index_worktree, Platform}; @@ -406,6 +406,19 @@ pub mod iter { }, } + /// Access + impl RewriteSource { + /// The repository-relative path of this source. + pub fn rela_path(&self) -> &BStr { + match self { + RewriteSource::RewriteFromIndex { source_rela_path, .. } => source_rela_path.as_ref(), + RewriteSource::CopyFromDirectoryEntry { + source_dirwalk_entry, .. + } => source_dirwalk_entry.rela_path.as_ref(), + } + } + } + impl<'index> From> for RewriteSource { diff --git a/gix/src/status/platform.rs b/gix/src/status/platform.rs index 10f8844107f..318db07435b 100644 --- a/gix/src/status/platform.rs +++ b/gix/src/status/platform.rs @@ -16,7 +16,13 @@ where } self } - + /// Like [dirwalk_options()](Self::dirwalk_options), but taking a mutable instance instead. + pub fn dirwalk_options_mut(&mut self, cb: impl FnOnce(&mut crate::dirwalk::Options)) -> &mut Self { + if let Some(opts) = self.index_worktree_options.dirwalk_options.as_mut() { + cb(opts); + } + self + } /// A simple way to explicitly set the desired way of listing `untracked_files`, overriding any value /// set by the git configuration. /// diff --git a/src/plumbing/main.rs b/src/plumbing/main.rs index 678306f1073..40840d545ea 100644 --- a/src/plumbing/main.rs +++ b/src/plumbing/main.rs @@ -210,6 +210,7 @@ pub fn main() -> Result<()> { submodules, no_write, pathspec, + index_worktree_renames, }) => prepare_and_run( "status", trace, @@ -230,6 +231,7 @@ pub fn main() -> Result<()> { statistics, 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! MacOS also scales if reading a lot of files for refresh index allow_write: !no_write, + index_worktree_renames: index_worktree_renames.map(|percentage| percentage.unwrap_or(0.5)), submodules: submodules.map(|submodules| match submodules { Submodules::All => core::repository::status::Submodules::All, Submodules::RefChange => core::repository::status::Submodules::RefChange, diff --git a/src/plumbing/options/mod.rs b/src/plumbing/options/mod.rs index 14749704181..65e62779482 100644 --- a/src/plumbing/options/mod.rs +++ b/src/plumbing/options/mod.rs @@ -201,7 +201,7 @@ pub mod archive { } pub mod status { - use gitoxide::shared::CheckPathSpec; + use gitoxide::shared::{CheckPathSpec, ParseRenameFraction}; use gix::bstr::BString; #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] @@ -229,6 +229,9 @@ pub mod status { /// Don't write back a changed index, which forces this operation to always be idempotent. #[clap(long)] pub no_write: bool, + /// Enable rename tracking between the index and the working tree, preventing the collapse of folders as well. + #[clap(long, value_parser = ParseRenameFraction)] + pub index_worktree_renames: Option>, /// The git path specifications to list attributes for, or unset to read from stdin one per line. #[clap(value_parser = CheckPathSpec)] pub pathspec: Vec, diff --git a/src/shared.rs b/src/shared.rs index b3d159c853b..8a51aa75993 100644 --- a/src/shared.rs +++ b/src/shared.rs @@ -361,6 +361,28 @@ mod clap { } } + #[derive(Clone)] + pub struct ParseRenameFraction; + + impl TypedValueParser for ParseRenameFraction { + type Value = f32; + + fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result { + StringValueParser::new() + .try_map(|arg: String| -> Result<_, Box> { + if arg.ends_with('%') { + let val = u32::from_str(&arg[..arg.len() - 1])?; + Ok(val as f32 / 100.0) + } else { + let val = u32::from_str(&arg)?; + let num = format!("0.{val}"); + Ok(f32::from_str(&num)?) + } + }) + .parse_ref(cmd, arg, value) + } + } + #[derive(Clone)] pub struct AsTime; @@ -387,4 +409,36 @@ mod clap { } } } -pub use self::clap::{AsBString, AsHashKind, AsOutputFormat, AsPartialRefName, AsPathSpec, AsTime, CheckPathSpec}; +pub use self::clap::{ + AsBString, AsHashKind, AsOutputFormat, AsPartialRefName, AsPathSpec, AsTime, CheckPathSpec, ParseRenameFraction, +}; + +#[cfg(test)] +mod value_parser_tests { + use super::ParseRenameFraction; + use clap::Parser; + + #[test] + fn rename_fraction() { + #[derive(Debug, clap::Parser)] + pub struct Cmd { + #[clap(long, short='a', value_parser = ParseRenameFraction)] + pub arg: Option>, + } + + let c = Cmd::parse_from(["cmd", "-a"]); + assert_eq!(c.arg, Some(None), "this means we need to fill in the default"); + + let c = Cmd::parse_from(["cmd", "-a=50%"]); + assert_eq!(c.arg, Some(Some(0.5)), "percentages become a fraction"); + + let c = Cmd::parse_from(["cmd", "-a=100%"]); + assert_eq!(c.arg, Some(Some(1.0))); + + let c = Cmd::parse_from(["cmd", "-a=5"]); + assert_eq!(c.arg, Some(Some(0.5)), "another way to specify fractions"); + + let c = Cmd::parse_from(["cmd", "-a=75"]); + assert_eq!(c.arg, Some(Some(0.75))); + } +}