Skip to content

Commit

Permalink
feat!: Add git-style metadata support.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Byron committed Nov 11, 2023
1 parent 63fa80e commit 3c8421f
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 23 deletions.
12 changes: 7 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions gix-index/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ bitflags = "2"

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

[target.'cfg(not(windows))'.dependencies]
rustix = { version = "0.38.20", default-features = false, features = ["std", "fs"] }
libc = { version = "0.2.149" }

[package.metadata.docs.rs]
features = ["document-features", "serde"]
rustdoc-args = ["--cfg", "docsrs"]
10 changes: 4 additions & 6 deletions gix-index/src/entry/mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl Mode {
/// can not be committed to git).
pub fn change_to_match_fs(
self,
stat: &std::fs::Metadata,
stat: &crate::fs::Metadata,
has_symlinks: bool,
executable_bit: bool,
) -> Option<Change> {
Expand All @@ -46,15 +46,13 @@ impl Mode {
Mode::SYMLINK if has_symlinks && !stat.is_symlink() => (),
Mode::SYMLINK if !has_symlinks && !stat.is_file() => (),
Mode::COMMIT | Mode::DIR if !stat.is_dir() => (),
Mode::FILE if executable_bit && gix_fs::is_executable(stat) => return Some(Change::ExecutableBit),
Mode::FILE_EXECUTABLE if executable_bit && !gix_fs::is_executable(stat) => {
return Some(Change::ExecutableBit)
}
Mode::FILE if executable_bit && stat.is_executable() => return Some(Change::ExecutableBit),
Mode::FILE_EXECUTABLE if executable_bit && !stat.is_executable() => return Some(Change::ExecutableBit),
_ => return None,
};
let new_mode = if stat.is_dir() {
Mode::COMMIT
} else if executable_bit && gix_fs::is_executable(stat) {
} else if executable_bit && stat.is_executable() {
Mode::FILE_EXECUTABLE
} else {
Mode::FILE
Expand Down
23 changes: 11 additions & 12 deletions gix-index/src/entry/stat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ impl Stat {
}

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

#[cfg(not(unix))]
#[cfg(windows)]
let res = Stat {
mtime: mtime.try_into()?,
ctime: ctime.try_into()?,
Expand All @@ -89,24 +89,23 @@ impl Stat {
uid: 0,
gid: 0,
// truncation to 32 bits is on purpose (git does the same).
size: fstat.len() as u32,
size: stat.len() as u32,
};
#[cfg(unix)]
#[cfg(not(windows))]
let res = {
use std::os::unix::fs::MetadataExt;
Stat {
mtime: mtime.try_into().unwrap_or_default(),
ctime: ctime.try_into().unwrap_or_default(),
// truncating to 32 bits is fine here because
// that's what the linux syscalls returns
// just rust upcasts to 64 bits for some reason?
// numbers this large are impractical anyway (that's a lot of hard-drives).
dev: fstat.dev() as u32,
ino: fstat.ino() as u32,
uid: fstat.uid(),
gid: fstat.gid(),
dev: stat.dev() as u32,
ino: stat.ino() as u32,
uid: stat.uid(),
gid: stat.gid(),
// truncation to 32 bits is on purpose (git does the same).
size: fstat.len() as u32,
size: stat.len() as u32,
}
};

Expand Down
166 changes: 166 additions & 0 deletions gix-index/src/fs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
//! This module contains a `Metadata` implementation that must be used instead of `std::fs::Metadata` to assure
//! that the `ctime` information is populated exactly like the one in `git`, which wouldn't be the case on unix.
#![allow(clippy::useless_conversion)] // on some MacOOS conversions are required, but on linux usually not.
#![allow(clippy::unnecessary_cast)]

// it's allowed for good measure, in case there are systems that use different types for that.
use std::path::Path;
use std::time::{Duration, SystemTime};

/// A structure to partially mirror [`std::fs::Metadata`].
#[cfg(not(windows))]
pub struct Metadata(rustix::fs::Stat);

#[cfg(windows)]
/// A structure to partially mirror [`std::fs::Metadata`].
pub struct Metadata(std::fs::Metadata);

/// Lifecycle
impl Metadata {
/// Obtain the metadata at `path` without following symlinks.
pub fn from_path_no_follow(path: &Path) -> Result<Self, std::io::Error> {
#[cfg(not(windows))]
{
rustix::fs::lstat(path).map(Metadata).map_err(Into::into)
}
#[cfg(windows)]
path.symlink_metadata().map(Metadata)
}

/// Obtain the metadata at `path` without following symlinks.
pub fn from_file(file: &std::fs::File) -> Result<Self, std::io::Error> {
#[cfg(not(windows))]
{
rustix::fs::fstat(file).map(Metadata).map_err(Into::into)
}
#[cfg(windows)]
file.metadata().map(Metadata)
}
}

/// Access
#[allow(clippy::len_without_is_empty)]
impl Metadata {
/// Return true if the metadata belongs to a directory
pub fn is_dir(&self) -> bool {
#[cfg(not(windows))]
{
(self.0.st_mode & libc::S_IFMT) == libc::S_IFDIR
}
#[cfg(windows)]
self.0.is_dir()
}

/// Return the time at which the underlying file was modified.
pub fn modified(&self) -> Option<SystemTime> {
#[cfg(not(windows))]
{
Some(system_time_from_secs_nanos(
self.0.st_mtime.try_into().ok()?,
self.0.st_mtime_nsec.try_into().ok()?,
))
}
#[cfg(windows)]
self.0.modified().ok()
}

/// Return the time at which the underlying file was created.
///
/// Note that this differes from [`std::fs::Metadata::created()`] which would return
/// the inode birth time, which is notably different to what `git` does.
pub fn created(&self) -> Option<SystemTime> {
#[cfg(not(windows))]
{
Some(system_time_from_secs_nanos(
self.0.st_ctime.try_into().ok()?,
self.0.st_ctime_nsec.try_into().ok()?,
))
}
#[cfg(windows)]
self.0.created().ok()
}

/// Return the size of the file in bytes.
pub fn len(&self) -> u64 {
#[cfg(not(windows))]
{
self.0.st_size as u64
}
#[cfg(windows)]
self.0.len()
}

/// Return the device id on which the file is located, or 0 on windows.
pub fn dev(&self) -> u64 {
#[cfg(not(windows))]
{
self.0.st_dev as u64
}
#[cfg(windows)]
0
}

/// Return the inode id tracking the file, or 0 on windows.
pub fn ino(&self) -> u64 {
#[cfg(not(windows))]
{
self.0.st_ino as u64
}
#[cfg(windows)]
0
}

/// Return the user-id of the file or 0 on windows.
pub fn uid(&self) -> u32 {
#[cfg(not(windows))]
{
self.0.st_uid as u32
}
#[cfg(windows)]
0
}

/// Return the group-id of the file or 0 on windows.
pub fn gid(&self) -> u32 {
#[cfg(not(windows))]
{
self.0.st_gid as u32
}
#[cfg(windows)]
0
}

/// Return `true` if the file's executable bit is set, or `false` on windows.
pub fn is_executable(&self) -> bool {
#[cfg(not(windows))]
{
(self.0.st_mode & libc::S_IFMT) == libc::S_IFREG && self.0.st_mode & libc::S_IXUSR == libc::S_IXUSR
}
#[cfg(windows)]
gix_fs::is_executable(&self.0)
}

/// Return `true` if the file's is a symbolic link.
pub fn is_symlink(&self) -> bool {
#[cfg(not(windows))]
{
(self.0.st_mode & libc::S_IFMT) == libc::S_IFLNK
}
#[cfg(windows)]
self.0.is_symlink()
}

/// Return `true` if this is a regular file, executable or not.
pub fn is_file(&self) -> bool {
#[cfg(not(windows))]
{
(self.0.st_mode & libc::S_IFMT) == libc::S_IFREG
}
#[cfg(windows)]
self.0.is_file()
}
}

fn system_time_from_secs_nanos(secs: u64, nanos: u32) -> SystemTime {
std::time::UNIX_EPOCH + Duration::new(secs, nanos)
}
2 changes: 2 additions & 0 deletions gix-index/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub mod verify;
///
pub mod write;

pub mod fs;

/// All known versions of a git index file.
#[derive(PartialEq, Eq, Debug, Hash, Ord, PartialOrd, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand Down

0 comments on commit 3c8421f

Please sign in to comment.