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 get_index_content_for_file(repo, path)?
56 };
57
58 staged_files.push(StagedFile {
59 path: path.to_string(),
60 change_type,
61 diff,
62 content,
63 content_excluded: should_exclude,
64 });
65 }
66 }
67
68 log_debug!("Found {} staged files", staged_files.len());
69 Ok(staged_files)
70}
71
72pub fn get_diff_for_file(repo: &Repository, path: &str) -> Result<String> {
83 log_debug!("Getting diff for file: {}", path);
84 let mut diff_options = DiffOptions::new();
85 diff_options.pathspec(path);
86
87 let tree = repo.head().ok().and_then(|head| head.peel_to_tree().ok());
88 let index = repo.index()?;
89 let diff = repo.diff_tree_to_index(tree.as_ref(), Some(&index), Some(&mut diff_options))?;
90
91 let mut diff_string = String::new();
92 diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
93 let origin = match line.origin() {
94 '+' | '-' | ' ' => line.origin(),
95 _ => ' ',
96 };
97 diff_string.push(origin);
98 diff_string.push_str(&String::from_utf8_lossy(line.content()));
99 true
100 })?;
101
102 if is_binary_diff(&diff_string) {
103 Ok("[Binary file changed]".to_string())
104 } else {
105 log_debug!("Generated diff for {} ({} bytes)", path, diff_string.len());
106 Ok(diff_string)
107 }
108}
109
110fn get_index_content_for_file(repo: &Repository, path: &str) -> Result<Option<String>> {
111 let index = repo.index()?;
112 let Some(entry) = index.get_path(Path::new(path), 0) else {
113 return Ok(None);
114 };
115
116 let blob = repo.find_blob(entry.id)?;
117 match std::str::from_utf8(blob.content()) {
118 Ok(content) => Ok(Some(content.to_string())),
119 Err(_) => Ok(None),
120 }
121}
122
123pub fn get_unstaged_file_statuses(repo: &Repository) -> Result<Vec<StagedFile>> {
129 log_debug!("Getting unstaged file statuses");
130 let mut unstaged_files = Vec::new();
131
132 let mut opts = StatusOptions::new();
133 opts.include_untracked(true);
134 let statuses = repo.statuses(Some(&mut opts))?;
135
136 for entry in statuses.iter() {
137 let path = entry.path().context("Could not get path")?;
138 let status = entry.status();
139
140 if status.is_wt_new() || status.is_wt_modified() || status.is_wt_deleted() {
142 let change_type = if status.is_wt_new() {
143 ChangeType::Added
144 } else if status.is_wt_modified() {
145 ChangeType::Modified
146 } else {
147 ChangeType::Deleted
148 };
149
150 let should_exclude = should_exclude_file(path);
151 let diff = if should_exclude {
152 String::from("[Content excluded]")
153 } else {
154 get_diff_for_unstaged_file(repo, path)?
155 };
156
157 let content =
158 if should_exclude || change_type != ChangeType::Modified || is_binary_diff(&diff) {
159 None
160 } else {
161 let path_obj = Path::new(path);
162 if path_obj.exists() {
163 Some(fs::read_to_string(path_obj)?)
164 } else {
165 None
166 }
167 };
168
169 unstaged_files.push(StagedFile {
170 path: path.to_string(),
171 change_type,
172 diff,
173 content,
174 content_excluded: should_exclude,
175 });
176 }
177 }
178
179 log_debug!("Found {} unstaged files", unstaged_files.len());
180 Ok(unstaged_files)
181}
182
183pub fn get_diff_for_unstaged_file(repo: &Repository, path: &str) -> Result<String> {
194 log_debug!("Getting unstaged diff for file: {}", path);
195 let mut diff_options = DiffOptions::new();
196 diff_options.pathspec(path);
197
198 let diff = repo.diff_index_to_workdir(None, Some(&mut diff_options))?;
200
201 let mut diff_string = String::new();
202 diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
203 let origin = match line.origin() {
204 '+' | '-' | ' ' => line.origin(),
205 _ => ' ',
206 };
207 diff_string.push(origin);
208 diff_string.push_str(&String::from_utf8_lossy(line.content()));
209 true
210 })?;
211
212 if is_binary_diff(&diff_string) {
213 Ok("[Binary file changed]".to_string())
214 } else {
215 log_debug!(
216 "Generated unstaged diff for {} ({} bytes)",
217 path,
218 diff_string.len()
219 );
220 Ok(diff_string)
221 }
222}
223
224pub fn get_untracked_files(repo: &Repository) -> Result<Vec<String>> {
230 log_debug!("Getting untracked files");
231 let mut untracked = Vec::new();
232
233 let mut opts = StatusOptions::new();
234 opts.include_untracked(true);
235 opts.exclude_submodules(true);
236 let statuses = repo.statuses(Some(&mut opts))?;
237
238 for entry in statuses.iter() {
239 let status = entry.status();
240 if status.is_wt_new()
242 && !status.is_index_new()
243 && let Some(path) = entry.path()
244 {
245 untracked.push(path.to_string());
246 }
247 }
248
249 log_debug!("Found {} untracked files", untracked.len());
250 Ok(untracked)
251}
252
253pub fn get_all_tracked_files(repo: &Repository) -> Result<Vec<String>> {
263 log_debug!("Getting all tracked files");
264 let mut files = std::collections::HashSet::new();
265
266 if let Ok(head) = repo.head()
268 && let Ok(tree) = head.peel_to_tree()
269 {
270 tree.walk(git2::TreeWalkMode::PreOrder, |dir, entry| {
271 if entry.kind() == Some(git2::ObjectType::Blob) {
272 let path = if dir.is_empty() {
273 entry.name().unwrap_or("").to_string()
274 } else {
275 format!("{}{}", dir, entry.name().unwrap_or(""))
276 };
277 if !path.is_empty() {
278 files.insert(path);
279 }
280 }
281 git2::TreeWalkResult::Ok
282 })?;
283 }
284
285 let index = repo.index()?;
287 for entry in index.iter() {
288 let path = String::from_utf8_lossy(&entry.path).to_string();
289 files.insert(path);
290 }
291
292 let mut result: Vec<_> = files.into_iter().collect();
293 result.sort();
294
295 log_debug!("Found {} tracked files", result.len());
296 Ok(result)
297}
298
299pub fn get_ahead_behind(repo: &Repository) -> (usize, usize) {
305 log_debug!("Getting ahead/behind counts");
306
307 let Ok(head) = repo.head() else {
309 return (0, 0); };
311
312 let Some(branch_name) = head.shorthand() else {
313 return (0, 0);
314 };
315
316 let Ok(branch) = repo.find_branch(branch_name, git2::BranchType::Local) else {
318 return (0, 0);
319 };
320
321 let Ok(upstream) = branch.upstream() else {
322 return (0, 0); };
324
325 let Some(local_oid) = head.target() else {
327 return (0, 0);
328 };
329
330 let Some(upstream_oid) = upstream.get().target() else {
331 return (0, 0);
332 };
333
334 match repo.graph_ahead_behind(local_oid, upstream_oid) {
336 Ok((ahead, behind)) => {
337 log_debug!("Branch is {} ahead, {} behind upstream", ahead, behind);
338 (ahead, behind)
339 }
340 Err(_) => (0, 0),
341 }
342}