From ac926a1a3bd353ef43170e3ae7904c3ebfccc974 Mon Sep 17 00:00:00 2001 From: Sebastian Thiel Date: Thu, 15 Feb 2024 17:08:07 +0100 Subject: [PATCH] feat: add index-to-worktree status with rename tracking --- Cargo.lock | 9 + gix-diff/tests/rewrites/tracker.rs | 6 +- gix-status/Cargo.toml | 14 +- gix-status/src/index_as_worktree/mod.rs | 2 +- gix-status/src/index_as_worktree/recorder.rs | 2 +- gix-status/src/index_as_worktree/types.rs | 3 + .../src/index_as_worktree_with_renames/mod.rs | 531 ++++++++++++++++++ .../recorder.rs | 17 + .../index_as_worktree_with_renames/types.rs | 245 ++++++++ gix-status/src/lib.rs | 12 + gix-status/tests/Cargo.toml | 9 +- .../generated-archives/status_many.tar.xz | Bin 0 -> 11216 bytes gix-status/tests/fixtures/status_many.sh | 40 ++ gix-status/tests/status/index_as_worktree.rs | 12 +- .../status/index_as_worktree_with_renames.rs | 332 +++++++++++ gix-status/tests/status/mod.rs | 1 + justfile | 2 + 17 files changed, 1224 insertions(+), 13 deletions(-) create mode 100644 gix-status/src/index_as_worktree_with_renames/mod.rs create mode 100644 gix-status/src/index_as_worktree_with_renames/recorder.rs create mode 100644 gix-status/src/index_as_worktree_with_renames/types.rs create mode 100644 gix-status/tests/fixtures/generated-archives/status_many.tar.xz create mode 100644 gix-status/tests/fixtures/status_many.sh create mode 100644 gix-status/tests/status/index_as_worktree_with_renames.rs diff --git a/Cargo.lock b/Cargo.lock index 0b1c07ad9ff..efe227eb3dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2511,7 +2511,10 @@ name = "gix-status" version = "0.6.0" dependencies = [ "bstr", + "document-features", "filetime", + "gix-diff", + "gix-dir", "gix-features 0.38.0", "gix-filter", "gix-fs 0.10.0", @@ -2530,15 +2533,21 @@ version = "0.0.0" dependencies = [ "bstr", "filetime", + "gix-diff", + "gix-dir", "gix-features 0.38.0", + "gix-filter", "gix-fs 0.10.0", "gix-hash 0.14.1", "gix-index 0.30.0", "gix-object 0.41.1", + "gix-odb", + "gix-path 0.10.6", "gix-pathspec", "gix-status", "gix-testtools", "gix-worktree 0.31.0", + "pretty_assertions", ] [[package]] diff --git a/gix-diff/tests/rewrites/tracker.rs b/gix-diff/tests/rewrites/tracker.rs index 2cc5d1b6527..9b3f4f5a928 100644 --- a/gix-diff/tests/rewrites/tracker.rs +++ b/gix-diff/tests/rewrites/tracker.rs @@ -151,7 +151,7 @@ fn copy_by_id() -> crate::Result { let id = hex_to_id("2e65efe2a145dda7ee51d1741299f848e5bf752e"); let source_a = Source { entry_mode: EntryKind::Blob.into(), - id: id, + id, kind: SourceKind::Copy, location: "a".into(), change: &Change { @@ -303,7 +303,7 @@ fn copy_by_50_percent_similarity() -> crate::Result { let id = hex_to_id("78981922613b2afb6025042ff6bd878ac1994e85"); let source_a = Source { entry_mode: EntryKind::Blob.into(), - id: id, + id, kind: SourceKind::Copy, location: "a".into(), change: &Change { @@ -480,7 +480,7 @@ fn rename_by_50_percent_similarity() -> crate::Result { src.unwrap(), Source { entry_mode: EntryKind::Blob.into(), - id: id, + id, kind: SourceKind::Rename, location: "a".into(), change: &Change { diff --git a/gix-status/Cargo.toml b/gix-status/Cargo.toml index 0d75b7dc3c0..6d36fd1c85e 100644 --- a/gix-status/Cargo.toml +++ b/gix-status/Cargo.toml @@ -13,17 +13,29 @@ autotests = false [lib] doctest = false +[features] +## Add support for tracking rewrites along with checking for worktree modifications. +worktree-rewrites = ["dep:gix-dir", "dep:gix-diff"] + [dependencies] gix-index = { version = "^0.30.0", path = "../gix-index" } gix-fs = { version = "^0.10.0", path = "../gix-fs" } gix-hash = { version = "^0.14.1", path = "../gix-hash" } gix-object = { version = "^0.41.1", path = "../gix-object" } gix-path = { version = "^0.10.6", path = "../gix-path" } -gix-features = { version = "^0.38.0", path = "../gix-features" } +gix-features = { version = "^0.38.0", path = "../gix-features", features = ["progress"] } gix-filter = { version = "^0.10.0", path = "../gix-filter" } gix-worktree = { version = "^0.31.0", path = "../gix-worktree", default-features = false, features = ["attributes"] } gix-pathspec = { version = "^0.7.0", path = "../gix-pathspec" } +gix-dir = { version = "^0.1.0", path = "../gix-dir", optional = true } +gix-diff = { version = "^0.41.0", path = "../gix-diff", default-features = false, features = ["blob"], optional = true } + thiserror = "1.0.26" filetime = "0.2.15" bstr = { version = "1.3.0", default-features = false } + +document-features = { version = "0.2.0", optional = true } + +[package.metadata.docs.rs] +features = ["document-features", "worktree-rewrites"] diff --git a/gix-status/src/index_as_worktree/mod.rs b/gix-status/src/index_as_worktree/mod.rs index 96694078bb8..7dce0c43306 100644 --- a/gix-status/src/index_as_worktree/mod.rs +++ b/gix-status/src/index_as_worktree/mod.rs @@ -6,6 +6,6 @@ pub use types::{Change, Conflict, Context, EntryStatus, Error, Options, Outcome, mod recorder; pub use recorder::{Record, Recorder}; -pub(crate) mod function; +pub(super) mod function; /// pub mod traits; diff --git a/gix-status/src/index_as_worktree/recorder.rs b/gix-status/src/index_as_worktree/recorder.rs index 0cf1aa6f367..407abfd557c 100644 --- a/gix-status/src/index_as_worktree/recorder.rs +++ b/gix-status/src/index_as_worktree/recorder.rs @@ -6,7 +6,7 @@ use crate::index_as_worktree::{EntryStatus, VisitEntry}; /// A record of a change. /// /// It's created either if there is a conflict or a change, or both. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Record<'index, T, U> { /// The index entry that is changed. pub entry: &'index index::Entry, diff --git a/gix-status/src/index_as_worktree/types.rs b/gix-status/src/index_as_worktree/types.rs index 6d93f784f12..c93aa7c62e2 100644 --- a/gix-status/src/index_as_worktree/types.rs +++ b/gix-status/src/index_as_worktree/types.rs @@ -37,6 +37,9 @@ pub struct Options { #[derive(Clone)] pub struct Context<'a> { /// The pathspec to limit the amount of paths that are checked. Can be empty to allow all paths. + /// + /// Note that these are expected to have a [commont_prefix()](gix_pathspec::Search::common_prefix()) according + /// to the prefix of the repository to efficiently limit the scope of the paths we process. pub pathspec: gix_pathspec::Search, /// A stack pre-configured to allow accessing attributes for each entry, as required for `filter` /// and possibly pathspecs. diff --git a/gix-status/src/index_as_worktree_with_renames/mod.rs b/gix-status/src/index_as_worktree_with_renames/mod.rs new file mode 100644 index 00000000000..8300f8c22a7 --- /dev/null +++ b/gix-status/src/index_as_worktree_with_renames/mod.rs @@ -0,0 +1,531 @@ +//! Changes between the index and the worktree along with optional rename tracking. +mod types; +pub use types::{Context, DirwalkContext, Entry, Error, Options, Outcome, RewriteSource, VisitEntry}; + +mod recorder; +pub use recorder::Recorder; + +pub(super) mod function { + use crate::index_as_worktree::traits::{CompareBlobs, SubmoduleStatus}; + use crate::index_as_worktree_with_renames::function::rewrite::ModificationOrDirwalkEntry; + use crate::index_as_worktree_with_renames::{Context, Entry, Error, Options, Outcome, RewriteSource, VisitEntry}; + use bstr::{BString, ByteSlice, ByteVec}; + use gix_worktree::stack::State; + use std::path::Path; + + /// Similar to [`index_as_worktree(…)`](crate::index_as_worktree()), except that it will automatically + /// track renames if enabled, while additionally providing information about untracked files + /// (or more, depending on the configuration). + /// + /// * `index` + /// - used for checking modifications, and also for knowing which files are tracked during + /// the working-dir traversal. + /// * `worktree` + /// - The root of the worktree, in a format that respects `core.precomposeUnicode`. + /// * `collector` + /// - A [`VisitEntry`] implementation that sees the results of this operation. + /// * `compare` + /// - An implementation to compare two blobs for equality, used during index modification checks. + /// * `submodule` + /// - An implementation to determine the status of a submodule when encountered during + /// index modification checks. + /// * `objects` + /// - A way to obtain objects from the git object database. + /// * `progress` + /// - A way to send progress information for the index modification checks. + /// * `ctx` + /// - Additional information that will be accessed during index modification checks and traversal. + /// * `options` + /// - a way to configure both paths of the operation. + #[allow(clippy::too_many_arguments)] + pub fn index_as_worktree_with_renames<'index, T, U, Find, E>( + index: &'index gix_index::State, + worktree: &Path, + collector: &mut impl VisitEntry<'index, ContentChange = T, SubmoduleStatus = U>, + compare: impl CompareBlobs + Send + Clone, + submodule: impl SubmoduleStatus + Send + Clone, + objects: Find, + progress: &mut dyn gix_features::progress::Progress, + mut ctx: Context<'_>, + options: Options, + ) -> Result + where + T: Send + Clone, + U: Send + Clone, + E: std::error::Error + Send + Sync + 'static, + Find: gix_object::Find + gix_object::FindHeader + Send + Clone, + { + gix_features::parallel::threads(|scope| -> Result { + let (tx, rx) = std::sync::mpsc::channel(); + let walk_outcome = options + .dirwalk + .map(|options| { + gix_features::parallel::build_thread() + .name("gix_status::dirwalk".into()) + .spawn_scoped(scope, { + let tx = tx.clone(); + let mut collect = dirwalk::Delegate { tx }; + let dirwalk_ctx = ctx.dirwalk; + let objects = objects.clone(); + let mut excludes = match ctx.resource_cache.attr_stack.state() { + State::CreateDirectoryAndAttributesStack { .. } | State::AttributesStack(_) => None, + State::AttributesAndIgnoreStack { .. } | State::IgnoreStack(_) => { + Some(ctx.resource_cache.attr_stack.clone()) + } + }; + let mut pathspec_attr_stack = ctx + .pathspec + .patterns() + .any(|p| !p.attributes.is_empty()) + .then(|| ctx.resource_cache.attr_stack.clone()); + let mut pathspec = ctx.pathspec.clone(); + move || -> Result<_, Error> { + gix_dir::walk( + worktree, + gix_dir::walk::Context { + git_dir_realpath: dirwalk_ctx.git_dir_realpath, + current_dir: dirwalk_ctx.current_dir, + index, + ignore_case_index_lookup: dirwalk_ctx.ignore_case_index_lookup, + pathspec: &mut pathspec, + pathspec_attributes: &mut |relative_path, case, is_dir, out| { + let stack = pathspec_attr_stack + .as_mut() + .expect("can only be called if attributes are used in patterns"); + stack + .set_case(case) + .at_entry(relative_path, Some(is_dir), &objects) + .map_or(false, |platform| platform.matching_attributes(out)) + }, + excludes: excludes.as_mut(), + objects: &objects, + explicit_traversal_root: Some(worktree), + }, + options, + &mut collect, + ) + .map_err(Error::DirWalk) + } + }) + .map_err(Error::SpawnThread) + }) + .transpose()?; + + let entries = &index.entries()[index + .prefixed_entries_range(ctx.pathspec.common_prefix()) + .unwrap_or(0..index.entries().len())]; + + let filter = options.rewrites.is_some().then(|| { + ( + ctx.resource_cache.filter.worktree_filter.clone(), + ctx.resource_cache.attr_stack.clone(), + ) + }); + let tracked_modifications_outcome = gix_features::parallel::build_thread() + .name("gix_status::index_as_worktree".into()) + .spawn_scoped(scope, { + let mut collect = tracked_modifications::Delegate { + tx, + should_interrupt: ctx.should_interrupt, + triggered_interrupt: false, + }; + let objects = objects.clone(); + let stack = ctx.resource_cache.attr_stack.clone(); + let filter = ctx.resource_cache.filter.worktree_filter.clone(); + move || -> Result<_, Error> { + crate::index_as_worktree( + index, + worktree, + &mut collect, + compare, + submodule, + objects, + progress, + crate::index_as_worktree::Context { + pathspec: ctx.pathspec, + stack, + filter, + should_interrupt: ctx.should_interrupt, + }, + options.tracked_file_modifications, + ) + .map_err(Error::TrackedFileModifications) + } + }) + .map_err(Error::SpawnThread)?; + + let tracker = options + .rewrites + .map(gix_diff::rewrites::Tracker::>::new) + .zip(filter); + let rewrite_outcome = match tracker { + Some((mut tracker, (mut filter, mut attrs))) => { + let mut location = BString::default(); + let mut buf = Vec::new(); + for event in rx { + let (change, location) = match event { + Event::IndexEntry(record) => { + let location = record.relative_path; + (rewrite::ModificationOrDirwalkEntry::Modification(record), location) + } + Event::DirEntry(entry, collapsed_directory_status) => { + location.clear(); + location.push_str(&entry.rela_path); + ( + rewrite::ModificationOrDirwalkEntry::DirwalkEntry { + id: rewrite::calculate_worktree_id( + options.object_hash, + worktree, + entry.disk_kind, + entry.rela_path.as_bstr(), + &mut filter, + &mut attrs, + &objects, + &mut buf, + ctx.should_interrupt, + )?, + entry, + collapsed_directory_status, + }, + location.as_bstr(), + ) + } + }; + if let Some(change) = tracker.try_push_change(change, location) { + collector.visit_entry(rewrite::change_to_entry(change, entries)) + } + } + + Some(tracker.emit( + |dest, src| { + match src { + None => collector.visit_entry(rewrite::change_to_entry(dest.change, entries)), + Some(src) => { + let rewrite::ModificationOrDirwalkEntry::DirwalkEntry { + id, + entry, + collapsed_directory_status, + } = dest.change + else { + unreachable!("BUG: only possible destinations are dirwalk entries (additions)"); + }; + let source = match src.change { + ModificationOrDirwalkEntry::Modification(record) => { + RewriteSource::RewriteFromIndex { + index_entries: entries, + source_entry: record.entry, + source_entry_index: record.entry_index, + source_rela_path: record.relative_path, + source_status: record.status.clone(), + } + } + ModificationOrDirwalkEntry::DirwalkEntry { + id, + entry, + collapsed_directory_status, + } => RewriteSource::CopyFromDirectoryEntry { + source_dirwalk_entry: entry.clone(), + source_dirwalk_entry_collapsed_directory_status: + *collapsed_directory_status, + source_dirwalk_entry_id: *id, + }, + }; + collector.visit_entry(Entry::Rewrite { + source, + dirwalk_entry: entry, + dirwalk_entry_collapsed_directory_status: collapsed_directory_status, + dirwalk_entry_id: id, + diff: src.diff, + copy: src.kind == gix_diff::rewrites::tracker::visit::SourceKind::Copy, + }); + } + } + gix_diff::tree::visit::Action::Continue + }, + &mut ctx.resource_cache, + &objects, + |_cb| { + // NOTE: to make this work, we'd want to wait the index modification check to complete. + // Then it's possible to efficiently emit the tracked files along with what we already sent, + // i.e. untracked and ignored files. + gix_features::trace::debug!("full-tree copy tracking isn't currently supported"); + Ok::<_, std::io::Error>(()) + }, + )?) + } + None => { + for event in rx { + let entry = match event { + Event::IndexEntry(record) => Entry::Modification { + entries, + entry: record.entry, + entry_index: record.entry_index, + rela_path: record.relative_path, + status: record.status, + }, + Event::DirEntry(entry, collapsed_directory_status) => Entry::DirectoryContents { + entry, + collapsed_directory_status, + }, + }; + + collector.visit_entry(entry) + } + None + } + }; + + let walk_outcome = walk_outcome + .map(|handle| handle.join().expect("no panic")) + .transpose()?; + let tracked_modifications_outcome = tracked_modifications_outcome.join().expect("no panic")?; + Ok(Outcome { + dirwalk: walk_outcome.map(|t| t.0), + tracked_file_modification: tracked_modifications_outcome, + rewrites: rewrite_outcome, + }) + }) + } + + enum Event<'index, T, U> { + IndexEntry(crate::index_as_worktree::Record<'index, T, U>), + DirEntry(gix_dir::Entry, Option), + } + + mod tracked_modifications { + use crate::index_as_worktree::{EntryStatus, Record}; + use crate::index_as_worktree_with_renames::function::Event; + use bstr::BStr; + use gix_index::Entry; + use std::sync::atomic::Ordering; + + pub struct Delegate<'index, 'a, T, U> { + pub tx: std::sync::mpsc::Sender>, + pub should_interrupt: &'a std::sync::atomic::AtomicBool, + pub triggered_interrupt: bool, + } + + impl<'index, 'a, T, U> Drop for Delegate<'index, 'a, T, U> { + fn drop(&mut self) { + if self.triggered_interrupt { + self.should_interrupt.store(false, Ordering::Relaxed); + } + } + } + + impl<'index, 'a, T, U> crate::index_as_worktree::VisitEntry<'index> for Delegate<'index, 'a, T, U> { + type ContentChange = T; + type SubmoduleStatus = U; + + fn visit_entry( + &mut self, + _entries: &'index [Entry], + entry: &'index Entry, + entry_index: usize, + rela_path: &'index BStr, + status: EntryStatus, + ) { + if self + .tx + .send(Event::IndexEntry(Record { + entry, + entry_index, + relative_path: rela_path, + status, + })) + .is_err() + { + self.should_interrupt.store(true, Ordering::Relaxed); + self.triggered_interrupt = true; + } + } + } + } + + mod dirwalk { + use super::Event; + use gix_dir::entry::Status; + use gix_dir::walk::Action; + use gix_dir::EntryRef; + + pub struct Delegate<'index, T, U> { + pub tx: std::sync::mpsc::Sender>, + } + + impl<'index, T, U> gix_dir::walk::Delegate for Delegate<'index, T, U> { + fn emit(&mut self, entry: EntryRef<'_>, collapsed_directory_status: Option) -> Action { + let entry = entry.to_owned(); + if self.tx.send(Event::DirEntry(entry, collapsed_directory_status)).is_ok() { + Action::Continue + } else { + Action::Cancel + } + } + } + } + + mod rewrite { + use crate::index_as_worktree::{Change, EntryStatus}; + use crate::index_as_worktree_with_renames::{Entry, Error}; + use bstr::BStr; + use gix_diff::rewrites::tracker::ChangeKind; + use gix_dir::entry::Kind; + use gix_filter::pipeline::convert::ToGitOutcome; + use gix_hash::oid; + use gix_object::tree::EntryMode; + use std::io::Read; + use std::path::Path; + + #[derive(Clone)] + pub enum ModificationOrDirwalkEntry<'index, T, U> + where + T: Clone, + U: Clone, + { + Modification(crate::index_as_worktree::Record<'index, T, U>), + DirwalkEntry { + id: gix_hash::ObjectId, + entry: gix_dir::Entry, + collapsed_directory_status: Option, + }, + } + + impl<'index, T, U> gix_diff::rewrites::tracker::Change for ModificationOrDirwalkEntry<'index, T, U> + where + T: Clone, + U: Clone, + { + fn id(&self) -> &oid { + match self { + ModificationOrDirwalkEntry::Modification(m) => &m.entry.id, + ModificationOrDirwalkEntry::DirwalkEntry { id, .. } => id, + } + } + + fn kind(&self) -> ChangeKind { + match self { + ModificationOrDirwalkEntry::Modification(m) => match &m.status { + EntryStatus::Conflict(_) | EntryStatus::IntentToAdd | EntryStatus::NeedsUpdate(_) => { + ChangeKind::Modification + } + EntryStatus::Change(c) => match c { + Change::Removed => ChangeKind::Deletion, + Change::Type | Change::Modification { .. } | Change::SubmoduleModification(_) => { + ChangeKind::Modification + } + }, + }, + ModificationOrDirwalkEntry::DirwalkEntry { .. } => ChangeKind::Addition, + } + } + + fn entry_mode(&self) -> EntryMode { + match self { + ModificationOrDirwalkEntry::Modification(c) => c.entry.mode.to_tree_entry_mode(), + ModificationOrDirwalkEntry::DirwalkEntry { entry, .. } => entry.disk_kind.map(|kind| { + match kind { + Kind::File => gix_object::tree::EntryKind::Blob, + Kind::Symlink => gix_object::tree::EntryKind::Link, + Kind::Repository | Kind::Directory => gix_object::tree::EntryKind::Tree, + } + .into() + }), + } + .unwrap_or(gix_object::tree::EntryKind::Blob.into()) + } + + fn id_and_entry_mode(&self) -> (&oid, EntryMode) { + (self.id(), self.entry_mode()) + } + } + + /// Note that for non-files, we always return a null-sha and assume that the rename-tracking + /// does nothing for these anyway. + #[allow(clippy::too_many_arguments)] + pub(super) fn calculate_worktree_id( + object_hash: gix_hash::Kind, + worktree_root: &Path, + disk_kind: Option, + rela_path: &BStr, + filter: &mut gix_filter::Pipeline, + attrs: &mut gix_worktree::Stack, + objects: &dyn gix_object::Find, + buf: &mut Vec, + should_interrupt: &std::sync::atomic::AtomicBool, + ) -> Result { + let Some(kind) = disk_kind else { + return Ok(object_hash.null()); + }; + + Ok(match kind { + Kind::File => { + let platform = attrs + .at_entry(rela_path, Some(false), objects) + .map_err(Error::SetAttributeContext)?; + let rela_path = gix_path::from_bstr(rela_path); + let file_path = worktree_root.join(rela_path.as_ref()); + let file = std::fs::File::open(&file_path).map_err(Error::OpenWorktreeFile)?; + let out = filter.convert_to_git( + file, + rela_path.as_ref(), + &mut |_path, attrs| { + platform.matching_attributes(attrs); + }, + &mut |_buf| Ok(None), + )?; + match out { + ToGitOutcome::Unchanged(mut file) => gix_object::compute_stream_hash( + object_hash, + gix_object::Kind::Blob, + &mut file, + file_path.metadata().map_err(Error::OpenWorktreeFile)?.len(), + &mut gix_features::progress::Discard, + should_interrupt, + ) + .map_err(Error::HashFile)?, + ToGitOutcome::Buffer(buf) => gix_object::compute_hash(object_hash, gix_object::Kind::Blob, buf), + ToGitOutcome::Process(mut stream) => { + buf.clear(); + stream.read_to_end(buf).map_err(Error::HashFile)?; + gix_object::compute_hash(object_hash, gix_object::Kind::Blob, buf) + } + } + } + Kind::Symlink => { + let path = worktree_root.join(gix_path::from_bstr(rela_path)); + let target = gix_path::into_bstr(std::fs::read_link(path).map_err(Error::ReadLink)?); + gix_object::compute_hash(object_hash, gix_object::Kind::Blob, &target) + } + Kind::Directory | Kind::Repository => object_hash.null(), + }) + } + + #[inline] + pub(super) fn change_to_entry<'index, T, U>( + change: ModificationOrDirwalkEntry<'index, T, U>, + entries: &'index [gix_index::Entry], + ) -> Entry<'index, T, U> + where + T: Clone, + U: Clone, + { + match change { + ModificationOrDirwalkEntry::Modification(r) => Entry::Modification { + entries, + entry: r.entry, + entry_index: r.entry_index, + rela_path: r.relative_path, + status: r.status, + }, + ModificationOrDirwalkEntry::DirwalkEntry { + id: _, + entry, + collapsed_directory_status, + } => Entry::DirectoryContents { + entry, + collapsed_directory_status, + }, + } + } + } +} diff --git a/gix-status/src/index_as_worktree_with_renames/recorder.rs b/gix-status/src/index_as_worktree_with_renames/recorder.rs new file mode 100644 index 00000000000..81d05f1b064 --- /dev/null +++ b/gix-status/src/index_as_worktree_with_renames/recorder.rs @@ -0,0 +1,17 @@ +use crate::index_as_worktree_with_renames::{Entry, VisitEntry}; + +/// Convenience implementation of [`VisitEntry`] that collects all changes into a `Vec`. +#[derive(Debug, Default)] +pub struct Recorder<'index, T = (), U = ()> { + /// The collected changes. + pub records: Vec>, +} + +impl<'index, T: Send, U: Send> VisitEntry<'index> for Recorder<'index, T, U> { + type ContentChange = T; + type SubmoduleStatus = U; + + fn visit_entry(&mut self, entry: Entry<'index, Self::ContentChange, Self::SubmoduleStatus>) { + self.records.push(entry) + } +} diff --git a/gix-status/src/index_as_worktree_with_renames/types.rs b/gix-status/src/index_as_worktree_with_renames/types.rs new file mode 100644 index 00000000000..bbc353f9f42 --- /dev/null +++ b/gix-status/src/index_as_worktree_with_renames/types.rs @@ -0,0 +1,245 @@ +use crate::index_as_worktree::EntryStatus; +use bstr::{BStr, ByteSlice}; +use std::sync::atomic::AtomicBool; + +/// The error returned by [index_as_worktree_with_renames()`](crate::index_as_worktree_with_renames()). +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error(transparent)] + TrackedFileModifications(#[from] crate::index_as_worktree::Error), + #[error(transparent)] + DirWalk(gix_dir::walk::Error), + #[error(transparent)] + SpawnThread(std::io::Error), + #[error("Failed to change the context for querying gitattributes to the respective path")] + SetAttributeContext(std::io::Error), + #[error("Could not open worktree file for reading")] + OpenWorktreeFile(std::io::Error), + #[error(transparent)] + HashFile(std::io::Error), + #[error("Could not read worktree link content")] + ReadLink(std::io::Error), + #[error(transparent)] + ConvertToGit(#[from] gix_filter::pipeline::convert::to_git::Error), + #[error(transparent)] + RewriteTracker(#[from] gix_diff::rewrites::tracker::emit::Error), +} + +/// Options for use in [index_as_worktree_with_renames()](crate::index_as_worktree_with_renames()). +#[derive(Clone, Default)] +pub struct Options { + /// The kind of hash to create when hashing worktree entries. + pub object_hash: gix_hash::Kind, + /// Options to configure how modifications to tracked files should be obtained. + pub tracked_file_modifications: crate::index_as_worktree::Options, + /// Options to control the directory walk that informs about untracked files. + /// + /// Note that we forcefully disable emission of tracked files to avoid any overlap + /// between emissions to indicate modifications, and those that are obtained by + /// the directory walk. + /// + /// If `None`, the directory walk portion will not run at all, yielding data similar + /// to a bare [index_as_worktree()](crate::index_as_worktree()) call. + pub dirwalk: Option, + /// The configuration for the rewrite tracking. Note that if set, the [`dirwalk`](Self::dirwalk) should be configured + /// to *not* collapse untracked and ignored entries, as rewrite tracking is on a file-by-file basis. + /// + /// If `None`, no tracking will occour, which means that all output becomes visible to the delegate immediately. + pub rewrites: Option, +} + +/// Provide additional information collected during the runtime of [`index_as_worktree_with_renames()`](crate::index_as_worktree_with_renames()). +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Outcome { + /// The outcome of the modification check of tracked files. + pub tracked_file_modification: crate::index_as_worktree::Outcome, + /// The outcome of the directory walk, or `None` if its [options](Options::dirwalk) also weren't present which means + /// the dirwalk never ran. + pub dirwalk: Option, + /// The result of the rewrite operation, if [rewrites were configured](Options::rewrites). + pub rewrites: Option, +} + +/// Either an index entry for renames or another directory entry in case of copies. +#[derive(Clone, PartialEq, Debug)] +pub enum RewriteSource<'index, ContentChange, SubmoduleStatus> { + /// The source originates in the index and is detected as missing in the working tree. + /// This can also happen for copies + RewriteFromIndex { + /// All entries in the index. + index_entries: &'index [gix_index::Entry], + /// The entry that is the source of the rewrite, which means it was removed on disk, + /// equivalent to [Change::Removed](crate::index_as_worktree::Change::Removed). + /// + /// Note that the [entry-id](gix_index::Entry::id) is the content-id of the source of the rewrite. + source_entry: &'index gix_index::Entry, + /// The index of the `source_entry` for lookup in `index_entries` - useful to look at neighbors. + source_entry_index: usize, + /// The repository-relative path of the `source_entry`. + source_rela_path: &'index BStr, + /// The computed status of the `source_entry`, which would always be + source_status: EntryStatus, + }, + /// This source originates in the directory tree and is always the source of copies. + CopyFromDirectoryEntry { + /// The source of the copy operation, which is also an entry of the directory walk. + /// + /// Note that its [`rela_path`](gix_dir::EntryRef::rela_path) is the source of the rewrite. + source_dirwalk_entry: gix_dir::Entry, + /// `collapsed_directory_status` is `Some(dir_status)` if this `dirwalk_entry` was part of a directory with the given + /// `dir_status` that wasn't the same as the one of `entry` and if [gix_dir::walk::Options::emit_collapsed] was + /// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch). + /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All). + source_dirwalk_entry_collapsed_directory_status: Option, + /// The object id as it would appear if the entry was written to the object database. + /// It's the same as `dirwalk_entry_id`, or `diff` is `Some(_)` to indicate that the copy was determined by similarity. + source_dirwalk_entry_id: gix_hash::ObjectId, + }, +} + +/// An 'entry' in the sense of a merge of modified tracked files and results from a directory walk. +#[derive(Clone, PartialEq, Debug)] +pub enum Entry<'index, ContentChange, SubmoduleStatus> { + /// A tracked file was modified, and index-specific information is passed. + Modification { + /// All entries in the index. + entries: &'index [gix_index::Entry], + /// The entry with modifications. + entry: &'index gix_index::Entry, + /// The index of the `entry` for lookup in `entries` - useful to look at neighbors. + entry_index: usize, + /// The repository-relative path of the entry. + rela_path: &'index BStr, + /// The computed status of the entry. + status: EntryStatus, + }, + /// An entry returned by the directory walk, without any relation to the index. + /// + /// This can happen if ignored files are returned as well, or if rename-tracking is disabled. + DirectoryContents { + /// The entry found during the disk traversal. + entry: gix_dir::Entry, + /// `collapsed_directory_status` is `Some(dir_status)` if this `entry` was part of a directory with the given + /// `dir_status` that wasn't the same as the one of `entry` and if [gix_dir::walk::Options::emit_collapsed] was + /// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch). + /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All). + collapsed_directory_status: Option, + }, + /// The rewrite tracking discovered a match between a deleted and added file, and considers them equal enough, + /// depending on the tracker settings. + /// + /// Note that the source of the rewrite is always the index as it detects the absence of entries, something that + /// can't be done during a directory walk. + Rewrite { + /// The source of the rewrite operation. + source: RewriteSource<'index, ContentChange, SubmoduleStatus>, + /// The untracked entry found during the disk traversal, the destination of the rewrite. + /// + /// Note that its [`rela_path`](gix_dir::EntryRef::rela_path) is the destination of the rewrite, and the current + /// location of the entry. + dirwalk_entry: gix_dir::Entry, + /// `collapsed_directory_status` is `Some(dir_status)` if this `dirwalk_entry` was part of a directory with the given + /// `dir_status` that wasn't the same as the one of `entry` and if [gix_dir::walk::Options::emit_collapsed] was + /// [CollapsedEntriesEmissionMode::OnStatusMismatch](gix_dir::walk::CollapsedEntriesEmissionMode::OnStatusMismatch). + /// It will also be `Some(dir_status)` if that option was [CollapsedEntriesEmissionMode::All](gix_dir::walk::CollapsedEntriesEmissionMode::All). + dirwalk_entry_collapsed_directory_status: Option, + /// The object id after the rename, specifically hashed in order to determine equality. + dirwalk_entry_id: gix_hash::ObjectId, + /// It's `None` if `source_entry.id` is equal to `dirwalk_entry_id`, as identity made an actual diff computation unnecessary. + /// Otherwise, and if enabled, it's `Some(stats)` to indicate how similar both entries were. + diff: Option, + + /// If true, this rewrite is created by copy, and `source_entry.id` is pointing to its source. + /// Otherwise, it's a rename, and `source_entry.id` points to a deleted object, + /// as renames are tracked as deletions and additions of the same or similar content. + copy: bool, + }, +} + +/// Access +impl RewriteSource<'_, ContentChange, SubmoduleStatus> { + /// The repository-relative path of this source. + pub fn rela_path(&self) -> &BStr { + match self { + RewriteSource::RewriteFromIndex { source_rela_path, .. } => source_rela_path, + RewriteSource::CopyFromDirectoryEntry { + source_dirwalk_entry, .. + } => source_dirwalk_entry.rela_path.as_bstr(), + } + } +} + +/// Access +impl Entry<'_, ContentChange, SubmoduleStatus> { + /// The repository-relative path at which the source of a rewrite is located. + /// + /// If this isn't a rewrite, the path is the location of the entry itself. + pub fn source_rela_path(&self) -> &BStr { + match self { + Entry::Modification { rela_path, .. } => rela_path, + Entry::DirectoryContents { entry, .. } => entry.rela_path.as_bstr(), + Entry::Rewrite { source, .. } => source.rela_path(), + } + } +} + +/// The context for [index_as_worktree_with_renames()`](crate::index_as_worktree_with_renames()). +pub struct Context<'a> { + /// The pathspec to limit the amount of paths that are checked. Can be empty to allow all paths. + /// + /// Note that these are expected to have a [commont_prefix()](gix_pathspec::Search::common_prefix()) according + /// to the prefix of the repository to efficiently limit the scope of the paths we process, both for the + /// index modifications as well as for the directory walk. + pub pathspec: gix_pathspec::Search, + /// A fully-configured platform capable of producing diffable buffers similar to what Git would do, for use + /// with rewrite tracking. + /// + /// Note that it contains some resource that are additionally used here: + /// + /// * `attr_stack` + /// - A stack pre-configured to allow accessing attributes for each entry, as required for `filter` + /// and possibly pathspecs. + /// It *may* also allow accessing `.gitignore` information for use in the directory walk. + /// If no excludes information is present, the directory walk will identify ignored files as untracked, which + /// might be desirable under certain circumstances. + /// * `filter` + /// - A filter to be able to perform conversions from and to the worktree format. + /// It is needed to potentially refresh the index with data read from the worktree, which needs to be converted back + /// to the form stored in Git. + pub resource_cache: gix_diff::blob::Platform, + /// A flag to query to learn if cancellation is requested. + pub should_interrupt: &'a AtomicBool, + /// The context for the directory walk. + pub dirwalk: DirwalkContext<'a>, +} + +/// All information that is required to perform a [dirwalk](gix_dir::walk()). +pub struct DirwalkContext<'a> { + /// The `git_dir` of the parent repository, after a call to [`gix_path::realpath()`]. + /// + /// It's used to help us differentiate our own `.git` directory from nested unrelated repositories, + /// which is needed if `core.worktree` is used to nest the `.git` directory deeper within. + pub git_dir_realpath: &'a std::path::Path, + /// The current working directory as returned by `gix_fs::current_dir()` to assure it respects `core.precomposeUnicode`. + /// It's used to produce the realpath of the git-dir of a repository candidate to assure it's not our own repository. + pub current_dir: &'a std::path::Path, + /// A utility to lookup index entries faster, and deal with ignore-case handling. + /// + /// Must be set if `ignore_case` is `true`, or else some entries won't be found if their case is different. + /// + /// [Read more in `gix-dir`](gix_dir::walk::Context::ignore_case_index_lookup). + pub ignore_case_index_lookup: Option<&'a gix_index::AccelerateLookup<'a>>, +} + +/// Observe the status of an entry by comparing an index entry to the worktree, along +/// with potential directory walk results. +pub trait VisitEntry<'a> { + /// Data generated by comparing an entry with a file. + type ContentChange; + /// Data obtained when checking the submodule status. + type SubmoduleStatus; + /// Observe the `status` of `entry` at the repository-relative `rela_path` at `entry_index` + /// (for accessing `entry` and surrounding in the complete list of `entries`). + fn visit_entry(&mut self, entry: Entry<'a, Self::ContentChange, Self::SubmoduleStatus>); +} diff --git a/gix-status/src/lib.rs b/gix-status/src/lib.rs index 0749c5bd6cb..a2dbf6a4c51 100644 --- a/gix-status/src/lib.rs +++ b/gix-status/src/lib.rs @@ -6,11 +6,23 @@ //! * find untracked files //! //! While also being able to check check if the working tree is dirty, quickly. +//! +//! ### Feature Flags +#![cfg_attr( + all(doc, feature = "document-features"), + doc = ::document_features::document_features!() +)] +#![cfg_attr(all(doc, feature = "document-features"), feature(doc_cfg, doc_auto_cfg))] #![deny(missing_docs, rust_2018_idioms, unsafe_code)] pub mod index_as_worktree; pub use index_as_worktree::function::index_as_worktree; +#[cfg(feature = "worktree-rewrites")] +pub mod index_as_worktree_with_renames; +#[cfg(feature = "worktree-rewrites")] +pub use index_as_worktree_with_renames::function::index_as_worktree_with_renames; + /// A stack that validates we are not going through a symlink in a way that is read-only. /// /// It can efficiently validate paths when these are queried in sort-order, which leads to each component diff --git a/gix-status/tests/Cargo.toml b/gix-status/tests/Cargo.toml index 2e138b9a184..576a81829c4 100644 --- a/gix-status/tests/Cargo.toml +++ b/gix-status/tests/Cargo.toml @@ -17,10 +17,15 @@ path = "worktree.rs" gix-features-parallel = ["gix-features/parallel"] [dev-dependencies] -gix-status = { path = ".." } +gix-status = { path = "..", features = ["worktree-rewrites"] } gix-testtools = { path = "../../tests/tools" } gix-index = { path = "../../gix-index" } gix-fs = { path = "../../gix-fs" } +gix-diff = { path = "../../gix-diff" } +gix-filter = { path = "../../gix-filter" } +gix-path = { path = "../../gix-path" } +gix-dir = { path = "../../gix-dir" } +gix-odb = { path = "../../gix-odb" } gix-hash = { path = "../../gix-hash" } gix-object = { path = "../../gix-object" } gix-features = { path = "../../gix-features" } @@ -28,4 +33,4 @@ gix-pathspec = { path = "../../gix-pathspec" } gix-worktree = { path = "../../gix-worktree" } filetime = "0.2.15" bstr = { version = "1.3.0", default-features = false } - +pretty_assertions = "1.4.0" diff --git a/gix-status/tests/fixtures/generated-archives/status_many.tar.xz b/gix-status/tests/fixtures/generated-archives/status_many.tar.xz new file mode 100644 index 0000000000000000000000000000000000000000..0ba30dc3e292de0feadeb9dd9511318e5c35d54e GIT binary patch literal 11216 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 literal 0 HcmV?d00001 diff --git a/gix-status/tests/fixtures/status_many.sh b/gix-status/tests/fixtures/status_many.sh new file mode 100644 index 00000000000..2315ddc2a96 --- /dev/null +++ b/gix-status/tests/fixtures/status_many.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -eu -o pipefail + +git init -q changed-and-untracked +(cd changed-and-untracked + touch empty + echo "content" > executable + chmod +x executable + + mkdir dir + echo "other content" > dir/content + echo "different content" > dir/content2 + + git add -A + git commit -m "Commit" + echo "change" >> executable + + + mkdir dir/empty + >dir/untracked + >untracked + + git status +) + +cp -R changed-and-untracked changed-and-untracked-and-renamed +(cd changed-and-untracked-and-renamed + # it has a local change compared to the indexed version, hence it's rewritten + mv executable rewritten-executable + + cp dir/content content-copy + cp dir/content content-copy-with-rewrite + echo change >> content-copy-with-rewrite + + mv dir/content plainly-renamed-content + + mv dir/content2 content-with-rewrite + echo change >> content-with-rewrite + +) diff --git a/gix-status/tests/status/index_as_worktree.rs b/gix-status/tests/status/index_as_worktree.rs index d6753e26943..e485f0759c7 100644 --- a/gix-status/tests/status/index_as_worktree.rs +++ b/gix-status/tests/status/index_as_worktree.rs @@ -22,7 +22,7 @@ use crate::fixture_path; // changes when extracting the data so we need to disable all advanced stat // changes and only look at mtime seconds and file size to properly // test all code paths (and to trigger racy git). -const TEST_OPTIONS: index::entry::stat::Options = index::entry::stat::Options { +pub(super) const TEST_OPTIONS: index::entry::stat::Options = index::entry::stat::Options { trust_ctime: false, check_stat: false, use_nsec: false, @@ -128,7 +128,9 @@ fn fixture_filtered_detailed( } /// Note that we also reset certain information to assure there is no flakiness - everything regarding race-detection otherwise can cause failures. -fn records_to_tuple<'index>(records: impl IntoIterator>) -> Vec> { +pub(super) fn records_to_tuple<'index>( + records: impl IntoIterator>, +) -> Vec> { records .into_iter() .filter_map(|r| deracify_status(r.status).map(|status| (r.relative_path, r.entry_index, status))) @@ -159,8 +161,8 @@ fn deracify_status(status: EntryStatus) -> Option { } #[derive(Clone)] -struct SubmoduleStatusMock { - dirty: bool, +pub(super) struct SubmoduleStatusMock { + pub(super) dirty: bool, } impl SubmoduleStatus for SubmoduleStatusMock { @@ -172,7 +174,7 @@ impl SubmoduleStatus for SubmoduleStatusMock { } } -fn to_pathspecs(input: &[&str]) -> Vec { +pub(super) fn to_pathspecs(input: &[&str]) -> Vec { input .iter() .map(|pattern| gix_pathspec::parse(pattern.as_bytes(), Default::default()).expect("known to be valid")) diff --git a/gix-status/tests/status/index_as_worktree_with_renames.rs b/gix-status/tests/status/index_as_worktree_with_renames.rs new file mode 100644 index 00000000000..3f14fd694e1 --- /dev/null +++ b/gix-status/tests/status/index_as_worktree_with_renames.rs @@ -0,0 +1,332 @@ +use crate::status::fixture_path; +use bstr::ByteSlice; +use gix_diff::blob::pipeline::WorktreeRoots; +use gix_diff::rewrites::CopySource; +use gix_status::index_as_worktree::traits::FastEq; +use gix_status::index_as_worktree::{Change, EntryStatus}; +use gix_status::index_as_worktree_with_renames; +use gix_status::index_as_worktree_with_renames::{Context, DirwalkContext, Entry, Options, Outcome, Recorder}; +use pretty_assertions::assert_eq; + +#[test] +fn changed_and_untracked_and_renamed() { + let expectations_with_dirwalk = [ + // Not always will we match the right source to destinations, there is ambiguity. + Expectation::DirwalkEntry { + rela_path: "content-copy-with-rewrite", + status: gix_dir::entry::Status::Untracked, + disk_kind: Some(gix_dir::entry::Kind::File), + }, + Expectation::Rewrite { + source_rela_path: "dir/content", + dest_rela_path: "content-copy", + dest_dirwalk_status: gix_dir::entry::Status::Untracked, + diff: None, + copy: false, + }, + Expectation::Rewrite { + source_rela_path: "dir/content2", + dest_rela_path: "content-with-rewrite", + dest_dirwalk_status: gix_dir::entry::Status::Untracked, + diff: Some(gix_diff::blob::DiffLineStats { + removals: 0, + insertions: 1, + before: 1, + after: 2, + similarity: 0.72, + }), + copy: false, + }, + Expectation::Rewrite { + source_rela_path: "empty", + dest_rela_path: "untracked", + dest_dirwalk_status: gix_dir::entry::Status::Untracked, + diff: None, + copy: true, + }, + Expectation::Rewrite { + source_rela_path: "empty", + dest_rela_path: "dir/untracked", + dest_dirwalk_status: gix_dir::entry::Status::Untracked, + diff: None, + copy: true, + }, + Expectation::Rewrite { + source_rela_path: "executable", + dest_rela_path: "rewritten-executable", + dest_dirwalk_status: gix_dir::entry::Status::Untracked, + diff: Some(gix_diff::blob::DiffLineStats { + removals: 0, + insertions: 1, + before: 1, + after: 2, + similarity: 0.53333336, + }), + copy: false, + }, + // This is just detected as untracked, related to how the rename-tracker matches pairs + Expectation::DirwalkEntry { + rela_path: "plainly-renamed-content", + status: gix_dir::entry::Status::Untracked, + disk_kind: Some(gix_dir::entry::Kind::File), + }, + ]; + let rewrites = gix_diff::Rewrites { + copies: Some(gix_diff::rewrites::Copies { + source: CopySource::FromSetOfModifiedFiles, + percentage: Some(0.3), + }), + percentage: Some(0.3), + limit: 0, + }; + let out = fixture_filtered_detailed( + "changed-and-untracked-and-renamed", + &[], + &expectations_with_dirwalk, + Some(rewrites), + Some(Default::default()), + ); + assert_eq!( + out.rewrites, + Some(gix_diff::rewrites::Outcome { + options: rewrites, + num_similarity_checks: 11, + num_similarity_checks_skipped_for_rename_tracking_due_to_limit: 0, + num_similarity_checks_skipped_for_copy_tracking_due_to_limit: 0, + }) + ) +} + +#[test] +fn changed_and_untracked() { + let out = fixture_filtered_detailed( + "changed-and-untracked", + &[], + &[Expectation::Modification { + rela_path: "executable", + status: EntryStatus::Change(Change::Modification { + executable_bit_changed: false, + content_change: Some(()), + set_entry_stat_size_zero: false, + }), + }], + None, + None, + ); + assert_eq!(out.tracked_file_modification.entries_processed, 4); + assert_eq!( + out.dirwalk, None, + "we didn't configure the dirwalk, so it's just like a modification check" + ); + assert_eq!(out.rewrites, None, "rewrite checking isn't configured either"); + + let expectations_with_dirwalk = [ + Expectation::DirwalkEntry { + rela_path: "dir/untracked", + status: gix_dir::entry::Status::Untracked, + disk_kind: Some(gix_dir::entry::Kind::File), + }, + Expectation::Modification { + rela_path: "executable", + status: EntryStatus::Change(Change::Modification { + executable_bit_changed: false, + content_change: Some(()), + set_entry_stat_size_zero: false, + }), + }, + Expectation::DirwalkEntry { + rela_path: "untracked", + status: gix_dir::entry::Status::Untracked, + disk_kind: Some(gix_dir::entry::Kind::File), + }, + ]; + let out = fixture_filtered_detailed( + "changed-and-untracked", + &[], + &expectations_with_dirwalk, + None, + Some(gix_dir::walk::Options::default()), + ); + + let dirwalk = out.dirwalk.expect("configured thus has output"); + assert_eq!( + dirwalk, + gix_dir::walk::Outcome { + read_dir_calls: 3, + returned_entries: 2, + seen_entries: 8, + } + ); + assert_eq!(out.rewrites, None, "rewrites are still not configured"); + + let out = fixture_filtered_detailed( + "changed-and-untracked", + &[], + &expectations_with_dirwalk, + Some(Default::default()), + Some(gix_dir::walk::Options::default()), + ); + + let rewrites = out.rewrites.expect("configured thus has output"); + assert_eq!( + rewrites, + gix_diff::rewrites::Outcome::default(), + "there actually is no candidates pairs as there are no deletions" + ); +} + +fn fixture_filtered_detailed( + subdir: &str, + pathspecs: &[&str], + expected: &[Expectation<'_>], + rewrites: Option, + dirwalk: Option, +) -> Outcome { + fn cleanup(mut out: Outcome) -> Outcome { + out.tracked_file_modification.worktree_bytes = 0; + out.tracked_file_modification.worktree_files_read = 0; + out.tracked_file_modification.entries_to_update = 0; + out.tracked_file_modification.racy_clean = 0; + out + } + + let worktree = fixture_path("status_many.sh").join(subdir); + let git_dir = worktree.join(".git"); + let index = gix_index::File::at(git_dir.join("index"), gix_hash::Kind::Sha1, false, Default::default()).unwrap(); + let search = gix_pathspec::Search::from_specs( + crate::status::index_as_worktree::to_pathspecs(pathspecs), + None, + std::path::Path::new(""), + ) + .expect("valid specs can be normalized"); + let stack = gix_worktree::Stack::from_state_and_ignore_case( + worktree.clone(), + false, + gix_worktree::stack::State::AttributesAndIgnoreStack { + attributes: Default::default(), + ignore: Default::default(), + }, + &index, + index.path_backing(), + ); + let capabilities = gix_fs::Capabilities::probe(&git_dir); + let resource_cache = gix_diff::blob::Platform::new( + Default::default(), + gix_diff::blob::Pipeline::new( + WorktreeRoots { + old_root: None, + new_root: Some(worktree.to_owned()), + }, + gix_filter::Pipeline::new(Default::default(), Default::default()), + vec![], + gix_diff::blob::pipeline::Options { + large_file_threshold_bytes: 0, + fs: capabilities, + }, + ), + gix_diff::blob::pipeline::Mode::ToGit, + stack, + ); + + let git_dir_real = gix_path::realpath(&git_dir).unwrap(); + let cwd = gix_fs::current_dir(capabilities.precompose_unicode).unwrap(); + let context = Context { + pathspec: search, + resource_cache, + should_interrupt: &Default::default(), + dirwalk: DirwalkContext { + git_dir_realpath: &git_dir_real, + current_dir: &cwd, + ignore_case_index_lookup: None, + }, + }; + let options = Options { + object_hash: gix_hash::Kind::Sha1, + tracked_file_modifications: gix_status::index_as_worktree::Options { + fs: capabilities, + stat: crate::status::index_as_worktree::TEST_OPTIONS, + ..Default::default() + }, + dirwalk, + rewrites, + }; + + let mut recorder = Recorder::default(); + let objects = gix_odb::at(git_dir.join("objects")).unwrap().into_arc().unwrap(); + let outcome = index_as_worktree_with_renames( + &index, + &worktree, + &mut recorder, + FastEq, + crate::status::index_as_worktree::SubmoduleStatusMock { dirty: false }, + objects, + &mut gix_features::progress::Discard, + context, + options, + ) + .unwrap(); + + recorder + .records + .sort_unstable_by_key(|r| r.source_rela_path().to_owned()); + assert_eq!(records_to_expectations(&recorder.records), expected); + cleanup(outcome) +} + +fn records_to_expectations<'a>(recs: &'a [Entry<'_, (), ()>]) -> Vec> { + recs.iter() + .filter(|r| { + !matches!( + r, + Entry::Modification { + status: EntryStatus::NeedsUpdate(..), + .. + } + ) + }) + .map(|r| match r { + Entry::Modification { rela_path, status, .. } => Expectation::Modification { + rela_path: rela_path.to_str().unwrap(), + status: status.clone(), + }, + Entry::DirectoryContents { entry, .. } => Expectation::DirwalkEntry { + rela_path: entry.rela_path.to_str().unwrap(), + status: entry.status, + disk_kind: entry.disk_kind, + }, + Entry::Rewrite { + source, + dirwalk_entry, + diff, + copy, + .. + } => Expectation::Rewrite { + source_rela_path: source.rela_path().to_str().unwrap(), + dest_rela_path: dirwalk_entry.rela_path.to_str().unwrap(), + dest_dirwalk_status: dirwalk_entry.status, + diff: *diff, + copy: *copy, + }, + }) + .collect() +} + +#[derive(Debug, Clone, PartialEq)] +enum Expectation<'a> { + Modification { + rela_path: &'a str, + status: EntryStatus<(), ()>, + }, + DirwalkEntry { + rela_path: &'a str, + status: gix_dir::entry::Status, + disk_kind: Option, + }, + Rewrite { + source_rela_path: &'a str, + dest_rela_path: &'a str, + dest_dirwalk_status: gix_dir::entry::Status, + diff: Option, + copy: bool, + }, +} diff --git a/gix-status/tests/status/mod.rs b/gix-status/tests/status/mod.rs index e758770cf90..4da345b7a8d 100644 --- a/gix-status/tests/status/mod.rs +++ b/gix-status/tests/status/mod.rs @@ -1,4 +1,5 @@ mod index_as_worktree; +mod index_as_worktree_with_renames; pub fn fixture_path(name: &str) -> std::path::PathBuf { let dir = gix_testtools::scripted_fixture_read_only_standalone(std::path::Path::new(name).with_extension("sh")) diff --git a/justfile b/justfile index c43c6c94826..38294a5070f 100755 --- a/justfile +++ b/justfile @@ -92,6 +92,8 @@ check: cargo check -p gix-revision --no-default-features --features describe cargo check -p gix-mailmap --features serde cargo check -p gix-url --all-features + cargo check -p gix-status + cargo check -p gix-status --all-features cargo check -p gix-features --all-features cargo check -p gix-features --features parallel cargo check -p gix-features --features fs-walkdir-parallel