Unified · Split
| @@ -2307,6 +2307,13 @@ dependencies = [ · 2306 unmodified lines | |||
| 2307 | "url", | 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 | [[package]] | 2317 | [[package]] |
| 2311 | name = "gix-validate" | 2318 | name = "gix-validate" |
| 2312 | version = "0.7.3" | 2319 | version = "0.7.3" |
| @@ -176,6 +176,7 @@ members = [ · 175 unmodified lines | |||
| 176 | "gix-pathspec", | 176 | "gix-pathspec", |
| 177 | "gix-refspec", | 177 | "gix-refspec", |
| 178 | "gix-path", | 178 | "gix-path", |
| 179 | "gix-utils", | ||
| 179 | "gix", | 180 | "gix", |
| 180 | "gitoxide-core", | 181 | "gitoxide-core", |
| 181 | "gix-hashtable", | 182 | "gix-hashtable", |
| @@ -84,6 +84,7 @@ is usable to some extent. · 83 unmodified lines | |||
| 84 | * [gix-refspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-refspec) | 84 | * [gix-refspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-refspec) |
| 85 | * `gitoxide-core` | 85 | * `gitoxide-core` |
| 86 | * **very early** _(possibly without any documentation and many rough edges)_ | 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 | * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree) | 88 | * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree) |
| 88 | * [gix-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-bitmap) | 89 | * [gix-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-bitmap) |
| 89 | * [gix-date](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-date) | 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 | * [x] write the table of contents | 101 | * [x] write the table of contents |
| 102 | 102 | ||
| 103 | ### gix-hashtable | 103 | ### gix-hashtable |
| 104 | |||
| 105 | * [x] hashmap | 104 | * [x] hashmap |
| 106 | * [x] hashset | 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 | ### gix-object | 114 | ### gix-object |
| 110 | * *decode (zero-copy)* borrowed objects | 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 | } | ||