1use crate::Result;
2use git2::{Repository, Status, StatusShow};
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum StatusKind {
8 Modified,
9 Added,
10 Deleted,
11 Renamed,
12 Untracked,
13 Ignored,
14 Conflicted,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct FileStatus {
19 pub path: String,
20 pub kind: StatusKind,
21 pub staged: bool,
22}
23
24#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25pub struct RepoStatus {
26 pub files: Vec<FileStatus>,
27 pub staged_count: usize,
28 pub modified_count: usize,
29 pub untracked_count: usize,
30}
31
32impl RepoStatus {
33 pub fn collect(repo: &Repository, _workdir: &Path) -> Result<Self> {
34 let mut files = Vec::new();
35 let mut staged_count = 0;
36 let mut modified_count = 0;
37 let mut untracked_count = 0;
38
39 let statuses = repo.statuses(Some(
40 git2::StatusOptions::new()
41 .show(StatusShow::IndexAndWorkdir)
42 .include_ignored(false)
43 .recurse_untracked_dirs(false),
44 ))?;
45
46 for status in statuses.iter() {
47 let path = status.path().unwrap_or("").to_string();
48 let status_flags = status.status();
49
50 let is_staged = status_flags.intersects(
51 Status::INDEX_NEW
52 | Status::INDEX_MODIFIED
53 | Status::INDEX_DELETED
54 | Status::INDEX_RENAMED
55 | Status::INDEX_TYPECHANGE,
56 );
57
58 let kind = if status_flags.intersects(Status::CONFLICTED) {
59 StatusKind::Conflicted
60 } else if status_flags.intersects(Status::WT_NEW) {
61 StatusKind::Untracked
62 } else if status_flags.intersects(Status::WT_MODIFIED) {
63 StatusKind::Modified
64 } else if status_flags.intersects(Status::WT_DELETED) {
65 StatusKind::Deleted
66 } else if status_flags.intersects(Status::WT_RENAMED) {
67 StatusKind::Renamed
68 } else if status_flags.intersects(Status::INDEX_NEW) {
69 StatusKind::Added
70 } else if status_flags.intersects(Status::INDEX_MODIFIED) {
71 StatusKind::Modified
72 } else if status_flags.intersects(Status::INDEX_DELETED) {
73 StatusKind::Deleted
74 } else if status_flags.intersects(Status::INDEX_RENAMED) {
75 StatusKind::Renamed
76 } else if status_flags.intersects(Status::INDEX_TYPECHANGE) {
77 StatusKind::Modified
78 } else {
79 continue;
80 };
81
82 if is_staged {
83 staged_count += 1;
84 }
85 match kind {
86 StatusKind::Modified
87 | StatusKind::Deleted
88 | StatusKind::Renamed
89 | StatusKind::Conflicted => modified_count += 1,
90 StatusKind::Added | StatusKind::Untracked => untracked_count += 1,
91 StatusKind::Ignored => {}
92 }
93
94 files.push(FileStatus {
95 path,
96 kind,
97 staged: is_staged,
98 });
99 }
100
101 Ok(Self {
102 files,
103 staged_count,
104 modified_count,
105 untracked_count,
106 })
107 }
108
109 pub fn is_clean(&self) -> bool {
110 self.files.is_empty()
111 }
112
113 pub fn has_staged(&self) -> bool {
114 self.staged_count > 0
115 }
116
117 pub fn has_unstaged(&self) -> bool {
118 self.modified_count > 0 || self.untracked_count > 0
119 }
120}