1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
use crate::entry::Mode;
impl Mode {
/// Return `true` if this is a sparse entry, as it points to a directory which usually isn't what an 'unsparse' index tracks.
pub fn is_sparse(&self) -> bool {
*self == Self::DIR
}
/// Return `true` if this is a submodule entry.
pub fn is_submodule(&self) -> bool {
*self == Self::DIR | Self::SYMLINK
}
/// Convert this instance to a tree's entry mode, or return `None` if for some
/// and unexpected reason the bitflags don't resemble any known entry-mode.
pub fn to_tree_entry_mode(&self) -> Option<gix_object::tree::EntryMode> {
gix_object::tree::EntryMode::try_from(self.bits()).ok()
}
/// Compares this mode to the file system version ([`std::fs::symlink_metadata`])
/// and returns the change needed to update this mode to match the file.
///
/// * if `has_symlinks` is false symlink entries will simply check if there
/// is a normal file on disk
/// * if `executable_bit` is false the executable bit will not be compared
/// `Change::ExecutableBit` will never be generated
///
/// If there is a type change then we will use whatever information is
/// present on the FS. Specifically if `has_symlinks` is false we will
/// never generate `Change::TypeChange { new_mode: Mode::SYMLINK }`. and
/// if `executable_bit` is false we will never generate `Change::TypeChange
/// { new_mode: Mode::FILE_EXECUTABLE }` (all files are assumed to be not
/// executable). That means that unstaging and staging files can be a lossy
/// operation on such file systems.
///
/// If a directory replaced a normal file/symlink we assume that the
/// directory is a submodule. Normal (non-submodule) directories would
/// cause a file to be deleted from the index and should be handled before
/// calling this function.
///
/// If the stat information belongs to something other than a normal file/
/// directory (like a socket) we just return an identity change (non-files
/// can not be committed to git).
pub fn change_to_match_fs(
self,
stat: &crate::fs::Metadata,
has_symlinks: bool,
executable_bit: bool,
) -> Option<Change> {
self.change_to_match_fs_with_values(
stat.is_file(),
stat.is_dir(),
stat.is_symlink(),
stat.is_executable(),
has_symlinks,
executable_bit,
)
}
/// Like [`change_to_match_fs`](Self::change_to_match_fs) but accepts pre-extracted
/// file-type and permission bits, for callers that already have them (e.g. cached
/// metadata from a batched directory enumeration).
///
/// Note that some parameters are mutually exclusive, but inconsistency won't be a problem
/// if the source of the data is consistent.
///
/// * `is_file` is `true` if the file-system entry is a regular file.
/// * `is_dir` is `true` if the file-system entry is a directory.
/// * `is_symlink` is `true` if the file-system entry is a symbolic link.
/// * `is_executable` is `true` if the file-system entry has executable permissions.
///
/// These parameters match [`change_to_match_fs`](Self::change_to_match_fs).
///
/// * `has_symlinks` is `true` if the file system represents symbolic links faithfully.
/// * `executable_bit` is `true` if the file system represents executable permissions faithfully.
pub fn change_to_match_fs_with_values(
self,
is_file: bool,
is_dir: bool,
is_symlink: bool,
is_executable: bool,
has_symlinks: bool,
executable_bit: bool,
) -> Option<Change> {
match self {
Mode::FILE if !is_file => (),
Mode::SYMLINK if is_symlink => return None,
Mode::SYMLINK if has_symlinks && !is_symlink => (),
Mode::SYMLINK if !has_symlinks && !is_file => (),
Mode::COMMIT | Mode::DIR if !is_dir => (),
Mode::FILE if executable_bit && is_executable => return Some(Change::ExecutableBit),
Mode::FILE_EXECUTABLE if executable_bit && !is_executable => return Some(Change::ExecutableBit),
_ => return None,
}
let new_mode = if is_dir {
Mode::COMMIT
} else if executable_bit && is_executable {
Mode::FILE_EXECUTABLE
} else if has_symlinks && is_symlink {
Mode::SYMLINK
} else {
Mode::FILE
};
Some(Change::Type { new_mode })
}
}
impl From<gix_object::tree::EntryMode> for Mode {
fn from(value: gix_object::tree::EntryMode) -> Self {
let value: u16 = value.value();
Self::from_bits_truncate(u32::from(value))
}
}
/// A change of a [`Mode`].
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Change {
/// The type of mode changed, like symlink => file.
Type {
/// The mode representing the new index type.
new_mode: Mode,
},
/// The executable permission of this file has changed.
ExecutableBit,
}
impl Change {
/// Applies this change to `mode` and returns the changed one.
pub fn apply(self, mode: Mode) -> Mode {
match self {
Change::Type { new_mode } => new_mode,
Change::ExecutableBit => match mode {
Mode::FILE => Mode::FILE_EXECUTABLE,
Mode::FILE_EXECUTABLE => Mode::FILE,
_ => unreachable!("invalid mode change: can't flip executable bit of {mode:?}"),
},
}
}
}