diff options
Diffstat (limited to 'vendor/gix-fs')
-rw-r--r-- | vendor/gix-fs/.cargo-checksum.json | 1 | ||||
-rw-r--r-- | vendor/gix-fs/CHANGELOG.md | 60 | ||||
-rw-r--r-- | vendor/gix-fs/Cargo.toml | 29 | ||||
-rw-r--r-- | vendor/gix-fs/src/capabilities.rs | 123 | ||||
-rw-r--r-- | vendor/gix-fs/src/dir/create.rs | 202 | ||||
-rw-r--r-- | vendor/gix-fs/src/dir/mod.rs | 4 | ||||
-rw-r--r-- | vendor/gix-fs/src/dir/remove.rs | 108 | ||||
-rw-r--r-- | vendor/gix-fs/src/lib.rs | 55 | ||||
-rw-r--r-- | vendor/gix-fs/src/snapshot.rs | 127 | ||||
-rw-r--r-- | vendor/gix-fs/src/stack.rs | 124 | ||||
-rw-r--r-- | vendor/gix-fs/src/symlink.rs | 60 | ||||
-rw-r--r-- | vendor/gix-fs/tests/capabilities/mod.rs | 19 | ||||
-rw-r--r-- | vendor/gix-fs/tests/dir/create.rs | 194 | ||||
-rw-r--r-- | vendor/gix-fs/tests/dir/mod.rs | 2 | ||||
-rw-r--r-- | vendor/gix-fs/tests/dir/remove.rs | 146 | ||||
-rw-r--r-- | vendor/gix-fs/tests/fs.rs | 4 | ||||
-rw-r--r-- | vendor/gix-fs/tests/stack/mod.rs | 145 |
17 files changed, 1403 insertions, 0 deletions
diff --git a/vendor/gix-fs/.cargo-checksum.json b/vendor/gix-fs/.cargo-checksum.json new file mode 100644 index 000000000..ad5347cb5 --- /dev/null +++ b/vendor/gix-fs/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"CHANGELOG.md":"1a492de2272405f2305d9672f252e49aa9090de433cf8555a5326a49f49b3b67","Cargo.toml":"127ce51a1ddd9cd94281dc8aa66e5d0a62b9220cd68b82a986041ba6b0832154","src/capabilities.rs":"2a3856a55cf4df3cd5313bee18bc38e09f1b6b9aa6e22e4c80f246b07608f190","src/dir/create.rs":"36d24b9390eda5fbed67c2caf4caa9f5514382d8da17e2cf64f0fa958a6e3c2d","src/dir/mod.rs":"bf7413a8cc754d8921159d830d8df5b532e9b50aac708c24d6865b4e4b39c788","src/dir/remove.rs":"0e4f9236826598a7c048476c2f8c6674bb6dbdb9b6ae7d6373ac97f38aa7bf4b","src/lib.rs":"ba177fec41b8a3eceb6f9da128bafb88962194130da61157538b9afbc7c07cd4","src/snapshot.rs":"b112638a5da73a724925bb0a28fdcf5edc644dde384d204825dfa2fb5efe70b6","src/stack.rs":"ec82e34e4b675d1f9366fd4d91d0082b0e553a169cdae6ed440ad9f4c973b417","src/symlink.rs":"8e69a221190d82cf97ebbbdf7acc1443e1329aaa9d5ec614d49f13388f253bdf","tests/capabilities/mod.rs":"98206af8cbed0ac408f26d4886459e36e9078a7f6dcfe83ab5c105560798f34d","tests/dir/create.rs":"6943308c9509b3dfffa0fafd633ecf531d95ca2a6b5893ec54ae9a3f9bf2d91e","tests/dir/mod.rs":"0655b7cfcce3dc4e28e9e854564ab2532942582ff4da2f375a930888615651da","tests/dir/remove.rs":"573a0c958e729821084482fd499cf9ed4c03f1ec48c7e5431f8db5e033949dd3","tests/fs.rs":"313928b6aef5f194231f40c93aea015ef44d27d356eb6979fc8cfa34126160ca","tests/stack/mod.rs":"f986ae13f4230483845548d37711c2c49f4e7be3c4db2f9d2021b8a29671c797"},"package":"9b37a1832f691fdc09910bd267f9a2e413737c1f9ec68c6e31f9e802616278a9"}
\ No newline at end of file diff --git a/vendor/gix-fs/CHANGELOG.md b/vendor/gix-fs/CHANGELOG.md new file mode 100644 index 000000000..a604a5e38 --- /dev/null +++ b/vendor/gix-fs/CHANGELOG.md @@ -0,0 +1,60 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.1.1 (2023-04-26) + +A maintenance release without user-facing changes. + +### Commit Statistics + +<csr-read-only-do-not-edit/> + + - 3 commits contributed to the release over the course of 1 calendar day. + - 7 days passed between releases. + - 0 commits were understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' were seen in commit messages + +### Commit Details + +<csr-read-only-do-not-edit/> + +<details><summary>view details</summary> + + * **Uncategorized** + - Prepare changelogs prior to release ([`30a1a71`](https://github.com/Byron/gitoxide/commit/30a1a71f36f24faac0e0b362ffdfedea7f9cdbf1)) + - Merge branch 'index-entries-attrs' ([`f37a930`](https://github.com/Byron/gitoxide/commit/f37a930aefa27e67f0b693ba9669cc26d49044fa)) + - Add remaining docs to get `gix-fs` into 'early' mode. ([`5783df2`](https://github.com/Byron/gitoxide/commit/5783df24df627cf6993a59e5dbaedef4e31a2d0b)) +</details> + +## 0.1.0 (2023-04-19) + +The initial release. + +### Commit Statistics + +<csr-read-only-do-not-edit/> + + - 7 commits contributed to the release over the course of 2 calendar days. + - 0 commits were understood as [conventional](https://www.conventionalcommits.org). + - 0 issues like '(#ID)' were seen in commit messages + +### Commit Details + +<csr-read-only-do-not-edit/> + +<details><summary>view details</summary> + + * **Uncategorized** + - Release gix-fs v0.1.0 ([`1d64ef6`](https://github.com/Byron/gitoxide/commit/1d64ef65258ffd693a1d42d1c15f33e7e86f4464)) + - Fix `gix-fs` manifest ([`d1c7605`](https://github.com/Byron/gitoxide/commit/d1c7605fc9deb3f4bbdafa043ecc6523a6917de0)) + - Release gix-utils v0.1.0, gix-hash v0.11.0, gix-date v0.5.0, gix-features v0.29.0, gix-actor v0.20.0, gix-object v0.29.0, gix-archive v0.1.0, gix-fs v0.1.0, safety bump 25 crates ([`8dbd0a6`](https://github.com/Byron/gitoxide/commit/8dbd0a60557a85acfa231800a058cbac0271a8cf)) + - Prepare changelog prior to release ([`7f06458`](https://github.com/Byron/gitoxide/commit/7f064583bd0e1b078df89a7750f5a25deb70f516)) + - Make fmt ([`5d2b5d0`](https://github.com/Byron/gitoxide/commit/5d2b5d02c3869e07dc2507a8f2519ee1df633df7)) + - Merge branch 'main' into dev ([`cdef398`](https://github.com/Byron/gitoxide/commit/cdef398c4a3bd01baf0be2c27a3f77a400172b0d)) + - Create new `gix-fs` crate to consolidate all filesystem utilities ([`f8cc33c`](https://github.com/Byron/gitoxide/commit/f8cc33cb372dd2b4bbe4a09cf4f64916681ab1dd)) +</details> + diff --git a/vendor/gix-fs/Cargo.toml b/vendor/gix-fs/Cargo.toml new file mode 100644 index 000000000..7a74aef03 --- /dev/null +++ b/vendor/gix-fs/Cargo.toml @@ -0,0 +1,29 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +rust-version = "1.64" +name = "gix-fs" +version = "0.1.1" +authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"] +description = "A crate providing file system specific utilities to `gitoxide`" +license = "MIT/Apache-2.0" +repository = "https://github.com/Byron/gitoxide" + +[lib] +doctest = false + +[dependencies.gix-features] +version = "0.29.0" + +[dev-dependencies.tempfile] +version = "3.5.0" diff --git a/vendor/gix-fs/src/capabilities.rs b/vendor/gix-fs/src/capabilities.rs new file mode 100644 index 000000000..2b51deacc --- /dev/null +++ b/vendor/gix-fs/src/capabilities.rs @@ -0,0 +1,123 @@ +// TODO: tests +use std::path::Path; + +use crate::Capabilities; + +#[cfg(windows)] +impl Default for Capabilities { + fn default() -> Self { + Capabilities { + precompose_unicode: false, + ignore_case: true, + executable_bit: false, + symlink: false, + } + } +} + +#[cfg(target_os = "macos")] +impl Default for Capabilities { + fn default() -> Self { + Capabilities { + precompose_unicode: true, + ignore_case: true, + executable_bit: true, + symlink: true, + } + } +} + +#[cfg(all(unix, not(target_os = "macos")))] +impl Default for Capabilities { + fn default() -> Self { + Capabilities { + precompose_unicode: false, + ignore_case: false, + executable_bit: true, + symlink: true, + } + } +} + +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`. + /// + /// 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<Path>) -> Self { + let root = git_dir.as_ref(); + 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), + executable_bit: Self::probe_file_mode(root).unwrap_or(ctx.executable_bit), + } + } + + #[cfg(unix)] + fn probe_file_mode(root: &Path) -> std::io::Result<bool> { + use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; + + // test it exactly as we typically create executable files, not using chmod. + let test_path = root.join("_test_executable_bit"); + let res = std::fs::OpenOptions::new() + .create_new(true) + .write(true) + .mode(0o777) + .open(&test_path) + .and_then(|f| f.metadata().map(|m| m.mode() & 0o100 == 0o100)); + std::fs::remove_file(test_path)?; + res + } + + #[cfg(not(unix))] + fn probe_file_mode(_root: &Path) -> std::io::Result<bool> { + Ok(false) + } + + fn probe_ignore_case(git_dir: &Path) -> std::io::Result<bool> { + std::fs::metadata(git_dir.join("cOnFiG")).map(|_| true).or_else(|err| { + if err.kind() == std::io::ErrorKind::NotFound { + Ok(false) + } else { + Err(err) + } + }) + } + + fn probe_precompose_unicode(root: &Path) -> std::io::Result<bool> { + let precomposed = "ä"; + let decomposed = "a\u{308}"; + + let precomposed = root.join(precomposed); + std::fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(&precomposed)?; + let res = root.join(decomposed).symlink_metadata().map(|_| true); + std::fs::remove_file(precomposed)?; + res + } + + fn probe_symlink(root: &Path) -> std::io::Result<bool> { + let src_path = root.join("__link_src_file"); + std::fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(&src_path)?; + let link_path = root.join("__file_link"); + if crate::symlink::create(&src_path, &link_path).is_err() { + std::fs::remove_file(&src_path)?; + return Ok(false); + } + + let res = std::fs::symlink_metadata(&link_path).map(|m| m.file_type().is_symlink()); + + let cleanup = crate::symlink::remove(&link_path).or_else(|_| std::fs::remove_file(&link_path)); + std::fs::remove_file(&src_path).and(cleanup)?; + + res + } +} diff --git a/vendor/gix-fs/src/dir/create.rs b/vendor/gix-fs/src/dir/create.rs new file mode 100644 index 000000000..7c7c9a033 --- /dev/null +++ b/vendor/gix-fs/src/dir/create.rs @@ -0,0 +1,202 @@ +//! +use std::path::Path; + +/// The amount of retries to do during various aspects of the directory creation. +#[derive(Debug, Clone, Copy, Ord, PartialOrd, Eq, PartialEq)] +pub struct Retries { + /// How many times the whole directory can be created in the light of racy interference. + /// This count combats racy situations where another process is trying to remove a directory that we want to create, + /// and is deliberately higher than those who do deletion. That way, creation usually wins. + pub to_create_entire_directory: usize, + /// The amount of times we can try to create a directory because we couldn't as the parent didn't exist. + /// This amounts to the maximum subdirectory depth we allow to be created. Counts once per attempt to create the entire directory. + pub on_create_directory_failure: usize, + /// How often to retry to create a single directory if an interrupt happens, as caused by signals. + pub on_interrupt: usize, +} + +impl Default for Retries { + fn default() -> Self { + Retries { + on_interrupt: 10, + to_create_entire_directory: 5, + on_create_directory_failure: 25, + } + } +} + +mod error { + use std::{fmt, path::Path}; + + use crate::dir::create::Retries; + + /// The error returned by [all()][super::all()]. + #[allow(missing_docs)] + #[derive(Debug)] + pub enum Error<'a> { + /// A failure we will probably recover from by trying again. + Intermediate { dir: &'a Path, kind: std::io::ErrorKind }, + /// A failure that ends the operation. + Permanent { + dir: &'a Path, + err: std::io::Error, + /// The retries left after running the operation + retries_left: Retries, + /// The original amount of retries to allow determining how many were actually used + retries: Retries, + }, + } + + impl<'a> fmt::Display for Error<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Intermediate { dir, kind } => write!( + f, + "Intermediae failure creating {:?} with error: {:?}", + dir.display(), + kind + ), + Error::Permanent { + err: _, + dir, + retries_left, + retries, + } => write!( + f, + "Permanently failing to create directory {dir:?} ({retries_left:?} of {retries:?})", + ), + } + } + } + + impl<'a> std::error::Error for Error<'a> { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Error::Permanent { err, .. } => Some(err), + _ => None, + } + } + } +} +pub use error::Error; + +enum State { + CurrentlyCreatingDirectories, + SearchingUpwardsForExistingDirectory, +} + +/// A special iterator which communicates its operation through results where… +/// +/// * `Some(Ok(created_directory))` is yielded once or more success, followed by `None` +/// * `Some(Err(Error::Intermediate))` is yielded zero or more times while trying to create the directory. +/// * `Some(Err(Error::Permanent))` is yielded exactly once on failure. +pub struct Iter<'a> { + cursors: Vec<&'a Path>, + retries: Retries, + original_retries: Retries, + state: State, +} + +/// Construction +impl<'a> Iter<'a> { + /// Create a new instance that creates `target` when iterated with the default amount of [`Retries`]. + pub fn new(target: &'a Path) -> Self { + Self::new_with_retries(target, Default::default()) + } + + /// Create a new instance that creates `target` when iterated with the specified amount of `retries`. + pub fn new_with_retries(target: &'a Path, retries: Retries) -> Self { + Iter { + cursors: vec![target], + original_retries: retries, + retries, + state: State::SearchingUpwardsForExistingDirectory, + } + } +} + +impl<'a> Iter<'a> { + fn pernanent_failure( + &mut self, + dir: &'a Path, + err: impl Into<std::io::Error>, + ) -> Option<Result<&'a Path, Error<'a>>> { + self.cursors.clear(); + Some(Err(Error::Permanent { + err: err.into(), + dir, + retries_left: self.retries, + retries: self.original_retries, + })) + } + + fn intermediate_failure(&self, dir: &'a Path, err: std::io::Error) -> Option<Result<&'a Path, Error<'a>>> { + Some(Err(Error::Intermediate { dir, kind: err.kind() })) + } +} + +impl<'a> Iterator for Iter<'a> { + type Item = Result<&'a Path, Error<'a>>; + + fn next(&mut self) -> Option<Self::Item> { + use std::io::ErrorKind::*; + match self.cursors.pop() { + Some(dir) => match std::fs::create_dir(dir) { + Ok(()) => { + self.state = State::CurrentlyCreatingDirectories; + Some(Ok(dir)) + } + Err(err) => match err.kind() { + AlreadyExists if dir.is_dir() => { + self.state = State::CurrentlyCreatingDirectories; + Some(Ok(dir)) + } + AlreadyExists => self.pernanent_failure(dir, err), // is non-directory + NotFound => { + self.retries.on_create_directory_failure -= 1; + if let State::CurrentlyCreatingDirectories = self.state { + self.state = State::SearchingUpwardsForExistingDirectory; + self.retries.to_create_entire_directory -= 1; + if self.retries.to_create_entire_directory < 1 { + return self.pernanent_failure(dir, NotFound); + } + self.retries.on_create_directory_failure = + self.original_retries.on_create_directory_failure; + } + if self.retries.on_create_directory_failure < 1 { + return self.pernanent_failure(dir, NotFound); + }; + self.cursors.push(dir); + self.cursors.push(match dir.parent() { + None => return self.pernanent_failure(dir, InvalidInput), + Some(parent) => parent, + }); + self.intermediate_failure(dir, err) + } + Interrupted => { + self.retries.on_interrupt -= 1; + if self.retries.on_interrupt <= 1 { + return self.pernanent_failure(dir, Interrupted); + }; + self.cursors.push(dir); + self.intermediate_failure(dir, err) + } + _unexpected_kind => self.pernanent_failure(dir, err), + }, + }, + None => None, + } + } +} + +/// Create all directories leading to `dir` including `dir` itself with the specified amount of `retries`. +/// Returns the input `dir` on success that make it useful in expressions. +pub fn all(dir: &Path, retries: Retries) -> std::io::Result<&Path> { + for res in Iter::new_with_retries(dir, retries) { + match res { + Err(Error::Permanent { err, .. }) => return Err(err), + Err(Error::Intermediate { .. }) | Ok(_) => continue, + } + } + Ok(dir) +} diff --git a/vendor/gix-fs/src/dir/mod.rs b/vendor/gix-fs/src/dir/mod.rs new file mode 100644 index 000000000..4c709a6a8 --- /dev/null +++ b/vendor/gix-fs/src/dir/mod.rs @@ -0,0 +1,4 @@ +/// +pub mod create; +/// +pub mod remove; diff --git a/vendor/gix-fs/src/dir/remove.rs b/vendor/gix-fs/src/dir/remove.rs new file mode 100644 index 000000000..ac7b212fa --- /dev/null +++ b/vendor/gix-fs/src/dir/remove.rs @@ -0,0 +1,108 @@ +//! +use std::path::{Path, PathBuf}; + +/// A special iterator which communicates its operation through results where… +/// +/// * `Some(Ok(removed_directory))` is yielded once or more success, followed by `None` +/// * `Some(Err(std::io::Error))` is yielded exactly once on failure. +pub struct Iter<'a> { + cursor: Option<&'a Path>, + boundary: &'a Path, +} + +/// Construction +impl<'a> Iter<'a> { + /// Create a new instance that deletes `target` but will stop at `boundary`, without deleting the latter. + /// Returns an error if `boundary` doesn't contain `target` + /// + /// **Note** that we don't canonicalize the path for performance reasons. + pub fn new(target: &'a Path, boundary: &'a Path) -> std::io::Result<Self> { + if !target.starts_with(boundary) { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Removal target {target:?} must be contained in boundary {boundary:?}"), + )); + } + let cursor = if target == boundary { + None + } else if target.exists() { + Some(target) + } else { + None + }; + Ok(Iter { cursor, boundary }) + } +} + +impl<'a> Iterator for Iter<'a> { + type Item = std::io::Result<&'a Path>; + + fn next(&mut self) -> Option<Self::Item> { + match self.cursor.take() { + Some(dir) => { + let next = match std::fs::remove_dir(dir) { + Ok(()) => Some(Ok(dir)), + Err(err) => match err.kind() { + std::io::ErrorKind::NotFound => Some(Ok(dir)), + _other_error_kind => return Some(Err(err)), + }, + }; + self.cursor = match dir.parent() { + Some(parent) => (parent != self.boundary).then_some(parent), + None => { + unreachable!("directory {:?} ran out of parents, this really shouldn't happen before hitting the boundary {:?}", dir, self.boundary) + } + }; + next + } + None => None, + } + } +} + +/// Delete all empty directories from `delete_dir` upward and until (not including) the `boundary_dir`. +/// +/// Note that `boundary_dir` must contain `delete_dir` or an error is returned, otherwise `delete_dir` is returned on success. +pub fn empty_upward_until_boundary<'a>(delete_dir: &'a Path, boundary_dir: &'a Path) -> std::io::Result<&'a Path> { + for item in Iter::new(delete_dir, boundary_dir)? { + match item { + Ok(_dir) => continue, + Err(err) => return Err(err), + } + } + Ok(delete_dir) +} + +/// Delete all empty directories reachable from `delete_dir` from empty leaves moving upward to and including `delete_dir`. +/// +/// If any encountered directory contains a file the entire operation is aborted. +/// Please note that this is inherently racy and no attempts are made to counter that, which will allow creators to win +/// as long as they retry. +pub fn empty_depth_first(delete_dir: impl Into<PathBuf>) -> std::io::Result<()> { + let delete_dir = delete_dir.into(); + if let Ok(()) = std::fs::remove_dir(&delete_dir) { + return Ok(()); + } + + let mut stack = vec![delete_dir]; + let mut next_to_push = Vec::new(); + while let Some(dir_to_delete) = stack.pop() { + let mut num_entries = 0; + for entry in std::fs::read_dir(&dir_to_delete)? { + num_entries += 1; + let entry = entry?; + if entry.file_type()?.is_dir() { + next_to_push.push(entry.path()); + } else { + return Err(std::io::Error::new(std::io::ErrorKind::Other, "Directory not empty")); + } + } + if num_entries == 0 { + std::fs::remove_dir(&dir_to_delete)?; + } else { + stack.push(dir_to_delete); + stack.append(&mut next_to_push); + } + } + Ok(()) +} diff --git a/vendor/gix-fs/src/lib.rs b/vendor/gix-fs/src/lib.rs new file mode 100644 index 000000000..aa576c240 --- /dev/null +++ b/vendor/gix-fs/src/lib.rs @@ -0,0 +1,55 @@ +//! A crate with file-system specific utilities. +#![deny(rust_2018_idioms, missing_docs)] +#![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; + +use std::path::PathBuf; + +pub use snapshot::{FileSnapshot, SharedFileSnapshot, SharedFileSnapshotMut}; + +/// +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/vendor/gix-fs/src/snapshot.rs b/vendor/gix-fs/src/snapshot.rs new file mode 100644 index 000000000..02a0ec843 --- /dev/null +++ b/vendor/gix-fs/src/snapshot.rs @@ -0,0 +1,127 @@ +// TODO: tests +use std::ops::Deref; + +use gix_features::threading::{get_mut, get_ref, MutableOnDemand, OwnShared}; + +/// A structure holding enough information to reload a value if its on-disk representation changes as determined by its modified time. +#[derive(Debug)] +pub struct FileSnapshot<T: std::fmt::Debug> { + value: T, + modified: std::time::SystemTime, +} + +impl<T: Clone + std::fmt::Debug> Clone for FileSnapshot<T> { + fn clone(&self) -> Self { + Self { + value: self.value.clone(), + modified: self.modified, + } + } +} + +/// A snapshot of a resource which is up-to-date in the moment it is retrieved. +pub type SharedFileSnapshot<T> = OwnShared<FileSnapshot<T>>; + +/// Use this type for fields in structs that are to store the [`FileSnapshot`], typically behind an [`OwnShared`]. +/// +/// Note that the resource itself is behind another [`OwnShared`] to allow it to be used without holding any kind of lock, hence +/// without blocking updates while it is used. +#[derive(Debug, Default)] +pub struct SharedFileSnapshotMut<T: std::fmt::Debug>(pub MutableOnDemand<Option<SharedFileSnapshot<T>>>); + +impl<T: std::fmt::Debug> Deref for FileSnapshot<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl<T: std::fmt::Debug> Deref for SharedFileSnapshotMut<T> { + type Target = MutableOnDemand<Option<SharedFileSnapshot<T>>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<T: std::fmt::Debug> SharedFileSnapshotMut<T> { + /// Create a new instance of this type. + /// + /// Useful in case `Default::default()` isn't working for some reason. + pub fn new() -> Self { + SharedFileSnapshotMut(MutableOnDemand::new(None)) + } + + /// Refresh `state` forcefully by re-`open`ing the resource. Note that `open()` returns `None` if the resource isn't + /// present on disk, and that it's critical that the modified time is obtained _before_ opening the resource. + pub fn force_refresh<E>( + &self, + open: impl FnOnce() -> Result<Option<(std::time::SystemTime, T)>, E>, + ) -> Result<(), E> { + let mut state = get_mut(&self.0); + *state = open()?.map(|(modified, value)| OwnShared::new(FileSnapshot { value, modified })); + Ok(()) + } + + /// Assure that the resource in `state` is up-to-date by comparing the `current_modification_time` with the one we know in `state` + /// and by acting accordingly. + /// Returns the potentially updated/reloaded resource if it is still present on disk, which then represents a snapshot that is up-to-date + /// in that very moment, or `None` if the underlying file doesn't exist. + /// + /// Note that even though this is racy, each time a request is made there is a chance to see the actual state. + pub fn recent_snapshot<E>( + &self, + mut current_modification_time: impl FnMut() -> Option<std::time::SystemTime>, + open: impl FnOnce() -> Result<Option<T>, E>, + ) -> Result<Option<SharedFileSnapshot<T>>, E> { + let state = get_ref(self); + let recent_modification = current_modification_time(); + let buffer = match (&*state, recent_modification) { + (None, None) => (*state).clone(), + (Some(_), None) => { + drop(state); + let mut state = get_mut(self); + *state = None; + (*state).clone() + } + (Some(snapshot), Some(modified_time)) => { + if snapshot.modified < modified_time { + drop(state); + let mut state = get_mut(self); + + if let (Some(_snapshot), Some(modified_time)) = (&*state, current_modification_time()) { + *state = open()?.map(|value| { + OwnShared::new(FileSnapshot { + value, + modified: modified_time, + }) + }); + } + + (*state).clone() + } else { + // Note that this relies on sub-section precision or else is a race when the packed file was just changed. + // It's nothing we can know though, so… up to the caller unfortunately. + Some(snapshot.clone()) + } + } + (None, Some(_modified_time)) => { + drop(state); + let mut state = get_mut(self); + // Still in the same situation? If so, load the buffer. This compensates for the trampling herd + // during lazy-loading at the expense of another mtime check. + if let (None, Some(modified_time)) = (&*state, current_modification_time()) { + *state = open()?.map(|value| { + OwnShared::new(FileSnapshot { + value, + modified: modified_time, + }) + }); + } + (*state).clone() + } + }; + Ok(buffer) + } +} diff --git a/vendor/gix-fs/src/stack.rs b/vendor/gix-fs/src/stack.rs new file mode 100644 index 000000000..33e94812a --- /dev/null +++ b/vendor/gix-fs/src/stack.rs @@ -0,0 +1,124 @@ +use std::path::{Path, PathBuf}; + +use crate::Stack; + +/// Access +impl Stack { + /// Returns the top-level path of the stack. + pub fn root(&self) -> &Path { + &self.root + } + + /// Returns the absolute path the currently set path. + pub fn current(&self) -> &Path { + &self.current + } + + /// Returns the currently set path relative to the [`root()`][Stack::root()]. + pub fn current_relative(&self) -> &Path { + &self.current_relative + } +} + +/// A delegate for use in a [`Stack`]. +pub trait Delegate { + /// Called whenever we push a directory on top of the stack, after the fact. + /// + /// It is also called if the currently acted on path is a directory in itself. + /// Use `stack.current()` to see the directory. + fn push_directory(&mut self, stack: &Stack) -> std::io::Result<()>; + + /// Called after any component was pushed, with the path available at `stack.current()`. + /// + /// `is_last_component` is true if the path is completely built. + fn push(&mut self, is_last_component: bool, stack: &Stack) -> std::io::Result<()>; + + /// Called right after a directory-component was popped off the stack. + /// + /// Use it to pop information off internal data structures. + fn pop_directory(&mut self); +} + +impl Stack { + /// Create a new instance with `root` being the base for all future paths we handle, assuming it to be valid which includes + /// symbolic links to be included in it as well. + pub fn new(root: impl Into<PathBuf>) -> Self { + let root = root.into(); + Stack { + current: root.clone(), + current_relative: PathBuf::with_capacity(128), + valid_components: 0, + root, + current_is_directory: true, + } + } + + /// Set the current stack to point to the `relative` path and call `push_comp()` each time a new path component is popped + /// along with the stacks state for inspection to perform an operation that produces some data. + /// + /// The full path to `relative` will be returned along with the data returned by push_comp. + /// Note that this only works correctly for the delegate's `push_directory()` and `pop_directory()` methods if + /// `relative` paths are terminal, so point to their designated file or directory. + pub fn make_relative_path_current( + &mut self, + relative: impl AsRef<Path>, + delegate: &mut impl Delegate, + ) -> std::io::Result<()> { + let relative = relative.as_ref(); + debug_assert!( + relative.is_relative(), + "only index paths are handled correctly here, must be relative" + ); + debug_assert!(!relative.to_string_lossy().is_empty(), "empty paths are not allowed"); + + if self.valid_components == 0 { + delegate.push_directory(self)?; + } + + let mut components = relative.components().peekable(); + let mut existing_components = self.current_relative.components(); + let mut matching_components = 0; + while let (Some(existing_comp), Some(new_comp)) = (existing_components.next(), components.peek()) { + if existing_comp == *new_comp { + components.next(); + matching_components += 1; + } else { + break; + } + } + + for _ in 0..self.valid_components - matching_components { + self.current.pop(); + self.current_relative.pop(); + if self.current_is_directory { + delegate.pop_directory(); + } + self.current_is_directory = true; + } + self.valid_components = matching_components; + + if !self.current_is_directory && components.peek().is_some() { + delegate.push_directory(self)?; + } + + while let Some(comp) = components.next() { + let is_last_component = components.peek().is_none(); + self.current_is_directory = !is_last_component; + self.current.push(comp); + self.current_relative.push(comp); + self.valid_components += 1; + let res = delegate.push(is_last_component, self); + if self.current_is_directory { + delegate.push_directory(self)?; + } + + if let Err(err) = res { + self.current.pop(); + self.current_relative.pop(); + self.valid_components -= 1; + return Err(err); + } + } + Ok(()) + } +} diff --git a/vendor/gix-fs/src/symlink.rs b/vendor/gix-fs/src/symlink.rs new file mode 100644 index 000000000..55798daa3 --- /dev/null +++ b/vendor/gix-fs/src/symlink.rs @@ -0,0 +1,60 @@ +use std::{io, io::ErrorKind::AlreadyExists, path::Path}; + +#[cfg(not(windows))] +/// Create a new symlink at `link` which points to `original`. +pub fn create(original: &Path, link: &Path) -> io::Result<()> { + std::os::unix::fs::symlink(original, link) +} + +#[cfg(not(windows))] +/// Remove a symlink. +/// +/// Note that on only on windows this is special. +pub fn remove(path: &Path) -> io::Result<()> { + std::fs::remove_file(path) +} + +// TODO: use the `symlink` crate once it can delete directory symlinks +/// Remove a symlink. +#[cfg(windows)] +pub fn remove(path: &Path) -> io::Result<()> { + if let Ok(meta) = std::fs::metadata(path) { + if meta.is_file() { + std::fs::remove_file(path) // this removes the link itself + } else { + std::fs::remove_dir(path) // however, this sees the destination directory, which isn't the right thing actually + } + } else { + std::fs::remove_file(path).or_else(|_| std::fs::remove_dir(path)) + } +} + +#[cfg(windows)] +/// Create a new symlink at `link` which points to `original`. +pub fn create(original: &Path, link: &Path) -> io::Result<()> { + use std::os::windows::fs::{symlink_dir, symlink_file}; + // TODO: figure out if links to links count as files or whatever they point at + if std::fs::metadata(link.parent().expect("dir for link").join(original))?.is_dir() { + symlink_dir(original, link) + } else { + symlink_file(original, link) + } +} + +#[cfg(not(windows))] +/// Return true if `err` indicates that a file collision happened, i.e. a symlink couldn't be created as the `link` +/// already exists as filesystem object. +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)] +/// Return true if `err` indicates that a file collision happened, i.e. a symlink couldn't be created as the `link` +/// already exists as filesystem object. +pub fn is_collision_error(err: &std::io::Error) -> bool { + err.kind() == AlreadyExists || err.kind() == std::io::ErrorKind::PermissionDenied +} diff --git a/vendor/gix-fs/tests/capabilities/mod.rs b/vendor/gix-fs/tests/capabilities/mod.rs new file mode 100644 index 000000000..c4b6fa99a --- /dev/null +++ b/vendor/gix-fs/tests/capabilities/mod.rs @@ -0,0 +1,19 @@ +#[test] +fn probe() { + let dir = tempfile::tempdir().unwrap(); + std::fs::File::create(dir.path().join("config")).unwrap(); + let ctx = gix_fs::Capabilities::probe(dir.path()); + dbg!(ctx); + let entries: Vec<_> = std::fs::read_dir(dir.path()) + .unwrap() + .filter_map(Result::ok) + .filter(|e| e.file_name().to_str() != Some("config")) + .map(|e| e.path()) + .collect(); + assert_eq!( + entries.len(), + 0, + "there should be no left-over files after probing, found {:?}", + entries + ); +} diff --git a/vendor/gix-fs/tests/dir/create.rs b/vendor/gix-fs/tests/dir/create.rs new file mode 100644 index 000000000..6693fd071 --- /dev/null +++ b/vendor/gix-fs/tests/dir/create.rs @@ -0,0 +1,194 @@ +mod all { + 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::all(target, Default::default())?; + assert_eq!(dir, target, "all subdirectories can be created"); + Ok(()) + } +} +mod iter { + pub use std::io::ErrorKind::*; + + 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::Iter::new(dir.path()); + assert_eq!( + it.next().expect("item").expect("success"), + dir.path(), + "first iteration is immediately successful" + ); + assert!(it.next().is_none(), "iterator exhausted afterwards"); + Ok(()) + } + + #[test] + 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::Iter::new(&new_dir); + assert_eq!( + it.next().expect("item").expect("success"), + &new_dir, + "first iteration is immediately successful" + ); + assert!(it.next().is_none(), "iterator exhausted afterwards"); + assert!(new_dir.is_dir(), "the directory exists"); + Ok(()) + } + + #[test] + 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::Iter::new(&new_dir); + assert!( + matches!(it.next(), Some(Err(Intermediate{dir, kind: k})) if k == NotFound && dir == new_dir), + "dir is not present" + ); + assert!( + matches!(it.next(), Some(Err(Intermediate{dir, kind:k})) if k == NotFound && dir == new_dir.parent().unwrap()), + "parent dir is not present" + ); + assert_eq!( + it.next().expect("item").expect("success"), + new_dir.parent().unwrap().parent().unwrap(), + "first subdir is created" + ); + assert_eq!( + it.next().expect("item").expect("success"), + new_dir.parent().unwrap(), + "second subdir is created" + ); + assert_eq!( + it.next().expect("item").expect("success"), + new_dir, + "target directory is created" + ); + assert!(it.next().is_none(), "iterator depleted"); + assert!(new_dir.is_dir(), "the directory exists"); + Ok(()) + } + + #[test] + 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::Iter::new_with_retries( + &new_dir, + Retries { + on_create_directory_failure: 1, + ..Default::default() + }, + ); + assert!( + matches!(it.next(), Some(Err(Permanent{ retries_left, dir, err, ..})) if retries_left.on_create_directory_failure == 0 + && err.kind() == NotFound + && dir == new_dir), + "parent dir is not present and we run out of attempts" + ); + assert!(it.next().is_none(), "iterator depleted"); + assert!(!new_dir.is_dir(), "the wasn't created"); + Ok(()) + } + + #[test] + fn an_existing_file_makes_directory_creation_fail_permanently() -> crate::Result { + let dir = tempfile::tempdir()?; + let new_dir = dir.path().join("also-file"); + std::fs::write(&new_dir, [42])?; + assert!(new_dir.is_file()); + + let mut it = create::Iter::new(&new_dir); + assert!( + matches!(it.next(), Some(Err(Permanent{ dir, err, .. })) if err.kind() == AlreadyExists + && dir == new_dir), + "parent dir is not present and we run out of attempts" + ); + assert!(it.next().is_none(), "iterator depleted"); + assert!(new_dir.is_file(), "file is untouched"); + Ok(()) + } + #[test] + fn racy_directory_creation_with_new_directory_being_deleted_not_enough_retries() -> crate::Result { + let dir = tempfile::tempdir()?; + let new_dir = dir.path().join("a").join("new"); + let parent_dir = new_dir.parent().unwrap(); + let mut it = create::Iter::new_with_retries( + &new_dir, + Retries { + to_create_entire_directory: 2, + on_create_directory_failure: 2, + ..Default::default() + }, + ); + + assert!( + matches!(it.nth(1), Some(Ok(dir)) if dir == parent_dir), + "parent dir is created" + ); + // Someone deletes the new directory + std::fs::remove_dir(parent_dir)?; + + assert!( + matches!(it.nth(1), Some(Ok(dir)) if dir == parent_dir), + "parent dir is created" + ); + // Someone deletes the new directory, again + std::fs::remove_dir(parent_dir)?; + + assert!( + matches!(it.next(), Some(Err(Permanent{ retries_left, dir, err, .. })) if retries_left.to_create_entire_directory == 0 + && retries_left.on_create_directory_failure == 1 + && err.kind() == NotFound + && dir == new_dir), + "we run out of attempts to retry to combat against raciness" + ); + Ok(()) + } + + #[test] + fn racy_directory_creation_with_new_directory_being_deleted() -> crate::Result { + let dir = tempfile::tempdir()?; + let new_dir = dir.path().join("a").join("new"); + let parent_dir = new_dir.parent().unwrap(); + 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, and we go up a level" + ); + assert!( + matches!(it.next(), Some(Ok(dir)) if dir == parent_dir), + "parent dir is created" + ); + // Someone deletes the new directory + std::fs::remove_dir(parent_dir)?; + + assert!( + matches!(it.next(), Some(Err(Intermediate{dir, kind:k})) if k == NotFound && dir == new_dir), + "now when it tries the actual dir its not found" + ); + assert!( + matches!(it.next(), Some(Ok(dir)) if dir == parent_dir), + "parent dir is created as it retries" + ); + assert!( + matches!(it.next(), Some(Ok(dir)) if dir == new_dir), + "target dir is created successfully" + ); + assert!(it.next().is_none(), "iterator depleted"); + assert!(new_dir.is_dir()); + + Ok(()) + } +} diff --git a/vendor/gix-fs/tests/dir/mod.rs b/vendor/gix-fs/tests/dir/mod.rs new file mode 100644 index 000000000..0008e7ee8 --- /dev/null +++ b/vendor/gix-fs/tests/dir/mod.rs @@ -0,0 +1,2 @@ +mod create; +mod remove; diff --git a/vendor/gix-fs/tests/dir/remove.rs b/vendor/gix-fs/tests/dir/remove.rs new file mode 100644 index 000000000..4b08e5147 --- /dev/null +++ b/vendor/gix-fs/tests/dir/remove.rs @@ -0,0 +1,146 @@ +mod empty_upwards_until_boundary { + use std::{io, path::Path}; + + use gix_fs::dir::remove; + + #[test] + fn boundary_must_contain_target_dir() -> crate::Result { + let dir = tempfile::tempdir()?; + let (target, boundary) = (dir.path().join("a"), dir.path().join("b")); + std::fs::create_dir(&target)?; + std::fs::create_dir(&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()); + Ok(()) + } + #[test] + fn target_directory_non_existing_causes_existing_parents_not_to_be_deleted() -> crate::Result { + let dir = tempfile::tempdir()?; + let parent = dir.path().join("a"); + std::fs::create_dir(&parent)?; + let target = parent.join("not-existing"); + 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" + ); + Ok(()) + } + + #[test] + fn target_directory_being_a_file_immediately_fails() -> crate::Result { + let dir = tempfile::tempdir()?; + let target = dir.path().join("actually-a-file"); + std::fs::write(&target, [42])?; + 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(()) + } + #[test] + fn boundary_being_the_target_dir_always_succeeds_and_we_do_nothing() -> crate::Result { + let dir = tempfile::tempdir()?; + 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(()) + } + #[test] + 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::empty_upward_until_boundary(&target, dir.path())?, target); + assert!(dir.path().is_dir(), "it won't touch the boundary"); + Ok(()) + } + #[test] + 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::empty_upward_until_boundary(target, boundary)?, target); + Ok(()) + } + #[test] + fn nested_directory_deletion_works() -> crate::Result { + let dir = tempfile::tempdir()?; + let nested = dir.path().join("a").join("b").join("to-delete"); + std::fs::create_dir_all(&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!( + !nested.parent().unwrap().parent().unwrap().is_dir(), + "parent two was deleted" + ); + assert!(dir.path().is_dir(), "it won't touch the boundary"); + Ok(()) + } +} + +mod empty_depth_first { + use std::{ + fs::{create_dir, create_dir_all}, + path::Path, + }; + + #[test] + fn non_empty_anywhere_and_deletion_fails() -> crate::Result { + let dir = tempfile::TempDir::new()?; + let touch = |base: &Path, name: &str| create_dir_all(base).and_then(|_| std::fs::write(base.join(name), b"")); + + let nested_parent = dir.path().join("a"); + touch(&nested_parent, "hello.ext")?; + + let tree_parent = dir.path().join("tree"); + touch(&tree_parent.join("a").join("b"), "hello.ext")?; + create_dir_all(tree_parent.join("one").join("two").join("empty"))?; + + assert!(gix_fs::dir::remove::empty_depth_first(nested_parent).is_err()); + Ok(()) + } + + #[test] + fn nested_empty_and_single_empty_delete_successfully() { + let dir = tempfile::TempDir::new().unwrap(); + let nested_parent = dir.path().join("a"); + let nested = nested_parent.join("b").join("leaf"); + create_dir_all(nested).unwrap(); + + let single_parent = dir.path().join("single"); + create_dir(&single_parent).unwrap(); + + let tree_parent = dir.path().join("tree"); + create_dir_all(tree_parent.join("a").join("b")).unwrap(); + 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_fs::dir::remove::empty_depth_first(empty).unwrap(); + } + } +} + +/// 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_fs::dir::remove; + + #[test] + fn racy_directory_creation_during_deletion_always_wins_immediately() -> crate::Result { + let dir = tempfile::tempdir()?; + let nested = dir.path().join("a").join("b").join("to-delete"); + std::fs::create_dir_all(&nested)?; + + 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. + std::fs::create_dir(&nested)?; + assert!( + it.next().expect("err item").is_err(), + "cannot delete non-empty directory" // TODO: check for IsADirectory when it becomes stable + ); + assert!(it.next().is_none(), "iterator is depleted"); + Ok(()) + } +} diff --git a/vendor/gix-fs/tests/fs.rs b/vendor/gix-fs/tests/fs.rs new file mode 100644 index 000000000..63b597956 --- /dev/null +++ b/vendor/gix-fs/tests/fs.rs @@ -0,0 +1,4 @@ +type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>; +mod capabilities; +mod dir; +mod stack; diff --git a/vendor/gix-fs/tests/stack/mod.rs b/vendor/gix-fs/tests/stack/mod.rs new file mode 100644 index 000000000..43eeac057 --- /dev/null +++ b/vendor/gix-fs/tests/stack/mod.rs @@ -0,0 +1,145 @@ +use std::path::{Path, PathBuf}; + +use gix_fs::Stack; + +#[derive(Debug, Default, Eq, PartialEq)] +struct Record { + push_dir: usize, + dirs: Vec<PathBuf>, + push: usize, +} + +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()); + Ok(()) + } + + fn push(&mut self, _is_last_component: bool, _stack: &Stack) -> std::io::Result<()> { + self.push += 1; + Ok(()) + } + + fn pop_directory(&mut self) { + self.dirs.pop(); + } +} + +#[test] +fn delegate_calls_are_consistent() -> crate::Result { + let root = PathBuf::from("."); + let mut s = Stack::new(&root); + + assert_eq!(s.current(), root); + assert_eq!(s.current_relative(), Path::new("")); + + let mut r = Record::default(); + s.make_relative_path_current("a/b", &mut r)?; + let mut dirs = vec![root.clone(), root.join("a")]; + assert_eq!( + r, + Record { + push_dir: 2, + dirs: dirs.clone(), + push: 2, + } + ); + + s.make_relative_path_current("a/b2", &mut r)?; + assert_eq!( + r, + Record { + push_dir: 2, + dirs: dirs.clone(), + push: 3, + } + ); + + s.make_relative_path_current("c/d/e", &mut r)?; + dirs.pop(); + dirs.extend([root.join("c"), root.join("c").join("d")]); + assert_eq!( + r, + Record { + push_dir: 4, + dirs: dirs.clone(), + push: 6, + } + ); + + dirs.push(root.join("c").join("d").join("x")); + s.make_relative_path_current("c/d/x/z", &mut r)?; + assert_eq!( + r, + Record { + push_dir: 5, + dirs: dirs.clone(), + push: 8, + } + ); + + dirs.drain(dirs.len() - 3..).count(); + s.make_relative_path_current("f", &mut r)?; + assert_eq!(s.current_relative(), Path::new("f")); + assert_eq!( + r, + Record { + push_dir: 5, + dirs: dirs.clone(), + push: 9, + } + ); + + dirs.push(root.join("x")); + s.make_relative_path_current("x/z", &mut r)?; + assert_eq!( + r, + Record { + push_dir: 6, + dirs: dirs.clone(), + push: 11, + } + ); + + dirs.push(root.join("x").join("z")); + s.make_relative_path_current("x/z/a", &mut r)?; + assert_eq!( + r, + Record { + push_dir: 7, + dirs: dirs.clone(), + push: 12, + } + ); + + dirs.push(root.join("x").join("z").join("a")); + dirs.push(root.join("x").join("z").join("a").join("b")); + s.make_relative_path_current("x/z/a/b/c", &mut r)?; + assert_eq!( + r, + Record { + push_dir: 9, + dirs: dirs.clone(), + push: 14, + } + ); + + dirs.drain(dirs.len() - 2..).count(); + s.make_relative_path_current("x/z", &mut r)?; + assert_eq!( + r, + Record { + push_dir: 9, + dirs: dirs.clone(), + push: 14, + } + ); + assert_eq!( + dirs.last(), + Some(&PathBuf::from("./x/z")), + "the stack is state so keeps thinking it's a directory which is consistent. Git does it differently though." + ); + + Ok(()) +} |