Skip to content

Commit ef8f405

Browse files
committed
A new create for filesystem specific utilities
1 parent 042154b commit ef8f405

File tree

10 files changed

+371
-1
lines changed

10 files changed

+371
-1
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ members = [
176176
"gix-pathspec",
177177
"gix-refspec",
178178
"gix-path",
179+
"gix-utils",
179180
"gix",
180181
"gitoxide-core",
181182
"gix-hashtable",

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ is usable to some extent.
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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,15 @@ and itself relies on all `git-*` crates. It's not meant for consumption, for app
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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)