Unified · Split
| @@ -2307,6 +2307,13 @@ dependencies = [ · 2306 unmodified lines | ||
| 2307 | 2307 | "url", |
| 2308 | 2308 | ] |
| 2309 | 2309 | |
| 2310 | +[[package]] | |
| 2311 | +name = "gix-utils" | |
| 2312 | +version = "0.1.0" | |
| 2313 | +dependencies = [ | |
| 2314 | + "gix-features 0.28.1", | |
| 2315 | +] | |
| 2316 | + | |
| 2310 | 2317 | [[package]] |
| 2311 | 2318 | name = "gix-validate" |
| 2312 | 2319 | version = "0.7.3" |
| @@ -176,6 +176,7 @@ members = [ · 175 unmodified lines | ||
| 176 | 176 | "gix-pathspec", |
| 177 | 177 | "gix-refspec", |
| 178 | 178 | "gix-path", |
| 179 | + "gix-utils", | |
| 179 | 180 | "gix", |
| 180 | 181 | "gitoxide-core", |
| 181 | 182 | "gix-hashtable", |
| @@ -84,6 +84,7 @@ is usable to some extent. · 83 unmodified lines | ||
| 84 | 84 | * [gix-refspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-refspec) |
| 85 | 85 | * `gitoxide-core` |
| 86 | 86 | * **very early** _(possibly without any documentation and many rough edges)_ |
| 87 | + * [gix-utils](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-utils) | |
| 87 | 88 | * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree) |
| 88 | 89 | * [gix-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-bitmap) |
| 89 | 90 | * [gix-date](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-date) |
| @@ -101,10 +101,15 @@ and itself relies on all `git-*` crates. It's not meant for consumption, for app · 100 unmodified lines | ||
| 101 | 101 | * [x] write the table of contents |
| 102 | 102 | |
| 103 | 103 | ### gix-hashtable |
| 104 | − | |
| 105 | 104 | * [x] hashmap |
| 106 | 105 | * [x] hashset |
| 107 | 106 | |
| 107 | +### gix-utils | |
| 108 | + | |
| 109 | +* **filesystem** | |
| 110 | + * [x] probe capabilities | |
| 111 | + * [x] symlink creation and removal | |
| 112 | + * [x] file snapshots | |
| 108 | 113 | |
| 109 | 114 | ### gix-object |
| 110 | 115 | * *decode (zero-copy)* borrowed objects |
| @@ -0,0 +1,6 @@ | ||
| 1 | +# Changelog | |
| 2 | + | |
| 3 | +All notable changes to this project will be documented in this file. | |
| 4 | + | |
| 5 | +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | |
| 6 | +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | |
| @@ -0,0 +1,15 @@ | ||
| 1 | +[package] | |
| 2 | +name = "gix-utils" | |
| 3 | +version = "0.1.0" | |
| 4 | +repository = "https://github.com/Byron/gitoxide" | |
| 5 | +license = "MIT/Apache-2.0" | |
| 6 | +description = "A crate with `gitoxide` utilities that don't need feature toggles" | |
| 7 | +authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"] | |
| 8 | +edition = "2021" | |
| 9 | +rust-version = "1.64" | |
| 10 | + | |
| 11 | +[lib] | |
| 12 | +doctest = false | |
| 13 | + | |
| 14 | +[dependencies] | |
| 15 | +gix-features = { path = "../gix-features" } | |
| @@ -0,0 +1,122 @@ | ||
| 1 | +// TODO: tests | |
| 2 | +use crate::FilesystemCapabilities; | |
| 3 | +use std::path::Path; | |
| 4 | + | |
| 5 | +#[cfg(windows)] | |
| 6 | +impl Default for FilesystemCapabilities { | |
| 7 | + fn default() -> Self { | |
| 8 | + FilesystemCapabilities { | |
| 9 | + precompose_unicode: false, | |
| 10 | + ignore_case: true, | |
| 11 | + executable_bit: false, | |
| 12 | + symlink: false, | |
| 13 | + } | |
| 14 | + } | |
| 15 | +} | |
| 16 | + | |
| 17 | +#[cfg(target_os = "macos")] | |
| 18 | +impl Default for FilesystemCapabilities { | |
| 19 | + fn default() -> Self { | |
| 20 | + FilesystemCapabilities { | |
| 21 | + precompose_unicode: true, | |
| 22 | + ignore_case: true, | |
| 23 | + executable_bit: true, | |
| 24 | + symlink: true, | |
| 25 | + } | |
| 26 | + } | |
| 27 | +} | |
| 28 | + | |
| 29 | +#[cfg(all(unix, not(target_os = "macos")))] | |
| 30 | +impl Default for FilesystemCapabilities { | |
| 31 | + fn default() -> Self { | |
| 32 | + FilesystemCapabilities { | |
| 33 | + precompose_unicode: false, | |
| 34 | + ignore_case: false, | |
| 35 | + executable_bit: true, | |
| 36 | + symlink: true, | |
| 37 | + } | |
| 38 | + } | |
| 39 | +} | |
| 40 | + | |
| 41 | +impl FilesystemCapabilities { | |
| 42 | + /// try to determine all values in this context by probing them in the given `git_dir`, which | |
| 43 | + /// should be on the file system the git repository is located on. | |
| 44 | + /// `git_dir` is a typical git repository, expected to be populated with the typical files like `config`. | |
| 45 | + /// | |
| 46 | + /// All errors are ignored and interpreted on top of the default for the platform the binary is compiled for. | |
| 47 | + pub fn probe(git_dir: impl AsRef<Path>) -> Self { | |
| 48 | + let root = git_dir.as_ref(); | |
| 49 | + let ctx = FilesystemCapabilities::default(); | |
| 50 | + FilesystemCapabilities { | |
| 51 | + symlink: Self::probe_symlink(root).unwrap_or(ctx.symlink), | |
| 52 | + ignore_case: Self::probe_ignore_case(root).unwrap_or(ctx.ignore_case), | |
| 53 | + precompose_unicode: Self::probe_precompose_unicode(root).unwrap_or(ctx.precompose_unicode), | |
| 54 | + executable_bit: Self::probe_file_mode(root).unwrap_or(ctx.executable_bit), | |
| 55 | + } | |
| 56 | + } | |
| 57 | + | |
| 58 | + #[cfg(unix)] | |
| 59 | + fn probe_file_mode(root: &Path) -> std::io::Result<bool> { | |
| 60 | + use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; | |
| 61 | + | |
| 62 | + // test it exactly as we typically create executable files, not using chmod. | |
| 63 | + let test_path = root.join("_test_executable_bit"); | |
| 64 | + let res = std::fs::OpenOptions::new() | |
| 65 | + .create_new(true) | |
| 66 | + .write(true) | |
| 67 | + .mode(0o777) | |
| 68 | + .open(&test_path) | |
| 69 | + .and_then(|f| f.metadata().map(|m| m.mode() & 0o100 == 0o100)); | |
| 70 | + std::fs::remove_file(test_path)?; | |
| 71 | + res | |
| 72 | + } | |
| 73 | + | |
| 74 | + #[cfg(not(unix))] | |
| 75 | + fn probe_file_mode(_root: &Path) -> std::io::Result<bool> { | |
| 76 | + Ok(false) | |
| 77 | + } | |
| 78 | + | |
| 79 | + fn probe_ignore_case(git_dir: &Path) -> std::io::Result<bool> { | |
| 80 | + std::fs::metadata(git_dir.join("cOnFiG")).map(|_| true).or_else(|err| { | |
| 81 | + if err.kind() == std::io::ErrorKind::NotFound { | |
| 82 | + Ok(false) | |
| 83 | + } else { | |
| 84 | + Err(err) | |
| 85 | + } | |
| 86 | + }) | |
| 87 | + } | |
| 88 | + | |
| 89 | + fn probe_precompose_unicode(root: &Path) -> std::io::Result<bool> { | |
| 90 | + let precomposed = "ä"; | |
| 91 | + let decomposed = "a\u{308}"; | |
| 92 | + | |
| 93 | + let precomposed = root.join(precomposed); | |
| 94 | + std::fs::OpenOptions::new() | |
| 95 | + .create_new(true) | |
| 96 | + .write(true) | |
| 97 | + .open(&precomposed)?; | |
| 98 | + let res = root.join(decomposed).symlink_metadata().map(|_| true); | |
| 99 | + std::fs::remove_file(precomposed)?; | |
| 100 | + res | |
| 101 | + } | |
| 102 | + | |
| 103 | + fn probe_symlink(root: &Path) -> std::io::Result<bool> { | |
| 104 | + let src_path = root.join("__link_src_file"); | |
| 105 | + std::fs::OpenOptions::new() | |
| 106 | + .create_new(true) | |
| 107 | + .write(true) | |
| 108 | + .open(&src_path)?; | |
| 109 | + let link_path = root.join("__file_link"); | |
| 110 | + if crate::symlink::create(&src_path, &link_path).is_err() { | |
| 111 | + std::fs::remove_file(&src_path)?; | |
| 112 | + return Ok(false); | |
| 113 | + } | |
| 114 | + | |
| 115 | + let res = std::fs::symlink_metadata(&link_path).map(|m| m.file_type().is_symlink()); | |
| 116 | + | |
| 117 | + let cleanup = crate::symlink::remove(&link_path).or_else(|_| std::fs::remove_file(&link_path)); | |
| 118 | + std::fs::remove_file(&src_path).and(cleanup)?; | |
| 119 | + | |
| 120 | + res | |
| 121 | + } | |
| 122 | +} | |
| @@ -0,0 +1,32 @@ | ||
| 1 | +//! A crate with utilities that don't need feature toggles. | |
| 2 | +//! | |
| 3 | +//! If they would need feature toggles, they should be in `gix-features` instead. | |
| 4 | +#![deny(rust_2018_idioms)] | |
| 5 | +#![forbid(unsafe_code)] | |
| 6 | + | |
| 7 | +/// Common knowledge about the worktree that is needed across most interactions with the work tree | |
| 8 | +#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))] | |
| 9 | +#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)] | |
| 10 | +pub struct FilesystemCapabilities { | |
| 11 | + /// If true, the filesystem will store paths as decomposed unicode, i.e. `ä` becomes `"a\u{308}"`, which means that | |
| 12 | + /// we have to turn these forms back from decomposed to precomposed unicode before storing it in the index or generally | |
| 13 | + /// using it. This also applies to input received from the command-line, so callers may have to be aware of this and | |
| 14 | + /// perform conversions accordingly. | |
| 15 | + /// If false, no conversions will be performed. | |
| 16 | + pub precompose_unicode: bool, | |
| 17 | + /// If true, the filesystem ignores the case of input, which makes `A` the same file as `a`. | |
| 18 | + /// This is also called case-folding. | |
| 19 | + pub ignore_case: bool, | |
| 20 | + /// If true, we assume the executable bit is honored as part of the files mode. If false, we assume the file system | |
| 21 | + /// ignores the executable bit, hence it will be reported as 'off' even though we just tried to set it to be on. | |
| 22 | + pub executable_bit: bool, | |
| 23 | + /// If true, the file system supports symbolic links and we should try to create them. Otherwise symbolic links will be checked | |
| 24 | + /// out as files which contain the link as text. | |
| 25 | + pub symlink: bool, | |
| 26 | +} | |
| 27 | + | |
| 28 | +mod snapshot; | |
| 29 | +pub use snapshot::{FileSnapshot, SharedFileSnapshot, SharedFileSnapshotMut}; | |
| 30 | + | |
| 31 | +mod fs_capabilities; | |
| 32 | +pub mod symlink; | |
| @@ -0,0 +1,127 @@ | ||
| 1 | +// TODO: tests | |
| 2 | +use std::ops::Deref; | |
| 3 | + | |
| 4 | +use gix_features::threading::{get_mut, get_ref, MutableOnDemand, OwnShared}; | |
| 5 | + | |
| 6 | +/// A structure holding enough information to reload a value if its on-disk representation changes as determined by its modified time. | |
| 7 | +#[derive(Debug)] | |
| 8 | +pub struct FileSnapshot<T: std::fmt::Debug> { | |
| 9 | + value: T, | |
| 10 | + modified: std::time::SystemTime, | |
| 11 | +} | |
| 12 | + | |
| 13 | +impl<T: Clone + std::fmt::Debug> Clone for FileSnapshot<T> { | |
| 14 | + fn clone(&self) -> Self { | |
| 15 | + Self { | |
| 16 | + value: self.value.clone(), | |
| 17 | + modified: self.modified, | |
| 18 | + } | |
| 19 | + } | |
| 20 | +} | |
| 21 | + | |
| 22 | +/// A snapshot of a resource which is up-to-date in the moment it is retrieved. | |
| 23 | +pub type SharedFileSnapshot<T> = OwnShared<FileSnapshot<T>>; | |
| 24 | + | |
| 25 | +/// Use this type for fields in structs that are to store the [`FileSnapshot`], typically behind an [`OwnShared`]. | |
| 26 | +/// | |
| 27 | +/// Note that the resource itself is behind another [`OwnShared`] to allow it to be used without holding any kind of lock, hence | |
| 28 | +/// without blocking updates while it is used. | |
| 29 | +#[derive(Debug, Default)] | |
| 30 | +pub struct SharedFileSnapshotMut<T: std::fmt::Debug>(pub MutableOnDemand<Option<SharedFileSnapshot<T>>>); | |
| 31 | + | |
| 32 | +impl<T: std::fmt::Debug> Deref for FileSnapshot<T> { | |
| 33 | + type Target = T; | |
| 34 | + | |
| 35 | + fn deref(&self) -> &Self::Target { | |
| 36 | + &self.value | |
| 37 | + } | |
| 38 | +} | |
| 39 | + | |
| 40 | +impl<T: std::fmt::Debug> Deref for SharedFileSnapshotMut<T> { | |
| 41 | + type Target = MutableOnDemand<Option<SharedFileSnapshot<T>>>; | |
| 42 | + | |
| 43 | + fn deref(&self) -> &Self::Target { | |
| 44 | + &self.0 | |
| 45 | + } | |
| 46 | +} | |
| 47 | + | |
| 48 | +impl<T: std::fmt::Debug> SharedFileSnapshotMut<T> { | |
| 49 | + /// Create a new instance of this type. | |
| 50 | + /// | |
| 51 | + /// Useful in case `Default::default()` isn't working for some reason. | |
| 52 | + pub fn new() -> Self { | |
| 53 | + SharedFileSnapshotMut(MutableOnDemand::new(None)) | |
| 54 | + } | |
| 55 | + | |
| 56 | + /// Refresh `state` forcefully by re-`open`ing the resource. Note that `open()` returns `None` if the resource isn't | |
| 57 | + /// present on disk, and that it's critical that the modified time is obtained _before_ opening the resource. | |
| 58 | + pub fn force_refresh<E>( | |
| 59 | + &self, | |
| 60 | + open: impl FnOnce() -> Result<Option<(std::time::SystemTime, T)>, E>, | |
| 61 | + ) -> Result<(), E> { | |
| 62 | + let mut state = get_mut(&self.0); | |
| 63 | + *state = open()?.map(|(modified, value)| OwnShared::new(FileSnapshot { value, modified })); | |
| 64 | + Ok(()) | |
| 65 | + } | |
| 66 | + | |
| 67 | + /// Assure that the resource in `state` is up-to-date by comparing the `current_modification_time` with the one we know in `state` | |
| 68 | + /// and by acting accordingly. | |
| 69 | + /// Returns the potentially updated/reloaded resource if it is still present on disk, which then represents a snapshot that is up-to-date | |
| 70 | + /// in that very moment, or `None` if the underlying file doesn't exist. | |
| 71 | + /// | |
| 72 | + /// Note that even though this is racy, each time a request is made there is a chance to see the actual state. | |
| 73 | + pub fn recent_snapshot<E>( | |
| 74 | + &self, | |
| 75 | + mut current_modification_time: impl FnMut() -> Option<std::time::SystemTime>, | |
| 76 | + open: impl FnOnce() -> Result<Option<T>, E>, | |
| 77 | + ) -> Result<Option<SharedFileSnapshot<T>>, E> { | |
| 78 | + let state = get_ref(self); | |
| 79 | + let recent_modification = current_modification_time(); | |
| 80 | + let buffer = match (&*state, recent_modification) { | |
| 81 | + (None, None) => (*state).clone(), | |
| 82 | + (Some(_), None) => { | |
| 83 | + drop(state); | |
| 84 | + let mut state = get_mut(self); | |
| 85 | + *state = None; | |
| 86 | + (*state).clone() | |
| 87 | + } | |
| 88 | + (Some(snapshot), Some(modified_time)) => { | |
| 89 | + if snapshot.modified < modified_time { | |
| 90 | + drop(state); | |
| 91 | + let mut state = get_mut(self); | |
| 92 | + | |
| 93 | + if let (Some(_snapshot), Some(modified_time)) = (&*state, current_modification_time()) { | |
| 94 | + *state = open()?.map(|value| { | |
| 95 | + OwnShared::new(FileSnapshot { | |
| 96 | + value, | |
| 97 | + modified: modified_time, | |
| 98 | + }) | |
| 99 | + }); | |
| 100 | + } | |
| 101 | + | |
| 102 | + (*state).clone() | |
| 103 | + } else { | |
| 104 | + // Note that this relies on sub-section precision or else is a race when the packed file was just changed. | |
| 105 | + // It's nothing we can know though, so… up to the caller unfortunately. | |
| 106 | + Some(snapshot.clone()) | |
| 107 | + } | |
| 108 | + } | |
| 109 | + (None, Some(_modified_time)) => { | |
| 110 | + drop(state); | |
| 111 | + let mut state = get_mut(self); | |
| 112 | + // Still in the same situation? If so, load the buffer. This compensates for the trampling herd | |
| 113 | + // during lazy-loading at the expense of another mtime check. | |
| 114 | + if let (None, Some(modified_time)) = (&*state, current_modification_time()) { | |
| 115 | + *state = open()?.map(|value| { | |
| 116 | + OwnShared::new(FileSnapshot { | |
| 117 | + value, | |
| 118 | + modified: modified_time, | |
| 119 | + }) | |
| 120 | + }); | |
| 121 | + } | |
| 122 | + (*state).clone() | |
| 123 | + } | |
| 124 | + }; | |
| 125 | + Ok(buffer) | |
| 126 | + } | |
| 127 | +} | |
| @@ -0,0 +1,54 @@ | ||
| 1 | +use std::{io, path::Path}; | |
| 2 | + | |
| 3 | +#[cfg(not(windows))] | |
| 4 | +pub fn create(original: &Path, link: &Path) -> io::Result<()> { | |
| 5 | + std::os::unix::fs::symlink(original, link) | |
| 6 | +} | |
| 7 | + | |
| 8 | +#[cfg(not(windows))] | |
| 9 | +pub fn remove(path: &Path) -> io::Result<()> { | |
| 10 | + std::fs::remove_file(path) | |
| 11 | +} | |
| 12 | + | |
| 13 | +// TODO: use the `symlink` crate once it can delete directory symlinks | |
| 14 | +#[cfg(windows)] | |
| 15 | +pub fn remove(path: &Path) -> io::Result<()> { | |
| 16 | + if let Ok(meta) = std::fs::metadata(path) { | |
| 17 | + if meta.is_file() { | |
| 18 | + std::fs::remove_file(path) // this removes the link itself | |
| 19 | + } else { | |
| 20 | + std::fs::remove_dir(path) // however, this sees the destination directory, which isn't the right thing actually | |
| 21 | + } | |
| 22 | + } else { | |
| 23 | + std::fs::remove_file(path).or_else(|_| std::fs::remove_dir(path)) | |
| 24 | + } | |
| 25 | +} | |
| 26 | + | |
| 27 | +#[cfg(windows)] | |
| 28 | +pub fn create(original: &Path, link: &Path) -> io::Result<()> { | |
| 29 | + use std::os::windows::fs::{symlink_dir, symlink_file}; | |
| 30 | + // TODO: figure out if links to links count as files or whatever they point at | |
| 31 | + if std::fs::metadata(link.parent().expect("dir for link").join(original))?.is_dir() { | |
| 32 | + symlink_dir(original, link) | |
| 33 | + } else { | |
| 34 | + symlink_file(original, link) | |
| 35 | + } | |
| 36 | +} | |
| 37 | + | |
| 38 | +pub mod error { | |
| 39 | + use std::io::ErrorKind::AlreadyExists; | |
| 40 | + | |
| 41 | + #[cfg(not(windows))] | |
| 42 | + pub fn indicates_collision(err: &std::io::Error) -> bool { | |
| 43 | + // TODO: use ::IsDirectory as well when stabilized instead of raw_os_error(), and ::FileSystemLoop respectively | |
| 44 | + err.kind() == AlreadyExists | |
| 45 | + || err.raw_os_error() == Some(21) | |
| 46 | + || err.raw_os_error() == Some(62) // no-follow on symlnk on mac-os | |
| 47 | + || err.raw_os_error() == Some(40) // no-follow on symlnk on ubuntu | |
| 48 | + } | |
| 49 | + | |
| 50 | + #[cfg(windows)] | |
| 51 | + pub fn indicates_collision(err: &std::io::Error) -> bool { | |
| 52 | + err.kind() == AlreadyExists || err.kind() == std::io::ErrorKind::PermissionDenied | |
| 53 | + } | |
| 54 | +} | |