ai_code_buddy/core/
git.rs

1use anyhow::{anyhow, Result};
2use git2::{Repository, Status};
3
4pub struct GitAnalyzer {
5    repo: Repository,
6}
7
8impl GitAnalyzer {
9    pub fn new(repo_path: &str) -> Result<Self> {
10        let repo =
11            Repository::open(repo_path).map_err(|e| anyhow!("Failed to open repository: {}", e))?;
12
13        Ok(Self { repo })
14    }
15
16    pub fn get_changed_files(
17        &self,
18        source_branch: &str,
19        target_branch: &str,
20    ) -> Result<Vec<String>> {
21        let mut all_files = Vec::new();
22        let mut committed_files = std::collections::HashSet::new();
23
24        // Get committed changes between branches
25        if source_branch != target_branch {
26            let source_commit = self.get_commit(source_branch)?;
27            let target_commit = self.get_commit(target_branch)?;
28
29            let source_tree = source_commit.tree()?;
30            let target_tree = target_commit.tree()?;
31
32            let diff = self
33                .repo
34                .diff_tree_to_tree(Some(&source_tree), Some(&target_tree), None)?;
35
36            diff.foreach(
37                &mut |delta, _progress| {
38                    if let Some(file) = delta.new_file().path() {
39                        let file_path = file.to_string_lossy().to_string();
40                        committed_files.insert(file_path.clone());
41                        all_files.push(file_path);
42                    }
43                    true
44                },
45                None,
46                None,
47                None,
48            )?;
49        }
50
51        // Get uncommitted changes (staged and modified)
52        let uncommitted_files = self.get_uncommitted_files()?;
53        for file in uncommitted_files {
54            if !committed_files.contains(&file) {
55                all_files.push(file);
56            }
57        }
58
59        Ok(all_files)
60    }
61
62    pub fn get_uncommitted_files(&self) -> Result<Vec<String>> {
63        let mut files = Vec::new();
64        let statuses = self.repo.statuses(None)?;
65
66        for entry in statuses.iter() {
67            let status = entry.status();
68            let is_index_change = status.contains(Status::INDEX_NEW)
69                || status.contains(Status::INDEX_MODIFIED)
70                || status.contains(Status::INDEX_DELETED);
71            let is_worktree_change = status.contains(Status::WT_NEW)
72                || status.contains(Status::WT_MODIFIED)
73                || status.contains(Status::WT_DELETED);
74
75            if is_index_change || is_worktree_change {
76                if let Some(path) = entry.path() {
77                    files.push(path.to_string());
78                }
79            }
80        }
81
82        Ok(files)
83    }
84
85    pub fn get_file_status(&self, file_path: &str) -> Result<super::review::CommitStatus> {
86        let statuses = self.repo.statuses(None)?;
87
88        for entry in statuses.iter() {
89            if let Some(path) = entry.path() {
90                if path == file_path {
91                    let status = entry.status();
92
93                    if status.contains(Status::INDEX_NEW)
94                        || status.contains(Status::INDEX_MODIFIED)
95                        || status.contains(Status::INDEX_DELETED)
96                    {
97                        return Ok(super::review::CommitStatus::Staged);
98                    }
99
100                    if status.contains(Status::WT_NEW) {
101                        return Ok(super::review::CommitStatus::Untracked);
102                    }
103
104                    if status.contains(Status::WT_MODIFIED) || status.contains(Status::WT_DELETED) {
105                        return Ok(super::review::CommitStatus::Modified);
106                    }
107                }
108            }
109        }
110
111        // If not in status, assume it's committed
112        Ok(super::review::CommitStatus::Committed)
113    }
114
115    pub fn get_file_content(&self, file_path: &str, branch: &str) -> Result<String> {
116        // First check if file has uncommitted changes
117        let file_status = self.get_file_status(file_path)?;
118
119        match file_status {
120            super::review::CommitStatus::Untracked | super::review::CommitStatus::Modified => {
121                // Read from working directory
122                let full_path = self
123                    .repo
124                    .workdir()
125                    .ok_or_else(|| anyhow!("Repository has no working directory"))?
126                    .join(file_path);
127
128                std::fs::read_to_string(&full_path)
129                    .map_err(|e| anyhow!("Failed to read file from working directory: {}", e))
130            }
131            super::review::CommitStatus::Staged => {
132                // Try to read from index first, fall back to working directory
133                match self.get_file_content_from_index(file_path) {
134                    Ok(content) => Ok(content),
135                    Err(_) => {
136                        let full_path = self
137                            .repo
138                            .workdir()
139                            .ok_or_else(|| anyhow!("Repository has no working directory"))?
140                            .join(file_path);
141
142                        std::fs::read_to_string(&full_path).map_err(|e| {
143                            anyhow!("Failed to read file from working directory: {}", e)
144                        })
145                    }
146                }
147            }
148            super::review::CommitStatus::Committed => {
149                // Read from commit
150                let commit = self.get_commit(branch)?;
151                let tree = commit.tree()?;
152
153                let entry = tree.get_path(std::path::Path::new(file_path))?;
154                let object = self
155                    .repo
156                    .find_object(entry.id(), Some(git2::ObjectType::Blob))?;
157                let blob = object
158                    .as_blob()
159                    .ok_or_else(|| anyhow!("Object is not a blob"))?;
160
161                let content = std::str::from_utf8(blob.content())
162                    .map_err(|e| anyhow!("Invalid UTF-8 in file: {}", e))?;
163
164                Ok(content.to_string())
165            }
166        }
167    }
168
169    fn get_file_content_from_index(&self, file_path: &str) -> Result<String> {
170        let index = self.repo.index()?;
171        let entry = index
172            .get_path(std::path::Path::new(file_path), 0)
173            .ok_or_else(|| anyhow!("File not found in index"))?;
174
175        let object = self
176            .repo
177            .find_object(entry.id, Some(git2::ObjectType::Blob))?;
178        let blob = object
179            .as_blob()
180            .ok_or_else(|| anyhow!("Object is not a blob"))?;
181
182        let content = std::str::from_utf8(blob.content())
183            .map_err(|e| anyhow!("Invalid UTF-8 in file: {}", e))?;
184
185        Ok(content.to_string())
186    }
187
188    fn get_commit(&self, branch_name: &str) -> Result<git2::Commit<'_>> {
189        let reference = if branch_name == "HEAD" {
190            self.repo.head()?
191        } else {
192            self.repo
193                .find_reference(&format!("refs/heads/{branch_name}"))?
194        };
195
196        let oid = reference
197            .target()
198            .ok_or_else(|| anyhow!("Invalid reference"))?;
199
200        self.repo
201            .find_commit(oid)
202            .map_err(|e| anyhow!("Failed to find commit: {}", e))
203    }
204}