git_policy_cli/
commit_command.rs1use super::service_commit::{ServiceCommit, service_commit};
16use super::service_handoff::{ServiceHandoff, hand_over};
17use super::service_messages::render;
18use super::commit_transaction::{CommitInvocation, TransactionStop};
19use super::config_schema::CliGitConfig;
20use super::command_commit::CommitRegion;
21use super::diagnostics::{EngineFailureCode, render_engine_failure};
22use super::git_version::check_git_version;
23use super::policy_checks::ShippedChecks;
24use super::policy_engine::StageRequest;
25use super::policy_events::{PolicyEvent, render_policy_events};
26use super::policy_trigger::Trigger;
27use super::post_commit::{POST_COMMIT_BLOCKED_EXIT_CODE, PostCommitRequest, run_post_commit_gate};
28use super::repository_facts::RepositoryFacts;
29use super::transaction_git::GitContext;
30use super::wrapper_controls::Controls;
31use std::ffi::OsString;
32use std::io::Write;
33use std::path::{Path, PathBuf};
34
35pub fn effective_directory(start: &Path, global_prefix: &[OsString]) -> PathBuf {
45 let mut directory: PathBuf = start.to_path_buf();
46 let mut index: usize = 0;
47 while index < global_prefix.len() {
48 if global_prefix[index] == "-C"
49 && let Some(value) = global_prefix.get(index + 1)
50 {
51 directory = directory.join(value);
52 index += 2;
53 continue;
54 }
55 index += 1;
56 }
57 return directory;
58}
59
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub struct CommitEnding {
71 pub code: i32,
73}
74
75fn write_stderr(text: &str) {
85 let mut stream = std::io::stderr();
86 let _ = stream.write_all(text.as_bytes());
87 let _ = stream.flush();
88}
89
90fn report_stop(stop: TransactionStop) -> CommitEnding {
101 match stop {
102 TransactionStop::NativeExit(code) => return CommitEnding { code },
103 TransactionStop::Message(message) => {
104 write_stderr(format!("{message}\n").as_str());
105 return CommitEnding { code: 1 };
106 }
107 TransactionStop::Failed { code, message } => {
108 let event: PolicyEvent = PolicyEvent::EngineFailure {
109 code,
110 message,
111 trigger: Some(Trigger::PreForward),
112 policy: None,
113 path: None,
114 };
115 write_stderr(render_policy_events(0, std::slice::from_ref(&event)).as_str());
116 return CommitEnding { code: 2 };
117 }
118 }
119}
120
121pub struct CommitCommand<'a> {
131 pub context: &'a GitContext,
133 pub environment: &'a [(OsString, OsString)],
135 pub arguments: &'a [OsString],
137 pub subcommand_index: usize,
139 pub region: &'a CommitRegion,
141 pub config: &'a CliGitConfig,
143 pub controls: &'a Controls,
145}
146
147pub fn run_commit_command<F: RepositoryFacts>(command: &CommitCommand<'_>, checks: &mut ShippedChecks<F>) -> CommitEnding {
158 if let Err(reason) = check_git_version(command.context) {
159 write_stderr(render_engine_failure(0, EngineFailureCode::ContentUnavailable, reason.as_str()).as_str());
160 return CommitEnding { code: 2 };
161 }
162 let start: PathBuf = std::env::current_dir().unwrap_or_else(|_| { return PathBuf::from("."); });
163 let cwd: PathBuf = effective_directory(start.as_path(), command.context.global_prefix.as_slice());
164 let invocation: CommitInvocation<'_> = CommitInvocation {
165 context: command.context,
166 environment: command.environment,
167 cwd: cwd.as_path(),
168 arguments: command.arguments,
169 subcommand_index: command.subcommand_index,
170 region: command.region,
171 config: command.config,
172 controls: command.controls,
173 };
174 let request: StageRequest = StageRequest {
175 trigger: Trigger::PreForward,
176 config: command.config.policies.clone(),
177 controls: command.controls.clone(),
178 selected: Vec::new(),
179 };
180 let served: ServiceCommit = match service_commit(&invocation, checks, &request) {
183 Ok(found) => found,
184 Err(stop) => return report_stop(stop),
185 };
186 write_stderr(render_policy_events(0, served.result.events.as_slice()).as_str());
187 let (Some(landed), Some(handoff)) = (served.result.landed_oid, served.handoff) else {
188 return CommitEnding { code: served.result.exit_code };
189 };
190 return post_commit(command, &handoff, landed.as_str());
191}
192
193fn post_commit(command: &CommitCommand<'_>, handoff: &ServiceHandoff, landed: &str) -> CommitEnding {
204 let request: PostCommitRequest<'_> = PostCommitRequest {
205 real_git: command.context.real_git.as_path(),
206 landed_oid: landed,
207 arguments: command.arguments,
208 global_prefix: command.context.global_prefix.as_slice(),
209 overlay: command.context.overlay.as_slice(),
210 environment: command.environment,
211 config: command.config.policies.clone(),
212 controls: command.controls.clone(),
213 };
214 let mut stderr = std::io::stderr();
215 let gate = run_post_commit_gate(&request);
216 write_stderr(render_policy_events(0, gate.events.as_slice()).as_str());
217 if gate.blocked {
218 return CommitEnding { code: POST_COMMIT_BLOCKED_EXIT_CODE };
219 }
220 if let Err(failure) = hand_over(handoff, landed, &mut stderr) {
221 write_stderr(format!("{}\n", render(&failure)).as_str());
222 }
223 return CommitEnding { code: 0 };
224}
225
226#[cfg(test)]
228#[path = "commit_command_tests.rs"]
229mod tests;