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