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        self.perform_local_change(
96            message,
97            "commit",
98            "Cannot commit to a remote repository",
99            GitRepo::commit,
100        )
101    }
102
103    /// Amend the previous commit with staged changes and a new message
104    ///
105    /// This method:
106    /// 1. Validates the repository is not remote
107    /// 2. Uses the exact message provided
108    /// 3. Runs pre-commit hook (if verify is enabled)
109    /// 4. Amends the commit (replaces HEAD)
110    /// 5. Runs post-commit hook (if verify is enabled)
111    ///
112    /// # Arguments
113    /// * `message` - The new commit message
114    ///
115    /// # Returns
116    /// The result of the amend operation
117    ///
118    /// # Errors
119    ///
120    /// Returns an error when the repository is remote, hooks fail, or Git cannot amend the commit.
121    pub fn perform_amend(&self, message: &str) -> Result<CommitResult> {
122        self.perform_local_change(
123            message,
124            "amend",
125            "Cannot amend a commit in a remote repository",
126            GitRepo::amend_commit,
127        )
128    }
129
130    fn perform_local_change(
131        &self,
132        message: &str,
133        action: &str,
134        remote_error: &str,
135        operation: fn(&GitRepo, &str) -> Result<CommitResult>,
136    ) -> Result<CommitResult> {
137        if self.is_remote() {
138            return Err(anyhow::anyhow!("{remote_error}"));
139        }
140
141        log_debug!("Performing {} with message: {}", action, message);
142
143        if !self.verify {
144            log_debug!("Skipping pre-commit hook (verify=false)");
145            return operation(&self.repo, message);
146        }
147
148        self.run_pre_commit_hook()?;
149        self.finish_local_change(message, action, operation)
150    }
151
152    fn run_pre_commit_hook(&self) -> Result<()> {
153        log_debug!("Executing pre-commit hook");
154        self.repo
155            .execute_hook("pre-commit")
156            .inspect(|()| {
157                log_debug!("Pre-commit hook executed successfully");
158            })
159            .inspect_err(|e| {
160                log_debug!("Pre-commit hook failed: {}", e);
161            })
162    }
163
164    fn finish_local_change(
165        &self,
166        message: &str,
167        action: &str,
168        operation: fn(&GitRepo, &str) -> Result<CommitResult>,
169    ) -> Result<CommitResult> {
170        match operation(&self.repo, message) {
171            Ok(result) => {
172                self.run_post_commit_hook();
173                log_debug!("{} performed successfully", capitalized_action(action));
174                Ok(result)
175            }
176            Err(e) => {
177                log_debug!("{} failed: {}", capitalized_action(action), e);
178                Err(e)
179            }
180        }
181    }
182
183    fn run_post_commit_hook(&self) {
184        log_debug!("Executing post-commit hook");
185        if let Err(e) = self.repo.execute_hook("post-commit") {
186            log_debug!("Post-commit hook failed: {}", e);
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
208fn capitalized_action(action: &str) -> String {
209    let mut chars = action.chars();
210    chars.next().map_or_else(String::new, |first| {
211        first.to_uppercase().collect::<String>() + chars.as_str()
212    })
213}
214
215#[cfg(test)]
216mod tests {
217    #[test]
218    fn test_git_commit_service_construction() {
219        // This test just verifies the API compiles correctly
220        // Real tests would need a mock GitRepo
221    }
222}