1use crate::context::{ChangeType, RecentCommit, StagedFile};
2use crate::git::utils::{is_binary_diff, should_exclude_file};
3use crate::log_debug;
4use anyhow::{Context, Result};
5use git2::{DiffOptions, Repository, StatusOptions};
6use std::fs;
7use std::path::Path;
8
9#[derive(Debug)]
11pub struct RepoFilesInfo {
12 pub branch: String,
13 pub recent_commits: Vec<RecentCommit>,
14 pub staged_files: Vec<StagedFile>,
15 pub file_paths: Vec<String>,
16}
17
18pub fn get_file_statuses(repo: &Repository) -> Result<Vec<StagedFile>> {
24 log_debug!("Getting file statuses");
25 let mut staged_files = Vec::new();
26
27 let mut opts = StatusOptions::new();
28 opts.include_untracked(true);
29 let statuses = repo.statuses(Some(&mut opts))?;
30
31 for entry in statuses.iter() {
32 let path = entry.path().context("Could not get path")?;
33 let status = entry.status();
34
35 if status.is_index_new() || status.is_index_modified() || status.is_index_deleted() {
36 let change_type = if status.is_index_new() {
37 ChangeType::Added
38 } else if status.is_index_modified() {
39 ChangeType::Modified
40 } else {
41 ChangeType::Deleted
42 };
43
44 let should_exclude = should_exclude_file(path);
45 let diff = if should_exclude {
46 String::from("[Content excluded]")
47 } else {
48 get_diff_for_file(repo, path)?
49 };
50
51 let content =
52 if should_exclude || change_type != ChangeType::Modified || is_binary_diff(&diff) {
53 None
54 } else {
55 let path_obj = Path::new(path);
56 if path_obj.exists() {
57 Some(fs::read_to_string(path_obj)?)
58 } else {
59 None
60 }
61 };
62
63 staged_files.push(StagedFile {
64 path: path.to_string(),
65 change_type,
66 diff,
67 content,
68 content_excluded: should_exclude,
69 });
70 }
71 }
72
73 log_debug!("Found {} staged files", staged_files.len());
74 Ok(staged_files)
75}
76
77pub fn get_diff_for_file(repo: &Repository, path: &str) -> Result<String> {
88 log_debug!("Getting diff for file: {}", path);
89 let mut diff_options = DiffOptions::new();
90 diff_options.pathspec(path);
91
92 let tree = Some(repo.head()?.peel_to_tree()?);
93
94 let diff = repo.diff_tree_to_workdir_with_index(tree.as_ref(), Some(&mut diff_options))?;
95
96 let mut diff_string = String::new();
97 diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
98 let origin = match line.origin() {
99 '+' | '-' | ' ' => line.origin(),
100 _ => ' ',
101 };
102 diff_string.push(origin);
103 diff_string.push_str(&String::from_utf8_lossy(line.content()));
104 true
105 })?;
106
107 if is_binary_diff(&diff_string) {
108 Ok("[Binary file changed]".to_string())
109 } else {
110 log_debug!("Generated diff for {} ({} bytes)", path, diff_string.len());
111 Ok(diff_string)
112 }
113}
114
115pub fn get_unstaged_file_statuses(repo: &Repository) -> Result<Vec<StagedFile>> {
121 log_debug!("Getting unstaged file statuses");
122 let mut unstaged_files = Vec::new();
123
124 let mut opts = StatusOptions::new();
125 opts.include_untracked(true);
126 let statuses = repo.statuses(Some(&mut opts))?;
127
128 for entry in statuses.iter() {
129 let path = entry.path().context("Could not get path")?;
130 let status = entry.status();
131
132 if status.is_wt_new() || status.is_wt_modified() || status.is_wt_deleted() {
134 let change_type = if status.is_wt_new() {
135 ChangeType::Added
136 } else if status.is_wt_modified() {
137 ChangeType::Modified
138 } else {
139 ChangeType::Deleted
140 };
141
142 let should_exclude = should_exclude_file(path);
143 let diff = if should_exclude {
144 String::from("[Content excluded]")
145 } else {
146 get_diff_for_unstaged_file(repo, path)?
147 };
148
149 let content =
150 if should_exclude || change_type != ChangeType::Modified || is_binary_diff(&diff) {
151 None
152 } else {
153 let path_obj = Path::new(path);
154 if path_obj.exists() {
155 Some(fs::read_to_string(path_obj)?)
156 } else {
157 None
158 }
159 };
160
161 unstaged_files.push(StagedFile {
162 path: path.to_string(),
163 change_type,
164 diff,
165 content,
166 content_excluded: should_exclude,
167 });
168 }
169 }
170
171 log_debug!("Found {} unstaged files", unstaged_files.len());
172 Ok(unstaged_files)
173}
174
175pub fn get_diff_for_unstaged_file(repo: &Repository, path: &str) -> Result<String> {
186 log_debug!("Getting unstaged diff for file: {}", path);
187 let mut diff_options = DiffOptions::new();
188 diff_options.pathspec(path);
189
190 let diff = repo.diff_index_to_workdir(None, Some(&mut diff_options))?;
192
193 let mut diff_string = String::new();
194 diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
195 let origin = match line.origin() {
196 '+' | '-' | ' ' => line.origin(),
197 _ => ' ',
198 };
199 diff_string.push(origin);
200 diff_string.push_str(&String::from_utf8_lossy(line.content()));
201 true
202 })?;
203
204 if is_binary_diff(&diff_string) {
205 Ok("[Binary file changed]".to_string())
206 } else {
207 log_debug!(
208 "Generated unstaged diff for {} ({} bytes)",
209 path,
210 diff_string.len()
211 );
212 Ok(diff_string)
213 }
214}
215
216pub fn get_untracked_files(repo: &Repository) -> Result<Vec<String>> {
222 log_debug!("Getting untracked files");
223 let mut untracked = Vec::new();
224
225 let mut opts = StatusOptions::new();
226 opts.include_untracked(true);
227 opts.exclude_submodules(true);
228 let statuses = repo.statuses(Some(&mut opts))?;
229
230 for entry in statuses.iter() {
231 let status = entry.status();
232 if status.is_wt_new()
234 && !status.is_index_new()
235 && let Some(path) = entry.path()
236 {
237 untracked.push(path.to_string());
238 }
239 }
240
241 log_debug!("Found {} untracked files", untracked.len());
242 Ok(untracked)
243}
244
245pub fn get_all_tracked_files(repo: &Repository) -> Result<Vec<String>> {
255 log_debug!("Getting all tracked files");
256 let mut files = std::collections::HashSet::new();
257
258 if let Ok(head) = repo.head()
260 && let Ok(tree) = head.peel_to_tree()
261 {
262 tree.walk(git2::TreeWalkMode::PreOrder, |dir, entry| {
263 if entry.kind() == Some(git2::ObjectType::Blob) {
264 let path = if dir.is_empty() {
265 entry.name().unwrap_or("").to_string()
266 } else {
267 format!("{}{}", dir, entry.name().unwrap_or(""))
268 };
269 if !path.is_empty() {
270 files.insert(path);
271 }
272 }
273 git2::TreeWalkResult::Ok
274 })?;
275 }
276
277 let index = repo.index()?;
279 for entry in index.iter() {
280 let path = String::from_utf8_lossy(&entry.path).to_string();
281 files.insert(path);
282 }
283
284 let mut result: Vec<_> = files.into_iter().collect();
285 result.sort();
286
287 log_debug!("Found {} tracked files", result.len());
288 Ok(result)
289}
290
291pub fn get_ahead_behind(repo: &Repository) -> (usize, usize) {
297 log_debug!("Getting ahead/behind counts");
298
299 let Ok(head) = repo.head() else {
301 return (0, 0); };
303
304 let Some(branch_name) = head.shorthand() else {
305 return (0, 0);
306 };
307
308 let Ok(branch) = repo.find_branch(branch_name, git2::BranchType::Local) else {
310 return (0, 0);
311 };
312
313 let Ok(upstream) = branch.upstream() else {
314 return (0, 0); };
316
317 let Some(local_oid) = head.target() else {
319 return (0, 0);
320 };
321
322 let Some(upstream_oid) = upstream.get().target() else {
323 return (0, 0);
324 };
325
326 match repo.graph_ahead_behind(local_oid, upstream_oid) {
328 Ok((ahead, behind)) => {
329 log_debug!("Branch is {} ahead, {} behind upstream", ahead, behind);
330 (ahead, behind)
331 }
332 Err(_) => (0, 0),
333 }
334}