git_commit_sage/
git.rs

1use git2::{DiffOptions, Repository, StatusOptions};
2use crate::{Error, Result, GitConfig};
3
4pub struct GitRepo {
5    repo: Repository,
6    config: GitConfig,
7}
8
9impl GitRepo {
10    pub fn new(config: GitConfig) -> Result<Self> {
11        Ok(Self {
12            repo: Repository::open(&config.repo_path)?,
13            config,
14        })
15    }
16
17    pub fn get_diff(&self) -> Result<String> {
18        let mut diff_options = DiffOptions::new();
19        diff_options.include_untracked(self.config.include_untracked);
20        
21        let diff = if self.is_initial_commit()? {
22            // For initial commits, diff against an empty tree
23            let empty_tree = self.repo.find_tree(self.repo.treebuilder(None)?.write()?)?;
24            let mut index = self.repo.index()?;
25            index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?;
26            index.write()?;
27            let tree_id = index.write_tree()?;
28            let tree = self.repo.find_tree(tree_id)?;
29            self.repo.diff_tree_to_tree(Some(&empty_tree), Some(&tree), Some(&mut diff_options))?
30        } else {
31            // For normal commits, diff against HEAD using the index
32            let head_tree = self.repo.head()?.peel_to_tree()?;
33            let mut index = self.repo.index()?;
34            index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?;
35            index.write()?;
36            let tree_id = index.write_tree()?;
37            let tree = self.repo.find_tree(tree_id)?;
38            self.repo.diff_tree_to_tree(Some(&head_tree), Some(&tree), Some(&mut diff_options))?
39        };
40        
41        let mut diff_string = String::new();
42        diff.print(git2::DiffFormat::Patch, |_, _, line| {
43            diff_string.push_str(&String::from_utf8_lossy(line.content()));
44            true
45        })?;
46        
47        if diff_string.is_empty() {
48            return Err(Error::NoChanges);
49        }
50        
51        Ok(diff_string)
52    }
53
54    fn is_initial_commit(&self) -> Result<bool> {
55        Ok(self.repo.head().is_err())
56    }
57
58    pub fn has_changes(&self) -> Result<bool> {
59        let mut status_options = StatusOptions::new();
60        status_options.include_untracked(self.config.include_untracked);
61        
62        let statuses = self.repo.statuses(Some(&mut status_options))?;
63        Ok(!statuses.is_empty())
64    }
65
66    pub fn commit(&self, message: &str) -> Result<()> {
67        // First stage all changes
68        self.stage_all()?;
69
70        let mut index = self.repo.index()?;
71        let tree_id = index.write_tree()?;
72        let tree = self.repo.find_tree(tree_id)?;
73
74        let signature = self.repo.signature()?;
75        let parent = match self.repo.head() {
76            Ok(head) => Some(head.peel_to_commit()?),
77            Err(_) => None,
78        };
79
80        let parents = parent.as_ref().map(|p| vec![p]).unwrap_or_default();
81        self.repo.commit(
82            Some("HEAD"),
83            &signature,
84            &signature,
85            message,
86            &tree,
87            parents.as_slice(),
88        )?;
89
90        Ok(())
91    }
92
93    pub fn stage_all(&self) -> Result<()> {
94        let mut index = self.repo.index()?;
95        index.add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None)?;
96        index.write()?;
97        Ok(())
98    }
99}