1use crate::error::FileError;
4use crate::models::{FileOperation, GitStatus, OperationType};
5use git2::{Repository, Status, StatusOptions};
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone)]
10pub struct GitIntegration;
11
12impl GitIntegration {
13 pub fn new() -> Self {
15 GitIntegration
16 }
17
18 pub fn check_status(repo_path: &Path) -> Result<GitStatus, FileError> {
34 let repo = Repository::open(repo_path)
35 .map_err(|e| FileError::GitError(format!("Failed to open repository: {}", e)))?;
36
37 let branch = Self::get_current_branch_internal(&repo)?;
38
39 let mut status_opts = StatusOptions::new();
40 status_opts.include_untracked(true);
41 status_opts.include_ignored(false);
42
43 let statuses = repo
44 .statuses(Some(&mut status_opts))
45 .map_err(|e| FileError::GitError(format!("Failed to get status: {}", e)))?;
46
47 let mut modified = Vec::new();
48 let mut staged = Vec::new();
49 let mut untracked = Vec::new();
50
51 for entry in statuses.iter() {
52 let path = PathBuf::from(entry.path().unwrap_or(""));
53 let status = entry.status();
54
55 if status.contains(Status::WT_MODIFIED) || status.contains(Status::WT_DELETED) {
56 modified.push(path);
57 } else if status.contains(Status::INDEX_NEW)
58 || status.contains(Status::INDEX_MODIFIED)
59 || status.contains(Status::INDEX_DELETED)
60 {
61 staged.push(path);
62 } else if status.contains(Status::WT_NEW) {
63 untracked.push(path);
64 }
65 }
66
67 Ok(GitStatus {
68 branch,
69 modified,
70 staged,
71 untracked,
72 })
73 }
74
75 pub fn get_current_branch(repo_path: &Path) -> Result<String, FileError> {
89 let repo = Repository::open(repo_path)
90 .map_err(|e| FileError::GitError(format!("Failed to open repository: {}", e)))?;
91
92 Self::get_current_branch_internal(&repo)
93 }
94
95 fn get_current_branch_internal(repo: &Repository) -> Result<String, FileError> {
97 let head = repo
98 .head()
99 .map_err(|e| FileError::GitError(format!("Failed to get HEAD: {}", e)))?;
100
101 if let Some(name) = head.shorthand() {
102 Ok(name.to_string())
103 } else {
104 Ok("HEAD".to_string())
105 }
106 }
107
108 pub fn stage_files(repo_path: &Path, files: &[PathBuf]) -> Result<(), FileError> {
119 let repo = Repository::open(repo_path)
120 .map_err(|e| FileError::GitError(format!("Failed to open repository: {}", e)))?;
121
122 let mut index = repo
123 .index()
124 .map_err(|e| FileError::GitError(format!("Failed to get index: {}", e)))?;
125
126 for file in files {
127 index
128 .add_path(file)
129 .map_err(|e| FileError::GitError(format!("Failed to stage file: {}", e)))?;
130 }
131
132 index
133 .write()
134 .map_err(|e| FileError::GitError(format!("Failed to write index: {}", e)))?;
135
136 Ok(())
137 }
138
139 pub fn create_commit(repo_path: &Path, message: &str) -> Result<(), FileError> {
150 let repo = Repository::open(repo_path)
151 .map_err(|e| FileError::GitError(format!("Failed to open repository: {}", e)))?;
152
153 let signature = repo
154 .signature()
155 .map_err(|e| FileError::GitError(format!("Failed to get signature: {}", e)))?;
156
157 let tree_id = {
158 let mut index = repo
159 .index()
160 .map_err(|e| FileError::GitError(format!("Failed to get index: {}", e)))?;
161
162 index
163 .write_tree()
164 .map_err(|e| FileError::GitError(format!("Failed to write tree: {}", e)))?
165 };
166
167 let tree = repo
168 .find_tree(tree_id)
169 .map_err(|e| FileError::GitError(format!("Failed to find tree: {}", e)))?;
170
171 let parent_commit = repo.head().ok().and_then(|head| head.peel_to_commit().ok());
172
173 let parents = if let Some(parent) = parent_commit {
174 vec![parent]
175 } else {
176 vec![]
177 };
178
179 let parent_refs: Vec<&git2::Commit> = parents.iter().collect();
180
181 repo.commit(
182 Some("HEAD"),
183 &signature,
184 &signature,
185 message,
186 &tree,
187 &parent_refs,
188 )
189 .map_err(|e| FileError::GitError(format!("Failed to create commit: {}", e)))?;
190
191 Ok(())
192 }
193
194 pub fn review_changes(repo_path: &Path) -> Result<Vec<crate::models::FileDiff>, FileError> {
210 let repo = Repository::open(repo_path)
211 .map_err(|e| FileError::GitError(format!("Failed to open repository: {}", e)))?;
212
213 let mut diffs = Vec::new();
214
215 let head = repo
217 .head()
218 .map_err(|e| FileError::GitError(format!("Failed to get HEAD: {}", e)))?;
219
220 let head_tree = head
221 .peel_to_tree()
222 .map_err(|e| FileError::GitError(format!("Failed to get HEAD tree: {}", e)))?;
223
224 let mut index = repo
226 .index()
227 .map_err(|e| FileError::GitError(format!("Failed to get index: {}", e)))?;
228
229 let index_tree = index
230 .write_tree_to(&repo)
231 .map_err(|e| FileError::GitError(format!("Failed to write index tree: {}", e)))?;
232
233 let index_tree_obj = repo
234 .find_tree(index_tree)
235 .map_err(|e| FileError::GitError(format!("Failed to find index tree: {}", e)))?;
236
237 let diff = repo
239 .diff_tree_to_tree(Some(&head_tree), Some(&index_tree_obj), None)
240 .map_err(|e| FileError::GitError(format!("Failed to generate diff: {}", e)))?;
241
242 for delta in diff.deltas() {
244 if let Some(path) = delta.new_file().path() {
245 let file_diff = crate::models::FileDiff {
246 path: path.to_path_buf(),
247 hunks: vec![],
248 stats: crate::models::DiffStats {
249 additions: 0,
250 deletions: 0,
251 files_changed: 1,
252 },
253 };
254 diffs.push(file_diff);
255 }
256 }
257
258 Ok(diffs)
259 }
260
261 pub fn accept_changes(repo_path: &Path) -> Result<(), FileError> {
271 let _repo = Repository::open(repo_path)
274 .map_err(|e| FileError::GitError(format!("Failed to open repository: {}", e)))?;
275
276 Ok(())
277 }
278
279 pub fn reject_changes(repo_path: &Path) -> Result<(), FileError> {
289 let repo = Repository::open(repo_path)
290 .map_err(|e| FileError::GitError(format!("Failed to open repository: {}", e)))?;
291
292 let head = repo
294 .head()
295 .map_err(|e| FileError::GitError(format!("Failed to get HEAD: {}", e)))?;
296
297 let head_commit = head
298 .peel_to_commit()
299 .map_err(|e| FileError::GitError(format!("Failed to get HEAD commit: {}", e)))?;
300
301 repo.reset(head_commit.as_object(), git2::ResetType::Mixed, None)
302 .map_err(|e| FileError::GitError(format!("Failed to reset changes: {}", e)))?;
303
304 Ok(())
305 }
306
307 pub fn generate_commit_message(operations: &[FileOperation]) -> String {
317 if operations.is_empty() {
318 return "Update files".to_string();
319 }
320
321 let mut creates = 0;
322 let mut updates = 0;
323 let mut deletes = 0;
324
325 for op in operations {
326 match op.operation {
327 OperationType::Create => creates += 1,
328 OperationType::Update => updates += 1,
329 OperationType::Delete => deletes += 1,
330 OperationType::Rename { .. } => updates += 1,
331 }
332 }
333
334 let mut parts = Vec::new();
335
336 if creates > 0 {
337 parts.push(format!("Create {} file(s)", creates));
338 }
339 if updates > 0 {
340 parts.push(format!("Update {} file(s)", updates));
341 }
342 if deletes > 0 {
343 parts.push(format!("Delete {} file(s)", deletes));
344 }
345
346 if parts.is_empty() {
347 "Update files".to_string()
348 } else {
349 parts.join(", ")
350 }
351 }
352}
353
354impl Default for GitIntegration {
355 fn default() -> Self {
356 Self::new()
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn test_generate_commit_message_empty() {
366 let operations = vec![];
367 let message = GitIntegration::generate_commit_message(&operations);
368 assert_eq!(message, "Update files");
369 }
370
371 #[test]
372 fn test_generate_commit_message_creates() {
373 let operations = vec![
374 FileOperation {
375 path: PathBuf::from("file1.rs"),
376 operation: OperationType::Create,
377 content: Some("content".to_string()),
378 backup_path: None,
379 content_hash: None,
380 },
381 FileOperation {
382 path: PathBuf::from("file2.rs"),
383 operation: OperationType::Create,
384 content: Some("content".to_string()),
385 backup_path: None,
386 content_hash: None,
387 },
388 ];
389 let message = GitIntegration::generate_commit_message(&operations);
390 assert!(message.contains("Create 2 file(s)"));
391 }
392
393 #[test]
394 fn test_generate_commit_message_mixed() {
395 let operations = vec![
396 FileOperation {
397 path: PathBuf::from("file1.rs"),
398 operation: OperationType::Create,
399 content: Some("content".to_string()),
400 backup_path: None,
401 content_hash: None,
402 },
403 FileOperation {
404 path: PathBuf::from("file2.rs"),
405 operation: OperationType::Update,
406 content: Some("content".to_string()),
407 backup_path: None,
408 content_hash: None,
409 },
410 FileOperation {
411 path: PathBuf::from("file3.rs"),
412 operation: OperationType::Delete,
413 content: None,
414 backup_path: None,
415 content_hash: None,
416 },
417 ];
418 let message = GitIntegration::generate_commit_message(&operations);
419 assert!(message.contains("Create 1 file(s)"));
420 assert!(message.contains("Update 1 file(s)"));
421 assert!(message.contains("Delete 1 file(s)"));
422 }
423
424 #[test]
425 fn test_git_integration_new() {
426 let git = GitIntegration::new();
427 assert_eq!(format!("{:?}", git), "GitIntegration");
428 }
429
430 #[test]
431 fn test_git_integration_default() {
432 let git = GitIntegration::default();
433 assert_eq!(format!("{:?}", git), "GitIntegration");
434 }
435
436 #[test]
437 fn test_generate_commit_message_updates() {
438 let operations = vec![
439 FileOperation {
440 path: PathBuf::from("file1.rs"),
441 operation: OperationType::Update,
442 content: Some("content".to_string()),
443 backup_path: None,
444 content_hash: None,
445 },
446 FileOperation {
447 path: PathBuf::from("file2.rs"),
448 operation: OperationType::Update,
449 content: Some("content".to_string()),
450 backup_path: None,
451 content_hash: None,
452 },
453 ];
454 let message = GitIntegration::generate_commit_message(&operations);
455 assert_eq!(message, "Update 2 file(s)");
456 }
457
458 #[test]
459 fn test_generate_commit_message_deletes() {
460 let operations = vec![FileOperation {
461 path: PathBuf::from("file1.rs"),
462 operation: OperationType::Delete,
463 content: None,
464 backup_path: None,
465 content_hash: None,
466 }];
467 let message = GitIntegration::generate_commit_message(&operations);
468 assert_eq!(message, "Delete 1 file(s)");
469 }
470
471 #[test]
472 fn test_generate_commit_message_renames() {
473 let operations = vec![FileOperation {
474 path: PathBuf::from("file1.rs"),
475 operation: OperationType::Rename {
476 to: PathBuf::from("file2.rs"),
477 },
478 content: None,
479 backup_path: None,
480 content_hash: None,
481 }];
482 let message = GitIntegration::generate_commit_message(&operations);
483 assert!(message.contains("Update 1 file(s)"));
484 }
485
486 #[test]
487 fn test_accept_changes_invalid_repo() {
488 let result = GitIntegration::accept_changes(Path::new("/nonexistent/path"));
489 assert!(result.is_err());
490 }
491
492 #[test]
493 fn test_reject_changes_invalid_repo() {
494 let result = GitIntegration::reject_changes(Path::new("/nonexistent/path"));
495 assert!(result.is_err());
496 }
497
498 #[test]
499 fn test_review_changes_invalid_repo() {
500 let result = GitIntegration::review_changes(Path::new("/nonexistent/path"));
501 assert!(result.is_err());
502 }
503}