Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 32 additions & 10 deletions gix-index/src/entry/mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,44 @@ impl Mode {
stat: &crate::fs::Metadata,
has_symlinks: bool,
executable_bit: bool,
) -> Option<Change> {
self.change_to_match_fs_with_values(
stat.is_file(),
stat.is_dir(),
stat.is_symlink(),
stat.is_executable(),
has_symlinks,
executable_bit,
)
}

/// Like [`change_to_match_fs`](Self::change_to_match_fs) but accepts pre-extracted
/// file-type and permission bits, for callers that already have them (e.g. cached
/// metadata from a batched directory enumeration).
pub fn change_to_match_fs_with_values(
self,
is_file: bool,
is_dir: bool,
is_symlink: bool,
is_executable: bool,
has_symlinks: bool,
executable_bit: bool,
) -> Option<Change> {
match self {
Mode::FILE if !stat.is_file() => (),
Mode::SYMLINK if stat.is_symlink() => return None,
Mode::SYMLINK if has_symlinks && !stat.is_symlink() => (),
Mode::SYMLINK if !has_symlinks && !stat.is_file() => (),
Mode::COMMIT | Mode::DIR if !stat.is_dir() => (),
Mode::FILE if executable_bit && stat.is_executable() => return Some(Change::ExecutableBit),
Mode::FILE_EXECUTABLE if executable_bit && !stat.is_executable() => return Some(Change::ExecutableBit),
Mode::FILE if !is_file => (),
Mode::SYMLINK if is_symlink => return None,
Mode::SYMLINK if has_symlinks && !is_symlink => (),
Mode::SYMLINK if !has_symlinks && !is_file => (),
Mode::COMMIT | Mode::DIR if !is_dir => (),
Mode::FILE if executable_bit && is_executable => return Some(Change::ExecutableBit),
Mode::FILE_EXECUTABLE if executable_bit && !is_executable => return Some(Change::ExecutableBit),
_ => return None,
}
let new_mode = if stat.is_dir() {
let new_mode = if is_dir {
Mode::COMMIT
} else if executable_bit && stat.is_executable() {
} else if executable_bit && is_executable {
Mode::FILE_EXECUTABLE
} else if has_symlinks && stat.is_symlink() {
} else if has_symlinks && is_symlink {
Mode::SYMLINK
} else {
Mode::FILE
Expand Down
10 changes: 10 additions & 0 deletions gix-status/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,22 @@ gix-diff = { version = "^0.63.0", path = "../gix-diff", default-features = false
thiserror = "2.0.18"
filetime = "0.2.27"
bstr = { version = "1.12.0", default-features = false }
hashbrown = "0.16.0"

document-features = { version = "0.2.0", optional = true }

[target.'cfg(not(target_has_atomic = "64"))'.dependencies]
portable-atomic = "1"

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61.1", features = [
"Win32_Foundation",
"Win32_Storage_FileSystem",
# Needed for `SECURITY_ATTRIBUTES` in the signature of `CreateFileW`,
# which is gated behind this windows-sys feature even when the call passes null.
"Win32_Security",
] }

[dev-dependencies]
gix-status = { path = ".", features = ["worktree-rewrites", "parallel"] }
gix-hash = { path = "../gix-hash", features = ["sha1"] }
Expand Down
111 changes: 74 additions & 37 deletions gix-status/src/index_as_worktree/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use gix_filter::pipeline::convert::ToGitOutcome;
use gix_object::FindExt;

use crate::index_as_worktree::types::ConflictIndexEntry;
#[cfg(windows)]
use crate::worktree_stats::{FileMetadata, WorktreeStats};
use crate::{
AtomicU64, SymlinkCheck,
index_as_worktree::{
Expand Down Expand Up @@ -63,6 +65,8 @@ pub fn index_as_worktree<'index, T, U, Find, E>(
stack,
filter,
should_interrupt,
#[cfg(windows)]
worktree_stats,
}: Context<'_>,
options: Options,
) -> Result<Outcome, Error>
Expand Down Expand Up @@ -122,6 +126,8 @@ where
path_backing,
filter,
options,
#[cfg(windows)]
worktree_stats,

skipped_by_pathspec,
skipped_by_entry_flags,
Expand Down Expand Up @@ -228,6 +234,10 @@ struct State<'a, 'b> {
filter: gix_filter::Pipeline,
path_backing: &'b gix_index::PathStorageRef,
options: &'a Options,
/// Optional precomputed worktree stats for faster status checks on Windows.
/// Lookups happen before falling back to per-file syscalls.
#[cfg(windows)]
worktree_stats: Option<&'a WorktreeStats>,

skipped_by_pathspec: &'a AtomicUsize,
skipped_by_entry_flags: &'a AtomicUsize,
Expand Down Expand Up @@ -374,53 +384,80 @@ impl<'index> State<'_, 'index> {
}
Err(err) => return Err(Error::Io(err.into())),
};
self.symlink_metadata_calls.fetch_add(1, Ordering::Relaxed);
let metadata = match gix_index::fs::Metadata::from_path_no_follow(worktree_path) {
Ok(metadata) if metadata.is_dir() => {
// index entries are normally only for files/symlinks
// if a file turned into a directory it was removed
// the only exception here are submodules which are
// part of the index despite being directories
if entry.mode.is_submodule() {
let status = submodule
.status(entry, rela_path)
.map_err(|err| Error::SubmoduleStatus {
rela_path: rela_path.into(),
source: Box::new(err),
})?;
return Ok(status.map(|status| Change::SubmoduleModification(status).into()));
} else {

// Acquire metadata. On Windows we consult the precomputed stats first and
// only fall back to a syscall on miss; on other platforms per-file
// `lstat` is already fast, so we just do the syscall directly.
#[cfg(windows)]
let metadata = if let Some(cached) = self.worktree_stats.and_then(|c| c.get(rela_path)) {
FileMetadata::Cached(cached)
} else {
self.symlink_metadata_calls.fetch_add(1, Ordering::Relaxed);
match gix_index::fs::Metadata::from_path_no_follow(worktree_path) {
Ok(m) => FileMetadata::Live(m),
Err(err) if gix_fs::io_err::is_not_found(err.kind(), err.raw_os_error()) => {
return Ok(Some(Change::Removed.into()));
}
Err(err) => return Err(Error::Io(err.into())),
}
Ok(metadata) => metadata,
Err(err) if gix_fs::io_err::is_not_found(err.kind(), err.raw_os_error()) => {
return Ok(Some(Change::Removed.into()));
}
Err(err) => {
return Err(Error::Io(err.into()));
};
#[cfg(not(windows))]
let metadata = {
self.symlink_metadata_calls.fetch_add(1, Ordering::Relaxed);
match gix_index::fs::Metadata::from_path_no_follow(worktree_path) {
Ok(m) => m,
Err(err) if gix_fs::io_err::is_not_found(err.kind(), err.raw_os_error()) => {
return Ok(Some(Change::Removed.into()));
}
Err(err) => return Err(Error::Io(err.into())),
}
};

// Handle directory: index entries are normally only for files/symlinks.
// If a file turned into a directory it was removed.
// The only exception here are submodules which are part of the index despite being directories.
if metadata.is_dir() {
if entry.mode.is_submodule() {
let status = submodule
.status(entry, rela_path)
.map_err(|err| Error::SubmoduleStatus {
rela_path: rela_path.into(),
source: Box::new(err),
})?;
return Ok(status.map(|status| Change::SubmoduleModification(status).into()));
} else {
return Ok(Some(Change::Removed.into()));
}
}

if entry.flags.contains(gix_index::entry::Flags::INTENT_TO_ADD) {
return Ok(Some(EntryStatus::IntentToAdd));
}

#[cfg(windows)]
let new_stat = metadata.to_stat()?;
#[cfg(not(windows))]
let new_stat = gix_index::entry::Stat::from_fs(&metadata)?;
let executable_bit_changed =
match entry

#[cfg(windows)]
let mode_change = metadata.mode_change(entry.mode, self.options.fs.symlink, self.options.fs.executable_bit);
#[cfg(not(windows))]
let mode_change =
entry
.mode
.change_to_match_fs(&metadata, self.options.fs.symlink, self.options.fs.executable_bit)
{
Some(gix_index::entry::mode::Change::Type { new_mode }) => {
return Ok(Some(
Change::Type {
worktree_mode: new_mode,
}
.into(),
));
}
Some(gix_index::entry::mode::Change::ExecutableBit) => true,
None => false,
};
.change_to_match_fs(&metadata, self.options.fs.symlink, self.options.fs.executable_bit);
let executable_bit_changed = match mode_change {
Some(gix_index::entry::mode::Change::Type { new_mode }) => {
return Ok(Some(
Change::Type {
worktree_mode: new_mode,
}
.into(),
));
}
Some(gix_index::entry::mode::Change::ExecutableBit) => true,
None => false,
};

// We implement racy-git. See racy-git.txt in the git documentation for detailed documentation.
//
Expand Down
8 changes: 8 additions & 0 deletions gix-status/src/index_as_worktree/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ use std::sync::atomic::AtomicBool;
use bstr::{BStr, BString};
use gix_index::entry;

#[cfg(windows)]
use crate::worktree_stats::WorktreeStats;

/// The error returned by [index_as_worktree()`](crate::index_as_worktree()).
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
Expand Down Expand Up @@ -55,6 +58,11 @@ pub struct Context<'a> {
pub filter: gix_filter::Pipeline,
/// A flag to query to learn if cancellation is requested.
pub should_interrupt: &'a AtomicBool,
/// Windows-only precomputed worktree stats from
/// [`crate::worktree_stats::prepare`]. Look-through: `None`/empty/partial
/// are all correct, misses fall through to a live `lstat`.
#[cfg(windows)]
pub worktree_stats: Option<&'a WorktreeStats>,
}

/// Provide additional information collected during the runtime of [`index_as_worktree()`](crate::index_as_worktree()).
Expand Down
2 changes: 2 additions & 0 deletions gix-status/src/index_as_worktree_with_renames/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ pub(super) mod function {
stack,
filter,
should_interrupt: ctx.should_interrupt,
#[cfg(windows)]
worktree_stats: ctx.worktree_stats,
},
options.tracked_file_modifications,
)
Expand Down
5 changes: 5 additions & 0 deletions gix-status/src/index_as_worktree_with_renames/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,11 @@ pub struct Context<'a> {
pub should_interrupt: &'a AtomicBool,
/// The context for the directory walk.
pub dirwalk: DirwalkContext<'a>,
/// Optional precomputed worktree stats for faster status checks on Windows.
///
/// See [`crate::index_as_worktree::Context::worktree_stats`] for details.
#[cfg(windows)]
pub worktree_stats: Option<&'a crate::worktree_stats::WorktreeStats>,
}

/// All information that is required to perform a [dirwalk](gix_dir::walk()).
Expand Down
9 changes: 9 additions & 0 deletions gix-status/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ use portable_atomic::AtomicU64;
pub mod index_as_worktree;
pub use index_as_worktree::function::index_as_worktree;

/// **Windows-only** worktree metadata preprocessing. Before per-entry
/// modification checks, one batched parallel directory walk gathers stat
/// results so that `index_as_worktree` can look them up instead of issuing a
/// per-file `lstat`. This trade only pays off where per-file stat is expensive
/// (Windows); on Linux/macOS `lstat` is sub-microsecond and the walk would be
/// pure overhead.
#[cfg(windows)]
pub mod worktree_stats;

#[cfg(feature = "worktree-rewrites")]
pub mod index_as_worktree_with_renames;
#[cfg(feature = "worktree-rewrites")]
Expand Down
Loading
Loading