summaryrefslogtreecommitdiffstats
path: root/vendor/gix-fs
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/gix-fs')
-rw-r--r--vendor/gix-fs/.cargo-checksum.json1
-rw-r--r--vendor/gix-fs/CHANGELOG.md60
-rw-r--r--vendor/gix-fs/Cargo.toml29
-rw-r--r--vendor/gix-fs/src/capabilities.rs123
-rw-r--r--vendor/gix-fs/src/dir/create.rs202
-rw-r--r--vendor/gix-fs/src/dir/mod.rs4
-rw-r--r--vendor/gix-fs/src/dir/remove.rs108
-rw-r--r--vendor/gix-fs/src/lib.rs55
-rw-r--r--vendor/gix-fs/src/snapshot.rs127
-rw-r--r--vendor/gix-fs/src/stack.rs124
-rw-r--r--vendor/gix-fs/src/symlink.rs60
-rw-r--r--vendor/gix-fs/tests/capabilities/mod.rs19
-rw-r--r--vendor/gix-fs/tests/dir/create.rs194
-rw-r--r--vendor/gix-fs/tests/dir/mod.rs2
-rw-r--r--vendor/gix-fs/tests/dir/remove.rs146
-rw-r--r--vendor/gix-fs/tests/fs.rs4
-rw-r--r--vendor/gix-fs/tests/stack/mod.rs145
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(())
+}