1use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11use gravityfile_core::GitStatus;
12
13#[derive(Debug, Default)]
15pub struct GitStatusCache {
16 statuses: HashMap<PathBuf, GitStatus>,
18 repo_root: Option<PathBuf>,
20}
21
22impl GitStatusCache {
23 pub fn new() -> Self {
25 Self::default()
26 }
27
28 #[cfg(feature = "git")]
37 pub fn initialize(&mut self, start_path: &Path) -> bool {
38 use git2::{Repository, StatusOptions};
39
40 let repo = match Repository::discover(start_path) {
42 Ok(repo) => repo,
43 Err(_) => return false,
44 };
45
46 let workdir = match repo.workdir() {
48 Some(dir) => dir.to_path_buf(),
49 None => return false, };
51
52 self.repo_root = Some(workdir.clone());
53
54 let pathspec_str = start_path
58 .strip_prefix(&workdir)
59 .ok()
60 .and_then(|rel| rel.to_str())
61 .map(|s| {
62 if s.is_empty() {
63 "**".to_string()
65 } else {
66 format!("{s}/**")
67 }
68 })
69 .unwrap_or_else(|| "**".to_string());
70
71 let mut opts = StatusOptions::new();
73 opts.include_untracked(true)
74 .include_ignored(true)
75 .recurse_untracked_dirs(true)
76 .include_unmodified(false)
77 .pathspec(&pathspec_str);
78
79 let statuses = match repo.statuses(Some(&mut opts)) {
80 Ok(s) => s,
81 Err(_) => return true, };
83
84 for entry in statuses.iter() {
86 let status = entry.status();
87 let path = match entry.path() {
88 Some(p) => workdir.join(p),
89 None => continue,
90 };
91
92 let git_status = if status.is_conflicted() {
93 GitStatus::Conflict
94 } else if status.is_index_new()
95 || status.is_index_modified()
96 || status.is_index_deleted()
97 || status.is_index_renamed()
98 || status.is_index_typechange()
99 {
100 GitStatus::Staged
101 } else if status.is_wt_new() {
102 GitStatus::Untracked
103 } else if status.is_wt_modified()
104 || status.is_wt_deleted()
105 || status.is_wt_renamed()
106 || status.is_wt_typechange()
107 {
108 GitStatus::Modified
109 } else if status.is_ignored() {
110 GitStatus::Ignored
111 } else {
112 continue; };
114
115 self.statuses.insert(path, git_status);
116 }
117
118 true
119 }
120
121 #[cfg(not(feature = "git"))]
123 pub fn initialize(&mut self, _start_path: &Path) -> bool {
124 false
125 }
126
127 pub fn get_status(&self, path: &Path) -> Option<GitStatus> {
129 self.statuses.get(path).copied()
130 }
131
132 pub fn is_in_repo(&self, path: &Path) -> bool {
134 if let Some(ref root) = self.repo_root {
135 path.starts_with(root)
136 } else {
137 false
138 }
139 }
140
141 pub fn repo_root(&self) -> Option<&Path> {
143 self.repo_root.as_deref()
144 }
145
146 pub fn len(&self) -> usize {
148 self.statuses.len()
149 }
150
151 pub fn is_empty(&self) -> bool {
153 self.statuses.is_empty()
154 }
155}
156
157pub fn apply_git_status(tree: &mut gravityfile_core::FileTree) {
159 let mut cache = GitStatusCache::new();
160 if !cache.initialize(&tree.root_path) {
161 return; }
163
164 apply_status_recursive(&mut tree.root, &tree.root_path, &cache);
165}
166
167fn apply_status_recursive(
169 node: &mut gravityfile_core::FileNode,
170 current_path: &Path,
171 cache: &GitStatusCache,
172) {
173 node.git_status = cache.get_status(current_path);
175
176 for child in &mut node.children {
178 let child_path = current_path.join(&*child.name);
179 apply_status_recursive(child, &child_path, cache);
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_empty_cache() {
189 let cache = GitStatusCache::new();
190 assert!(cache.is_empty());
191 assert_eq!(cache.len(), 0);
192 assert!(cache.repo_root().is_none());
193 }
194
195 #[test]
196 fn test_get_status_nonexistent() {
197 let cache = GitStatusCache::new();
198 assert!(cache.get_status(Path::new("/nonexistent")).is_none());
199 }
200}