1use crate::context::{ChangeType, RecentCommit, StagedFile};
2use crate::file_analyzers::{self, should_exclude_file};
3use crate::git::utils::is_binary_diff;
4use crate::log_debug;
5use anyhow::{Context, Result};
6use git2::{DiffOptions, Repository, StatusOptions};
7use std::fs;
8use std::path::Path;
9
10#[derive(Debug)]
12pub struct RepoFilesInfo {
13 pub branch: String,
14 pub recent_commits: Vec<RecentCommit>,
15 pub staged_files: Vec<StagedFile>,
16 pub file_paths: Vec<String>,
17}
18
19pub fn get_file_statuses(repo: &Repository) -> Result<Vec<StagedFile>> {
25 log_debug!("Getting file statuses");
26 let mut staged_files = Vec::new();
27
28 let mut opts = StatusOptions::new();
29 opts.include_untracked(true);
30 let statuses = repo.statuses(Some(&mut opts))?;
31
32 for entry in statuses.iter() {
33 let path = entry.path().context("Could not get path")?;
34 let status = entry.status();
35
36 if status.is_index_new() || status.is_index_modified() || status.is_index_deleted() {
37 let change_type = if status.is_index_new() {
38 ChangeType::Added
39 } else if status.is_index_modified() {
40 ChangeType::Modified
41 } else {
42 ChangeType::Deleted
43 };
44
45 let should_exclude = should_exclude_file(path);
46 let diff = if should_exclude {
47 String::from("[Content excluded]")
48 } else {
49 get_diff_for_file(repo, path)?
50 };
51
52 let content =
53 if should_exclude || change_type != ChangeType::Modified || is_binary_diff(&diff) {
54 None
55 } else {
56 let path_obj = Path::new(path);
57 if path_obj.exists() {
58 Some(fs::read_to_string(path_obj)?)
59 } else {
60 None
61 }
62 };
63
64 let analyzer = file_analyzers::get_analyzer(path);
65 let staged_file = StagedFile {
66 path: path.to_string(),
67 change_type: change_type.clone(),
68 diff: diff.clone(),
69 analysis: Vec::new(),
70 content: content.clone(),
71 content_excluded: should_exclude,
72 };
73
74 let analysis = if should_exclude {
75 vec!["[Analysis excluded]".to_string()]
76 } else {
77 analyzer.analyze(path, &staged_file)
78 };
79
80 staged_files.push(StagedFile {
81 path: path.to_string(),
82 change_type,
83 diff,
84 analysis,
85 content,
86 content_excluded: should_exclude,
87 });
88 }
89 }
90
91 log_debug!("Found {} staged files", staged_files.len());
92 Ok(staged_files)
93}
94
95pub fn get_diff_for_file(repo: &Repository, path: &str) -> Result<String> {
106 log_debug!("Getting diff for file: {}", path);
107 let mut diff_options = DiffOptions::new();
108 diff_options.pathspec(path);
109
110 let tree = Some(repo.head()?.peel_to_tree()?);
111
112 let diff = repo.diff_tree_to_workdir_with_index(tree.as_ref(), Some(&mut diff_options))?;
113
114 let mut diff_string = String::new();
115 diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
116 let origin = match line.origin() {
117 '+' | '-' | ' ' => line.origin(),
118 _ => ' ',
119 };
120 diff_string.push(origin);
121 diff_string.push_str(&String::from_utf8_lossy(line.content()));
122 true
123 })?;
124
125 if is_binary_diff(&diff_string) {
126 Ok("[Binary file changed]".to_string())
127 } else {
128 log_debug!("Generated diff for {} ({} bytes)", path, diff_string.len());
129 Ok(diff_string)
130 }
131}
132
133pub fn get_unstaged_file_statuses(repo: &Repository) -> Result<Vec<StagedFile>> {
139 log_debug!("Getting unstaged file statuses");
140 let mut unstaged_files = Vec::new();
141
142 let mut opts = StatusOptions::new();
143 opts.include_untracked(true);
144 let statuses = repo.statuses(Some(&mut opts))?;
145
146 for entry in statuses.iter() {
147 let path = entry.path().context("Could not get path")?;
148 let status = entry.status();
149
150 if status.is_wt_new() || status.is_wt_modified() || status.is_wt_deleted() {
152 let change_type = if status.is_wt_new() {
153 ChangeType::Added
154 } else if status.is_wt_modified() {
155 ChangeType::Modified
156 } else {
157 ChangeType::Deleted
158 };
159
160 let should_exclude = should_exclude_file(path);
161 let diff = if should_exclude {
162 String::from("[Content excluded]")
163 } else {
164 get_diff_for_unstaged_file(repo, path)?
165 };
166
167 let content =
168 if should_exclude || change_type != ChangeType::Modified || is_binary_diff(&diff) {
169 None
170 } else {
171 let path_obj = Path::new(path);
172 if path_obj.exists() {
173 Some(fs::read_to_string(path_obj)?)
174 } else {
175 None
176 }
177 };
178
179 let analyzer = file_analyzers::get_analyzer(path);
180 let unstaged_file = StagedFile {
181 path: path.to_string(),
182 change_type: change_type.clone(),
183 diff: diff.clone(),
184 analysis: Vec::new(),
185 content: content.clone(),
186 content_excluded: should_exclude,
187 };
188
189 let analysis = if should_exclude {
190 vec!["[Analysis excluded]".to_string()]
191 } else {
192 analyzer.analyze(path, &unstaged_file)
193 };
194
195 unstaged_files.push(StagedFile {
196 path: path.to_string(),
197 change_type,
198 diff,
199 analysis,
200 content,
201 content_excluded: should_exclude,
202 });
203 }
204 }
205
206 log_debug!("Found {} unstaged files", unstaged_files.len());
207 Ok(unstaged_files)
208}
209
210pub fn get_diff_for_unstaged_file(repo: &Repository, path: &str) -> Result<String> {
221 log_debug!("Getting unstaged diff for file: {}", path);
222 let mut diff_options = DiffOptions::new();
223 diff_options.pathspec(path);
224
225 let diff = repo.diff_index_to_workdir(None, Some(&mut diff_options))?;
227
228 let mut diff_string = String::new();
229 diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
230 let origin = match line.origin() {
231 '+' | '-' | ' ' => line.origin(),
232 _ => ' ',
233 };
234 diff_string.push(origin);
235 diff_string.push_str(&String::from_utf8_lossy(line.content()));
236 true
237 })?;
238
239 if is_binary_diff(&diff_string) {
240 Ok("[Binary file changed]".to_string())
241 } else {
242 log_debug!(
243 "Generated unstaged diff for {} ({} bytes)",
244 path,
245 diff_string.len()
246 );
247 Ok(diff_string)
248 }
249}