use super::auto_push_single_flight::{PushAttempt, PushGit, SingleFlightError};
use super::bounded_push::{STOPPED_EXIT_CODE, STOP_GRACE, push_deadline, run_bounded_push};
use super::child_environment::environment_value;
use super::forwarding::{ChildOutcome, exit_code, outcome_of};
use super::lfs_pre_push::{failed_message, find_program, missing_program_message};
use super::lfs_output_mask::{mask_userinfo, mask_userinfo_text};
use super::lfs_server_retry::{next_delay, retry_delays, retry_notice};
use super::lfs_upload_plan::UploadPlan;
use super::scratch_location::scratch_file;
use super::signal_relay::SignalGuard;
use super::start_retry::RetryStart;
use std::ffi::OsString;
use std::path::Path;
use std::process::{Command, Stdio};
use std::io::{Read, Seek, SeekFrom};
fn path_with_real_git_first(environment: &[(OsString, OsString)], real_git: &Path) -> OsString {
let existing: OsString = environment_value(environment, "PATH").unwrap_or_default();
let mut directories: Vec<std::path::PathBuf> = Vec::new();
if let Some(directory) = real_git.parent() {
directories.push(directory.to_path_buf());
}
directories.extend(std::env::split_paths(existing.as_os_str()));
return std::env::join_paths(directories).unwrap_or(existing);
}
pub fn prefix_variables(global_prefix: &[OsString], environment: &[(OsString, OsString)]) -> Vec<(OsString, OsString)> {
let mut pairs: Vec<(OsString, OsString)> = Vec::new();
let mut variables: Vec<(OsString, OsString)> = Vec::new();
let mut tokens = global_prefix.iter();
while let Some(token) = tokens.next() {
let text: String = token.to_string_lossy().into_owned();
if text == "-c" {
if let Some(setting) = tokens.next() {
let setting_text: String = setting.to_string_lossy().into_owned();
let (name, value) = setting_text.split_once('=').unwrap_or((setting_text.as_str(), "true"));
pairs.push((OsString::from(name), OsString::from(value)));
}
continue;
}
for (option, variable) in [("--git-dir", "GIT_DIR"), ("--work-tree", "GIT_WORK_TREE"), ("--namespace", "GIT_NAMESPACE")] {
if text == option {
if let Some(value) = tokens.next() {
variables.push((OsString::from(variable), value.clone()));
}
} else if let Some(value) = text.strip_prefix(format!("{option}=").as_str()) {
variables.push((OsString::from(variable), OsString::from(value)));
}
}
}
let existing: usize = environment_value(environment, "GIT_CONFIG_COUNT")
.and_then(|value| {
return value.to_str().and_then(|count| {
return count.parse::<usize>().ok();
});
})
.unwrap_or(0);
if !pairs.is_empty() {
variables.push((OsString::from("GIT_CONFIG_COUNT"), OsString::from((existing + pairs.len()).to_string())));
}
for (offset, (name, value)) in pairs.into_iter().enumerate() {
variables.push((OsString::from(format!("GIT_CONFIG_KEY_{}", existing + offset)), name));
variables.push((OsString::from(format!("GIT_CONFIG_VALUE_{}", existing + offset)), value));
}
return variables;
}
enum CaptureError {
Scratch(String),
Start,
}
const MAX_RELAYED_BYTES: u64 = 4 * 1024 * 1024;
fn run_with_captured_stderr(
command: &mut Command,
environment: &[(OsString, OsString)],
) -> Result<(ChildOutcome, Vec<u8>), CaptureError> {
let mut captured: std::fs::File = scratch_file(environment).map_err(|error| {
return CaptureError::Scratch(error.to_string());
})?;
let for_child: std::fs::File = captured.try_clone().map_err(|error| {
return CaptureError::Scratch(error.to_string());
})?;
command.stderr(Stdio::from(for_child));
let outcome: std::io::Result<ChildOutcome> = match SignalGuard::install() {
Ok(guard) => guard.run_child(command),
Err(_) => command.status_retrying().map(outcome_of),
};
let Ok(ended) = outcome else {
return Err(CaptureError::Start);
};
let mut bytes: Vec<u8> = Vec::new();
captured
.seek(SeekFrom::Start(0))
.and_then(|_| {
return captured.by_ref().take(MAX_RELAYED_BYTES).read_to_end(&mut bytes);
})
.map_err(|error| {
return CaptureError::Scratch(error.to_string());
})?;
return Ok((ended, mask_userinfo(bytes.as_slice())));
}
fn capture_failed_message(why: &str) -> String {
return format!(
"cli-git: this push was stopped before it sent anything. The wrapper reads the output of `git-lfs push` through \
a private temporary file, to run it again after a server error and 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 \
wrapper needs a writable `XDG_STATE_HOME`, or a `HOME`, for its private scratch directory) and push again; or \
skip the upload for this push with `GIT_LFS_SKIP_PUSH=1` or `git push --no-verify`, which publishes pointer \
files without their objects.\n"
);
}
pub fn run_upload_interactive(
plan: &UploadPlan,
global_prefix: &[OsString],
directory: &Path,
real_git: &Path,
environment: &[(OsString, OsString)],
overlay: &[(OsString, OsString)],
diagnostics: &mut dyn std::io::Write,
) -> Option<i32> {
let Some(program) = find_program(environment_value(environment, "PATH").unwrap_or_default().as_os_str()) else {
let _written = diagnostics.write_all(missing_program_message().as_bytes());
return Some(1);
};
if plan.submodules {
let _written = diagnostics.write_all(submodule_notice().as_bytes());
}
for arguments in plan.remotes.iter().flat_map(|remote| { return plan.runs(remote.as_os_str()); }) {
let delays: Vec<std::time::Duration> = retry_delays(environment);
let mut retries_done: usize = 0;
loop {
let mut command: Command = Command::new(&program);
command
.current_dir(directory)
.args(arguments.as_slice())
.env("PATH", path_with_real_git_first(environment, real_git));
for (name, value) in prefix_variables(global_prefix, environment).into_iter().chain(overlay.iter().cloned()) {
command.env(name, value);
}
let (ended, relayed): (ChildOutcome, Vec<u8>) = match run_with_captured_stderr(&mut command, environment) {
Ok(finished) => finished,
Err(CaptureError::Scratch(why)) => {
let _written = diagnostics.write_all(capture_failed_message(why.as_str()).as_bytes());
return Some(1);
}
Err(CaptureError::Start) => {
let _written = diagnostics.write_all(missing_program_message().as_bytes());
return Some(1);
}
};
let _relayed = diagnostics.write_all(relayed.as_slice());
let code: i32 = exit_code(ended);
if code == 0 {
break;
}
if matches!(ended, ChildOutcome::Exited(_))
&& let Some(delay) = next_delay(String::from_utf8_lossy(relayed.as_slice()).as_ref(), retries_done, delays.as_slice())
{
let _written = diagnostics.write_all(retry_notice(delay, retries_done, delays.len()).as_bytes());
retries_done += 1;
std::thread::sleep(delay);
continue;
}
let description: String = match ended {
ChildOutcome::Exited(status) => format!("exited with status {status}"),
ChildOutcome::Signaled(signal) => format!("was ended by signal {signal}"),
};
let _written = diagnostics.write_all(failed_message(description.as_str()).as_bytes());
return Some(code);
}
}
return None;
}
pub fn submodule_notice() -> String {
return String::from(
"cli-git: this push also pushes submodules (`--recurse-submodules`), and the wrapper uploads Git LFS objects only \\
for this repository's own refs. If a submodule uses Git LFS, upload its objects first by running \\
`git lfs push --all <remote>` inside it, or push the submodule on its own; otherwise its pointer files are \\
published without their objects.\\n",
);
}
pub fn run_upload_bounded(git: &PushGit, plan: &UploadPlan) -> Result<Option<PushAttempt>, SingleFlightError> {
let Some(program) = find_program(environment_value(git.environment.as_slice(), "PATH").unwrap_or_default().as_os_str())
else {
return Ok(Some(PushAttempt { exit_code: 1, output: missing_program_message().trim_end().to_owned() }));
};
for arguments in plan.remotes.iter().flat_map(|remote| { return plan.runs(remote.as_os_str()); }) {
let delays: Vec<std::time::Duration> = retry_delays(git.environment.as_slice());
let mut earlier: String = String::new();
let mut retries_done: usize = 0;
loop {
let mut command: Command = Command::new(&program);
command
.current_dir(&git.cwd)
.env_clear()
.envs(git.environment.iter().map(|(name, value)| {
return (name, value);
}))
.env("PATH", path_with_real_git_first(git.environment.as_slice(), git.real_git.as_path()))
.args(arguments.as_slice());
let attempt: PushAttempt =
match run_bounded_push(command, push_deadline(git.environment.as_slice()), STOP_GRACE, git.environment.as_slice()) {
Ok(finished) => finished,
Err(SingleFlightError::Start(_)) => {
return Ok(Some(PushAttempt { exit_code: 1, output: missing_program_message().trim_end().to_owned() }));
}
Err(other) => return Err(other),
};
if attempt.exit_code == 0 {
break;
}
let shown: String = mask_userinfo_text(attempt.output.as_str());
let delay: Option<std::time::Duration> = next_delay(shown.as_str(), retries_done, delays.as_slice())
.filter(|_| {
return attempt.exit_code != STOPPED_EXIT_CODE;
});
let Some(wait) = delay else {
let joined: String = [earlier.as_str(), shown.as_str()].concat();
return Ok(Some(explained(PushAttempt { exit_code: attempt.exit_code, output: joined })));
};
let notice: String = retry_notice(wait, retries_done, delays.len());
earlier.push_str(shown.as_str());
if !earlier.is_empty() && !earlier.ends_with('\n') {
earlier.push('\n');
}
earlier.push_str(notice.as_str());
retries_done += 1;
std::thread::sleep(wait);
}
}
return Ok(None);
}
fn explained(attempt: PushAttempt) -> PushAttempt {
let description: String = if attempt.exit_code == STOPPED_EXIT_CODE {
String::from("did not finish and was stopped")
} else {
format!("exited with status {}", attempt.exit_code)
};
let explanation: String = failed_message(description.as_str());
let shown: &str = explanation.trim_end();
let output: String = if attempt.output.is_empty() { shown.to_owned() } else { format!("{}\n{shown}", attempt.output) };
return PushAttempt { exit_code: attempt.exit_code, output };
}
#[cfg(test)]
#[path = "lfs_upload_tests.rs"]
mod tests;