Skip to content

Commit f76d8ed

Browse files
committed
feat: add index-to-worktree status with rename tracking
1 parent effb5bf commit f76d8ed

File tree

8 files changed

+233
-2
lines changed

8 files changed

+233
-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,199 @@
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+
use std::sync::atomic::AtomicBool;
6+
7+
/// The error returned by [index_as_worktree_with_renames()`](crate::index_as_worktree_with_renames()).
8+
#[derive(Debug, thiserror::Error)]
9+
#[allow(missing_docs)]
10+
pub enum Error {
11+
#[error(transparent)]
12+
TrackedFileModifications(#[from] crate::index_as_worktree::Error),
13+
#[error(transparent)]
14+
DirWalk(#[from] gix_dir::walk::Error),
15+
}
16+
17+
/// Options for use in [index_as_worktree_with_renames()](super::).
18+
#[derive(Clone, Default)]
19+
pub struct Options {
20+
/// Options to configure how modifications to tracked files should be obtained.
21+
pub tracked_file_modifications: crate::index_as_worktree::Options,
22+
/// Options to control the directory walk that informs about untracked files.
23+
///
24+
/// Note that we forcefully disable emission of tracked files to avoid any overlap
25+
/// between emissions to indicate modifications, and those that are obtained by
26+
/// the directory walk.
27+
///
28+
/// If `None`, the directory walk portion will not run at all, yielding data similar
29+
/// to a bare [index_as_worktree()](crate::index_as_worktree()) call.
30+
pub dirwalk: Option<gix_dir::walk::Options>,
31+
}
32+
33+
/// Provide additional information collected during the runtime of [`index_as_worktree_with_renames()`](crate::index_as_worktree_with_renames()).
34+
#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)]
35+
pub struct Outcome {
36+
/// The outcome of the modification check of tracked files.
37+
tracked_file_modification: crate::index_as_worktree::Outcome,
38+
/// The outcome of the directory walk.
39+
dirwalk: gix_dir::walk::Outcome,
40+
}
41+
42+
/// An 'entry' in the sense of a merge of modified tracked files and results from a directory walk.
43+
#[derive(Clone, PartialEq, Debug)]
44+
pub enum Entry<'a, ContentChange, SubmoduleStatus> {
45+
/// A tracked file was modified, and index-specific information is passed.
46+
Modification {
47+
/// All entries in the index.
48+
entries: &'a [gix_index::Entry],
49+
/// The entry with modifications.
50+
entry: &'a gix_index::Entry,
51+
/// The index of the `entry` for lookup in `entries` - useful to look at neighbors.
52+
entry_index: usize,
53+
/// The repository-relative path of the entry.
54+
rela_path: &'a BStr,
55+
/// The computed status of the entry.
56+
status: EntryStatus<ContentChange, SubmoduleStatus>,
57+
},
58+
/// An entry returned by the directory walk, without any relation to the index.
59+
///
60+
/// This can happen if ignored files are returned as well, or if rename-tracking is disabled.
61+
DirectoryContents {
62+
/// The entry found during the disk traversal.
63+
entry: gix_dir::EntryRef<'a>,
64+
/// `collapsed_directory_status` is `Some(dir_status)` if this `entry` was part of a directory with the given
65+
/// `dir_status` that wasn't the same as the one of `entry` and if [gix_dir::walk::Options::emit_collapsed] was
66+
/// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch).
67+
/// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All).
68+
collapsed_directory_status: Option<gix_dir::entry::Status>,
69+
},
70+
/// The rewrite tracking discovered a match between a deleted and added file, and considers them equal enough,
71+
/// depending on the tracker settings.
72+
///
73+
/// Note that the source of the rewrite is always the index as it detects the absence of entries, something that
74+
/// can't be done during a directory walk.
75+
Rewrite {
76+
/// All entries in the index.
77+
index_entries: &'a [gix_index::Entry],
78+
/// The entry that is the source of the rewrite, which means it was removed on disk,
79+
/// equivalent to [Change::Removed](crate::index_as_worktree::Change::Removed).
80+
///
81+
/// Note that the [entry-id](gix_index::Entry::id) is the content-id of the source of the rewrite.
82+
source_entry: &'a gix_index::Entry,
83+
/// The index of the `source_entry` for lookup in `index_entries` - useful to look at neighbors.
84+
source_entry_index: usize,
85+
/// The repository-relative path of the `source_entry`.
86+
source_rela_path: &'a BStr,
87+
/// The computed status of the `source_entry`, which would always be
88+
source_status: EntryStatus<ContentChange, SubmoduleStatus>,
89+
90+
/// The untracked entry found during the disk traversal, the destination of the rewrite.
91+
///
92+
/// Note that its [`rela_path`](gix_dir::EntryRef::rela_path) is the destination of the rewrite, and the current
93+
/// location of the entry.
94+
dirwalk_entry: gix_dir::EntryRef<'a>,
95+
/// `collapsed_directory_status` is `Some(dir_status)` if this `dirwalk_entry` was part of a directory with the given
96+
/// `dir_status` that wasn't the same as the one of `entry` and if [gix_dir::walk::Options::emit_collapsed] was
97+
/// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch).
98+
/// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All).
99+
dirwalk_entry_collapsed_directory_status: Option<gix_dir::entry::Status>,
100+
/// The object id after the rename, specifically hashed in order to determine equality.
101+
dirwalk_entry_id: gix_hash::ObjectId,
102+
/// It's `None` if `source_entry.id` is equal to `dirwalk_entry_id`, as identity made an actual diff computation unnecessary.
103+
/// Otherwise, and if enabled, it's `Some(stats)` to indicate how similar both entries were.
104+
diff: Option<gix_diff::blob::DiffLineStats>,
105+
106+
/// If true, this rewrite is created by copy, and `source_entry.id` is pointing to its source.
107+
/// Otherwise it's a rename, and `source_entry.id` points to a deleted object,
108+
/// as renames are tracked as deletions and additions of the same or similar content.
109+
copy: bool,
110+
},
111+
}
112+
113+
/// The context for [index_as_worktree_with_renames()`](crate::index_as_worktree_with_renames()).
114+
pub struct Context<'a> {
115+
/// The pathspec to limit the amount of paths that are checked. Can be empty to allow all paths.
116+
///
117+
/// Note that these are expected to have a [commont_prefix()](gix_pathspec::Search::common_prefix()) according
118+
/// to the prefix of the repository to efficiently limit the scope of the paths we process, both for the
119+
/// index modifications as well as for the
120+
pub pathspec: gix_pathspec::Search,
121+
/// A stack pre-configured to allow accessing attributes for each entry, as required for `filter`
122+
/// and possibly pathspecs.
123+
/// It must also allow accessing `.gitignore` information for use in the directory walk.
124+
pub attr_and_ignore_stack: gix_worktree::Stack,
125+
/// A filter to be able to perform conversions from and to the worktree format.
126+
///
127+
/// It is needed to potentially refresh the index with data read from the worktree, which needs to be converted back
128+
/// to the form stored in Git.
129+
///
130+
/// Note that for this to be correct, the attribute `stack` must be configured correctly as well.
131+
pub filter: gix_filter::Pipeline,
132+
/// A flag to query to learn if cancellation is requested.
133+
pub should_interrupt: &'a AtomicBool,
134+
/// The context for the directory walk.
135+
pub dirwalk: DirwalkContext<'a>,
136+
}
137+
138+
/// All information that is required to perform a [dirwalk](gix_dir::walk()).
139+
pub struct DirwalkContext<'a> {
140+
/// The `git_dir` of the parent repository, after a call to [`gix_path::realpath()`].
141+
///
142+
/// It's used to help us differentiate our own `.git` directory from nested unrelated repositories,
143+
/// which is needed if `core.worktree` is used to nest the `.git` directory deeper within.
144+
pub git_dir_realpath: &'a std::path::Path,
145+
/// The current working directory as returned by `gix_fs::current_dir()` to assure it respects `core.precomposeUnicode`.
146+
/// It's used to produce the realpath of the git-dir of a repository candidate to assure it's not our own repository.
147+
pub current_dir: &'a std::path::Path,
148+
/// A utility to lookup index entries faster, and deal with ignore-case handling.
149+
///
150+
/// Must be set if `ignore_case` is `true`, or else some entries won't be found if their case is different.
151+
///
152+
/// [Read more in `gix-dir`](gix_dir::walk::Context::ignore_case_index_lookup).
153+
pub ignore_case_index_lookup: Option<&'a gix_index::AccelerateLookup<'a>>,
154+
}
155+
156+
/// Observe the status of an entry by comparing an index entry to the worktree, along
157+
/// with potential directory walk results.
158+
pub trait VisitEntry<'a> {
159+
/// Data generated by comparing an entry with a file.
160+
type ContentChange;
161+
/// Data obtained when checking the submodule status.
162+
type SubmoduleStatus;
163+
/// Observe the `status` of `entry` at the repository-relative `rela_path` at `entry_index`
164+
/// (for accessing `entry` and surrounding in the complete list of `entries`).
165+
fn visit_entry(&mut self, entry: Entry<'a, Self::ContentChange, Self::SubmoduleStatus>);
166+
}
167+
}
168+
pub use types::{Context, DirwalkContext, Entry, Error, Options, Outcome, VisitEntry};
169+
170+
#[allow(dead_code, unused_variables)]
171+
pub(super) mod function {
172+
use crate::index_as_worktree::traits::{CompareBlobs, SubmoduleStatus};
173+
use crate::index_as_worktree_with_renames::{Context, Error, Options, Outcome, VisitEntry};
174+
use std::path::Path;
175+
176+
/// Similar to [`index_as_worktree(…)`](crate::index_as_worktree()), except that it will automatically
177+
/// track renames if enabled, while additionally providing information about untracked files
178+
/// (or more, depending on the configuration).
179+
#[allow(clippy::too_many_arguments)]
180+
pub fn index_as_worktree_with_renames<'index, T, U, Find, E>(
181+
index: &'index gix_index::State,
182+
worktree: &Path,
183+
collector: &mut impl VisitEntry<'index, ContentChange = T, SubmoduleStatus = U>,
184+
compare: impl CompareBlobs<Output = T> + Send + Clone,
185+
submodule: impl SubmoduleStatus<Output = U, Error = E> + Send + Clone,
186+
objects: Find,
187+
progress: &mut dyn gix_features::progress::Progress,
188+
ctx: Context<'_>,
189+
options: Options,
190+
) -> Result<Outcome, Error>
191+
where
192+
T: Send,
193+
U: Send,
194+
E: std::error::Error + Send + Sync + 'static,
195+
Find: gix_object::Find + Send + Clone,
196+
{
197+
todo!()
198+
}
199+
}

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)