1use std::fmt::Display;
2use std::path::PathBuf;
3use std::str::FromStr;
4
5use bstr::ByteVec;
6use lazy_static::lazy_static;
7use regex::bytes::Regex;
8use tracing::{instrument, warn};
9
10#[allow(missing_docs)]
13#[derive(Copy, Clone, Debug, PartialEq, Eq)]
14pub enum FileStatus {
15 Unmodified,
16 Modified,
17 Added,
18 Deleted,
19 Renamed,
20 Copied,
21 Unmerged,
22 Untracked,
23 Ignored,
24}
25
26impl FileStatus {
27 pub fn is_changed(&self) -> bool {
30 match self {
31 FileStatus::Added
32 | FileStatus::Copied
33 | FileStatus::Deleted
34 | FileStatus::Modified
35 | FileStatus::Renamed => true,
36 FileStatus::Ignored
37 | FileStatus::Unmerged
38 | FileStatus::Unmodified
39 | FileStatus::Untracked => false,
40 }
41 }
42}
43
44impl From<u8> for FileStatus {
45 fn from(status: u8) -> Self {
46 match status {
47 b'.' => FileStatus::Unmodified,
48 b'M' => FileStatus::Modified,
49 b'A' => FileStatus::Added,
50 b'D' => FileStatus::Deleted,
51 b'R' => FileStatus::Renamed,
52 b'C' => FileStatus::Copied,
53 b'U' => FileStatus::Unmerged,
54 b'?' => FileStatus::Untracked,
55 b'!' => FileStatus::Ignored,
56 _ => {
57 warn!(?status, "invalid status indicator");
58 FileStatus::Untracked
59 }
60 }
61 }
62}
63
64#[allow(missing_docs)]
66#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
67pub enum FileMode {
68 Unreadable,
69 Tree,
70 Blob,
71 BlobExecutable,
72 BlobGroupWritable,
73 Link,
74 Commit,
75}
76
77impl From<git2::FileMode> for FileMode {
78 fn from(file_mode: git2::FileMode) -> Self {
79 match file_mode {
80 git2::FileMode::Blob => FileMode::Blob,
81 git2::FileMode::BlobExecutable => FileMode::BlobExecutable,
82 git2::FileMode::BlobGroupWritable => FileMode::BlobGroupWritable,
83 git2::FileMode::Commit => FileMode::Commit,
84 git2::FileMode::Link => FileMode::Link,
85 git2::FileMode::Tree => FileMode::Tree,
86 git2::FileMode::Unreadable => FileMode::Unreadable,
87 }
88 }
89}
90
91impl From<FileMode> for git2::FileMode {
92 fn from(file_mode: FileMode) -> Self {
93 match file_mode {
94 FileMode::Blob => git2::FileMode::Blob,
95 FileMode::BlobExecutable => git2::FileMode::BlobExecutable,
96 FileMode::BlobGroupWritable => git2::FileMode::BlobGroupWritable,
97 FileMode::Commit => git2::FileMode::Commit,
98 FileMode::Link => git2::FileMode::Link,
99 FileMode::Tree => git2::FileMode::Tree,
100 FileMode::Unreadable => git2::FileMode::Unreadable,
101 }
102 }
103}
104
105impl From<i32> for FileMode {
106 fn from(file_mode: i32) -> Self {
107 if file_mode == i32::from(git2::FileMode::Blob) {
108 FileMode::Blob
109 } else if file_mode == i32::from(git2::FileMode::BlobExecutable) {
110 FileMode::BlobExecutable
111 } else if file_mode == i32::from(git2::FileMode::Commit) {
112 FileMode::Commit
113 } else if file_mode == i32::from(git2::FileMode::Link) {
114 FileMode::Link
115 } else if file_mode == i32::from(git2::FileMode::Tree) {
116 FileMode::Tree
117 } else {
118 FileMode::Unreadable
119 }
120 }
121}
122
123impl From<FileMode> for i32 {
124 fn from(file_mode: FileMode) -> Self {
125 match file_mode {
126 FileMode::Blob => git2::FileMode::Blob.into(),
127 FileMode::BlobExecutable => git2::FileMode::BlobExecutable.into(),
128 FileMode::BlobGroupWritable => git2::FileMode::BlobGroupWritable.into(),
129 FileMode::Commit => git2::FileMode::Commit.into(),
130 FileMode::Link => git2::FileMode::Link.into(),
131 FileMode::Tree => git2::FileMode::Tree.into(),
132 FileMode::Unreadable => git2::FileMode::Unreadable.into(),
133 }
134 }
135}
136
137impl From<FileMode> for u32 {
138 fn from(file_mode: FileMode) -> Self {
139 i32::from(file_mode).try_into().unwrap()
140 }
141}
142
143impl From<scm_record::FileMode> for FileMode {
144 fn from(file_mode: scm_record::FileMode) -> Self {
145 match file_mode {
146 scm_record::FileMode::Unix(file_mode) => {
147 let file_mode: i32 = file_mode.try_into().unwrap();
148 Self::from(file_mode)
149 }
150 scm_record::FileMode::Absent => FileMode::Unreadable,
151 }
152 }
153}
154
155impl FromStr for FileMode {
156 type Err = eyre::Error;
157
158 fn from_str(file_mode: &str) -> eyre::Result<Self> {
162 let file_mode = match file_mode {
163 "000000" => FileMode::Unreadable,
164 "040000" => FileMode::Tree,
165 "100644" => FileMode::Blob,
166 "100755" => FileMode::BlobExecutable,
167 "100664" => FileMode::BlobGroupWritable,
168 "120000" => FileMode::Link,
169 "160000" => FileMode::Commit,
170 _ => eyre::bail!("unknown file mode: {}", file_mode),
171 };
172 Ok(file_mode)
173 }
174}
175
176impl Display for FileMode {
177 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178 match self {
179 FileMode::Unreadable => write!(f, "000000"),
180 FileMode::Tree => write!(f, "040000"),
181 FileMode::Blob => write!(f, "100644"),
182 FileMode::BlobExecutable => write!(f, "100755"),
183 FileMode::BlobGroupWritable => write!(f, "100664"),
184 FileMode::Link => write!(f, "120000"),
185 FileMode::Commit => write!(f, "160000"),
186 }
187 }
188}
189
190#[derive(Clone, Debug, PartialEq, Eq)]
192pub struct StatusEntry {
193 pub index_status: FileStatus,
195 pub working_copy_status: FileStatus,
197 pub working_copy_file_mode: FileMode,
199 pub path: PathBuf,
201 pub orig_path: Option<PathBuf>,
203}
204
205impl StatusEntry {
206 pub fn new_untracked(filename: String) -> Self {
208 StatusEntry {
209 index_status: FileStatus::Untracked,
210 working_copy_status: FileStatus::Untracked,
211 working_copy_file_mode: FileMode::Blob,
212 path: PathBuf::from(filename),
213 orig_path: None,
214 }
215 }
216
217 pub fn paths(&self) -> Vec<PathBuf> {
219 let mut result = vec![self.path.clone()];
220 if let Some(orig_path) = &self.orig_path {
221 result.push(orig_path.clone());
222 }
223 result
224 }
225}
226
227impl TryFrom<&[u8]> for StatusEntry {
228 type Error = eyre::Error;
229
230 #[instrument]
231 fn try_from(line: &[u8]) -> eyre::Result<StatusEntry> {
232 lazy_static! {
233 static ref STATUS_PORCELAIN_V2_REGEXP: Regex = Regex::new(concat!(
236 r#"^(?P<prefix>1|2|u) "#, r#"(?P<index_status>[\w.])(?P<working_copy_status>[\w.]) "#, r#"[\w.]+ "#, r#"(\d{6} ){2,3}(?P<working_copy_filemode>\d{6}) "#, r#"([a-f\d]+ ){2,3}([CR]\d{1,3} )?"#, r#"(?P<path>[^\x00]+)(\x00(?P<orig_path>[^\x00]+))?$"# ))
245 .expect("porcelain v2 status line regex");
246 }
247
248 let status_line_parts = STATUS_PORCELAIN_V2_REGEXP
249 .captures(line)
250 .ok_or_else(|| eyre::eyre!("unable to parse status line into parts"))?;
251
252 let index_status: FileStatus = match status_line_parts.name("prefix") {
253 Some(m) if m.as_bytes() == b"u" => FileStatus::Unmerged,
254 _ => status_line_parts
255 .name("index_status")
256 .and_then(|m| m.as_bytes().iter().next().copied())
257 .ok_or_else(|| eyre::eyre!("no index status indicator"))?
258 .into(),
259 };
260 let working_copy_status: FileStatus = status_line_parts
261 .name("working_copy_status")
262 .and_then(|m| m.as_bytes().iter().next().copied())
263 .ok_or_else(|| eyre::eyre!("no working copy status indicator"))?
264 .into();
265 let working_copy_file_mode = status_line_parts
266 .name("working_copy_filemode")
267 .ok_or_else(|| eyre::eyre!("no working copy filemode in status line"))
268 .and_then(|m| {
269 std::str::from_utf8(m.as_bytes())
270 .map_err(|err| {
271 eyre::eyre!("unable to decode working copy file mode: {:?}", err)
272 })
273 .and_then(|working_copy_file_mode| working_copy_file_mode.parse::<FileMode>())
274 })?;
275 let path = status_line_parts
276 .name("path")
277 .ok_or_else(|| eyre::eyre!("no path in status line"))?
278 .as_bytes();
279 let orig_path = status_line_parts
280 .name("orig_path")
281 .map(|orig_path| orig_path.as_bytes());
282
283 Ok(StatusEntry {
284 index_status,
285 working_copy_status,
286 working_copy_file_mode,
287 path: path.to_vec().into_path_buf()?,
288 orig_path: orig_path
289 .map(|orig_path| orig_path.to_vec().into_path_buf())
290 .transpose()?,
291 })
292 }
293}