git_iris/git/
files.rs

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/// Collects repository information about files and branches
11#[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
19/// Retrieves the status of files in the repository.
20///
21/// # Returns
22///
23/// A Result containing a Vec of `StagedFile` objects or an error.
24pub 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
95/// Retrieves the diff for a specific file.
96///
97/// # Arguments
98///
99/// * `repo` - The git repository
100/// * `path` - The path of the file to get the diff for.
101///
102/// # Returns
103///
104/// A Result containing the diff as a String or an error.
105pub 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
133/// Gets unstaged file changes from the repository
134///
135/// # Returns
136///
137/// A Result containing a Vec of `StagedFile` objects for unstaged changes or an error.
138pub 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        // Look for changes in the working directory (unstaged)
151        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
210/// Gets the diff for an unstaged file
211///
212/// # Arguments
213///
214/// * `repo` - The git repository
215/// * `path` - The path of the file to get the diff for.
216///
217/// # Returns
218///
219/// A Result containing the diff as a String or an error.
220pub 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    // For unstaged changes, we compare the index (staged) to the working directory
226    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}