1use crate::auto_push_single_flight::{PushAttempt, PushGit, SingleFlightError};
20use crate::bounded_push::{STOPPED_EXIT_CODE, STOP_GRACE, push_patience, run_bounded_push};
22use crate::child_environment::environment_value;
24use crate::forwarding::{ChildOutcome, exit_code, outcome_of};
26use crate::lfs_pre_push::{failed_message, find_program, missing_program_message};
28use crate::lfs_output_mask::{mask_userinfo, mask_userinfo_text};
30use crate::lfs_upload_plan::UploadPlan;
31use crate::scratch_location::scratch_file;
33use crate::signal_relay::SignalGuard;
35use crate::start_retry::RetryStart;
37use std::ffi::OsString;
39use std::path::Path;
41use std::process::{Command, Stdio};
43use std::io::{Read, Seek, SeekFrom};
45
46fn path_with_real_git_first(environment: &[(OsString, OsString)], real_git: &Path) -> OsString {
49 let existing: OsString = environment_value(environment, "PATH").unwrap_or_default();
50 let mut directories: Vec<std::path::PathBuf> = Vec::new();
51 if let Some(directory) = real_git.parent() {
52 directories.push(directory.to_path_buf());
53 }
54 directories.extend(std::env::split_paths(existing.as_os_str()));
55 return std::env::join_paths(directories).unwrap_or(existing);
56}
57
58pub fn prefix_variables(global_prefix: &[OsString], environment: &[(OsString, OsString)]) -> Vec<(OsString, OsString)> {
72 let mut pairs: Vec<(OsString, OsString)> = Vec::new();
73 let mut variables: Vec<(OsString, OsString)> = Vec::new();
74 let mut tokens = global_prefix.iter();
75 while let Some(token) = tokens.next() {
76 let text: String = token.to_string_lossy().into_owned();
77 if text == "-c" {
78 if let Some(setting) = tokens.next() {
79 let setting_text: String = setting.to_string_lossy().into_owned();
80 let (name, value) = setting_text.split_once('=').unwrap_or((setting_text.as_str(), "true"));
81 pairs.push((OsString::from(name), OsString::from(value)));
82 }
83 continue;
84 }
85 for (option, variable) in [("--git-dir", "GIT_DIR"), ("--work-tree", "GIT_WORK_TREE"), ("--namespace", "GIT_NAMESPACE")] {
86 if text == option {
87 if let Some(value) = tokens.next() {
88 variables.push((OsString::from(variable), value.clone()));
89 }
90 } else if let Some(value) = text.strip_prefix(format!("{option}=").as_str()) {
91 variables.push((OsString::from(variable), OsString::from(value)));
92 }
93 }
94 }
95 let existing: usize = environment_value(environment, "GIT_CONFIG_COUNT")
96 .and_then(|value| {
97 return value.to_str().and_then(|count| {
98 return count.parse::<usize>().ok();
99 });
100 })
101 .unwrap_or(0);
102 if !pairs.is_empty() {
103 variables.push((OsString::from("GIT_CONFIG_COUNT"), OsString::from((existing + pairs.len()).to_string())));
104 }
105 for (offset, (name, value)) in pairs.into_iter().enumerate() {
106 variables.push((OsString::from(format!("GIT_CONFIG_KEY_{}", existing + offset)), name));
107 variables.push((OsString::from(format!("GIT_CONFIG_VALUE_{}", existing + offset)), value));
108 }
109 return variables;
110}
111
112enum CaptureError {
114 Scratch(String),
116 Start,
118}
119
120const MAX_RELAYED_BYTES: u64 = 4 * 1024 * 1024;
122
123fn run_with_captured_stderr(
136 command: &mut Command,
137 environment: &[(OsString, OsString)],
138) -> Result<(ChildOutcome, Vec<u8>), CaptureError> {
139 let mut captured: std::fs::File = scratch_file(environment).map_err(|error| {
140 return CaptureError::Scratch(error.to_string());
141 })?;
142 let for_child: std::fs::File = captured.try_clone().map_err(|error| {
143 return CaptureError::Scratch(error.to_string());
144 })?;
145 command.stderr(Stdio::from(for_child));
146 let outcome: std::io::Result<ChildOutcome> = match SignalGuard::install() {
147 Ok(guard) => guard.run_child(command),
148 Err(_) => command.status_retrying().map(outcome_of),
149 };
150 let Ok(ended) = outcome else {
151 return Err(CaptureError::Start);
152 };
153 let mut bytes: Vec<u8> = Vec::new();
154 captured
155 .seek(SeekFrom::Start(0))
156 .and_then(|_| {
157 return captured.by_ref().take(MAX_RELAYED_BYTES).read_to_end(&mut bytes);
158 })
159 .map_err(|error| {
160 return CaptureError::Scratch(error.to_string());
161 })?;
162 return Ok((ended, mask_userinfo(bytes.as_slice())));
163}
164
165fn capture_failed_message(why: &str) -> String {
167 return format!(
168 "cli-git: this push was stopped before it sent anything. The wrapper reads the output of `git-lfs push` through \
169 a private temporary file, to hide the credentials in the URLs it names, and that file could not be made ({why}). Ways forward: fix the cause named in the parentheses (the \
170 wrapper needs a writable `XDG_STATE_HOME`, or a `HOME`, for its private scratch directory) and push again; or \
171 skip the upload for this push with `GIT_LFS_SKIP_PUSH=1` or `git push --no-verify`, which publishes pointer \
172 files without their objects.\n"
173 );
174}
175
176pub fn run_upload_interactive(
188 plan: &UploadPlan,
189 global_prefix: &[OsString],
190 directory: &Path,
191 real_git: &Path,
192 environment: &[(OsString, OsString)],
193 overlay: &[(OsString, OsString)],
194 diagnostics: &mut dyn std::io::Write,
195) -> Option<i32> {
196 let Some(program) = find_program(environment_value(environment, "PATH").unwrap_or_default().as_os_str()) else {
197 let _written = diagnostics.write_all(missing_program_message().as_bytes());
199 return Some(1);
200 };
201 if plan.submodules {
202 let _written = diagnostics.write_all(submodule_notice().as_bytes());
204 }
205 for arguments in plan.remotes.iter().flat_map(|remote| { return plan.runs(remote.as_os_str()); }) {
209 let mut command: Command = Command::new(&program);
210 command
211 .current_dir(directory)
212 .args(arguments.as_slice())
213 .env("PATH", path_with_real_git_first(environment, real_git));
214 for (name, value) in prefix_variables(global_prefix, environment).into_iter().chain(overlay.iter().cloned()) {
215 command.env(name, value);
216 }
217 let (ended, relayed): (ChildOutcome, Vec<u8>) = match run_with_captured_stderr(&mut command, environment) {
218 Ok(finished) => finished,
219 Err(CaptureError::Scratch(why)) => {
220 let _written = diagnostics.write_all(capture_failed_message(why.as_str()).as_bytes());
222 return Some(1);
223 }
224 Err(CaptureError::Start) => {
225 let _written = diagnostics.write_all(missing_program_message().as_bytes());
227 return Some(1);
228 }
229 };
230 let _relayed = diagnostics.write_all(relayed.as_slice());
232 let code: i32 = exit_code(ended);
233 if code == 0 {
234 continue;
235 }
236 let description: String = match ended {
237 ChildOutcome::Exited(status) => format!("exited with status {status}"),
238 ChildOutcome::Signaled(signal) => format!("was ended by signal {signal}"),
239 };
240 let _written = diagnostics.write_all(failed_message(description.as_str()).as_bytes());
242 return Some(code);
243 }
244 return None;
245}
246
247pub fn submodule_notice() -> String {
258 return String::from(
259 "cli-git: this push also pushes submodules (`--recurse-submodules`), and the wrapper uploads Git LFS objects only \\
260 for this repository's own refs. If a submodule uses Git LFS, upload its objects first by running \\
261 `git lfs push --all <remote>` inside it, or push the submodule on its own; otherwise its pointer files are \\
262 published without their objects.\\n",
263 );
264}
265
266pub fn run_upload_bounded(git: &PushGit, plan: &UploadPlan) -> Result<Option<PushAttempt>, SingleFlightError> {
278 let Some(program) = find_program(environment_value(git.environment.as_slice(), "PATH").unwrap_or_default().as_os_str())
279 else {
280 return Ok(Some(PushAttempt { exit_code: 1, output: missing_program_message().trim_end().to_owned() }));
281 };
282 for arguments in plan.remotes.iter().flat_map(|remote| { return plan.runs(remote.as_os_str()); }) {
285 let mut command: Command = Command::new(&program);
286 command
287 .current_dir(&git.cwd)
288 .env_clear()
289 .envs(git.environment.iter().map(|(name, value)| {
290 return (name, value);
291 }))
292 .env("PATH", path_with_real_git_first(git.environment.as_slice(), git.real_git.as_path()))
293 .args(arguments.as_slice());
294 let attempt: PushAttempt =
295 match run_bounded_push(command, push_patience(git.environment.as_slice()), STOP_GRACE, git.environment.as_slice()) {
296 Ok(finished) => finished,
297 Err(SingleFlightError::Start(_)) => {
298 return Ok(Some(PushAttempt { exit_code: 1, output: missing_program_message().trim_end().to_owned() }));
299 }
300 Err(other) => return Err(other),
301 };
302 if attempt.exit_code == 0 {
303 continue;
304 }
305 let shown: String = mask_userinfo_text(attempt.output.as_str());
306 return Ok(Some(explained(PushAttempt { exit_code: attempt.exit_code, output: shown })));
307 }
308 return Ok(None);
309}
310
311fn explained(attempt: PushAttempt) -> PushAttempt {
313 let description: String = if attempt.exit_code == STOPPED_EXIT_CODE {
314 String::from("did not finish and was stopped")
315 } else {
316 format!("exited with status {}", attempt.exit_code)
317 };
318 let explanation: String = failed_message(description.as_str());
319 let shown: &str = explanation.trim_end();
320 let output: String = if attempt.output.is_empty() { shown.to_owned() } else { format!("{}\n{shown}", attempt.output) };
321 return PushAttempt { exit_code: attempt.exit_code, output };
322}
323
324#[cfg(test)]
326#[path = "lfs_upload_tests.rs"]
327mod tests;