1use super::auto_push_single_flight::PushGit;
16use super::child_environment::environment_with_overlay;
17use super::service_commit::{ServiceCommit, try_service_commit};
18use super::service_handoff::{ServiceHandoff, hand_over, show_notices};
19use super::service_messages::render;
20use super::commit_transaction::{CommitInvocation, TransactionResult, TransactionStop, run_commit_transaction};
21use super::config_schema::CliGitConfig;
22use super::command_commit::CommitRegion;
23use super::diagnostic_log::warn;
24use super::diagnostics::{EngineFailureCode, render_engine_failure};
25use super::git_version::check_git_version;
26use super::policy_checks::ShippedChecks;
27use super::policy_engine::StageRequest;
28use super::policy_events::{PolicyEvent, render_policy_events};
29use super::policy_trigger::Trigger;
30use super::post_commit::{POST_COMMIT_BLOCKED_EXIT_CODE, PostCommitOutcome, PostCommitRequest, run_post_commit, run_post_commit_gate};
31use super::repository_facts::RepositoryFacts;
32use super::transaction_git::GitContext;
33use super::wrapper_controls::Controls;
34use std::ffi::OsString;
35use std::io::Write;
36use std::path::{Path, PathBuf};
37
38const TAG: &str = "runCommitCommand";
40
41pub fn effective_directory(start: &Path, global_prefix: &[OsString]) -> PathBuf {
51 let mut directory: PathBuf = start.to_path_buf();
52 let mut index: usize = 0;
53 while index < global_prefix.len() {
54 if global_prefix[index] == "-C"
55 && let Some(value) = global_prefix.get(index + 1)
56 {
57 directory = directory.join(value);
58 index += 2;
59 continue;
60 }
61 index += 1;
62 }
63 return directory;
64}
65
66#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub struct CommitEnding {
77 pub code: i32,
79}
80
81fn write_stderr(text: &str) {
91 let mut stream = std::io::stderr();
92 let _ = stream.write_all(text.as_bytes());
93 let _ = stream.flush();
94}
95
96fn report_stop(stop: TransactionStop) -> CommitEnding {
107 match stop {
108 TransactionStop::NativeExit(code) => return CommitEnding { code },
109 TransactionStop::Message(message) => {
110 write_stderr(format!("{message}\n").as_str());
111 return CommitEnding { code: 1 };
112 }
113 TransactionStop::Failed { code, message } => {
114 let event: PolicyEvent = PolicyEvent::EngineFailure {
115 code,
116 message,
117 trigger: Some(Trigger::PreForward),
118 policy: None,
119 path: None,
120 };
121 write_stderr(render_policy_events(0, std::slice::from_ref(&event)).as_str());
122 return CommitEnding { code: 2 };
123 }
124 }
125}
126
127pub struct CommitCommand<'a> {
137 pub context: &'a GitContext,
139 pub environment: &'a [(OsString, OsString)],
141 pub arguments: &'a [OsString],
143 pub subcommand_index: usize,
145 pub region: &'a CommitRegion,
147 pub config: &'a CliGitConfig,
149 pub controls: &'a Controls,
151}
152
153pub fn run_commit_command<F: RepositoryFacts>(command: &CommitCommand<'_>, checks: &mut ShippedChecks<F>) -> CommitEnding {
164 if let Err(reason) = check_git_version(command.context) {
165 write_stderr(render_engine_failure(0, EngineFailureCode::ContentUnavailable, reason.as_str()).as_str());
166 return CommitEnding { code: 2 };
167 }
168 let start: PathBuf = std::env::current_dir().unwrap_or_else(|_| { return PathBuf::from("."); });
169 let cwd: PathBuf = effective_directory(start.as_path(), command.context.global_prefix.as_slice());
170 let invocation: CommitInvocation<'_> = CommitInvocation {
171 context: command.context,
172 environment: command.environment,
173 cwd: cwd.as_path(),
174 arguments: command.arguments,
175 subcommand_index: command.subcommand_index,
176 region: command.region,
177 config: command.config,
178 controls: command.controls,
179 };
180 let request: StageRequest = StageRequest {
181 trigger: Trigger::PreForward,
182 config: command.config.policies.clone(),
183 controls: command.controls.clone(),
184 selected: Vec::new(),
185 };
186 let served: Result<Option<ServiceCommit>, TransactionStop> = try_service_commit(&invocation, checks, &request);
189 let (result, handoff): (TransactionResult, Option<ServiceHandoff>) = match served {
190 Ok(Some(found)) => (found.result, found.handoff),
191 Ok(None) => match run_commit_transaction(&invocation, checks, &request) {
192 Ok(found) => (found, None),
193 Err(stop) => return report_stop(stop),
194 },
195 Err(stop) => return report_stop(stop),
196 };
197 write_stderr(render_policy_events(0, result.events.as_slice()).as_str());
198 let Some(landed) = result.landed_oid else {
199 return CommitEnding { code: result.exit_code };
200 };
201 return post_commit(command, &invocation, landed.as_str(), result.uses_lfs, handoff.as_ref());
202}
203
204fn post_commit(
214 command: &CommitCommand<'_>,
215 invocation: &CommitInvocation<'_>,
216 landed: &str,
217 uses_lfs: bool,
218 handoff: Option<&ServiceHandoff>,
219) -> CommitEnding {
220 let request: PostCommitRequest<'_> = PostCommitRequest {
221 real_git: command.context.real_git.as_path(),
222 landed_oid: landed,
223 arguments: command.arguments,
224 global_prefix: command.context.global_prefix.as_slice(),
225 overlay: command.context.overlay.as_slice(),
226 environment: command.environment,
227 config: command.config.policies.clone(),
228 controls: command.controls.clone(),
229 };
230 if let Some(handed) = handoff {
232 return post_commit_handed(&request, handed, landed);
233 }
234 let git: PushGit = PushGit {
235 real_git: command.context.real_git.clone(),
236 cwd: invocation.cwd.to_path_buf(),
237 environment: environment_with_overlay(command.environment, command.context.overlay.as_slice()),
238 lfs: uses_lfs,
239 };
240 let mut stderr = std::io::stderr();
241 match run_post_commit(&request, &git, &mut stderr) {
242 Ok(PostCommitOutcome { exit_code, .. }) => return CommitEnding { code: exit_code },
243 Err(error) => {
244 warn(TAG, format!("the post-commit lifecycle failed: {error:?}").as_str());
245 write_stderr(format!("{error:?}\n").as_str());
246 return CommitEnding { code: 1 };
247 }
248 }
249}
250
251fn post_commit_handed(request: &PostCommitRequest<'_>, handoff: &ServiceHandoff, landed: &str) -> CommitEnding {
263 let mut stderr = std::io::stderr();
264 show_notices(handoff, &mut stderr);
265 let gate = run_post_commit_gate(request);
266 write_stderr(render_policy_events(0, gate.events.as_slice()).as_str());
267 if gate.blocked {
268 return CommitEnding { code: POST_COMMIT_BLOCKED_EXIT_CODE };
269 }
270 if let Err(failure) = hand_over(handoff, landed, &mut stderr) {
271 write_stderr(format!("{}\n", render(&failure)).as_str());
272 }
273 return CommitEnding { code: 0 };
274}
275
276#[cfg(test)]
278#[path = "commit_command_tests.rs"]
279mod tests;