1use crate::repository::RepoPath;
2use anyhow::Result;
3use serde::{Deserialize, Serialize};
4use std::{path::Path, str::FromStr, sync::Arc};
5use util::ResultExt;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum FileStatus {
9 Untracked,
10 Ignored,
11 Unmerged(UnmergedStatus),
12 Tracked(TrackedStatus),
13}
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub struct UnmergedStatus {
17 pub first_head: UnmergedStatusCode,
18 pub second_head: UnmergedStatusCode,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub enum UnmergedStatusCode {
23 Added,
24 Deleted,
25 Updated,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub struct TrackedStatus {
30 pub index_status: StatusCode,
31 pub worktree_status: StatusCode,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
35pub enum StatusCode {
36 Modified,
37 TypeChanged,
38 Added,
39 Deleted,
40 Renamed,
41 Copied,
42 Unmodified,
43}
44
45impl From<UnmergedStatus> for FileStatus {
46 fn from(value: UnmergedStatus) -> Self {
47 FileStatus::Unmerged(value)
48 }
49}
50
51impl From<TrackedStatus> for FileStatus {
52 fn from(value: TrackedStatus) -> Self {
53 FileStatus::Tracked(value)
54 }
55}
56
57#[derive(Debug, PartialEq, Eq, Clone, Copy)]
58pub enum StageStatus {
59 Staged,
60 Unstaged,
61 PartiallyStaged,
62}
63
64impl StageStatus {
65 pub fn is_fully_staged(&self) -> bool {
66 matches!(self, StageStatus::Staged)
67 }
68
69 pub fn is_fully_unstaged(&self) -> bool {
70 matches!(self, StageStatus::Unstaged)
71 }
72
73 pub fn has_staged(&self) -> bool {
74 matches!(self, StageStatus::Staged | StageStatus::PartiallyStaged)
75 }
76
77 pub fn has_unstaged(&self) -> bool {
78 matches!(self, StageStatus::Unstaged | StageStatus::PartiallyStaged)
79 }
80
81 pub fn as_bool(self) -> Option<bool> {
82 match self {
83 StageStatus::Staged => Some(true),
84 StageStatus::Unstaged => Some(false),
85 StageStatus::PartiallyStaged => None,
86 }
87 }
88}
89
90impl FileStatus {
91 pub const fn worktree(worktree_status: StatusCode) -> Self {
92 FileStatus::Tracked(TrackedStatus {
93 index_status: StatusCode::Unmodified,
94 worktree_status,
95 })
96 }
97
98 pub const fn index(index_status: StatusCode) -> Self {
99 FileStatus::Tracked(TrackedStatus {
100 worktree_status: StatusCode::Unmodified,
101 index_status,
102 })
103 }
104
105 /// Generate a FileStatus Code from a byte pair, as described in
106 /// https://git-scm.com/docs/git-status#_output
107 ///
108 /// NOTE: That instead of '', we use ' ' to denote no change
109 fn from_bytes(bytes: [u8; 2]) -> anyhow::Result<Self> {
110 let status = match bytes {
111 [b'?', b'?'] => FileStatus::Untracked,
112 [b'!', b'!'] => FileStatus::Ignored,
113 [b'A', b'A'] => UnmergedStatus {
114 first_head: UnmergedStatusCode::Added,
115 second_head: UnmergedStatusCode::Added,
116 }
117 .into(),
118 [b'D', b'D'] => UnmergedStatus {
119 first_head: UnmergedStatusCode::Added,
120 second_head: UnmergedStatusCode::Added,
121 }
122 .into(),
123 [x, b'U'] => UnmergedStatus {
124 first_head: UnmergedStatusCode::from_byte(x)?,
125 second_head: UnmergedStatusCode::Updated,
126 }
127 .into(),
128 [b'U', y] => UnmergedStatus {
129 first_head: UnmergedStatusCode::Updated,
130 second_head: UnmergedStatusCode::from_byte(y)?,
131 }
132 .into(),
133 [x, y] => TrackedStatus {
134 index_status: StatusCode::from_byte(x)?,
135 worktree_status: StatusCode::from_byte(y)?,
136 }
137 .into(),
138 };
139 Ok(status)
140 }
141
142 pub fn staging(self) -> StageStatus {
143 match self {
144 FileStatus::Untracked | FileStatus::Ignored | FileStatus::Unmerged { .. } => {
145 StageStatus::Unstaged
146 }
147 FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
148 (StatusCode::Unmodified, _) => StageStatus::Unstaged,
149 (_, StatusCode::Unmodified) => StageStatus::Staged,
150 _ => StageStatus::PartiallyStaged,
151 },
152 }
153 }
154
155 pub fn is_conflicted(self) -> bool {
156 match self {
157 FileStatus::Unmerged { .. } => true,
158 _ => false,
159 }
160 }
161
162 pub fn is_ignored(self) -> bool {
163 match self {
164 FileStatus::Ignored => true,
165 _ => false,
166 }
167 }
168
169 pub fn has_changes(&self) -> bool {
170 self.is_modified()
171 || self.is_created()
172 || self.is_deleted()
173 || self.is_untracked()
174 || self.is_conflicted()
175 }
176
177 pub fn is_modified(self) -> bool {
178 match self {
179 FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
180 (StatusCode::Modified, _) | (_, StatusCode::Modified) => true,
181 _ => false,
182 },
183 _ => false,
184 }
185 }
186
187 pub fn is_created(self) -> bool {
188 match self {
189 FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
190 (StatusCode::Added, _) | (_, StatusCode::Added) => true,
191 _ => false,
192 },
193 FileStatus::Untracked => true,
194 _ => false,
195 }
196 }
197
198 pub fn is_deleted(self) -> bool {
199 match self {
200 FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) {
201 (StatusCode::Deleted, _) | (_, StatusCode::Deleted) => true,
202 _ => false,
203 },
204 _ => false,
205 }
206 }
207
208 pub fn is_untracked(self) -> bool {
209 match self {
210 FileStatus::Untracked => true,
211 _ => false,
212 }
213 }
214
215 pub fn summary(self) -> GitSummary {
216 match self {
217 FileStatus::Ignored => GitSummary::UNCHANGED,
218 FileStatus::Untracked => GitSummary::UNTRACKED,
219 FileStatus::Unmerged(_) => GitSummary::CONFLICT,
220 FileStatus::Tracked(TrackedStatus {
221 index_status,
222 worktree_status,
223 }) => GitSummary {
224 index: index_status.to_summary(),
225 worktree: worktree_status.to_summary(),
226 conflict: 0,
227 untracked: 0,
228 count: 1,
229 },
230 }
231 }
232}
233
234impl StatusCode {
235 fn from_byte(byte: u8) -> anyhow::Result<Self> {
236 match byte {
237 b'M' => Ok(StatusCode::Modified),
238 b'T' => Ok(StatusCode::TypeChanged),
239 b'A' => Ok(StatusCode::Added),
240 b'D' => Ok(StatusCode::Deleted),
241 b'R' => Ok(StatusCode::Renamed),
242 b'C' => Ok(StatusCode::Copied),
243 b' ' => Ok(StatusCode::Unmodified),
244 _ => anyhow::bail!("Invalid status code: {byte}"),
245 }
246 }
247
248 fn to_summary(self) -> TrackedSummary {
249 match self {
250 StatusCode::Modified | StatusCode::TypeChanged => TrackedSummary {
251 modified: 1,
252 ..TrackedSummary::UNCHANGED
253 },
254 StatusCode::Added => TrackedSummary {
255 added: 1,
256 ..TrackedSummary::UNCHANGED
257 },
258 StatusCode::Deleted => TrackedSummary {
259 deleted: 1,
260 ..TrackedSummary::UNCHANGED
261 },
262 StatusCode::Renamed | StatusCode::Copied | StatusCode::Unmodified => {
263 TrackedSummary::UNCHANGED
264 }
265 }
266 }
267
268 pub fn index(self) -> FileStatus {
269 FileStatus::Tracked(TrackedStatus {
270 index_status: self,
271 worktree_status: StatusCode::Unmodified,
272 })
273 }
274
275 pub fn worktree(self) -> FileStatus {
276 FileStatus::Tracked(TrackedStatus {
277 index_status: StatusCode::Unmodified,
278 worktree_status: self,
279 })
280 }
281}
282
283impl UnmergedStatusCode {
284 fn from_byte(byte: u8) -> anyhow::Result<Self> {
285 match byte {
286 b'A' => Ok(UnmergedStatusCode::Added),
287 b'D' => Ok(UnmergedStatusCode::Deleted),
288 b'U' => Ok(UnmergedStatusCode::Updated),
289 _ => anyhow::bail!("Invalid unmerged status code: {byte}"),
290 }
291 }
292}
293
294#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
295pub struct TrackedSummary {
296 pub added: usize,
297 pub modified: usize,
298 pub deleted: usize,
299}
300
301impl TrackedSummary {
302 pub const UNCHANGED: Self = Self {
303 added: 0,
304 modified: 0,
305 deleted: 0,
306 };
307
308 pub const ADDED: Self = Self {
309 added: 1,
310 modified: 0,
311 deleted: 0,
312 };
313
314 pub const MODIFIED: Self = Self {
315 added: 0,
316 modified: 1,
317 deleted: 0,
318 };
319
320 pub const DELETED: Self = Self {
321 added: 0,
322 modified: 0,
323 deleted: 1,
324 };
325}
326
327impl std::ops::AddAssign for TrackedSummary {
328 fn add_assign(&mut self, rhs: Self) {
329 self.added += rhs.added;
330 self.modified += rhs.modified;
331 self.deleted += rhs.deleted;
332 }
333}
334
335impl std::ops::Add for TrackedSummary {
336 type Output = Self;
337
338 fn add(self, rhs: Self) -> Self::Output {
339 TrackedSummary {
340 added: self.added + rhs.added,
341 modified: self.modified + rhs.modified,
342 deleted: self.deleted + rhs.deleted,
343 }
344 }
345}
346
347impl std::ops::Sub for TrackedSummary {
348 type Output = Self;
349
350 fn sub(self, rhs: Self) -> Self::Output {
351 TrackedSummary {
352 added: self.added - rhs.added,
353 modified: self.modified - rhs.modified,
354 deleted: self.deleted - rhs.deleted,
355 }
356 }
357}
358
359#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)]
360pub struct GitSummary {
361 pub index: TrackedSummary,
362 pub worktree: TrackedSummary,
363 pub conflict: usize,
364 pub untracked: usize,
365 pub count: usize,
366}
367
368impl GitSummary {
369 pub const CONFLICT: Self = Self {
370 conflict: 1,
371 count: 1,
372 ..Self::UNCHANGED
373 };
374
375 pub const UNTRACKED: Self = Self {
376 untracked: 1,
377 count: 1,
378 ..Self::UNCHANGED
379 };
380
381 pub const UNCHANGED: Self = Self {
382 index: TrackedSummary::UNCHANGED,
383 worktree: TrackedSummary::UNCHANGED,
384 conflict: 0,
385 untracked: 0,
386 count: 0,
387 };
388}
389
390impl From<FileStatus> for GitSummary {
391 fn from(status: FileStatus) -> Self {
392 status.summary()
393 }
394}
395
396impl sum_tree::Summary for GitSummary {
397 type Context = ();
398
399 fn zero(_: &Self::Context) -> Self {
400 Default::default()
401 }
402
403 fn add_summary(&mut self, rhs: &Self, _: &Self::Context) {
404 *self += *rhs;
405 }
406}
407
408impl std::ops::Add<Self> for GitSummary {
409 type Output = Self;
410
411 fn add(mut self, rhs: Self) -> Self {
412 self += rhs;
413 self
414 }
415}
416
417impl std::ops::AddAssign for GitSummary {
418 fn add_assign(&mut self, rhs: Self) {
419 self.index += rhs.index;
420 self.worktree += rhs.worktree;
421 self.conflict += rhs.conflict;
422 self.untracked += rhs.untracked;
423 self.count += rhs.count;
424 }
425}
426
427impl std::ops::Sub for GitSummary {
428 type Output = GitSummary;
429
430 fn sub(self, rhs: Self) -> Self::Output {
431 GitSummary {
432 index: self.index - rhs.index,
433 worktree: self.worktree - rhs.worktree,
434 conflict: self.conflict - rhs.conflict,
435 untracked: self.untracked - rhs.untracked,
436 count: self.count - rhs.count,
437 }
438 }
439}
440
441#[derive(Clone, Debug)]
442pub struct GitStatus {
443 pub entries: Arc<[(RepoPath, FileStatus)]>,
444}
445
446impl FromStr for GitStatus {
447 type Err = anyhow::Error;
448
449 fn from_str(s: &str) -> Result<Self> {
450 let mut entries = s
451 .split('\0')
452 .filter_map(|entry| {
453 let sep = entry.get(2..3)?;
454 if sep != " " {
455 return None;
456 };
457 let path = &entry[3..];
458 // The git status output includes untracked directories as well as untracked files.
459 // We do our own processing to compute the "summary" status of each directory,
460 // so just skip any directories in the output, since they'll otherwise interfere
461 // with our handling of nested repositories.
462 if path.ends_with('/') {
463 return None;
464 }
465 let status = entry.as_bytes()[0..2].try_into().unwrap();
466 let status = FileStatus::from_bytes(status).log_err()?;
467 let path = RepoPath(Path::new(path).into());
468 Some((path, status))
469 })
470 .collect::<Vec<_>>();
471 entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
472 // When a file exists in HEAD, is deleted in the index, and exists again in the working copy,
473 // git produces two lines for it, one reading `D ` (deleted in index, unmodified in working copy)
474 // and the other reading `??` (untracked). Merge these two into the equivalent of `DA`.
475 entries.dedup_by(|(a, a_status), (b, b_status)| {
476 const INDEX_DELETED: FileStatus = FileStatus::index(StatusCode::Deleted);
477 if a.ne(&b) {
478 return false;
479 }
480 match (*a_status, *b_status) {
481 (INDEX_DELETED, FileStatus::Untracked) | (FileStatus::Untracked, INDEX_DELETED) => {
482 *b_status = TrackedStatus {
483 index_status: StatusCode::Deleted,
484 worktree_status: StatusCode::Added,
485 }
486 .into();
487 }
488 _ => panic!("Unexpected duplicated status entries: {a_status:?} and {b_status:?}"),
489 }
490 true
491 });
492 Ok(Self {
493 entries: entries.into(),
494 })
495 }
496}
497
498impl Default for GitStatus {
499 fn default() -> Self {
500 Self {
501 entries: Arc::new([]),
502 }
503 }
504}