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
23072307 "url",
23082308 ]
23092309
2310+[[package]]
2311+name = "gix-utils"
2312+version = "0.1.0"
2313+dependencies = [
2314+ "gix-features 0.28.1",
2315+]
2316+
23102317 [[package]]
23112318 name = "gix-validate"
23122319 version = "0.7.3"
Cargo.toml+1−0
@@ -176,6 +176,7 @@ members = [ · 175 unmodified lines
176176 "gix-pathspec",
177177 "gix-refspec",
178178 "gix-path",
179+ "gix-utils",
179180 "gix",
180181 "gitoxide-core",
181182 "gix-hashtable",
README.md+1−0
@@ -84,6 +84,7 @@ is usable to some extent. · 83 unmodified lines
8484 * [gix-refspec](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-refspec)
8585 * `gitoxide-core`
8686 * **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)
8788 * [gix-worktree](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-worktree)
8889 * [gix-bitmap](https://github.com/Byron/gitoxide/blob/main/crate-status.md#gix-bitmap)
8990 * [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
101101 * [x] write the table of contents
102102
103103 ### gix-hashtable
104−
105104 * [x] hashmap
106105 * [x] hashset
107106
107+### gix-utils
108+
109+* **filesystem**
110+ * [x] probe capabilities
111+ * [x] symlink creation and removal
112+ * [x] file snapshots
108113
109114 ### gix-object
110115 * *decode (zero-copy)* borrowed objects
gix-utils/CHANGELOG.md+6−0
@@ -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).
gix-utils/Cargo.toml+15−0
@@ -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" }
gix-utils/src/fs_capabilities.rs+122−0
@@ -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+}
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)]
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;
gix-utils/src/snapshot.rs+127−0
@@ -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+}
gix-utils/src/symlink.rs+54−0
@@ -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+}