git_policy_cli/
commit_command.rs1use super::auto_push_single_flight::PushGit;
16use super::child_environment::environment_with_overlay;
17use super::commit_transaction::{CommitInvocation, TransactionResult, TransactionStop, run_commit_transaction};
18use super::config_schema::CliGitConfig;
19use super::command_commit::CommitRegion;
20use super::diagnostic_log::warn;
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::{PostCommitOutcome, PostCommitRequest, run_post_commit};
28use super::repository_facts::RepositoryFacts;
29use super::transaction_git::GitContext;
30use super::transaction_lease::has_valid_preparation_lease;
31use super::wrapper_controls::Controls;
32use std::ffi::OsString;
33use std::io::Write;
34use std::path::{Path, PathBuf};
35
36const TAG: &str = "runCommitCommand";
38
39pub fn effective_directory(start: &Path, global_prefix: &[OsString]) -> PathBuf {
49 let mut directory: PathBuf = start.to_path_buf();
50 let mut index: usize = 0;
51 while index < global_prefix.len() {
52 if global_prefix[index] == "-C"
53 && let Some(value) = global_prefix.get(index + 1)
54 {
55 directory = directory.join(value);
56 index += 2;
57 continue;
58 }
59 index += 1;
60 }
61 return directory;
62}
63
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub struct CommitEnding {
75 pub code: i32,
77}
78
79fn write_stderr(text: &str) {
89 let mut stream = std::io::stderr();
90 let _ = stream.write_all(text.as_bytes());
91 let _ = stream.flush();
92}
93
94fn report_stop(stop: TransactionStop) -> CommitEnding {
105 match stop {
106 TransactionStop::NativeExit(code) => return CommitEnding { code },
107 TransactionStop::Message(message) => {
108 write_stderr(format!("{message}\n").as_str());
109 return CommitEnding { code: 1 };
110 }
111 TransactionStop::Failed { code, message } => {
112 let event: PolicyEvent = PolicyEvent::EngineFailure {
113 code,
114 message,
115 trigger: Some(Trigger::PreForward),
116 policy: None,
117 path: None,
118 };
119 write_stderr(render_policy_events(0, std::slice::from_ref(&event)).as_str());
120 return CommitEnding { code: 2 };
121 }
122 }
123}
124
125pub struct CommitCommand<'a> {
135 pub context: &'a GitContext,
137 pub environment: &'a [(OsString, OsString)],
139 pub arguments: &'a [OsString],
141 pub subcommand_index: usize,
143 pub region: &'a CommitRegion,
145 pub config: &'a CliGitConfig,
147 pub controls: &'a Controls,
149}
150
151pub fn run_commit_command<F: RepositoryFacts>(command: &CommitCommand<'_>, checks: &mut ShippedChecks<F>) -> CommitEnding {
162 if let Err(reason) = check_git_version(command.context) {
163 write_stderr(render_engine_failure(0, EngineFailureCode::ContentUnavailable, reason.as_str()).as_str());
164 return CommitEnding { code: 2 };
165 }
166 let start: PathBuf = std::env::current_dir().unwrap_or_else(|_| { return PathBuf::from("."); });
167 let cwd: PathBuf = effective_directory(start.as_path(), command.context.global_prefix.as_slice());
168 let executable: PathBuf = match std::env::current_exe() {
169 Ok(found) => found,
170 Err(error) => {
171 write_stderr(format!("cli-git: cannot determine this executable: {error}\n").as_str());
172 return CommitEnding { code: 2 };
173 }
174 };
175 let invocation: CommitInvocation<'_> = CommitInvocation {
176 context: command.context,
177 environment: command.environment,
178 cwd: cwd.as_path(),
179 arguments: command.arguments,
180 subcommand_index: command.subcommand_index,
181 region: command.region,
182 config: command.config,
183 controls: command.controls,
184 inherited_lease_valid: has_valid_preparation_lease(command.environment),
185 executable: executable.as_path(),
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 result: TransactionResult = match run_commit_transaction(&invocation, checks, &request) {
194 Ok(found) => found,
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());
202}
203
204fn post_commit(command: &CommitCommand<'_>, invocation: &CommitInvocation<'_>, landed: &str) -> CommitEnding {
214 let request: PostCommitRequest<'_> = PostCommitRequest {
215 real_git: command.context.real_git.as_path(),
216 landed_oid: landed,
217 arguments: command.arguments,
218 global_prefix: command.context.global_prefix.as_slice(),
219 overlay: command.context.overlay.as_slice(),
220 environment: command.environment,
221 config: command.config.policies.clone(),
222 controls: command.controls.clone(),
223 };
224 let git: PushGit = PushGit {
225 real_git: command.context.real_git.clone(),
226 cwd: invocation.cwd.to_path_buf(),
227 environment: environment_with_overlay(command.environment, command.context.overlay.as_slice()),
228 };
229 let mut stderr = std::io::stderr();
230 match run_post_commit(&request, &git, &mut stderr) {
231 Ok(PostCommitOutcome { exit_code, .. }) => return CommitEnding { code: exit_code },
232 Err(error) => {
233 warn(TAG, format!("the post-commit lifecycle failed: {error:?}").as_str());
234 write_stderr(format!("{error:?}\n").as_str());
235 return CommitEnding { code: 1 };
236 }
237 }
238}
239
240#[cfg(test)]
242#[path = "commit_command_tests.rs"]
243mod tests;