diff --git a/Cargo.lock b/Cargo.lock index 803c0d585c2..49be9376fb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1362,6 +1362,7 @@ dependencies = [ "gix-diff", "gix-discover 0.16.2", "gix-features 0.28.1", + "gix-fs", "gix-glob 0.5.5", "gix-hash 0.10.4", "gix-hashtable 0.1.3", @@ -1460,11 +1461,11 @@ version = "0.10.0" dependencies = [ "bstr", "document-features", + "gix-fs", "gix-glob 0.5.5", "gix-path 0.7.3", "gix-quote 0.4.3", "gix-testtools", - "gix-utils", "kstring", "log", "serde", @@ -1718,6 +1719,14 @@ version = "0.0.0" name = "gix-filter" version = "0.0.0" +[[package]] +name = "gix-fs" +version = "0.1.0" +dependencies = [ + "gix-features 0.28.1", + "tempfile", +] + [[package]] name = "gix-glob" version = "0.5.5" @@ -1789,10 +1798,10 @@ version = "0.1.0" dependencies = [ "bstr", "document-features", + "gix-fs", "gix-glob 0.5.5", "gix-path 0.7.3", "gix-testtools", - "gix-utils", "serde", "unicode-bom 2.0.2", ] @@ -1873,8 +1882,8 @@ dependencies = [ name = "gix-lock" version = "5.0.0" dependencies = [ - "fastrand", "gix-tempfile 5.0.2", + "gix-utils", "tempfile", "thiserror", ] @@ -2138,6 +2147,7 @@ dependencies = [ "document-features", "gix-actor 0.19.0", "gix-features 0.28.1", + "gix-fs", "gix-hash 0.10.4", "gix-lock 5.0.0", "gix-object 0.28.0", @@ -2160,13 +2170,13 @@ dependencies = [ "gix-actor 0.19.0", "gix-discover 0.16.2", "gix-features 0.28.1", + "gix-fs", "gix-hash 0.10.4", "gix-lock 5.0.0", "gix-object 0.28.0", "gix-odb", "gix-ref 0.27.2", "gix-testtools", - "gix-utils", "gix-validate 0.7.4", "gix-worktree 0.15.2", "tempfile", @@ -2266,6 +2276,7 @@ version = "5.0.2" dependencies = [ "dashmap", "document-features", + "gix-fs", "libc", "once_cell", "parking_lot", @@ -2384,7 +2395,7 @@ dependencies = [ name = "gix-utils" version = "0.1.0" dependencies = [ - "gix-features 0.28.1", + "fastrand", ] [[package]] @@ -2430,8 +2441,10 @@ version = "0.15.2" dependencies = [ "bstr", "document-features", + "filetime", "gix-attributes 0.10.0", "gix-features 0.28.1", + "gix-fs", "gix-glob 0.5.5", "gix-hash 0.10.4", "gix-ignore", @@ -2440,7 +2453,6 @@ dependencies = [ "gix-odb", "gix-path 0.7.3", "gix-testtools", - "gix-utils", "io-close", "serde", "symlink", diff --git a/Cargo.toml b/Cargo.toml index 58c8be4b82d..41937970b32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -178,6 +178,7 @@ members = [ "gix-refspec", "gix-path", "gix-utils", + "gix-fs", "gix", "gitoxide-core", "gix-hashtable", diff --git a/README.md b/README.md index 7dd0b3a00d4..b143b1097b9 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ is usable to some extent. * `gitoxide-core` * **very early** _(possibly without any documentation and many rough edges)_ * [gix-utils](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-utils) + * [gix-fs](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-fs) * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree) * [gix-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-bitmap) * [gix-date](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-date) diff --git a/crate-status.md b/crate-status.md index bcf5a068cae..31181e9bf74 100644 --- a/crate-status.md +++ b/crate-status.md @@ -105,11 +105,16 @@ and itself relies on all `git-*` crates. It's not meant for consumption, for app * [x] hashset ### gix-utils - * **filesystem** * [x] probe capabilities * [x] symlink creation and removal * [x] file snapshots + +### gix-fs +* [x] probe capabilities +* [x] symlink creation and removal +* [x] file snapshots +* [x] stack abstraction ### gix-object * *decode (zero-copy)* borrowed objects diff --git a/etc/check-package-size.sh b/etc/check-package-size.sh index 93466d5fe25..e572c40f415 100755 --- a/etc/check-package-size.sh +++ b/etc/check-package-size.sh @@ -17,6 +17,9 @@ function indent () { echo "in root: gitoxide CLI" (enter cargo-smart-release && indent cargo diet -n --package-size-limit 110KB) (enter gix-actor && indent cargo diet -n --package-size-limit 5KB) +(enter gix-archive && indent cargo diet -n --package-size-limit 10KB) +(enter gix-utils && indent cargo diet -n --package-size-limit 10KB) +(enter gix-fs && indent cargo diet -n --package-size-limit 10KB) (enter gix-pathspec && indent cargo diet -n --package-size-limit 30KB) (enter gix-refspec && indent cargo diet -n --package-size-limit 30KB) (enter gix-path && indent cargo diet -n --package-size-limit 25KB) diff --git a/gitoxide-core/src/index/checkout.rs b/gitoxide-core/src/index/checkout.rs index 30268adfa79..b1d1674d37d 100644 --- a/gitoxide-core/src/index/checkout.rs +++ b/gitoxide-core/src/index/checkout.rs @@ -4,7 +4,7 @@ use std::{ }; use anyhow::bail; -use gix::{odb::FindExt, worktree::index::checkout, Progress}; +use gix::{odb::FindExt, worktree::checkout, Progress}; use crate::{ index, @@ -55,8 +55,8 @@ pub fn checkout_exclusive( progress.info(format!("Skipping {} DIR/SYMLINK/COMMIT entries", num_skipped)); } - let opts = gix::worktree::index::checkout::Options { - fs: gix::utils::FilesystemCapabilities::probe(dest_directory), + let opts = gix::worktree::checkout::Options { + fs: gix::fs::Capabilities::probe(dest_directory), destination_is_initially_empty: true, overwrite_existing: false, @@ -80,7 +80,7 @@ pub fn checkout_exclusive( files_updated, bytes_written, } = match repo { - Some(repo) => gix::worktree::index::checkout( + Some(repo) => gix::worktree::checkout( &mut index, dest_directory, { @@ -103,7 +103,7 @@ pub fn checkout_exclusive( should_interrupt, opts, ), - None => gix::worktree::index::checkout( + None => gix::worktree::checkout( &mut index, dest_directory, |_, buf| { diff --git a/gitoxide-core/src/repository/clone.rs b/gitoxide-core/src/repository/clone.rs index d7a53dee5e8..9ea7aa7cd72 100644 --- a/gitoxide-core/src/repository/clone.rs +++ b/gitoxide-core/src/repository/clone.rs @@ -98,7 +98,7 @@ pub(crate) mod function { } }; - if let Some(gix::worktree::index::checkout::Outcome { collisions, errors, .. }) = outcome { + if let Some(gix::worktree::checkout::Outcome { collisions, errors, .. }) = outcome { if !(collisions.is_empty() && errors.is_empty()) { let mut messages = Vec::new(); if !errors.is_empty() { diff --git a/gix-attributes/Cargo.toml b/gix-attributes/Cargo.toml index dfadd8e7f08..7cf7c6c5e1f 100644 --- a/gix-attributes/Cargo.toml +++ b/gix-attributes/Cargo.toml @@ -33,7 +33,7 @@ document-features = { version = "0.2.1", optional = true } [dev-dependencies] gix-testtools = { path = "../tests/tools"} -gix-utils = { path = "../gix-utils" } +gix-fs = { path = "../gix-fs" } [package.metadata.docs.rs] all-features = true diff --git a/gix-attributes/tests/search/mod.rs b/gix-attributes/tests/search/mod.rs index 7ab2b11d404..78149300065 100644 --- a/gix-attributes/tests/search/mod.rs +++ b/gix-attributes/tests/search/mod.rs @@ -2,7 +2,7 @@ use bstr::{BStr, ByteSlice}; use gix_attributes::search::{AttributeId, Outcome}; use gix_attributes::{AssignmentRef, NameRef, StateRef}; use gix_glob::pattern::Case; -use gix_utils::FilesystemCapabilities; + use std::collections::BTreeMap; mod specials { @@ -62,7 +62,7 @@ fn baseline() -> crate::Result { let mut buf = Vec::new(); // Due to the way our setup differs from gits dynamic stack (which involves trying to read files from disk // by path) we can only test one case baseline, so we require multiple platforms (or filesystems) to run this. - let case = if FilesystemCapabilities::probe("../.git").ignore_case { + let case = if gix_fs::Capabilities::probe("../.git").ignore_case { Case::Fold } else { Case::Sensitive diff --git a/gix-fs/Cargo.toml b/gix-fs/Cargo.toml new file mode 100644 index 00000000000..ec5a95b650c --- /dev/null +++ b/gix-fs/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "gix-fs" +version = "0.1.0" +repository = "https://github.com/Byron/gitoxide" +license = "MIT/Apache-2.0" +description = "A crate providing file system specific utilities to `gitoxide`" +authors = ["Sebastian Thiel "] +edition = "2021" +rust-version = "1.64" + +[lib] +doctest = false + +[dependencies] +gix-features = { path = "../gix-features" } + +[dev-dependencies] +tempfile = "3.5.0" diff --git a/gix-utils/src/fs_capabilities.rs b/gix-fs/src/capabilities.rs similarity index 90% rename from gix-utils/src/fs_capabilities.rs rename to gix-fs/src/capabilities.rs index 0c7d2b6bfd7..81ee11d70ea 100644 --- a/gix-utils/src/fs_capabilities.rs +++ b/gix-fs/src/capabilities.rs @@ -1,11 +1,11 @@ // TODO: tests -use crate::FilesystemCapabilities; +use crate::Capabilities; use std::path::Path; #[cfg(windows)] -impl Default for FilesystemCapabilities { +impl Default for Capabilities { fn default() -> Self { - FilesystemCapabilities { + Capabilities { precompose_unicode: false, ignore_case: true, executable_bit: false, @@ -15,9 +15,9 @@ impl Default for FilesystemCapabilities { } #[cfg(target_os = "macos")] -impl Default for FilesystemCapabilities { +impl Default for Capabilities { fn default() -> Self { - FilesystemCapabilities { + Capabilities { precompose_unicode: true, ignore_case: true, executable_bit: true, @@ -27,9 +27,9 @@ impl Default for FilesystemCapabilities { } #[cfg(all(unix, not(target_os = "macos")))] -impl Default for FilesystemCapabilities { +impl Default for Capabilities { fn default() -> Self { - FilesystemCapabilities { + Capabilities { precompose_unicode: false, ignore_case: false, executable_bit: true, @@ -38,7 +38,7 @@ impl Default for FilesystemCapabilities { } } -impl FilesystemCapabilities { +impl Capabilities { /// try to determine all values in this context by probing them in the given `git_dir`, which /// should be on the file system the git repository is located on. /// `git_dir` is a typical git repository, expected to be populated with the typical files like `config`. @@ -46,8 +46,8 @@ impl FilesystemCapabilities { /// All errors are ignored and interpreted on top of the default for the platform the binary is compiled for. pub fn probe(git_dir: impl AsRef) -> Self { let root = git_dir.as_ref(); - let ctx = FilesystemCapabilities::default(); - FilesystemCapabilities { + let ctx = Capabilities::default(); + Capabilities { symlink: Self::probe_symlink(root).unwrap_or(ctx.symlink), ignore_case: Self::probe_ignore_case(root).unwrap_or(ctx.ignore_case), precompose_unicode: Self::probe_precompose_unicode(root).unwrap_or(ctx.precompose_unicode), diff --git a/gix-tempfile/src/fs/create_dir.rs b/gix-fs/src/dir/create.rs similarity index 99% rename from gix-tempfile/src/fs/create_dir.rs rename to gix-fs/src/dir/create.rs index 65efdb2b889..7c7c9a03342 100644 --- a/gix-tempfile/src/fs/create_dir.rs +++ b/gix-fs/src/dir/create.rs @@ -28,7 +28,7 @@ impl Default for Retries { mod error { use std::{fmt, path::Path}; - use crate::fs::create_dir::Retries; + use crate::dir::create::Retries; /// The error returned by [all()][super::all()]. #[allow(missing_docs)] diff --git a/gix-fs/src/dir/mod.rs b/gix-fs/src/dir/mod.rs new file mode 100644 index 00000000000..4c709a6a88a --- /dev/null +++ b/gix-fs/src/dir/mod.rs @@ -0,0 +1,4 @@ +/// +pub mod create; +/// +pub mod remove; diff --git a/gix-tempfile/src/fs/remove_dir.rs b/gix-fs/src/dir/remove.rs similarity index 100% rename from gix-tempfile/src/fs/remove_dir.rs rename to gix-fs/src/dir/remove.rs diff --git a/gix-fs/src/lib.rs b/gix-fs/src/lib.rs new file mode 100644 index 00000000000..674f569c420 --- /dev/null +++ b/gix-fs/src/lib.rs @@ -0,0 +1,53 @@ +//! A crate with file-system specific utilities. +#![deny(rust_2018_idioms)] +#![forbid(unsafe_code)] + +/// Common knowledge about the worktree that is needed across most interactions with the work tree +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] +pub struct Capabilities { + /// If true, the filesystem will store paths as decomposed unicode, i.e. `รค` becomes `"a\u{308}"`, which means that + /// we have to turn these forms back from decomposed to precomposed unicode before storing it in the index or generally + /// using it. This also applies to input received from the command-line, so callers may have to be aware of this and + /// perform conversions accordingly. + /// If false, no conversions will be performed. + pub precompose_unicode: bool, + /// If true, the filesystem ignores the case of input, which makes `A` the same file as `a`. + /// This is also called case-folding. + pub ignore_case: bool, + /// If true, we assume the executable bit is honored as part of the files mode. If false, we assume the file system + /// ignores the executable bit, hence it will be reported as 'off' even though we just tried to set it to be on. + pub executable_bit: bool, + /// If true, the file system supports symbolic links and we should try to create them. Otherwise symbolic links will be checked + /// out as files which contain the link as text. + pub symlink: bool, +} +mod capabilities; + +mod snapshot; + +pub use snapshot::{FileSnapshot, SharedFileSnapshot, SharedFileSnapshotMut}; +use std::path::PathBuf; + +/// +pub mod symlink; + +/// +pub mod dir; + +/// A stack of path components with the delegation of side-effects as the currently set path changes, component by component. +#[derive(Clone)] +pub struct Stack { + /// The prefix/root for all paths we handle. + root: PathBuf, + /// the most recent known cached that we know is valid. + current: PathBuf, + /// The relative portion of `valid` that was added previously. + current_relative: PathBuf, + /// The amount of path components of 'current' beyond the roots components. + valid_components: usize, + /// If set, we assume the `current` element is a directory to affect calls to `(push|pop)_directory()`. + current_is_directory: bool, +} + +pub mod stack; diff --git a/gix-utils/src/snapshot.rs b/gix-fs/src/snapshot.rs similarity index 100% rename from gix-utils/src/snapshot.rs rename to gix-fs/src/snapshot.rs diff --git a/gix-worktree/src/fs/stack.rs b/gix-fs/src/stack.rs similarity index 99% rename from gix-worktree/src/fs/stack.rs rename to gix-fs/src/stack.rs index 734a4988b40..33e94812a2a 100644 --- a/gix-worktree/src/fs/stack.rs +++ b/gix-fs/src/stack.rs @@ -1,7 +1,8 @@ use std::path::{Path, PathBuf}; -use crate::fs::Stack; +use crate::Stack; +/// Access impl Stack { /// Returns the top-level path of the stack. pub fn root(&self) -> &Path { diff --git a/gix-utils/src/symlink.rs b/gix-fs/src/symlink.rs similarity index 74% rename from gix-utils/src/symlink.rs rename to gix-fs/src/symlink.rs index d8590823bfc..616c1137d2b 100644 --- a/gix-utils/src/symlink.rs +++ b/gix-fs/src/symlink.rs @@ -1,3 +1,4 @@ +use std::io::ErrorKind::AlreadyExists; use std::{io, path::Path}; #[cfg(not(windows))] @@ -35,20 +36,16 @@ pub fn create(original: &Path, link: &Path) -> io::Result<()> { } } -pub mod error { - use std::io::ErrorKind::AlreadyExists; - - #[cfg(not(windows))] - pub fn indicates_collision(err: &std::io::Error) -> bool { - // TODO: use ::IsDirectory as well when stabilized instead of raw_os_error(), and ::FileSystemLoop respectively - err.kind() == AlreadyExists +#[cfg(not(windows))] +pub fn is_collision_error(err: &std::io::Error) -> bool { + // TODO: use ::IsDirectory as well when stabilized instead of raw_os_error(), and ::FileSystemLoop respectively + err.kind() == AlreadyExists || err.raw_os_error() == Some(21) || err.raw_os_error() == Some(62) // no-follow on symlnk on mac-os || err.raw_os_error() == Some(40) // no-follow on symlnk on ubuntu - } +} - #[cfg(windows)] - pub fn indicates_collision(err: &std::io::Error) -> bool { - err.kind() == AlreadyExists || err.kind() == std::io::ErrorKind::PermissionDenied - } +#[cfg(windows)] +pub fn is_collision_error(err: &std::io::Error) -> bool { + err.kind() == AlreadyExists || err.kind() == std::io::ErrorKind::PermissionDenied } diff --git a/gix-worktree/tests/worktree/fs/mod.rs b/gix-fs/tests/capabilities/mod.rs similarity index 81% rename from gix-worktree/tests/worktree/fs/mod.rs rename to gix-fs/tests/capabilities/mod.rs index 50f09e14013..c4b6fa99ad2 100644 --- a/gix-worktree/tests/worktree/fs/mod.rs +++ b/gix-fs/tests/capabilities/mod.rs @@ -1,8 +1,8 @@ #[test] -fn from_probing_cwd() { +fn probe() { let dir = tempfile::tempdir().unwrap(); std::fs::File::create(dir.path().join("config")).unwrap(); - let ctx = gix_utils::FilesystemCapabilities::probe(dir.path()); + let ctx = gix_fs::Capabilities::probe(dir.path()); dbg!(ctx); let entries: Vec<_> = std::fs::read_dir(dir.path()) .unwrap() @@ -17,6 +17,3 @@ fn from_probing_cwd() { entries ); } - -mod cache; -mod stack; diff --git a/gix-tempfile/tests/tempfile/fs/create_dir.rs b/gix-fs/tests/dir/create.rs similarity index 92% rename from gix-tempfile/tests/tempfile/fs/create_dir.rs rename to gix-fs/tests/dir/create.rs index fa7c629d27f..6693fd071f8 100644 --- a/gix-tempfile/tests/tempfile/fs/create_dir.rs +++ b/gix-fs/tests/dir/create.rs @@ -1,11 +1,11 @@ mod all { - use gix_tempfile::create_dir; + use gix_fs::dir::create; #[test] fn a_deeply_nested_directory() -> crate::Result { let dir = tempfile::tempdir()?; let target = &dir.path().join("1").join("2").join("3").join("4").join("5").join("6"); - let dir = create_dir::all(target, Default::default())?; + let dir = create::all(target, Default::default())?; assert_eq!(dir, target, "all subdirectories can be created"); Ok(()) } @@ -13,15 +13,15 @@ mod all { mod iter { pub use std::io::ErrorKind::*; - use gix_tempfile::{ - create_dir, - create_dir::{Error::*, Retries}, + use gix_fs::dir::{ + create, + create::{Error::*, Retries}, }; #[test] fn an_existing_directory_causes_immediate_success() -> crate::Result { let dir = tempfile::tempdir()?; - let mut it = create_dir::Iter::new(dir.path()); + let mut it = create::Iter::new(dir.path()); assert_eq!( it.next().expect("item").expect("success"), dir.path(), @@ -35,7 +35,7 @@ mod iter { fn a_single_directory_can_be_created_too() -> crate::Result { let dir = tempfile::tempdir()?; let new_dir = dir.path().join("new"); - let mut it = create_dir::Iter::new(&new_dir); + let mut it = create::Iter::new(&new_dir); assert_eq!( it.next().expect("item").expect("success"), &new_dir, @@ -50,7 +50,7 @@ mod iter { fn multiple_intermediate_directories_are_created_automatically() -> crate::Result { let dir = tempfile::tempdir()?; let new_dir = dir.path().join("s1").join("s2").join("new"); - let mut it = create_dir::Iter::new(&new_dir); + let mut it = create::Iter::new(&new_dir); assert!( matches!(it.next(), Some(Err(Intermediate{dir, kind: k})) if k == NotFound && dir == new_dir), "dir is not present" @@ -83,7 +83,7 @@ mod iter { fn multiple_intermediate_directories_are_created_up_to_retries_limit() -> crate::Result { let dir = tempfile::tempdir()?; let new_dir = dir.path().join("s1").join("s2").join("new"); - let mut it = create_dir::Iter::new_with_retries( + let mut it = create::Iter::new_with_retries( &new_dir, Retries { on_create_directory_failure: 1, @@ -108,7 +108,7 @@ mod iter { std::fs::write(&new_dir, [42])?; assert!(new_dir.is_file()); - let mut it = create_dir::Iter::new(&new_dir); + let mut it = create::Iter::new(&new_dir); assert!( matches!(it.next(), Some(Err(Permanent{ dir, err, .. })) if err.kind() == AlreadyExists && dir == new_dir), @@ -123,7 +123,7 @@ mod iter { let dir = tempfile::tempdir()?; let new_dir = dir.path().join("a").join("new"); let parent_dir = new_dir.parent().unwrap(); - let mut it = create_dir::Iter::new_with_retries( + let mut it = create::Iter::new_with_retries( &new_dir, Retries { to_create_entire_directory: 2, @@ -161,7 +161,7 @@ mod iter { let dir = tempfile::tempdir()?; let new_dir = dir.path().join("a").join("new"); let parent_dir = new_dir.parent().unwrap(); - let mut it = create_dir::Iter::new(&new_dir); + let mut it = create::Iter::new(&new_dir); assert!( matches!(it.next(), Some(Err(Intermediate{dir, kind:k})) if k == NotFound && dir == new_dir), diff --git a/gix-fs/tests/dir/mod.rs b/gix-fs/tests/dir/mod.rs new file mode 100644 index 00000000000..0008e7ee8fb --- /dev/null +++ b/gix-fs/tests/dir/mod.rs @@ -0,0 +1,2 @@ +mod create; +mod remove; diff --git a/gix-tempfile/tests/tempfile/fs/remove_dir.rs b/gix-fs/tests/dir/remove.rs similarity index 82% rename from gix-tempfile/tests/tempfile/fs/remove_dir.rs rename to gix-fs/tests/dir/remove.rs index d77f7ec22a9..4b08e514710 100644 --- a/gix-tempfile/tests/tempfile/fs/remove_dir.rs +++ b/gix-fs/tests/dir/remove.rs @@ -1,7 +1,7 @@ mod empty_upwards_until_boundary { use std::{io, path::Path}; - use gix_tempfile::remove_dir; + use gix_fs::dir::remove; #[test] fn boundary_must_contain_target_dir() -> crate::Result { @@ -9,7 +9,7 @@ mod empty_upwards_until_boundary { let (target, boundary) = (dir.path().join("a"), dir.path().join("b")); std::fs::create_dir(&target)?; std::fs::create_dir(&boundary)?; - assert!(matches!(remove_dir::empty_upward_until_boundary(&target, &boundary), + assert!(matches!(remove::empty_upward_until_boundary(&target, &boundary), Err(err) if err.kind() == io::ErrorKind::InvalidInput)); assert!(target.is_dir()); assert!(boundary.is_dir()); @@ -21,7 +21,7 @@ mod empty_upwards_until_boundary { let parent = dir.path().join("a"); std::fs::create_dir(&parent)?; let target = parent.join("not-existing"); - assert_eq!(remove_dir::empty_upward_until_boundary(&target, dir.path())?, target); + assert_eq!(remove::empty_upward_until_boundary(&target, dir.path())?, target); assert!( parent.is_dir(), "the parent wasn't touched if the target already wasn't present" @@ -34,7 +34,7 @@ mod empty_upwards_until_boundary { let dir = tempfile::tempdir()?; let target = dir.path().join("actually-a-file"); std::fs::write(&target, [42])?; - assert!(remove_dir::empty_upward_until_boundary(&target, dir.path()).is_err()); // TODO: check for IsNotADirectory when it becomes stable + assert!(remove::empty_upward_until_boundary(&target, dir.path()).is_err()); // TODO: check for IsNotADirectory when it becomes stable assert!(target.is_file(), "it didn't touch the file"); assert!(dir.path().is_dir(), "it won't touch the boundary"); Ok(()) @@ -42,10 +42,7 @@ mod empty_upwards_until_boundary { #[test] fn boundary_being_the_target_dir_always_succeeds_and_we_do_nothing() -> crate::Result { let dir = tempfile::tempdir()?; - assert_eq!( - remove_dir::empty_upward_until_boundary(dir.path(), dir.path())?, - dir.path() - ); + assert_eq!(remove::empty_upward_until_boundary(dir.path(), dir.path())?, dir.path()); assert!(dir.path().is_dir(), "it won't touch the boundary"); Ok(()) } @@ -53,7 +50,7 @@ mod empty_upwards_until_boundary { fn a_directory_which_doesnt_exist_to_start_with_is_ok() -> crate::Result { let dir = tempfile::tempdir()?; let target = dir.path().join("does-not-exist"); - assert_eq!(remove_dir::empty_upward_until_boundary(&target, dir.path())?, target); + assert_eq!(remove::empty_upward_until_boundary(&target, dir.path())?, target); assert!(dir.path().is_dir(), "it won't touch the boundary"); Ok(()) } @@ -61,7 +58,7 @@ mod empty_upwards_until_boundary { fn boundary_directory_doesnt_have_to_exist_either_if_the_target_doesnt() -> crate::Result { let boundary = Path::new("/boundary"); let target = Path::new("/boundary/target"); - assert_eq!(remove_dir::empty_upward_until_boundary(target, boundary)?, target); + assert_eq!(remove::empty_upward_until_boundary(target, boundary)?, target); Ok(()) } #[test] @@ -69,7 +66,7 @@ mod empty_upwards_until_boundary { let dir = tempfile::tempdir()?; let nested = dir.path().join("a").join("b").join("to-delete"); std::fs::create_dir_all(&nested)?; - assert_eq!(remove_dir::empty_upward_until_boundary(&nested, dir.path())?, nested); + assert_eq!(remove::empty_upward_until_boundary(&nested, dir.path())?, nested); assert!(!nested.is_dir(), "it actually deleted the nested directory"); assert!(!nested.parent().unwrap().is_dir(), "parent one was deleted"); assert!( @@ -99,7 +96,7 @@ mod empty_depth_first { touch(&tree_parent.join("a").join("b"), "hello.ext")?; create_dir_all(tree_parent.join("one").join("two").join("empty"))?; - assert!(gix_tempfile::remove_dir::empty_depth_first(nested_parent).is_err()); + assert!(gix_fs::dir::remove::empty_depth_first(nested_parent).is_err()); Ok(()) } @@ -118,7 +115,7 @@ mod empty_depth_first { create_dir_all(tree_parent.join("one").join("two").join("three")).unwrap(); create_dir_all(tree_parent.join("c")).unwrap(); for empty in &[nested_parent, single_parent, tree_parent] { - gix_tempfile::remove_dir::empty_depth_first(empty).unwrap(); + gix_fs::dir::remove::empty_depth_first(empty).unwrap(); } } } @@ -126,7 +123,7 @@ mod empty_depth_first { /// We assume that all checks above also apply to the iterator, so won't repeat them here /// Test outside interference only mod iter { - use gix_tempfile::remove_dir; + use gix_fs::dir::remove; #[test] fn racy_directory_creation_during_deletion_always_wins_immediately() -> crate::Result { @@ -134,7 +131,7 @@ mod iter { let nested = dir.path().join("a").join("b").join("to-delete"); std::fs::create_dir_all(&nested)?; - let mut it = remove_dir::Iter::new(&nested, dir.path())?; + let mut it = remove::Iter::new(&nested, dir.path())?; assert_eq!(it.next().expect("item")?, nested, "delete leaves directory"); // recreate the deleted directory in racy fashion, causing the next-to-delete directory not to be empty. diff --git a/gix-fs/tests/fs.rs b/gix-fs/tests/fs.rs new file mode 100644 index 00000000000..63b597956b8 --- /dev/null +++ b/gix-fs/tests/fs.rs @@ -0,0 +1,4 @@ +type Result = std::result::Result>; +mod capabilities; +mod dir; +mod stack; diff --git a/gix-worktree/tests/worktree/fs/stack/mod.rs b/gix-fs/tests/stack/mod.rs similarity index 97% rename from gix-worktree/tests/worktree/fs/stack/mod.rs rename to gix-fs/tests/stack/mod.rs index ccb6fc0a5c4..43eeac05702 100644 --- a/gix-worktree/tests/worktree/fs/stack/mod.rs +++ b/gix-fs/tests/stack/mod.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -use gix_worktree::fs::Stack; +use gix_fs::Stack; #[derive(Debug, Default, Eq, PartialEq)] struct Record { @@ -9,7 +9,7 @@ struct Record { push: usize, } -impl gix_worktree::fs::stack::Delegate for Record { +impl gix_fs::stack::Delegate for Record { fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()> { self.push_dir += 1; self.dirs.push(stack.current().into()); diff --git a/gix-hash/src/kind.rs b/gix-hash/src/kind.rs index 86faddda261..efb7309cb26 100644 --- a/gix-hash/src/kind.rs +++ b/gix-hash/src/kind.rs @@ -39,13 +39,13 @@ impl std::fmt::Display for Kind { } impl Kind { - /// Returns the shortest hash we support + /// Returns the shortest hash we support. #[inline] pub const fn shortest() -> Self { Self::Sha1 } - /// Returns the longest hash we support + /// Returns the longest hash we support. #[inline] pub const fn longest() -> Self { Self::Sha1 @@ -63,14 +63,14 @@ impl Kind { [0u8; Kind::longest().len_in_bytes()] } - /// Returns the amount of ascii-characters needed to encode this has in hex + /// Returns the amount of ascii-characters needed to encode this has in hex. #[inline] pub const fn len_in_hex(&self) -> usize { match self { Kind::Sha1 => 40, } } - /// Returns the amount of bytes taken up by the hash of the current kind + /// Returns the amount of bytes taken up by the hash of the current kind. #[inline] pub const fn len_in_bytes(&self) -> usize { match self { diff --git a/gix-hash/src/lib.rs b/gix-hash/src/lib.rs index abed8ff9339..e4331f60284 100644 --- a/gix-hash/src/lib.rs +++ b/gix-hash/src/lib.rs @@ -28,10 +28,10 @@ pub struct Prefix { hex_len: usize, } -/// The size of a SHA1 hash digest in bytes +/// The size of a SHA1 hash digest in bytes. const SIZE_OF_SHA1_DIGEST: usize = 20; -/// Denotes the kind of function to produce a `Id` +/// Denotes the kind of function to produce a `Id`. #[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Kind { diff --git a/gix-hash/src/object_id.rs b/gix-hash/src/object_id.rs index 0258cbc48e8..12976f8edc1 100644 --- a/gix-hash/src/object_id.rs +++ b/gix-hash/src/object_id.rs @@ -77,21 +77,21 @@ pub mod decode { /// Access and conversion impl ObjectId { - /// Returns the kind of hash used in this `Id` + /// Returns the kind of hash used in this `Id`. #[inline] pub fn kind(&self) -> crate::Kind { match self { ObjectId::Sha1(_) => crate::Kind::Sha1, } } - /// Return the raw byte slice representing this hash + /// Return the raw byte slice representing this hash. #[inline] pub fn as_slice(&self) -> &[u8] { match self { Self::Sha1(b) => b.as_ref(), } } - /// Return the raw mutable byte slice representing this hash + /// Return the raw mutable byte slice representing this hash. #[inline] pub fn as_mut_slice(&mut self) -> &mut [u8] { match self { @@ -99,7 +99,7 @@ impl ObjectId { } } - /// The hash of an empty blob + /// The hash of an empty blob. #[inline] pub const fn empty_blob(hash: Kind) -> ObjectId { match hash { @@ -109,7 +109,7 @@ impl ObjectId { } } - /// The hash of an empty tree + /// The hash of an empty tree. #[inline] pub const fn empty_tree(hash: Kind) -> ObjectId { match hash { @@ -119,7 +119,7 @@ impl ObjectId { } } - /// Returns true if this hash consists of all null bytes + /// Returns true if this hash consists of all null bytes. #[inline] pub fn is_null(&self) -> bool { match self { @@ -127,6 +127,12 @@ impl ObjectId { } } + /// Returns `true` if this hash is equal to an empty blob. + #[inline] + pub fn is_empty_blob(&self) -> bool { + self == &Self::empty_blob(self.kind()) + } + /// Returns an Digest representing a hash with whose memory is zeroed. #[inline] pub const fn null(kind: crate::Kind) -> ObjectId { diff --git a/gix-hash/src/oid.rs b/gix-hash/src/oid.rs index 9ed1fd229be..e9a9034c61e 100644 --- a/gix-hash/src/oid.rs +++ b/gix-hash/src/oid.rs @@ -34,7 +34,7 @@ impl hash::Hash for oid { } } -/// A utility able to format itself with the given amount of characters in hex +/// A utility able to format itself with the given amount of characters in hex. #[derive(PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct HexDisplay<'a> { inner: &'a oid, @@ -91,7 +91,7 @@ impl oid { Self::from_bytes(value) } - /// Only from code that statically assures correct sizes using array conversions + /// Only from code that statically assures correct sizes using array conversions. pub(crate) fn from_bytes(value: &[u8]) -> &Self { #[allow(unsafe_code)] unsafe { @@ -102,13 +102,13 @@ impl oid { /// Access impl oid { - /// The kind of hash used for this Digest + /// The kind of hash used for this Digest. #[inline] pub fn kind(&self) -> crate::Kind { crate::Kind::from_len_in_bytes(self.bytes.len()) } - /// The first byte of the hash, commonly used to partition a set of `Id`s + /// The first byte of the hash, commonly used to partition a set of `Id`s. #[inline] pub fn first_byte(&self) -> u8 { self.bytes[0] @@ -152,7 +152,7 @@ impl oid { num_hex_bytes } - /// Write ourselves to `out` in hexadecimal notation + /// Write ourselves to `out` in hexadecimal notation. #[inline] pub fn write_hex_to(&self, mut out: impl std::io::Write) -> std::io::Result<()> { let mut hex = crate::Kind::hex_buf(); @@ -205,7 +205,7 @@ impl PartialEq for &oid { } /// Manually created from a version that uses a slice, and we forcefully try to convert it into a borrowed array of the desired size -/// Could be improved by fitting this into serde +/// Could be improved by fitting this into serde. /// Unfortunately the serde::Deserialize derive wouldn't work for borrowed arrays. #[cfg(feature = "serde")] impl<'de: 'a, 'a> serde::Deserialize<'de> for &'a oid { diff --git a/gix-ignore/Cargo.toml b/gix-ignore/Cargo.toml index 74c77b98b85..9daeb828415 100644 --- a/gix-ignore/Cargo.toml +++ b/gix-ignore/Cargo.toml @@ -28,7 +28,7 @@ document-features = { version = "0.2.1", optional = true } [dev-dependencies] gix-testtools = { path = "../tests/tools"} -gix-utils = { path = "../gix-utils" } +gix-fs = { path = "../gix-fs" } [package.metadata.docs.rs] all-features = true diff --git a/gix-ignore/tests/search/mod.rs b/gix-ignore/tests/search/mod.rs index 83256dea727..3cb8763dbc8 100644 --- a/gix-ignore/tests/search/mod.rs +++ b/gix-ignore/tests/search/mod.rs @@ -3,7 +3,6 @@ use std::io::Read; use bstr::{BStr, ByteSlice}; use gix_glob::pattern::Case; use gix_ignore::search::Match; -use gix_utils::FilesystemCapabilities; struct Expectations<'a> { lines: bstr::Lines<'a>, @@ -32,7 +31,7 @@ impl<'a> Iterator for Expectations<'a> { #[test] fn baseline_from_git_dir() -> crate::Result { - let case = if FilesystemCapabilities::probe("../.git").ignore_case { + let case = if gix_fs::Capabilities::probe("../.git").ignore_case { Case::Fold } else { Case::Sensitive diff --git a/gix-index/src/access/mod.rs b/gix-index/src/access/mod.rs index e8f2dc9f861..fa0215d9fda 100644 --- a/gix-index/src/access/mod.rs +++ b/gix-index/src/access/mod.rs @@ -1,6 +1,7 @@ use std::cmp::Ordering; use bstr::{BStr, ByteSlice, ByteVec}; +use filetime::FileTime; use crate::{entry, extension, Entry, PathStorage, State, Version}; @@ -15,6 +16,20 @@ impl State { self.version } + /// Returns time at which the state was created, indicating its freshness compared to other files on disk. + pub fn timestamp(&self) -> FileTime { + self.timestamp + } + + /// Updates the timestamp of this state, indicating its freshness compared to other files on disk. + /// + /// Be careful about using this as setting a timestamp without correctly updating the index + /// **will cause (file system) race conditions** see racy-git.txt in the git documentation + /// for more details. + pub fn set_timestamp(&mut self, timestamp: FileTime) { + self.timestamp = timestamp + } + /// Return the kind of hashes used in this instance. pub fn object_hash(&self) -> gix_hash::Kind { self.object_hash @@ -117,6 +132,12 @@ impl State { pub fn entries_mut(&mut self) -> &mut [Entry] { &mut self.entries } + + /// Return a writable slice to entries and read-access to their path storage at the same time. + pub fn entries_mut_and_pathbacking(&mut self) -> (&mut [Entry], &PathStorage) { + (&mut self.entries, &self.path_backing) + } + /// Return mutable entries along with their paths in an iterator. pub fn entries_mut_with_paths(&mut self) -> impl Iterator { let paths = &self.path_backing; diff --git a/gix-index/src/decode/entries.rs b/gix-index/src/decode/entries.rs index 74bc1cfc8e8..5de949fe130 100644 --- a/gix-index/src/decode/entries.rs +++ b/gix-index/src/decode/entries.rs @@ -142,11 +142,11 @@ fn load_one<'a>( Some(( Entry { stat: entry::Stat { - ctime: entry::Time { + ctime: entry::stat::Time { secs: ctime_secs, nsecs: ctime_nsecs, }, - mtime: entry::Time { + mtime: entry::stat::Time { secs: mtime_secs, nsecs: mtime_nsecs, }, diff --git a/gix-index/src/decode/mod.rs b/gix-index/src/decode/mod.rs index e84d8f71739..c94b0349570 100644 --- a/gix-index/src/decode/mod.rs +++ b/gix-index/src/decode/mod.rs @@ -302,11 +302,11 @@ pub(crate) fn stat(data: &[u8]) -> Option<(entry::Stat, &[u8])> { let (size, data) = read_u32(data)?; Some(( entry::Stat { - mtime: entry::Time { + mtime: entry::stat::Time { secs: ctime_secs, nsecs: ctime_nsecs, }, - ctime: entry::Time { + ctime: entry::stat::Time { secs: mtime_secs, nsecs: mtime_nsecs, }, diff --git a/gix-index/src/entry/flags.rs b/gix-index/src/entry/flags.rs index 5841f7c9148..05198bafb01 100644 --- a/gix-index/src/entry/flags.rs +++ b/gix-index/src/entry/flags.rs @@ -3,25 +3,27 @@ use bitflags::bitflags; use crate::entry::Stage; bitflags! { - /// In-memory flags + /// In-memory flags. + /// + /// Notably, not all of these will be persisted but can be used to aid all kinds of operations. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct Flags: u32 { - /// The mask to apply to obtain the stage number of an entry. - const STAGE_MASK = 0x3000; - /// If set, additional bits need to be written to storage. - const EXTENDED = 0x4000; // TODO: could we use the pathlen ourselves to save 8 bytes? And how to handle longer paths than that? 0 as sentinel maybe? - /// The mask to obtain the length of the path associated with this entry. + /// The mask to obtain the length of the path associated with this entry, up to 4095 characters without extension. const PATH_LEN = 0x0fff; + /// The mask to apply to obtain the stage number of an entry, encoding three value: 0 = base, 1 = ours, 2 = theirs. + const STAGE_MASK = 1<<12 | 1<<13; + /// If set, additional bits need to be written to storage. + const EXTENDED = 1<<14; /// If set, the entry be assumed to match with the version on the working tree, as a way to avoid `lstat()` checks. const ASSUME_VALID = 1 << 15; /// Indicates that an entry needs to be updated as it's in-memory representation doesn't match what's on disk. const UPDATE = 1 << 16; /// Indicates an entry should be removed - this typically happens during writing, by simply skipping over them. const REMOVE = 1 << 17; - /// Indicates that an entry is known to be uptodate. + /// Indicates that an entry is known to be up-to-date. const UPTODATE = 1 << 18; - /// Only temporarily used by unpack_trees() (in C) + /// Only temporarily used by unpack_trees() (in C). const ADDED = 1 << 19; /// Whether an up-to-date object hash exists for the entry. @@ -46,8 +48,8 @@ bitflags! { /// Indicates the entry name is present in the base/shared index, and thus doesn't have to be stored in this one. const STRIP_NAME = 1 << 28; - /// - /// stored at rest, see at_rest::FlagsExtended + /// Created with `git add --intent-to-add` to mark empty entries that have their counter-part in the worktree, but not + /// yet in the object database. const INTENT_TO_ADD = 1 << 29; /// Stored at rest const SKIP_WORKTREE = 1 << 30; @@ -102,7 +104,7 @@ pub(crate) mod at_rest { bitflags! { /// Extended flags - add flags for serialization here and offset them down to u16. - #[derive(Copy, Clone, Debug)] + #[derive(Copy, Clone, Debug, PartialEq)] pub struct FlagsExtended: u16 { const INTENT_TO_ADD = 1 << (29 - 16); const SKIP_WORKTREE = 1 << (30 - 16); @@ -124,6 +126,18 @@ pub(crate) mod at_rest { mod tests { use super::*; + #[test] + fn flags_extended_conversion() { + assert_eq!( + FlagsExtended::all().to_flags(), + Some(super::super::Flags::INTENT_TO_ADD | super::super::Flags::SKIP_WORKTREE) + ); + assert_eq!( + FlagsExtended::from_flags(super::super::Flags::all()), + FlagsExtended::all() + ); + } + #[test] fn flags_from_bits_with_conflict() { let input = 0b1110_0010_1000_1011; diff --git a/gix-index/src/entry/mod.rs b/gix-index/src/entry/mod.rs index 1f821ed8015..e680e08b06d 100644 --- a/gix-index/src/entry/mod.rs +++ b/gix-index/src/entry/mod.rs @@ -1,23 +1,38 @@ /// The stage of an entry, one of 0 = base, 1 = ours, 2 = theirs pub type Stage = u32; -mod mode; -pub use mode::Mode; +/// +pub mod mode; mod flags; pub(crate) use flags::at_rest; pub use flags::Flags; +/// +pub mod stat; mod write; -/// The time component in a [`Stat`] struct. -#[derive(Debug, Default, PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Copy)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct Time { - /// The amount of seconds elapsed since EPOCH - pub secs: u32, - /// The amount of nanoseconds elapsed in the current second, ranging from 0 to 999.999.999 . - pub nsecs: u32, +use bitflags::bitflags; + +// TODO: we essentially treat this as an enum withj the only exception being +// that `FILE_EXECUTABLE.contains(FILE)` works might want to turn this into an +// enum proper +bitflags! { + /// The kind of file of an entry. + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + pub struct Mode: u32 { + /// directory (only used for sparse checkouts), equivalent to a tree, which is _excluded_ from the index via + /// cone-mode. + const DIR = 0o040000; + /// regular file + const FILE = 0o100644; + /// regular file, executable + const FILE_EXECUTABLE = 0o100755; + /// Symbolic link + const SYMLINK = 0o120000; + /// A git commit for submodules + const COMMIT = 0o160000; + } } /// An entry's filesystem stat information. @@ -25,9 +40,9 @@ pub struct Time { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Stat { /// Modification time - pub mtime: Time, + pub mtime: stat::Time, /// Creation time - pub ctime: Time, + pub ctime: stat::Time, /// Device number pub dev: u32, /// Inode number @@ -64,29 +79,11 @@ mod access { } mod _impls { - use std::{cmp::Ordering, ops::Add, time::SystemTime}; + use std::cmp::Ordering; use bstr::BStr; - use crate::{entry::Time, Entry, State}; - - impl From for Time { - fn from(s: SystemTime) -> Self { - let d = s - .duration_since(std::time::UNIX_EPOCH) - .expect("system time is not before unix epoch!"); - Time { - secs: d.as_secs() as u32, - nsecs: d.subsec_nanos(), - } - } - } - - impl From