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