ralph_workflow/app/
plumbing.rs1use crate::agents::{AgentDrain, AgentRegistry};
18use crate::app::effect::{AppEffect, AppEffectHandler, AppEffectResult};
19use crate::config::Config;
20use crate::files::{
21 delete_commit_message_file_with_workspace, read_commit_message_file_with_workspace,
22 write_commit_message_file_with_workspace,
23};
24use crate::git_helpers::git_diff;
25use crate::logger::Colors;
26use crate::logger::Logger;
27use crate::phases::generate_commit_message_with_chain;
28
29use crate::prompts::TemplateContext;
30use crate::workspace::Workspace;
31use crate::ProcessExecutor;
32use std::sync::Arc;
33
34pub struct CommitGenerationConfig<'a> {
39 pub config: &'a Config,
41 pub template_context: &'a TemplateContext,
43 pub workspace: &'a dyn crate::workspace::Workspace,
45 pub workspace_arc: Arc<dyn crate::workspace::Workspace>,
47 pub registry: &'a AgentRegistry,
49 pub logger: &'a Logger,
51 pub colors: Colors,
53 pub developer_agent: &'a str,
55 pub reviewer_agent: &'a str,
57 pub executor: Arc<dyn ProcessExecutor>,
59}
60
61fn resolve_commit_message_agents(registry: &AgentRegistry, reviewer_agent: &str) -> Vec<String> {
62 if let Some(commit_binding) = registry.resolved_drain(AgentDrain::Commit) {
63 return commit_binding.agents.clone();
64 }
65
66 let review_chain = registry
67 .resolved_drain(AgentDrain::Review)
68 .map_or(&[] as &[String], |binding| binding.agents.as_slice());
69 if !review_chain.is_empty() {
70 return review_chain.to_vec();
71 }
72
73 vec![reviewer_agent.to_string()]
74}
75
76#[cfg(any(test, feature = "test-utils"))]
77#[must_use]
78pub fn resolve_commit_message_agents_for_testing(
79 config: &CommitGenerationConfig<'_>,
80) -> Vec<String> {
81 resolve_commit_message_agents(config.registry, config.reviewer_agent)
82}
83
84pub fn get_commit_message_from_workspace(workspace: &dyn Workspace) -> anyhow::Result<String> {
101 read_commit_message_file_with_workspace(workspace).map_err(anyhow::Error::from)
102}
103
104pub fn handle_apply_commit_with_handler<H: AppEffectHandler>(
124 workspace: &dyn Workspace,
125 handler: &mut H,
126 logger: &Logger,
127 colors: Colors,
128) -> anyhow::Result<()> {
129 let commit_msg = read_commit_message_file_with_workspace(workspace)?;
130
131 logger.info("Staging all changes...");
132
133 match handler.execute(AppEffect::GitAddAll) {
136 AppEffectResult::Ok | AppEffectResult::Bool(true | false) => {
137 }
139 AppEffectResult::Error(e) => anyhow::bail!("Failed to stage changes: {e}"),
140 other => anyhow::bail!("Unexpected result from GitAddAll: {other:?}"),
141 }
142
143 logger.info(&format!(
144 "Commit message: {}{}{}",
145 colors.cyan(),
146 commit_msg,
147 colors.reset()
148 ));
149
150 logger.info("Creating commit...");
151
152 match handler.execute(AppEffect::GitCommit {
156 message: commit_msg,
157 user_name: None,
158 user_email: None,
159 }) {
160 AppEffectResult::String(oid)
161 | AppEffectResult::Commit(crate::app::effect::CommitResult::Success(oid)) => {
162 logger.success(&format!("Commit created successfully: {oid}"));
163 if let Err(err) = delete_commit_message_file_with_workspace(workspace) {
165 logger.warn(&format!("Failed to delete commit-message.txt: {err}"));
166 }
167 Ok(())
168 }
169 AppEffectResult::Commit(crate::app::effect::CommitResult::NoChanges)
170 | AppEffectResult::Ok => {
171 logger.warn("Nothing to commit (working tree clean)");
173 Ok(())
174 }
175 AppEffectResult::Error(e) => anyhow::bail!("Failed to create commit: {e}"),
176 other => anyhow::bail!("Unexpected result from GitCommit: {other:?}"),
177 }
178}
179
180pub fn handle_generate_commit_msg(config: &CommitGenerationConfig<'_>) -> anyhow::Result<()> {
203 config.logger.info("Generating commit message...");
204
205 let diff = git_diff()?;
207 if diff.trim().is_empty() {
208 config
209 .logger
210 .warn("No changes detected to generate a commit message for");
211 anyhow::bail!("No changes to commit");
212 }
213
214 let agents = resolve_commit_message_agents(config.registry, config.reviewer_agent);
215
216 let result = generate_commit_message_with_chain(
218 &diff,
219 config.registry,
220 &mut crate::app::plumbing_boundary::run_pipeline_for_commit_message(
221 &mut crate::app::runtime_factory::create_timer(),
222 config,
223 )?,
224 &agents,
225 config.template_context,
226 config.workspace,
227 )
228 .map_err(|e| anyhow::anyhow!("Failed to generate commit message: {e}"))?;
229 let commit_message = match result.outcome {
230 crate::phases::commit::CommitMessageOutcome::Message(message) => message,
231 crate::phases::commit::CommitMessageOutcome::Skipped { reason } => {
232 config.logger.warn(&format!(
233 "No commit needed (agent requested skip): {reason}"
234 ));
235 return Ok(());
236 }
237 };
238
239 config.logger.success("Commit message generated:");
240 config.logger.info(&format!(
241 "{}{}{}",
242 config.colors.cyan(),
243 commit_message,
244 config.colors.reset()
245 ));
246
247 write_commit_message_file_with_workspace(config.workspace, &commit_message)?;
249
250 config
251 .logger
252 .info("Message saved to .agent/commit-message.txt");
253 config
254 .logger
255 .info("Run 'ralph --apply-commit' to create the commit");
256
257 Ok(())
258}