gix_index/entry/
stat.rs

1use std::{
2    cmp::Ordering,
3    time::{SystemTime, SystemTimeError},
4};
5
6use filetime::FileTime;
7
8use crate::entry::Stat;
9
10impl Stat {
11    /// Detect whether this stat entry is racy if stored in a file index with `timestamp`.
12    ///
13    /// An index entry is considered racy if it's `mtime` is larger or equal to the index `timestamp`.
14    /// The index `timestamp` marks the point in time before which we definitely resolved the racy git problem
15    /// for all index entries so any index entries that changed afterwards will need to be examined for
16    /// changes by actually reading the file from disk at least once.
17    pub fn is_racy(
18        &self,
19        timestamp: FileTime,
20        Options {
21            check_stat, use_nsec, ..
22        }: Options,
23    ) -> bool {
24        match timestamp.unix_seconds().cmp(&i64::from(self.mtime.secs)) {
25            Ordering::Less => true,
26            Ordering::Equal if use_nsec && check_stat => timestamp.nanoseconds() <= self.mtime.nsecs,
27            Ordering::Equal => true,
28            Ordering::Greater => false,
29        }
30    }
31
32    /// Compares the stat information of two index entries.
33    ///
34    /// Intuitively this is basically equivalent to `self == other`.
35    /// However there a lot of nobs in git that tweak whether certain stat information is used when checking
36    /// equality, see [`Options`].
37    /// This function respects those options while performing the stat comparison and may therefore ignore some fields.
38    pub fn matches(
39        &self,
40        other: &Self,
41        Options {
42            trust_ctime,
43            check_stat,
44            use_nsec,
45            use_stdev,
46        }: Options,
47    ) -> bool {
48        if self.mtime.secs != other.mtime.secs {
49            return false;
50        }
51        if check_stat && use_nsec && self.mtime.nsecs != other.mtime.nsecs {
52            return false;
53        }
54
55        if self.size != other.size {
56            return false;
57        }
58
59        if trust_ctime {
60            if self.ctime.secs != other.ctime.secs {
61                return false;
62            }
63            if check_stat && use_nsec && self.ctime.nsecs != other.ctime.nsecs {
64                return false;
65            }
66        }
67
68        if check_stat {
69            if use_stdev && self.dev != other.dev {
70                return false;
71            }
72            self.ino == other.ino && self.gid == other.gid && self.uid == other.uid
73        } else {
74            true
75        }
76    }
77
78    /// Creates stat information from the result of `symlink_metadata`.
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);
82
83        #[cfg(windows)]
84        let res = Stat {
85            mtime: mtime.try_into()?,
86            ctime: ctime.try_into()?,
87            dev: 0,
88            ino: 0,
89            uid: 0,
90            gid: 0,
91            // truncation to 32 bits is on purpose (git does the same).
92            size: stat.len() as u32,
93        };
94        #[cfg(not(windows))]
95        let res = {
96            Stat {
97                mtime: mtime.try_into().unwrap_or_default(),
98                ctime: ctime.try_into().unwrap_or_default(),
99                // truncating to 32 bits is fine here because
100                // that's what the linux syscalls returns
101                // just rust upcasts to 64 bits for some reason?
102                // numbers this large are impractical anyway (that's a lot of hard-drives).
103                dev: stat.dev() as u32,
104                ino: stat.ino() as u32,
105                uid: stat.uid(),
106                gid: stat.gid(),
107                // truncation to 32 bits is on purpose (git does the same).
108                size: stat.len() as u32,
109            }
110        };
111
112        Ok(res)
113    }
114}
115
116impl TryFrom<SystemTime> for Time {
117    type Error = SystemTimeError;
118    fn try_from(s: SystemTime) -> Result<Self, SystemTimeError> {
119        let d = s.duration_since(std::time::UNIX_EPOCH)?;
120        Ok(Time {
121            // truncation to 32 bits is on purpose (we only compare the low bits)
122            secs: d.as_secs() as u32,
123            nsecs: d.subsec_nanos(),
124        })
125    }
126}
127
128impl From<Time> for SystemTime {
129    fn from(s: Time) -> Self {
130        std::time::UNIX_EPOCH + std::time::Duration::new(s.secs.into(), s.nsecs)
131    }
132}
133
134/// The time component in a [`Stat`] struct.
135#[derive(Debug, Default, PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Copy)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
137pub struct Time {
138    /// The amount of seconds elapsed since EPOCH.
139    pub secs: u32,
140    /// The amount of nanoseconds elapsed in the current second, ranging from 0 to 999.999.999 .
141    pub nsecs: u32,
142}
143
144impl From<FileTime> for Time {
145    fn from(value: FileTime) -> Self {
146        Time {
147            secs: value.unix_seconds().try_into().expect("can't represent non-unix times"),
148            nsecs: value.nanoseconds(),
149        }
150    }
151}
152
153impl PartialEq<FileTime> for Time {
154    fn eq(&self, other: &FileTime) -> bool {
155        *self == Time::from(*other)
156    }
157}
158
159impl PartialOrd<FileTime> for Time {
160    fn partial_cmp(&self, other: &FileTime) -> Option<Ordering> {
161        self.partial_cmp(&Time::from(*other))
162    }
163}
164
165/// Configuration for comparing stat entries
166#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
167pub struct Options {
168    /// If true, a files creation time is taken into consideration when checking if a file changed.
169    /// Can be set to false in case other tools alter the creation time in ways that interfere with our operation.
170    ///
171    /// Default `true`.
172    pub trust_ctime: bool,
173    /// If true, all stat fields will be used when checking for up-to-date'ness of the entry. Otherwise
174    /// nano-second parts of mtime and ctime,uid, gid, inode and device number _will not_ be used, leaving only
175    /// the whole-second part of ctime and mtime and the file size to be checked.
176    ///
177    /// Default `true`.
178    pub check_stat: bool,
179    /// Whether to compare nano secs when comparing timestamps. This currently
180    /// leads to many false positives on linux and is therefore disabled there.
181    ///
182    /// Default `false`
183    pub use_nsec: bool,
184    /// Whether to compare network devices secs when comparing timestamps.
185    /// Disabled by default because this can cause many false positives on network
186    /// devices where the device number is not stable
187    ///
188    /// Default `false`.
189    pub use_stdev: bool,
190}
191
192impl Default for Options {
193    fn default() -> Self {
194        Self {
195            trust_ctime: true,
196            check_stat: true,
197            use_nsec: false,
198            use_stdev: false,
199        }
200    }
201}