Skip to content

Commit

Permalink
feat: add gix status --index-worktree-renames
Browse files Browse the repository at this point in the history
This enables rename-tracking between worktree and index, something
that Git also doesn't do or doesn't do by default.
It is, however, available in `git2`.
  • Loading branch information
Byron committed Mar 12, 2024
1 parent 22abf60 commit 66e87cd
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 7 deletions.
42 changes: 39 additions & 3 deletions gitoxide-core/src/repository/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub struct Options {
pub thread_limit: Option<usize>,
pub statistics: bool,
pub allow_write: bool,
pub index_worktree_renames: Option<f32>,
}

pub fn show(
Expand All @@ -37,6 +38,7 @@ pub fn show(
thread_limit,
allow_write,
statistics,
index_worktree_renames,
}: Options,
) -> anyhow::Result<()> {
if format != OutputFormat::Human {
Expand All @@ -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);
})
Expand Down Expand Up @@ -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() {
Expand Down
53 changes: 53 additions & 0 deletions gix/src/dirwalk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<EmissionMode>) -> 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<EmissionMode>) -> &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
Expand All @@ -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<ForDeletionMode>) -> &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.
Expand All @@ -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,
Expand All @@ -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<CollapsedEntriesEmissionMode>) -> 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<CollapsedEntriesEmissionMode>) -> &mut Self {
self.emit_collapsed = value;
self
}
}
15 changes: 14 additions & 1 deletion gix/src/status/index_worktree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<gix_status::index_as_worktree_with_renames::RewriteSource<'index, (), SubmoduleStatus>>
for RewriteSource
{
Expand Down
8 changes: 7 additions & 1 deletion gix/src/status/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
2 changes: 2 additions & 0 deletions src/plumbing/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ pub fn main() -> Result<()> {
submodules,
no_write,
pathspec,
index_worktree_renames,
}) => prepare_and_run(
"status",
trace,
Expand All @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion src/plumbing/options/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<Option<f32>>,
/// The git path specifications to list attributes for, or unset to read from stdin one per line.
#[clap(value_parser = CheckPathSpec)]
pub pathspec: Vec<BString>,
Expand Down
56 changes: 55 additions & 1 deletion src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self::Value, Error> {
StringValueParser::new()
.try_map(|arg: String| -> Result<_, Box<dyn std::error::Error + Send + Sync>> {
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;

Expand All @@ -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<Option<f32>>,
}

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)));
}
}

0 comments on commit 66e87cd

Please sign in to comment.