ralph_workflow/app/
plumbing.rs1use crate::agents::{AgentDrain, AgentRegistry};
18use crate::app::effect::{AppEffect, AppEffectHandler, AppEffectResult};
19use crate::config::Config;
20use crate::executor::ProcessExecutor;
21use crate::files::{
22 delete_commit_message_file_with_workspace, read_commit_message_file_with_workspace,
23 write_commit_message_file_with_workspace,
24};
25use crate::git_helpers::git_diff;
26use crate::logger::Colors;
27use crate::logger::Logger;
28use crate::phases::generate_commit_message_with_chain;
29use crate::pipeline::PipelineRuntime;
30use crate::pipeline::Timer;
31use crate::prompts::TemplateContext;
32use crate::workspace::Workspace;
33use std::sync::Arc;
34
35pub struct CommitGenerationConfig<'a> {
40 pub config: &'a Config,
42 pub template_context: &'a TemplateContext,
44 pub workspace: &'a dyn crate::workspace::Workspace,
46 pub workspace_arc: Arc<dyn crate::workspace::Workspace>,
48 pub registry: &'a AgentRegistry,
50 pub logger: &'a Logger,
52 pub colors: Colors,
54 pub developer_agent: &'a str,
56 pub reviewer_agent: &'a str,
58 pub executor: Arc<dyn ProcessExecutor>,
60}
61
62fn resolve_commit_message_agents(config: &CommitGenerationConfig<'_>) -> Vec<String> {
63 if let Some(commit_binding) = config.registry.resolved_drain(AgentDrain::Commit) {
64 return commit_binding.agents.clone();
65 }
66
67 let review_chain = config
68 .registry
69 .resolved_drain(AgentDrain::Review)
70 .map_or(&[] as &[String], |binding| binding.agents.as_slice());
71 if !review_chain.is_empty() {
72 return review_chain.to_vec();
73 }
74
75 vec![config.reviewer_agent.to_string()]
76}
77
78#[cfg(any(test, feature = "test-utils"))]
79#[must_use]
80pub fn resolve_commit_message_agents_for_testing(
81 config: &CommitGenerationConfig<'_>,
82) -> Vec<String> {
83 resolve_commit_message_agents(config)
84}
85
86pub fn handle_show_commit_msg_with_workspace(workspace: &dyn Workspace) -> anyhow::Result<()> {
103 match read_commit_message_file_with_workspace(workspace) {
104 Ok(msg) => {
105 println!("{msg}");
106 Ok(())
107 }
108 Err(e) => {
109 anyhow::bail!("Failed to read commit message: {e}");
110 }
111 }
112}
113
114pub fn handle_apply_commit_with_handler<H: AppEffectHandler>(
134 workspace: &dyn Workspace,
135 handler: &mut H,
136 logger: &Logger,
137 colors: Colors,
138) -> anyhow::Result<()> {
139 let commit_msg = read_commit_message_file_with_workspace(workspace)?;
140
141 logger.info("Staging all changes...");
142
143 match handler.execute(AppEffect::GitAddAll) {
146 AppEffectResult::Ok | AppEffectResult::Bool(true | false) => {
147 }
149 AppEffectResult::Error(e) => anyhow::bail!("Failed to stage changes: {e}"),
150 other => anyhow::bail!("Unexpected result from GitAddAll: {other:?}"),
151 }
152
153 logger.info(&format!(
154 "Commit message: {}{}{}",
155 colors.cyan(),
156 commit_msg,
157 colors.reset()
158 ));
159
160 logger.info("Creating commit...");
161
162 match handler.execute(AppEffect::GitCommit {
166 message: commit_msg,
167 user_name: None,
168 user_email: None,
169 }) {
170 AppEffectResult::String(oid)
171 | AppEffectResult::Commit(crate::app::effect::CommitResult::Success(oid)) => {
172 logger.success(&format!("Commit created successfully: {oid}"));
173 if let Err(err) = delete_commit_message_file_with_workspace(workspace) {
175 logger.warn(&format!("Failed to delete commit-message.txt: {err}"));
176 }
177 Ok(())
178 }
179 AppEffectResult::Commit(crate::app::effect::CommitResult::NoChanges)
180 | AppEffectResult::Ok => {
181 logger.warn("Nothing to commit (working tree clean)");
183 Ok(())
184 }
185 AppEffectResult::Error(e) => anyhow::bail!("Failed to create commit: {e}"),
186 other => anyhow::bail!("Unexpected result from GitCommit: {other:?}"),
187 }
188}
189
190pub fn handle_generate_commit_msg(config: &CommitGenerationConfig<'_>) -> anyhow::Result<()> {
213 config.logger.info("Generating commit message...");
214
215 let diff = git_diff()?;
217 if diff.trim().is_empty() {
218 config
219 .logger
220 .warn("No changes detected to generate a commit message for");
221 anyhow::bail!("No changes to commit");
222 }
223
224 let mut timer = Timer::new();
226
227 let executor_ref: &dyn ProcessExecutor = &*config.executor;
229 let mut runtime = PipelineRuntime {
230 timer: &mut timer,
231 logger: config.logger,
232 colors: &config.colors,
233 config: config.config,
234 executor: executor_ref,
235 executor_arc: Arc::clone(&config.executor),
236 workspace: config.workspace,
237 workspace_arc: Arc::clone(&config.workspace_arc),
238 };
239
240 let agents = resolve_commit_message_agents(config);
241
242 let result = generate_commit_message_with_chain(
244 &diff,
245 config.registry,
246 &mut runtime,
247 &agents,
248 config.template_context,
249 config.workspace,
250 )
251 .map_err(|e| anyhow::anyhow!("Failed to generate commit message: {e}"))?;
252 let commit_message = match result.outcome {
253 crate::phases::commit::CommitMessageOutcome::Message(message) => message,
254 crate::phases::commit::CommitMessageOutcome::Skipped { reason } => {
255 config.logger.warn(&format!(
256 "No commit needed (agent requested skip): {reason}"
257 ));
258 return Ok(());
259 }
260 };
261
262 config.logger.success("Commit message generated:");
263 println!();
264 println!(
265 "{}{}{}",
266 config.colors.cyan(),
267 commit_message,
268 config.colors.reset()
269 );
270 println!();
271
272 write_commit_message_file_with_workspace(config.workspace, &commit_message)?;
274
275 config
276 .logger
277 .info("Message saved to .agent/commit-message.txt");
278 config
279 .logger
280 .info("Run 'ralph --apply-commit' to create the commit");
281
282 Ok(())
283}