1use crate::core::{find_repo_root, get_current_branch};
2use crate::response::StatusResponse;
3use crate::storage::Index;
4use std::collections::HashSet;
5use std::fs;
6use std::path::Path;
7use walkdir::WalkDir;
8
9const MAX_WALK_FILES: usize = 50_000;
11
12pub fn execute() -> Result<StatusResponse, crate::errors::LitError> {
13 let repo_root = find_repo_root()?;
14 let index = Index::load(&repo_root)?;
15
16 let branch = get_current_branch(&repo_root).ok();
17 let ignore_dirs = load_ignore_dirs(&repo_root);
18
19 let staged_set: HashSet<String> = index.entries.keys().cloned().collect();
21 let mut modified = Vec::new();
22 for file in &staged_set {
23 if is_modified(&repo_root, file, &index)? {
24 modified.push(file.clone());
25 }
26 }
27
28 let cwd = std::env::current_dir().unwrap_or_else(|_| repo_root.clone());
32 let walk_root = if cwd.starts_with(&repo_root) {
33 &cwd
34 } else {
35 &repo_root
36 };
37
38 let mut untracked = Vec::new();
39 let mut visited: usize = 0;
40 for entry in WalkDir::new(walk_root)
41 .into_iter()
42 .filter_entry(|e| !should_skip_entry(e, &ignore_dirs))
43 {
44 let entry = match entry {
45 Ok(e) => e,
46 Err(_) => continue,
47 };
48
49 if entry.file_type().is_file() {
50 visited += 1;
51 if visited > MAX_WALK_FILES {
52 break;
53 }
54 if let Ok(rel_path) = entry.path().strip_prefix(&repo_root) {
55 let path_str = rel_path.to_string_lossy().replace('\\', "/");
56 if !staged_set.contains(&path_str) {
57 untracked.push(path_str);
58 }
59 }
60 }
61 }
62
63 modified.sort();
64 untracked.sort();
65 let mut staged: Vec<String> = staged_set.into_iter().collect();
66 staged.sort();
67
68 let clean = staged.is_empty() && modified.is_empty() && untracked.is_empty();
69
70 let head = crate::core::read_head(&repo_root).ok();
71
72 Ok(StatusResponse {
73 branch,
74 head,
75 staged,
76 modified,
77 untracked,
78 clean,
79 })
80}
81
82fn load_ignore_dirs(repo_root: &Path) -> HashSet<String> {
84 let mut dirs: HashSet<String> = [
85 "target",
87 "node_modules",
88 "venv",
89 "__pycache__",
90 "dist",
91 "out",
92 "bin",
93 "obj",
94 ".cargo",
96 ".rustup",
97 ".npm",
98 ".nvm",
99 ".pyenv",
100 ".conda",
101 ".local",
102 ".cache",
103 ".thumbnails",
104 "AppData",
106 "Application Data",
107 "Library",
108 "Caches",
109 "OneDrive",
111 "Dropbox",
112 "Google Drive",
113 "Documents",
115 "Downloads",
116 "Desktop",
117 "Music",
118 "Pictures",
119 "Videos",
120 "Contacts",
121 "Favorites",
122 "Links",
123 "Saved Games",
124 "Searches",
125 "3D Objects",
126 "scoop",
127 ]
128 .iter()
129 .map(|s| s.to_string())
130 .collect();
131
132 let ignore_path = repo_root.join(".litignore");
134 if let Ok(content) = fs::read_to_string(&ignore_path) {
135 for line in content.lines() {
136 let line = line.trim().trim_end_matches('/');
137 if !line.is_empty() && !line.starts_with('#') {
138 dirs.insert(line.to_string());
139 }
140 }
141 }
142
143 dirs
144}
145
146fn should_skip_entry(entry: &walkdir::DirEntry, ignore_dirs: &HashSet<String>) -> bool {
148 let name = entry.file_name().to_string_lossy();
149
150 if name.starts_with('.') {
152 return true;
153 }
154
155 entry.file_type().is_dir() && ignore_dirs.contains(name.as_ref())
157}
158
159fn is_modified(
160 repo_root: &Path,
161 file: &str,
162 index: &Index,
163) -> Result<bool, crate::errors::LitError> {
164 let file_path = repo_root.join(file);
165
166 if let Some(entry) = index.entries.get(file) {
167 if !file_path.exists() {
169 return Ok(true);
170 }
171
172 let current_content =
173 fs::read(&file_path).map_err(|e| format!("Failed to read file {}: {}", file, e))?;
174
175 use crate::core::{Blob, Object};
176 let blob = Blob::new(current_content);
177 let object = Object::Blob(blob);
178 let current_hash = object.hash();
179
180 Ok(current_hash.to_string() != entry.hash)
181 } else {
182 Ok(false)
183 }
184}