ai_code_buddy/core/
git.rs1use 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 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 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 Ok(super::review::CommitStatus::Committed)
113 }
114
115 pub fn get_file_content(&self, file_path: &str, branch: &str) -> Result<String> {
116 let file_status = self.get_file_status(file_path)?;
118
119 match file_status {
120 super::review::CommitStatus::Untracked | super::review::CommitStatus::Modified => {
121 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 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 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}