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}