Skip to content

Commit cf5406c

Browse files
committed
feat: add index-to-worktree status with rename tracking
1 parent 5b220db commit cf5406c

File tree

8 files changed

+182
-2
lines changed

8 files changed

+182
-2
lines changed

Cargo.lock

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-status/Cargo.toml

+12
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ autotests = false
1313
[lib]
1414
doctest = false
1515

16+
[features]
17+
## Add support for tracking rewrites along with checking for worktree modifications.
18+
worktree-rewrites = ["dep:gix-dir", "dep:gix-diff"]
19+
1620
[dependencies]
1721
gix-index = { version = "^0.30.0", path = "../gix-index" }
1822
gix-fs = { version = "^0.10.0", path = "../gix-fs" }
@@ -24,6 +28,14 @@ gix-filter = { version = "^0.9.0", path = "../gix-filter" }
2428
gix-worktree = { version = "^0.31.0", path = "../gix-worktree", default-features = false, features = ["attributes"] }
2529
gix-pathspec = { version = "^0.6.0", path = "../gix-pathspec" }
2630

31+
gix-dir = { version = "^0.1.0", path = "../gix-dir", optional = true }
32+
gix-diff = { version = "^0.41.0", path = "../gix-diff", default-features = false, features = ["blob"], optional = true }
33+
2734
thiserror = "1.0.26"
2835
filetime = "0.2.15"
2936
bstr = { version = "1.3.0", default-features = false }
37+
38+
document-features = { version = "0.2.0", optional = true }
39+
40+
[package.metadata.docs.rs]
41+
features = ["document-features", "worktree-rewrites"]

gix-status/src/index_as_worktree/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ pub use types::{Change, Conflict, Context, EntryStatus, Error, Options, Outcome,
66
mod recorder;
77
pub use recorder::{Record, Recorder};
88

9-
pub(crate) mod function;
9+
pub(super) mod function;
1010
///
1111
pub mod traits;

gix-status/src/index_as_worktree/types.rs

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ pub struct Options {
3737
#[derive(Clone)]
3838
pub struct Context<'a> {
3939
/// The pathspec to limit the amount of paths that are checked. Can be empty to allow all paths.
40+
///
41+
/// Note that these are expected to have a [commont_prefix()](gix_pathspec::Search::common_prefix()) according
42+
/// to the prefix of the repository to efficiently limit the scope of the paths we process.
4043
pub pathspec: gix_pathspec::Search,
4144
/// A stack pre-configured to allow accessing attributes for each entry, as required for `filter`
4245
/// and possibly pathspecs.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
//! Changes between the index and the worktree along with optional rename tracking.
2+
mod types {
3+
use crate::index_as_worktree::EntryStatus;
4+
use bstr::BStr;
5+
6+
/// Options for use in [index_as_worktree_with_renames()](super::).
7+
#[derive(Clone, Default)]
8+
pub struct Options {
9+
/// Options to configure how modifications to tracked files should be obtained.
10+
pub tracked_file_modifications: crate::index_as_worktree::Options,
11+
/// Options to control the directory walk that informs about untracked files.
12+
///
13+
/// Note that we forcefully disable emission of tracked files to avoid any overlap
14+
/// between emissions to indicate modifications, and those that are obtained by
15+
/// the directory walk.
16+
///
17+
/// If `None`, the directory walk portion will not run at all, yielding data similar
18+
/// to a bare [index_as_worktree()](crate::index_as_worktree()) call.
19+
pub untracked_file_options: Option<gix_dir::walk::Options>,
20+
}
21+
22+
/// An 'entry' in the sense of a merge of modified tracked files and results from a directory walk.
23+
pub enum Entry<'a, ContentChange, SubmoduleStatus> {
24+
/// A tracked file was modified, and index-specific information is passed.
25+
Modification {
26+
/// All entries in the index.
27+
entries: &'a [gix_index::Entry],
28+
/// The entry with modifications.
29+
entry: &'a gix_index::Entry,
30+
/// The index of the `entry` for lookup in `entries` - useful to look at neighbors.
31+
entry_index: usize,
32+
/// The repository-relative path of the entry.
33+
rela_path: &'a BStr,
34+
/// The computed status of the entry.
35+
status: EntryStatus<ContentChange, SubmoduleStatus>,
36+
},
37+
/// An entry returned by the directory walk, without any relation to the index.
38+
///
39+
/// This can happen if ignored files are returned as well, or if rename-tracking is disabled.
40+
DirectoryContents {
41+
/// The entry found during the disk traversal.
42+
entry: gix_dir::EntryRef<'a>,
43+
/// `collapsed_directory_status` is `Some(dir_status)` if this `entry` was part of a directory with the given
44+
/// `dir_status` that wasn't the same as the one of `entry` and if [gix_dir::walk::Options::emit_collapsed] was
45+
/// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch).
46+
/// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All).
47+
collapsed_directory_status: Option<gix_dir::entry::Status>,
48+
},
49+
/// The rewrite tracking discovered a match between a deleted and added file, and considers them equal enough,
50+
/// depending on the tracker settings.
51+
///
52+
/// Note that the source of the rewrite is always the index as it detects the absence of entries, something that
53+
/// can't be done during a directory walk.
54+
Rewrite {
55+
/// All entries in the index.
56+
index_entries: &'a [gix_index::Entry],
57+
/// The entry that is the source of the rewrite, which means it was removed on disk,
58+
/// equivalent to [Change::Removed](crate::index_as_worktree::Change::Removed).
59+
///
60+
/// Note that the [entry-id](gix_index::Entry::id) is the content-id of the source of the rewrite.
61+
source_entry: &'a gix_index::Entry,
62+
/// The index of the `source_entry` for lookup in `index_entries` - useful to look at neighbors.
63+
source_entry_index: usize,
64+
/// The repository-relative path of the `source_entry`.
65+
source_rela_path: &'a BStr,
66+
/// The computed status of the `source_entry`, which would always be
67+
source_status: EntryStatus<ContentChange, SubmoduleStatus>,
68+
69+
/// The untracked entry found during the disk traversal, the destination of the rewrite.
70+
///
71+
/// Note that its [`rela_path`](gix_dir::EntryRef::rela_path) is the destination of the rewrite, and the current
72+
/// location of the entry.
73+
dirwalk_entry: gix_dir::EntryRef<'a>,
74+
/// `collapsed_directory_status` is `Some(dir_status)` if this `dirwalk_entry` was part of a directory with the given
75+
/// `dir_status` that wasn't the same as the one of `entry` and if [gix_dir::walk::Options::emit_collapsed] was
76+
/// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch).
77+
/// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All).
78+
dirwalk_entry_collapsed_directory_status: Option<gix_dir::entry::Status>,
79+
/// The object id after the rename, specifically hashed in order to determine equality.
80+
dirwalk_entry_id: gix_hash::ObjectId,
81+
/// It's `None` if `source_entry.id` is equal to `dirwalk_entry_id`, as identity made an actual diff computation unnecessary.
82+
/// Otherwise, and if enabled, it's `Some(stats)` to indicate how similar both entries were.
83+
diff: Option<gix_diff::blob::DiffLineStats>,
84+
85+
/// If true, this rewrite is created by copy, and `source_entry.id` is pointing to its source.
86+
/// Otherwise it's a rename, and `source_entry.id` points to a deleted object,
87+
/// as renames are tracked as deletions and additions of the same or similar content.
88+
copy: bool,
89+
},
90+
}
91+
92+
/// Observe the status of an entry by comparing an index entry to the worktree, along
93+
/// with potential directory walk results.
94+
pub trait VisitEntry<'index> {
95+
/// Data generated by comparing an entry with a file.
96+
type ContentChange;
97+
/// Data obtained when checking the submodule status.
98+
type SubmoduleStatus;
99+
/// Observe the `status` of `entry` at the repository-relative `rela_path` at `entry_index`
100+
/// (for accessing `entry` and surrounding in the complete list of `entries`).
101+
fn visit_entry(
102+
&mut self,
103+
entries: &'index [gix_index::Entry],
104+
entry: &'index gix_index::Entry,
105+
entry_index: usize,
106+
rela_path: &'index BStr,
107+
status: EntryStatus<Self::ContentChange, Self::SubmoduleStatus>,
108+
);
109+
}
110+
}
111+
pub use types::{Entry, Options, VisitEntry};
112+
113+
#[allow(dead_code, unused_variables)]
114+
pub(super) mod function {
115+
use crate::index_as_worktree::traits::{CompareBlobs, SubmoduleStatus};
116+
use crate::index_as_worktree::{Context, Error, Outcome, VisitEntry};
117+
use crate::index_as_worktree_with_renames::Options;
118+
use std::path::Path;
119+
120+
/// Similar to [`index_as_worktree(…)`](crate::index_as_worktree()), except that it will automatically
121+
/// track renames if enabled, while additionally providing information about untracked files
122+
/// (or more, depending on the configuration).
123+
#[allow(clippy::too_many_arguments)]
124+
pub fn index_as_worktree_with_renames<'index, T, U, Find, E>(
125+
index: &'index gix_index::State,
126+
worktree: &Path,
127+
collector: &mut impl VisitEntry<'index, ContentChange = T, SubmoduleStatus = U>,
128+
compare: impl CompareBlobs<Output = T> + Send + Clone,
129+
submodule: impl SubmoduleStatus<Output = U, Error = E> + Send + Clone,
130+
objects: Find,
131+
progress: &mut dyn gix_features::progress::Progress,
132+
Context {
133+
pathspec,
134+
stack,
135+
filter,
136+
should_interrupt,
137+
}: Context<'_>,
138+
options: Options,
139+
) -> Result<Outcome, Error>
140+
where
141+
T: Send,
142+
U: Send,
143+
E: std::error::Error + Send + Sync + 'static,
144+
Find: gix_object::Find + Send + Clone,
145+
{
146+
todo!()
147+
}
148+
}

gix-status/src/lib.rs

+12
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,23 @@
66
//! * find untracked files
77
//!
88
//! While also being able to check check if the working tree is dirty, quickly.
9+
//!
10+
//! ### Feature Flags
11+
#![cfg_attr(
12+
all(doc, feature = "document-features"),
13+
doc = ::document_features::document_features!()
14+
)]
15+
#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg, doc_auto_cfg))]
916
#![deny(missing_docs, rust_2018_idioms, unsafe_code)]
1017

1118
pub mod index_as_worktree;
1219
pub use index_as_worktree::function::index_as_worktree;
1320

21+
#[cfg(feature = "worktree-rewrites")]
22+
pub mod index_as_worktree_with_renames;
23+
#[cfg(feature = "worktree-rewrites")]
24+
pub use index_as_worktree_with_renames::function::index_as_worktree_with_renames;
25+
1426
/// A stack that validates we are not going through a symlink in a way that is read-only.
1527
///
1628
/// It can efficiently validate paths when these are queried in sort-order, which leads to each component

gix-status/tests/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ path = "worktree.rs"
1717
gix-features-parallel = ["gix-features/parallel"]
1818

1919
[dev-dependencies]
20-
gix-status = { path = ".." }
20+
gix-status = { path = "..", features = ["worktree-rewrites"] }
2121
gix-testtools = { path = "../../tests/tools" }
2222
gix-index = { path = "../../gix-index" }
2323
gix-fs = { path = "../../gix-fs" }

justfile

+2
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ check:
9292
cargo check -p gix-revision --no-default-features --features describe
9393
cargo check -p gix-mailmap --features serde
9494
cargo check -p gix-url --all-features
95+
cargo check -p gix-status
96+
cargo check -p gix-status --all-features
9597
cargo check -p gix-features --all-features
9698
cargo check -p gix-features --features parallel
9799
cargo check -p gix-features --features fs-walkdir-parallel

0 commit comments

Comments
 (0)