Skip to content

Commit 66e87cd

Browse files
committed
feat: add gix status --index-worktree-renames
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`.
1 parent 22abf60 commit 66e87cd

File tree

7 files changed

+174
-7
lines changed

7 files changed

+174
-7
lines changed

gitoxide-core/src/repository/status.rs

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pub struct Options {
2323
pub thread_limit: Option<usize>,
2424
pub statistics: bool,
2525
pub allow_write: bool,
26+
pub index_worktree_renames: Option<f32>,
2627
}
2728

2829
pub fn show(
@@ -37,6 +38,7 @@ pub fn show(
3738
thread_limit,
3839
allow_write,
3940
statistics,
41+
index_worktree_renames,
4042
}: Options,
4143
) -> anyhow::Result<()> {
4244
if format != OutputFormat::Human {
@@ -50,6 +52,16 @@ pub fn show(
5052
.status(index_progress)?
5153
.should_interrupt_shared(&gix::interrupt::IS_INTERRUPTED)
5254
.index_worktree_options_mut(|opts| {
55+
opts.rewrites = index_worktree_renames.map(|percentage| gix::diff::Rewrites {
56+
copies: None,
57+
percentage: Some(percentage),
58+
limit: 0,
59+
});
60+
if opts.rewrites.is_some() {
61+
if let Some(opts) = opts.dirwalk_options.as_mut() {
62+
opts.set_emit_untracked(gix::dir::walk::EmissionMode::Matching);
63+
}
64+
}
5365
opts.thread_limit = thread_limit;
5466
opts.sorting = Some(gix::status::plumbing::index_as_worktree_with_renames::Sorting::ByPathCaseSensitive);
5567
})
@@ -85,14 +97,38 @@ pub fn show(
8597
if collapsed_directory_status.is_none() {
8698
writeln!(
8799
out,
88-
"{status: >3} {rela_path}",
100+
"{status: >3} {rela_path}{slash}",
89101
status = "?",
90102
rela_path =
91-
gix::path::relativize_with_prefix(&gix::path::from_bstr(entry.rela_path), prefix).display()
103+
gix::path::relativize_with_prefix(&gix::path::from_bstr(entry.rela_path), prefix).display(),
104+
slash = if entry.disk_kind.unwrap_or(gix::dir::entry::Kind::File).is_dir() {
105+
"/"
106+
} else {
107+
""
108+
}
92109
)?;
93110
}
94111
}
95-
Item::Rewrite { .. } => {}
112+
Item::Rewrite {
113+
source,
114+
dirwalk_entry,
115+
copy: _, // TODO: how to visualize copies?
116+
..
117+
} => {
118+
// TODO: handle multi-status characters, there can also be modifications at the same time as determined by their ID and potentially diffstats.
119+
writeln!(
120+
out,
121+
"{status: >3} {source_rela_path} → {dest_rela_path}",
122+
status = "R",
123+
source_rela_path =
124+
gix::path::relativize_with_prefix(&gix::path::from_bstr(source.rela_path()), prefix).display(),
125+
dest_rela_path = gix::path::relativize_with_prefix(
126+
&gix::path::from_bstr(dirwalk_entry.rela_path.as_ref()),
127+
prefix
128+
)
129+
.display(),
130+
)?;
131+
}
96132
}
97133
}
98134
if gix::interrupt::is_triggered() {

gix/src/dirwalk.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,23 +68,43 @@ impl Options {
6868
self.empty_patterns_match_prefix = toggle;
6969
self
7070
}
71+
/// Like [`empty_patterns_match_prefix()`](Self::empty_patterns_match_prefix), but only requires a mutably borrowed instance.
72+
pub fn set_empty_patterns_match_prefix(&mut self, toggle: bool) -> &mut Self {
73+
self.empty_patterns_match_prefix = toggle;
74+
self
75+
}
7176
/// If `toggle` is `true`, we will stop figuring out if any directory that is a candidate for recursion is also a nested repository,
7277
/// which saves time but leads to recurse into it. If `false`, nested repositories will not be traversed.
7378
pub fn recurse_repositories(mut self, toggle: bool) -> Self {
7479
self.recurse_repositories = toggle;
7580
self
7681
}
82+
/// Like [`recurse_repositories()`](Self::recurse_repositories), but only requires a mutably borrowed instance.
83+
pub fn set_recurse_repositories(&mut self, toggle: bool) -> &mut Self {
84+
self.recurse_repositories = toggle;
85+
self
86+
}
7787
/// If `toggle` is `true`, entries that are pruned and whose [Kind](gix_dir::entry::Kind) is known will be emitted.
7888
pub fn emit_pruned(mut self, toggle: bool) -> Self {
7989
self.emit_pruned = toggle;
8090
self
8191
}
92+
/// Like [`emit_pruned()`](Self::emit_pruned), but only requires a mutably borrowed instance.
93+
pub fn set_emit_pruned(&mut self, toggle: bool) -> &mut Self {
94+
self.emit_pruned = toggle;
95+
self
96+
}
8297
/// If `value` is `Some(mode)`, entries that are ignored will be emitted according to the given `mode`.
8398
/// If `None`, ignored entries will not be emitted at all.
8499
pub fn emit_ignored(mut self, value: Option<EmissionMode>) -> Self {
85100
self.emit_ignored = value;
86101
self
87102
}
103+
/// Like [`emit_ignored()`](Self::emit_ignored), but only requires a mutably borrowed instance.
104+
pub fn set_emit_ignored(&mut self, value: Option<EmissionMode>) -> &mut Self {
105+
self.emit_ignored = value;
106+
self
107+
}
88108
/// When the walk is for deletion, `value` must be `Some(_)` to assure we don't collapse directories that have precious files in
89109
/// them, and otherwise assure that no entries are observable that shouldn't be deleted.
90110
/// If `None`, precious files are treated like expendable files, which is usually what you want when displaying them
@@ -93,17 +113,32 @@ impl Options {
93113
self.for_deletion = value;
94114
self
95115
}
116+
/// Like [`for_deletion()`](Self::for_deletion), but only requires a mutably borrowed instance.
117+
pub fn set_for_deletion(&mut self, value: Option<ForDeletionMode>) -> &mut Self {
118+
self.for_deletion = value;
119+
self
120+
}
96121
/// If `toggle` is `true`, we will also emit entries for tracked items. Otherwise these will remain 'hidden',
97122
/// even if a pathspec directly refers to it.
98123
pub fn emit_tracked(mut self, toggle: bool) -> Self {
99124
self.emit_tracked = toggle;
100125
self
101126
}
127+
/// Like [`emit_tracked()`](Self::emit_tracked), but only requires a mutably borrowed instance.
128+
pub fn set_emit_tracked(&mut self, toggle: bool) -> &mut Self {
129+
self.emit_tracked = toggle;
130+
self
131+
}
102132
/// Controls the way untracked files are emitted. By default, this is happening immediately and without any simplification.
103133
pub fn emit_untracked(mut self, toggle: EmissionMode) -> Self {
104134
self.emit_untracked = toggle;
105135
self
106136
}
137+
/// Like [`emit_untracked()`](Self::emit_untracked), but only requires a mutably borrowed instance.
138+
pub fn set_emit_untracked(&mut self, toggle: EmissionMode) -> &mut Self {
139+
self.emit_untracked = toggle;
140+
self
141+
}
107142
/// If `toggle` is `true`, emit empty directories as well. Note that a directory also counts as empty if it has any
108143
/// amount or depth of nested subdirectories, as long as none of them includes a file.
109144
/// Thus, this makes leaf-level empty directories visible, as those don't have any content.
@@ -112,6 +147,12 @@ impl Options {
112147
self
113148
}
114149

150+
/// Like [`emit_empty_directories()`](Self::emit_empty_directories), but only requires a mutably borrowed instance.
151+
pub fn set_emit_empty_directories(&mut self, toggle: bool) -> &mut Self {
152+
self.emit_empty_directories = toggle;
153+
self
154+
}
155+
115156
/// If `toggle` is `true`, we will not only find non-bare repositories in untracked directories, but also bare ones.
116157
///
117158
/// Note that this is very costly, but without it, bare repositories will appear like untracked directories when collapsed,
@@ -121,10 +162,22 @@ impl Options {
121162
self
122163
}
123164

165+
/// Like [`classify_untracked_bare_repositories()`](Self::classify_untracked_bare_repositories), but only requires a mutably borrowed instance.
166+
pub fn set_classify_untracked_bare_repositories(&mut self, toggle: bool) -> &mut Self {
167+
self.classify_untracked_bare_repositories = toggle;
168+
self
169+
}
170+
124171
/// Control whether entries that are in an about-to-be collapsed directory will be emitted. The default is `None`,
125172
/// so entries in a collapsed directory are not observable.
126173
pub fn emit_collapsed(mut self, value: Option<CollapsedEntriesEmissionMode>) -> Self {
127174
self.emit_collapsed = value;
128175
self
129176
}
177+
178+
/// Like [`emit_collapsed()`](Self::emit_collapsed), but only requires a mutably borrowed instance.
179+
pub fn set_emit_collapsed(&mut self, value: Option<CollapsedEntriesEmissionMode>) -> &mut Self {
180+
self.emit_collapsed = value;
181+
self
182+
}
130183
}

gix/src/status/index_worktree.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ pub struct Iter {
310310
///
311311
#[allow(clippy::empty_docs)]
312312
pub mod iter {
313-
use crate::bstr::BString;
313+
use crate::bstr::{BStr, BString};
314314
use crate::config::cache::util::ApplyLeniencyDefault;
315315
use crate::status::index_worktree::{iter, BuiltinSubmoduleStatus};
316316
use crate::status::{index_worktree, Platform};
@@ -406,6 +406,19 @@ pub mod iter {
406406
},
407407
}
408408

409+
/// Access
410+
impl RewriteSource {
411+
/// The repository-relative path of this source.
412+
pub fn rela_path(&self) -> &BStr {
413+
match self {
414+
RewriteSource::RewriteFromIndex { source_rela_path, .. } => source_rela_path.as_ref(),
415+
RewriteSource::CopyFromDirectoryEntry {
416+
source_dirwalk_entry, ..
417+
} => source_dirwalk_entry.rela_path.as_ref(),
418+
}
419+
}
420+
}
421+
409422
impl<'index> From<gix_status::index_as_worktree_with_renames::RewriteSource<'index, (), SubmoduleStatus>>
410423
for RewriteSource
411424
{

gix/src/status/platform.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ where
1616
}
1717
self
1818
}
19-
19+
/// Like [dirwalk_options()](Self::dirwalk_options), but taking a mutable instance instead.
20+
pub fn dirwalk_options_mut(&mut self, cb: impl FnOnce(&mut crate::dirwalk::Options)) -> &mut Self {
21+
if let Some(opts) = self.index_worktree_options.dirwalk_options.as_mut() {
22+
cb(opts);
23+
}
24+
self
25+
}
2026
/// A simple way to explicitly set the desired way of listing `untracked_files`, overriding any value
2127
/// set by the git configuration.
2228
///

src/plumbing/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ pub fn main() -> Result<()> {
210210
submodules,
211211
no_write,
212212
pathspec,
213+
index_worktree_renames,
213214
}) => prepare_and_run(
214215
"status",
215216
trace,
@@ -230,6 +231,7 @@ pub fn main() -> Result<()> {
230231
statistics,
231232
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
232233
allow_write: !no_write,
234+
index_worktree_renames: index_worktree_renames.map(|percentage| percentage.unwrap_or(0.5)),
233235
submodules: submodules.map(|submodules| match submodules {
234236
Submodules::All => core::repository::status::Submodules::All,
235237
Submodules::RefChange => core::repository::status::Submodules::RefChange,

src/plumbing/options/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ pub mod archive {
201201
}
202202

203203
pub mod status {
204-
use gitoxide::shared::CheckPathSpec;
204+
use gitoxide::shared::{CheckPathSpec, ParseRenameFraction};
205205
use gix::bstr::BString;
206206

207207
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)]
@@ -229,6 +229,9 @@ pub mod status {
229229
/// Don't write back a changed index, which forces this operation to always be idempotent.
230230
#[clap(long)]
231231
pub no_write: bool,
232+
/// Enable rename tracking between the index and the working tree, preventing the collapse of folders as well.
233+
#[clap(long, value_parser = ParseRenameFraction)]
234+
pub index_worktree_renames: Option<Option<f32>>,
232235
/// The git path specifications to list attributes for, or unset to read from stdin one per line.
233236
#[clap(value_parser = CheckPathSpec)]
234237
pub pathspec: Vec<BString>,

src/shared.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,28 @@ mod clap {
361361
}
362362
}
363363

364+
#[derive(Clone)]
365+
pub struct ParseRenameFraction;
366+
367+
impl TypedValueParser for ParseRenameFraction {
368+
type Value = f32;
369+
370+
fn parse_ref(&self, cmd: &Command, arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, Error> {
371+
StringValueParser::new()
372+
.try_map(|arg: String| -> Result<_, Box<dyn std::error::Error + Send + Sync>> {
373+
if arg.ends_with('%') {
374+
let val = u32::from_str(&arg[..arg.len() - 1])?;
375+
Ok(val as f32 / 100.0)
376+
} else {
377+
let val = u32::from_str(&arg)?;
378+
let num = format!("0.{val}");
379+
Ok(f32::from_str(&num)?)
380+
}
381+
})
382+
.parse_ref(cmd, arg, value)
383+
}
384+
}
385+
364386
#[derive(Clone)]
365387
pub struct AsTime;
366388

@@ -387,4 +409,36 @@ mod clap {
387409
}
388410
}
389411
}
390-
pub use self::clap::{AsBString, AsHashKind, AsOutputFormat, AsPartialRefName, AsPathSpec, AsTime, CheckPathSpec};
412+
pub use self::clap::{
413+
AsBString, AsHashKind, AsOutputFormat, AsPartialRefName, AsPathSpec, AsTime, CheckPathSpec, ParseRenameFraction,
414+
};
415+
416+
#[cfg(test)]
417+
mod value_parser_tests {
418+
use super::ParseRenameFraction;
419+
use clap::Parser;
420+
421+
#[test]
422+
fn rename_fraction() {
423+
#[derive(Debug, clap::Parser)]
424+
pub struct Cmd {
425+
#[clap(long, short='a', value_parser = ParseRenameFraction)]
426+
pub arg: Option<Option<f32>>,
427+
}
428+
429+
let c = Cmd::parse_from(["cmd", "-a"]);
430+
assert_eq!(c.arg, Some(None), "this means we need to fill in the default");
431+
432+
let c = Cmd::parse_from(["cmd", "-a=50%"]);
433+
assert_eq!(c.arg, Some(Some(0.5)), "percentages become a fraction");
434+
435+
let c = Cmd::parse_from(["cmd", "-a=100%"]);
436+
assert_eq!(c.arg, Some(Some(1.0)));
437+
438+
let c = Cmd::parse_from(["cmd", "-a=5"]);
439+
assert_eq!(c.arg, Some(Some(0.5)), "another way to specify fractions");
440+
441+
let c = Cmd::parse_from(["cmd", "-a=75"]);
442+
assert_eq!(c.arg, Some(Some(0.75)));
443+
}
444+
}

0 commit comments

Comments
 (0)