Skip to content

Commit 3c8421f

Browse files
committed
feat!: Add git-style metadata support.
As opposed to the Rust standard library, this one will get the ctime from the file itself, instead of from the inode. That way, the index file written by `gix` will not continuously be expensively rewritten by `git`, and vice versa.
1 parent 63fa80e commit 3c8421f

File tree

6 files changed

+194
-23
lines changed

6 files changed

+194
-23
lines changed

Cargo.lock

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

gix-index/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ bitflags = "2"
4141

4242
document-features = { version = "0.2.0", optional = true }
4343

44+
[target.'cfg(not(windows))'.dependencies]
45+
rustix = { version = "0.38.20", default-features = false, features = ["std", "fs"] }
46+
libc = { version = "0.2.149" }
47+
4448
[package.metadata.docs.rs]
4549
features = ["document-features", "serde"]
4650
rustdoc-args = ["--cfg", "docsrs"]

gix-index/src/entry/mode.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ impl Mode {
3737
/// can not be committed to git).
3838
pub fn change_to_match_fs(
3939
self,
40-
stat: &std::fs::Metadata,
40+
stat: &crate::fs::Metadata,
4141
has_symlinks: bool,
4242
executable_bit: bool,
4343
) -> Option<Change> {
@@ -46,15 +46,13 @@ impl Mode {
4646
Mode::SYMLINK if has_symlinks && !stat.is_symlink() => (),
4747
Mode::SYMLINK if !has_symlinks && !stat.is_file() => (),
4848
Mode::COMMIT | Mode::DIR if !stat.is_dir() => (),
49-
Mode::FILE if executable_bit && gix_fs::is_executable(stat) => return Some(Change::ExecutableBit),
50-
Mode::FILE_EXECUTABLE if executable_bit && !gix_fs::is_executable(stat) => {
51-
return Some(Change::ExecutableBit)
52-
}
49+
Mode::FILE if executable_bit && stat.is_executable() => return Some(Change::ExecutableBit),
50+
Mode::FILE_EXECUTABLE if executable_bit && !stat.is_executable() => return Some(Change::ExecutableBit),
5351
_ => return None,
5452
};
5553
let new_mode = if stat.is_dir() {
5654
Mode::COMMIT
57-
} else if executable_bit && gix_fs::is_executable(stat) {
55+
} else if executable_bit && stat.is_executable() {
5856
Mode::FILE_EXECUTABLE
5957
} else {
6058
Mode::FILE

gix-index/src/entry/stat.rs

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ impl Stat {
7676
}
7777

7878
/// Creates stat information from the result of `symlink_metadata`.
79-
pub fn from_fs(fstat: &std::fs::Metadata) -> Result<Stat, SystemTimeError> {
80-
let mtime = fstat.modified().unwrap_or(std::time::UNIX_EPOCH);
81-
let ctime = fstat.created().unwrap_or(std::time::UNIX_EPOCH);
79+
pub fn from_fs(stat: &crate::fs::Metadata) -> Result<Stat, SystemTimeError> {
80+
let mtime = stat.modified().unwrap_or(std::time::UNIX_EPOCH);
81+
let ctime = stat.created().unwrap_or(std::time::UNIX_EPOCH);
8282

83-
#[cfg(not(unix))]
83+
#[cfg(windows)]
8484
let res = Stat {
8585
mtime: mtime.try_into()?,
8686
ctime: ctime.try_into()?,
@@ -89,24 +89,23 @@ impl Stat {
8989
uid: 0,
9090
gid: 0,
9191
// truncation to 32 bits is on purpose (git does the same).
92-
size: fstat.len() as u32,
92+
size: stat.len() as u32,
9393
};
94-
#[cfg(unix)]
94+
#[cfg(not(windows))]
9595
let res = {
96-
use std::os::unix::fs::MetadataExt;
9796
Stat {
9897
mtime: mtime.try_into().unwrap_or_default(),
9998
ctime: ctime.try_into().unwrap_or_default(),
10099
// truncating to 32 bits is fine here because
101100
// that's what the linux syscalls returns
102101
// just rust upcasts to 64 bits for some reason?
103102
// numbers this large are impractical anyway (that's a lot of hard-drives).
104-
dev: fstat.dev() as u32,
105-
ino: fstat.ino() as u32,
106-
uid: fstat.uid(),
107-
gid: fstat.gid(),
103+
dev: stat.dev() as u32,
104+
ino: stat.ino() as u32,
105+
uid: stat.uid(),
106+
gid: stat.gid(),
108107
// truncation to 32 bits is on purpose (git does the same).
109-
size: fstat.len() as u32,
108+
size: stat.len() as u32,
110109
}
111110
};
112111

gix-index/src/fs.rs

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
//! This module contains a `Metadata` implementation that must be used instead of `std::fs::Metadata` to assure
2+
//! that the `ctime` information is populated exactly like the one in `git`, which wouldn't be the case on unix.
3+
#![allow(clippy::useless_conversion)] // on some MacOOS conversions are required, but on linux usually not.
4+
#![allow(clippy::unnecessary_cast)]
5+
6+
// it's allowed for good measure, in case there are systems that use different types for that.
7+
use std::path::Path;
8+
use std::time::{Duration, SystemTime};
9+
10+
/// A structure to partially mirror [`std::fs::Metadata`].
11+
#[cfg(not(windows))]
12+
pub struct Metadata(rustix::fs::Stat);
13+
14+
#[cfg(windows)]
15+
/// A structure to partially mirror [`std::fs::Metadata`].
16+
pub struct Metadata(std::fs::Metadata);
17+
18+
/// Lifecycle
19+
impl Metadata {
20+
/// Obtain the metadata at `path` without following symlinks.
21+
pub fn from_path_no_follow(path: &Path) -> Result<Self, std::io::Error> {
22+
#[cfg(not(windows))]
23+
{
24+
rustix::fs::lstat(path).map(Metadata).map_err(Into::into)
25+
}
26+
#[cfg(windows)]
27+
path.symlink_metadata().map(Metadata)
28+
}
29+
30+
/// Obtain the metadata at `path` without following symlinks.
31+
pub fn from_file(file: &std::fs::File) -> Result<Self, std::io::Error> {
32+
#[cfg(not(windows))]
33+
{
34+
rustix::fs::fstat(file).map(Metadata).map_err(Into::into)
35+
}
36+
#[cfg(windows)]
37+
file.metadata().map(Metadata)
38+
}
39+
}
40+
41+
/// Access
42+
#[allow(clippy::len_without_is_empty)]
43+
impl Metadata {
44+
/// Return true if the metadata belongs to a directory
45+
pub fn is_dir(&self) -> bool {
46+
#[cfg(not(windows))]
47+
{
48+
(self.0.st_mode & libc::S_IFMT) == libc::S_IFDIR
49+
}
50+
#[cfg(windows)]
51+
self.0.is_dir()
52+
}
53+
54+
/// Return the time at which the underlying file was modified.
55+
pub fn modified(&self) -> Option<SystemTime> {
56+
#[cfg(not(windows))]
57+
{
58+
Some(system_time_from_secs_nanos(
59+
self.0.st_mtime.try_into().ok()?,
60+
self.0.st_mtime_nsec.try_into().ok()?,
61+
))
62+
}
63+
#[cfg(windows)]
64+
self.0.modified().ok()
65+
}
66+
67+
/// Return the time at which the underlying file was created.
68+
///
69+
/// Note that this differes from [`std::fs::Metadata::created()`] which would return
70+
/// the inode birth time, which is notably different to what `git` does.
71+
pub fn created(&self) -> Option<SystemTime> {
72+
#[cfg(not(windows))]
73+
{
74+
Some(system_time_from_secs_nanos(
75+
self.0.st_ctime.try_into().ok()?,
76+
self.0.st_ctime_nsec.try_into().ok()?,
77+
))
78+
}
79+
#[cfg(windows)]
80+
self.0.created().ok()
81+
}
82+
83+
/// Return the size of the file in bytes.
84+
pub fn len(&self) -> u64 {
85+
#[cfg(not(windows))]
86+
{
87+
self.0.st_size as u64
88+
}
89+
#[cfg(windows)]
90+
self.0.len()
91+
}
92+
93+
/// Return the device id on which the file is located, or 0 on windows.
94+
pub fn dev(&self) -> u64 {
95+
#[cfg(not(windows))]
96+
{
97+
self.0.st_dev as u64
98+
}
99+
#[cfg(windows)]
100+
0
101+
}
102+
103+
/// Return the inode id tracking the file, or 0 on windows.
104+
pub fn ino(&self) -> u64 {
105+
#[cfg(not(windows))]
106+
{
107+
self.0.st_ino as u64
108+
}
109+
#[cfg(windows)]
110+
0
111+
}
112+
113+
/// Return the user-id of the file or 0 on windows.
114+
pub fn uid(&self) -> u32 {
115+
#[cfg(not(windows))]
116+
{
117+
self.0.st_uid as u32
118+
}
119+
#[cfg(windows)]
120+
0
121+
}
122+
123+
/// Return the group-id of the file or 0 on windows.
124+
pub fn gid(&self) -> u32 {
125+
#[cfg(not(windows))]
126+
{
127+
self.0.st_gid as u32
128+
}
129+
#[cfg(windows)]
130+
0
131+
}
132+
133+
/// Return `true` if the file's executable bit is set, or `false` on windows.
134+
pub fn is_executable(&self) -> bool {
135+
#[cfg(not(windows))]
136+
{
137+
(self.0.st_mode & libc::S_IFMT) == libc::S_IFREG && self.0.st_mode & libc::S_IXUSR == libc::S_IXUSR
138+
}
139+
#[cfg(windows)]
140+
gix_fs::is_executable(&self.0)
141+
}
142+
143+
/// Return `true` if the file's is a symbolic link.
144+
pub fn is_symlink(&self) -> bool {
145+
#[cfg(not(windows))]
146+
{
147+
(self.0.st_mode & libc::S_IFMT) == libc::S_IFLNK
148+
}
149+
#[cfg(windows)]
150+
self.0.is_symlink()
151+
}
152+
153+
/// Return `true` if this is a regular file, executable or not.
154+
pub fn is_file(&self) -> bool {
155+
#[cfg(not(windows))]
156+
{
157+
(self.0.st_mode & libc::S_IFMT) == libc::S_IFREG
158+
}
159+
#[cfg(windows)]
160+
self.0.is_file()
161+
}
162+
}
163+
164+
fn system_time_from_secs_nanos(secs: u64, nanos: u32) -> SystemTime {
165+
std::time::UNIX_EPOCH + Duration::new(secs, nanos)
166+
}

gix-index/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ pub mod verify;
3333
///
3434
pub mod write;
3535

36+
pub mod fs;
37+
3638
/// All known versions of a git index file.
3739
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
3840
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]

0 commit comments

Comments
 (0)