Skip to main content

git_iris/services/
git_commit.rs

1//! Git commit service
2//!
3//! Focused service for git commit operations. This extracts the commit-specific
4//! functionality from the monolithic `IrisCommitService`.
5
6use anyhow::Result;
7use std::sync::Arc;
8
9use crate::git::{CommitResult, GitRepo};
10use crate::log_debug;
11
12/// Service for performing git commit operations
13///
14/// This service handles:
15/// - Creating commits with optional hook verification
16/// - Pre-commit hook execution
17/// - Remote repository detection
18///
19/// It does NOT handle:
20/// - LLM operations (handled by `IrisAgentService`)
21/// - Context gathering (handled by agents)
22/// - Message generation (handled by agents)
23pub struct GitCommitService {
24    repo: Arc<GitRepo>,
25    verify: bool,
26}
27
28impl GitCommitService {
29    /// Create a new `GitCommitService`
30    ///
31    /// # Arguments
32    /// * `repo` - The git repository to operate on
33    /// * `_use_gitmoji` - Retained for API compatibility; commit messages are
34    ///   stored exactly as provided
35    /// * `verify` - Whether to run pre/post-commit hooks
36    #[must_use]
37    pub fn new(repo: Arc<GitRepo>, _use_gitmoji: bool, verify: bool) -> Self {
38        Self { repo, verify }
39    }
40
41    /// Create from an existing `GitRepo` (convenience constructor)
42    #[must_use]
43    pub fn from_repo(repo: GitRepo, use_gitmoji: bool, verify: bool) -> Self {
44        Self::new(Arc::new(repo), use_gitmoji, verify)
45    }
46
47    /// Check if the repository is a remote repository
48    #[must_use]
49    pub fn is_remote(&self) -> bool {
50        self.repo.is_remote()
51    }
52
53    /// Execute the pre-commit hook if verification is enabled
54    ///
55    /// Returns Ok(()) if:
56    /// - verify is false (hooks disabled)
57    /// - repository is remote (hooks don't apply)
58    /// - pre-commit hook succeeds
59    ///
60    /// # Errors
61    ///
62    /// Returns an error when hook verification is enabled and the pre-commit hook fails.
63    pub fn pre_commit(&self) -> Result<()> {
64        if self.is_remote() {
65            log_debug!("Skipping pre-commit hook for remote repository");
66            return Ok(());
67        }
68
69        if self.verify {
70            self.repo.execute_hook("pre-commit")
71        } else {
72            Ok(())
73        }
74    }
75
76    /// Perform a commit with the given message
77    ///
78    /// This method:
79    /// 1. Validates the repository is not remote
80    /// 2. Uses the exact message provided
81    /// 3. Runs pre-commit hook (if verify is enabled)
82    /// 4. Creates the commit
83    /// 5. Runs post-commit hook (if verify is enabled)
84    ///
85    /// # Arguments
86    /// * `message` - The commit message to use
87    ///
88    /// # Returns
89    /// The result of the commit operation
90    ///
91    /// # Errors
92    ///
93    /// Returns an error when the repository is remote, hooks fail, or Git cannot create the commit.
94    pub fn perform_commit(&self, message: &str) -> Result<CommitResult> {
95        if self.is_remote() {
96            return Err(anyhow::anyhow!("Cannot commit to a remote repository"));
97        }
98
99        log_debug!("Performing commit with message: {}", message);
100
101        if !self.verify {
102            log_debug!("Skipping pre-commit hook (verify=false)");
103            return self.repo.commit(message);
104        }
105
106        // Execute pre-commit hook
107        log_debug!("Executing pre-commit hook");
108        if let Err(e) = self.repo.execute_hook("pre-commit") {
109            log_debug!("Pre-commit hook failed: {}", e);
110            return Err(e);
111        }
112        log_debug!("Pre-commit hook executed successfully");
113
114        // Perform the commit
115        match self.repo.commit(message) {
116            Ok(result) => {
117                // Execute post-commit hook (failure doesn't fail the commit)
118                log_debug!("Executing post-commit hook");
119                if let Err(e) = self.repo.execute_hook("post-commit") {
120                    log_debug!("Post-commit hook failed: {}", e);
121                }
122                log_debug!("Commit performed successfully");
123                Ok(result)
124            }
125            Err(e) => {
126                log_debug!("Commit failed: {}", e);
127                Err(e)
128            }
129        }
130    }
131
132    /// Amend the previous commit with staged changes and a new message
133    ///
134    /// This method:
135    /// 1. Validates the repository is not remote
136    /// 2. Uses the exact message provided
137    /// 3. Runs pre-commit hook (if verify is enabled)
138    /// 4. Amends the commit (replaces HEAD)
139    /// 5. Runs post-commit hook (if verify is enabled)
140    ///
141    /// # Arguments
142    /// * `message` - The new commit message
143    ///
144    /// # Returns
145    /// The result of the amend operation
146    ///
147    /// # Errors
148    ///
149    /// Returns an error when the repository is remote, hooks fail, or Git cannot amend the commit.
150    pub fn perform_amend(&self, message: &str) -> Result<CommitResult> {
151        if self.is_remote() {
152            return Err(anyhow::anyhow!(
153                "Cannot amend a commit in a remote repository"
154            ));
155        }
156
157        log_debug!("Performing amend with message: {}", message);
158
159        if !self.verify {
160            log_debug!("Skipping pre-commit hook (verify=false)");
161            return self.repo.amend_commit(message);
162        }
163
164        // Execute pre-commit hook
165        log_debug!("Executing pre-commit hook");
166        if let Err(e) = self.repo.execute_hook("pre-commit") {
167            log_debug!("Pre-commit hook failed: {}", e);
168            return Err(e);
169        }
170        log_debug!("Pre-commit hook executed successfully");
171
172        // Perform the amend
173        match self.repo.amend_commit(message) {
174            Ok(result) => {
175                // Execute post-commit hook (failure doesn't fail the amend)
176                log_debug!("Executing post-commit hook");
177                if let Err(e) = self.repo.execute_hook("post-commit") {
178                    log_debug!("Post-commit hook failed: {}", e);
179                }
180                log_debug!("Amend performed successfully");
181                Ok(result)
182            }
183            Err(e) => {
184                log_debug!("Amend failed: {}", e);
185                Err(e)
186            }
187        }
188    }
189
190    /// Get the message of the HEAD commit
191    ///
192    /// Useful for amend operations to provide original context
193    ///
194    /// # Errors
195    ///
196    /// Returns an error when the HEAD commit cannot be read.
197    pub fn get_head_commit_message(&self) -> Result<String> {
198        self.repo.get_head_commit_message()
199    }
200
201    /// Get a reference to the underlying repository
202    #[must_use]
203    pub fn repo(&self) -> &GitRepo {
204        &self.repo
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    #[test]
211    fn test_git_commit_service_construction() {
212        // This test just verifies the API compiles correctly
213        // Real tests would need a mock GitRepo
214    }
215}