git_policy_cli/commit/
commit_command.rs1use crate::commit_timing::CommitTiming;
16use crate::service_commit::{ServiceCommit, service_commit};
17use crate::service_handoff::{ServiceHandoff, hand_over};
18use crate::service_messages::render;
19use crate::commit_transaction::{CommitInvocation, TransactionStop};
20use crate::config_schema::CliGitConfig;
21use crate::command_commit::CommitRegion;
22use crate::diagnostics::{EngineFailureCode, render_engine_failure};
23use crate::git_version::check_git_version;
24use crate::policy_checks::ShippedChecks;
25use crate::policy_engine::StageRequest;
26use crate::policy_events::{PolicyEvent, render_policy_events};
27use crate::policy_trigger::Trigger;
28use crate::post_commit::{POST_COMMIT_BLOCKED_EXIT_CODE, PostCommitRequest, run_post_commit_gate};
29use crate::repository_facts::RepositoryFacts;
30use crate::transaction_git::GitContext;
31use crate::wrapper_controls::Controls;
32use std::ffi::OsString;
33use std::io::Write;
34use std::path::{Path, PathBuf};
35use std::time::Instant;
36
37pub fn effective_directory(start: &Path, global_prefix: &[OsString]) -> PathBuf {
47 let mut directory: PathBuf = start.to_path_buf();
48 let mut index: usize = 0;
49 while index < global_prefix.len() {
50 if global_prefix[index] == "-C"
51 && let Some(value) = global_prefix.get(index + 1)
52 {
53 directory = directory.join(value);
54 index += 2;
55 continue;
56 }
57 index += 1;
58 }
59 return directory;
60}
61
62#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub struct CommitEnding {
73 pub code: i32,
75}
76
77fn write_stderr(text: &str) {
87 let mut stream = std::io::stderr();
88 let _ = stream.write_all(text.as_bytes());
89 let _ = stream.flush();
90}
91
92fn report_stop(stop: TransactionStop) -> CommitEnding {
103 match stop {
104 TransactionStop::NativeExit(code) => return CommitEnding { code },
105 TransactionStop::Message(message) => {
106 write_stderr(format!("{message}\n").as_str());
107 return CommitEnding { code: 1 };
108 }
109 TransactionStop::Failed { code, message } => {
110 let event: PolicyEvent = PolicyEvent::EngineFailure {
111 code,
112 hint: crate::policy_events::EngineFailureHint::OfCode,
113 message,
114 trigger: Some(Trigger::PreForward),
115 policy: None,
116 path: None,
117 };
118 write_stderr(render_policy_events(0, std::slice::from_ref(&event)).as_str());
119 return CommitEnding { code: 2 };
120 }
121 }
122}
123
124pub struct CommitCommand<'a> {
134 pub context: &'a GitContext,
136 pub environment: &'a [(OsString, OsString)],
138 pub arguments: &'a [OsString],
140 pub subcommand_index: usize,
142 pub region: &'a CommitRegion,
144 pub config: &'a CliGitConfig,
146 pub controls: &'a Controls,
148}
149
150pub fn run_commit_command<F: RepositoryFacts>(command: &CommitCommand<'_>, checks: &mut ShippedChecks<F>) -> CommitEnding {
161 let mut timing: CommitTiming = CommitTiming::start();
162 let ending: CommitEnding = run_timed(command, checks, &mut timing);
163 timing.finish(ending.code);
165 return ending;
166}
167
168fn run_timed<F: RepositoryFacts>(command: &CommitCommand<'_>, checks: &mut ShippedChecks<F>, timing: &mut CommitTiming) -> CommitEnding {
171 if let Err(reason) = check_git_version(command.context) {
172 write_stderr(render_engine_failure(0, EngineFailureCode::ContentUnavailable, reason.as_str()).as_str());
173 return CommitEnding { code: 2 };
174 }
175 let start: PathBuf = std::env::current_dir().unwrap_or_else(|_| { return PathBuf::from("."); });
176 let cwd: PathBuf = effective_directory(start.as_path(), command.context.global_prefix.as_slice());
177 let invocation: CommitInvocation<'_> = CommitInvocation {
178 context: command.context,
179 environment: command.environment,
180 cwd: cwd.as_path(),
181 arguments: command.arguments,
182 subcommand_index: command.subcommand_index,
183 region: command.region,
184 config: command.config,
185 controls: command.controls,
186 };
187 let request: StageRequest = StageRequest {
188 trigger: Trigger::PreForward,
189 config: command.config.policies.clone(),
190 controls: command.controls.clone(),
191 selected: Vec::new(),
192 };
193 let served: ServiceCommit = match service_commit(&invocation, checks, &request, timing) {
196 Ok(found) => found,
197 Err(stop) => return report_stop(stop),
198 };
199 write_stderr(render_policy_events(0, served.result.events.as_slice()).as_str());
200 let (Some(landed), Some(handoff)) = (served.result.landed_oid, served.handoff) else {
201 timing.outcome = "ended";
202 return CommitEnding { code: served.result.exit_code };
203 };
204 timing.outcome = "landed";
205 return post_commit(command, &handoff, landed.as_str(), timing);
206}
207
208fn post_commit(command: &CommitCommand<'_>, handoff: &ServiceHandoff, landed: &str, timing: &mut CommitTiming) -> CommitEnding {
219 let request: PostCommitRequest<'_> = PostCommitRequest {
220 real_git: command.context.real_git.as_path(),
221 landed_oid: landed,
222 arguments: command.arguments,
223 global_prefix: command.context.global_prefix.as_slice(),
224 overlay: command.context.overlay.as_slice(),
225 environment: command.environment,
226 config: command.config.policies.clone(),
227 controls: command.controls.clone(),
228 };
229 let mut stderr = std::io::stderr();
230 let gating: Instant = Instant::now();
231 let gate = run_post_commit_gate(&request);
232 timing.gate += gating.elapsed();
233 write_stderr(render_policy_events(0, gate.events.as_slice()).as_str());
234 if gate.blocked {
235 return CommitEnding { code: POST_COMMIT_BLOCKED_EXIT_CODE };
236 }
237 let handing_over: Instant = Instant::now();
238 let handed = hand_over(handoff, landed, &mut stderr);
239 timing.handoff += handing_over.elapsed();
240 if let Err(failure) = handed {
241 write_stderr(format!("{}\n", render(&failure)).as_str());
242 }
243 return CommitEnding { code: 0 };
244}
245
246#[cfg(test)]
248#[path = "commit_command_tests.rs"]
249mod tests;