git_policy_cli/
lfs_upload.rs1use super::auto_push_single_flight::{PushAttempt, PushGit, SingleFlightError};
20use super::bounded_push::{STOPPED_EXIT_CODE, STOP_GRACE, budget_spent_message, push_deadline, run_bounded_push};
22use super::child_environment::environment_value;
24use super::forwarding::{ChildOutcome, exit_code, outcome_of};
26use super::lfs_pre_push::{failed_message, find_program, missing_program_message};
28use super::lfs_upload_plan::UploadPlan;
29use super::signal_relay::SignalGuard;
31use super::start_retry::RetryStart;
33use std::ffi::OsString;
35use std::path::Path;
37use std::process::Command;
39
40fn path_with_real_git_first(environment: &[(OsString, OsString)], real_git: &Path) -> OsString {
43 let existing: OsString = environment_value(environment, "PATH").unwrap_or_default();
44 let mut directories: Vec<std::path::PathBuf> = Vec::new();
45 if let Some(directory) = real_git.parent() {
46 directories.push(directory.to_path_buf());
47 }
48 directories.extend(std::env::split_paths(existing.as_os_str()));
49 return std::env::join_paths(directories).unwrap_or(existing);
50}
51
52pub fn prefix_variables(global_prefix: &[OsString], environment: &[(OsString, OsString)]) -> Vec<(OsString, OsString)> {
66 let mut pairs: Vec<(OsString, OsString)> = Vec::new();
67 let mut variables: Vec<(OsString, OsString)> = Vec::new();
68 let mut tokens = global_prefix.iter();
69 while let Some(token) = tokens.next() {
70 let text: String = token.to_string_lossy().into_owned();
71 if text == "-c" {
72 if let Some(setting) = tokens.next() {
73 let setting_text: String = setting.to_string_lossy().into_owned();
74 let (name, value) = setting_text.split_once('=').unwrap_or((setting_text.as_str(), "true"));
75 pairs.push((OsString::from(name), OsString::from(value)));
76 }
77 continue;
78 }
79 for (option, variable) in [("--git-dir", "GIT_DIR"), ("--work-tree", "GIT_WORK_TREE"), ("--namespace", "GIT_NAMESPACE")] {
80 if text == option {
81 if let Some(value) = tokens.next() {
82 variables.push((OsString::from(variable), value.clone()));
83 }
84 } else if let Some(value) = text.strip_prefix(format!("{option}=").as_str()) {
85 variables.push((OsString::from(variable), OsString::from(value)));
86 }
87 }
88 }
89 let existing: usize = environment_value(environment, "GIT_CONFIG_COUNT")
90 .and_then(|value| {
91 return value.to_str().and_then(|count| {
92 return count.parse::<usize>().ok();
93 });
94 })
95 .unwrap_or(0);
96 if !pairs.is_empty() {
97 variables.push((OsString::from("GIT_CONFIG_COUNT"), OsString::from((existing + pairs.len()).to_string())));
98 }
99 for (offset, (name, value)) in pairs.into_iter().enumerate() {
100 variables.push((OsString::from(format!("GIT_CONFIG_KEY_{}", existing + offset)), name));
101 variables.push((OsString::from(format!("GIT_CONFIG_VALUE_{}", existing + offset)), value));
102 }
103 return variables;
104}
105
106pub fn run_upload_interactive(
118 plan: &UploadPlan,
119 global_prefix: &[OsString],
120 directory: &Path,
121 real_git: &Path,
122 environment: &[(OsString, OsString)],
123 overlay: &[(OsString, OsString)],
124 diagnostics: &mut dyn std::io::Write,
125) -> Option<i32> {
126 let Some(program) = find_program(environment_value(environment, "PATH").unwrap_or_default().as_os_str()) else {
127 let _written = diagnostics.write_all(missing_program_message().as_bytes());
129 return Some(1);
130 };
131 if plan.submodules {
132 let _written = diagnostics.write_all(submodule_notice().as_bytes());
134 }
135 let mut command: Command = Command::new(program);
136 command
137 .current_dir(directory)
138 .args(plan.arguments())
139 .env("PATH", path_with_real_git_first(environment, real_git));
140 for (name, value) in prefix_variables(global_prefix, environment).into_iter().chain(overlay.iter().cloned()) {
141 command.env(name, value);
142 }
143 let outcome: std::io::Result<ChildOutcome> = match SignalGuard::install() {
144 Ok(guard) => guard.run_child(&mut command),
145 Err(_) => command.status_retrying().map(outcome_of),
146 };
147 let Ok(ended) = outcome else {
148 let _written = diagnostics.write_all(missing_program_message().as_bytes());
150 return Some(1);
151 };
152 let code: i32 = exit_code(ended);
153 if code == 0 {
154 return None;
155 }
156 let description: String = match ended {
157 ChildOutcome::Exited(status) => format!("exited with status {status}"),
158 ChildOutcome::Signaled(signal) => format!("was ended by signal {signal}"),
159 };
160 let _written = diagnostics.write_all(failed_message(description.as_str()).as_bytes());
162 return Some(code);
163}
164
165pub fn submodule_notice() -> String {
176 return String::from(
177 "cli-git: this push also pushes submodules (`--recurse-submodules`), and the wrapper uploads Git LFS objects only \\
178 for this repository's own refs. If a submodule uses Git LFS, upload its objects first by running \\
179 `git lfs push --all <remote>` inside it, or push the submodule on its own; otherwise its pointer files are \\
180 published without their objects.\\n",
181 );
182}
183
184pub fn run_upload_bounded(git: &PushGit, plan: &UploadPlan) -> Result<Option<PushAttempt>, SingleFlightError> {
196 if super::holder_budget::exhausted() {
197 return Ok(Some(PushAttempt { exit_code: STOPPED_EXIT_CODE, output: budget_spent_message() }));
198 }
199 let Some(program) = find_program(environment_value(git.environment.as_slice(), "PATH").unwrap_or_default().as_os_str())
200 else {
201 return Ok(Some(PushAttempt { exit_code: 1, output: missing_program_message().trim_end().to_owned() }));
202 };
203 let mut command: Command = Command::new(program);
204 command
205 .current_dir(&git.cwd)
206 .env_clear()
207 .envs(git.environment.iter().map(|(name, value)| {
208 return (name, value);
209 }))
210 .env("PATH", path_with_real_git_first(git.environment.as_slice(), git.real_git.as_path()))
211 .args(plan.arguments());
212 let attempt: PushAttempt =
213 match run_bounded_push(command, push_deadline(git.environment.as_slice()), STOP_GRACE, git.environment.as_slice()) {
214 Ok(finished) => finished,
215 Err(SingleFlightError::Start(_)) => {
216 return Ok(Some(PushAttempt { exit_code: 1, output: missing_program_message().trim_end().to_owned() }));
217 }
218 Err(other) => return Err(other),
219 };
220 if attempt.exit_code == 0 {
221 return Ok(None);
222 }
223 let description: String = if attempt.exit_code == STOPPED_EXIT_CODE {
224 String::from("did not finish and was stopped")
225 } else {
226 format!("exited with status {}", attempt.exit_code)
227 };
228 let explanation: String = failed_message(description.as_str());
229 let shown: &str = explanation.trim_end();
230 let output: String = if attempt.output.is_empty() { shown.to_owned() } else { format!("{}\n{shown}", attempt.output) };
231 return Ok(Some(PushAttempt { exit_code: attempt.exit_code, output }));
232}
233
234#[cfg(test)]
236#[path = "lfs_upload_tests.rs"]
237mod tests;