History

A new create for filesystem specific utilities

Sebastian Thiel committed 3 years ago

Commit ef8f4051 parent 042154b10 files · +371 −1Browse files

Unified · Split

10 files
  1. Cargo.lock +7 −0
  2. Cargo.toml +1 −0
  3. README.md +1 −0
  4. crate-status.md +6 −1
  5. gix-utils/CHANGELOG.md +6 −0
  6. gix-utils/Cargo.toml +15 −0
  7. gix-utils/src/fs_capabilities.rs +122 −0
  8. gix-utils/src/lib.rs +32 −0
  9. gix-utils/src/snapshot.rs +127 −0
  10. gix-utils/src/symlink.rs +54 −0
Cargo.lock+7−0
@@ -2307,6 +2307,13 @@ dependencies = [ · 2306 unmodified lines
2307 "url",2307 "url",
2308]2308]
2309 2309
2310[[package]]
2311name = "gix-utils"
2312version = "0.1.0"
2313dependencies = [
2314 "gix-features 0.28.1",
2315]
2316
2310[[package]]2317[[package]]
2311name = "gix-validate"2318name = "gix-validate"
2312version = "0.7.3"2319version = "0.7.3"
Cargo.toml+1−0
@@ -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",
README.md+1−0
@@ -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)
crate-status.md+6−1
@@ -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 contents101* [x] write the table of contents
102 102
103### gix-hashtable103### gix-hashtable
104
105* [x] hashmap104* [x] hashmap
106* [x] hashset105* [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-object114### gix-object
110* *decode (zero-copy)* borrowed objects115* *decode (zero-copy)* borrowed objects
gix-utils/CHANGELOG.md+6−0
@@ -0,0 +1,6 @@
1# Changelog
2
3All notable changes to this project will be documented in this file.
4
5The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
gix-utils/Cargo.toml+15−0
@@ -0,0 +1,15 @@
1[package]
2name = "gix-utils"
3version = "0.1.0"
4repository = "https://github.com/Byron/gitoxide"
5license = "MIT/Apache-2.0"
6description = "A crate with `gitoxide` utilities that don't need feature toggles"
7authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"]
8edition = "2021"
9rust-version = "1.64"
10
11[lib]
12doctest = false
13
14[dependencies]
15gix-features = { path = "../gix-features" }
gix-utils/src/fs_capabilities.rs+122−0
@@ -0,0 +1,122 @@
1// TODO: tests
2use crate::FilesystemCapabilities;
3use std::path::Path;
4
5#[cfg(windows)]
6impl 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")]
18impl 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")))]
30impl 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
41impl 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}
gix-utils/src/lib.rs+32−0
@@ -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)]
10pub 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
28mod snapshot;
29pub use snapshot::{FileSnapshot, SharedFileSnapshot, SharedFileSnapshotMut};
30
31mod fs_capabilities;
32pub mod symlink;
gix-utils/src/snapshot.rs+127−0
@@ -0,0 +1,127 @@
1// TODO: tests
2use std::ops::Deref;
3
4use 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)]
8pub struct FileSnapshot<T: std::fmt::Debug> {
9 value: T,
10 modified: std::time::SystemTime,
11}
12
13impl<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.
23pub 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)]
30pub struct SharedFileSnapshotMut<T: std::fmt::Debug>(pub MutableOnDemand<Option<SharedFileSnapshot<T>>>);
31
32impl<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
40impl<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
48impl<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}
gix-utils/src/symlink.rs+54−0
@@ -0,0 +1,54 @@
1use std::{io, path::Path};
2
3#[cfg(not(windows))]
4pub fn create(original: &Path, link: &Path) -> io::Result<()> {
5 std::os::unix::fs::symlink(original, link)
6}
7
8#[cfg(not(windows))]
9pub 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)]
15pub 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)]
28pub 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
38pub 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}