use crate::forwarding::git_command;
use crate::repo_read_hook::try_request;
use crate::child_class::class_of;
use crate::child_watch::{StepBound, WatchTerms, output_watched_within};
pub use crate::transaction_git_request_and_reply_types::{GitContext, GitFailure, GitOutput, GitRequest, command_text};
pub(crate) use crate::transaction_git_step_rerun_and_end_rules::{NO_REFLOG_SETTING, subcommand_of};
use crate::transaction_git_step_rerun_and_end_rules::may_be_rerun;
use crate::transaction_git_step_bounds::step_bound;
#[cfg(test)]
pub(crate) use crate::transaction_git_step_bounds::{EXTRA_WAIT_FOR_THIS_THREAD, STEP_CEILING_FOR_THIS_THREAD};
use crate::private_storage::write_exclusive_scratch;
use crate::scratch_location::prepare_repository_scratch;
use crate::start_retry::{RefusableStart, START_RETRY_BUDGET, retry_refused_start_with};
use crate::random_id::random_uuid;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::io::Write;
use std::process::{Command, Stdio};
use std::time::Duration;
fn input_file(bytes: &[u8], common_dir: Option<&Path>) -> std::io::Result<std::fs::File> {
let Some(common) = common_dir else {
return Err(std::io::Error::other(
"monochromatic-git-policy-cli has no repository scratch directory for the input of this Git command: the command was started \
before the repository was known, which is a defect in monochromatic-git-policy-cli",
));
};
let directory: PathBuf = prepare_repository_scratch(common)?;
let unique: String = match random_uuid() {
Ok(id) => id,
Err(error) => return Err(std::io::Error::other(error.0)),
};
let path: PathBuf = directory.join(format!("cli-git-input-{unique}"));
write_exclusive_scratch(path.as_path(), bytes)?;
let file: std::io::Result<std::fs::File> = std::fs::File::open(&path);
let _ = std::fs::remove_file(&path);
let opened: std::fs::File = file?;
return Ok(opened);
}
fn build_command(context: &GitContext, request: &GitRequest) -> Command {
let mut arguments: Vec<OsString> = Vec::new();
if !request.without_prefix {
arguments.extend(context.global_prefix.iter().cloned());
}
arguments.extend(request.arguments.iter().cloned());
let mut command: Command = git_command(
context.real_git.as_path(),
arguments.as_slice(),
context.overlay.as_slice(),
);
command.current_dir(request.cwd.as_path());
for name in &request.unset {
command.env_remove(name);
}
for (name, value) in &request.environment {
command.env(name, value);
}
if let Some(index) = &request.index {
command.env("GIT_INDEX_FILE", index);
}
if let Some(objects) = &request.object_directory {
command.env("GIT_OBJECT_DIRECTORY", objects);
}
return command;
}
pub fn run_git(context: &GitContext, request: &GitRequest) -> std::io::Result<GitOutput> {
if let Some(answered) = try_request(context, request) {
return Ok(answered);
}
let label: String = crate::add_timing::git_step_label(request.arguments.as_slice());
let entered: bool = crate::add_timing::begin_step(label.as_str());
let output: std::io::Result<GitOutput> = run_git_pausing(context, request, START_RETRY_BUDGET, &mut std::thread::sleep);
crate::add_timing::end_step(entered);
return output;
}
pub(crate) fn refused_by_the_system(output: &GitOutput) -> bool {
return output.code != Some(0)
&& String::from_utf8_lossy(output.stderr.as_slice()).contains("Resource temporarily unavailable");
}
fn run_git_pausing(
context: &GitContext,
request: &GitRequest,
budget: Duration,
pause: &mut dyn FnMut(Duration),
) -> std::io::Result<GitOutput> {
if !may_be_rerun(request) {
return run_git_once(context, request);
}
let what: String = command_text(&request.arguments);
let mut attempt: RefusableGitRun<'_> = RefusableGitRun { context, request, refused: None };
let result: std::io::Result<GitOutput> = retry_refused_start_with(what.as_str(), budget, pause, &mut attempt);
match (result, attempt.refused.take()) {
(Ok(finished), _) => return Ok(finished),
(Err(error), Some(last)) if error.kind() == std::io::ErrorKind::WouldBlock => return Ok(last),
(Err(error), _) => return Err(error),
}
}
struct RefusableGitRun<'a> {
context: &'a GitContext,
request: &'a GitRequest,
refused: Option<GitOutput>,
}
impl RefusableStart<GitOutput> for RefusableGitRun<'_> {
fn start(&mut self) -> std::io::Result<GitOutput> {
let output: GitOutput = match run_git_once(self.context, self.request) {
Ok(finished) => finished,
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => return Err(std::io::Error::other(error)),
Err(error) => return Err(error),
};
if refused_by_the_system(&output) {
self.refused = Some(output);
return Err(std::io::Error::from(std::io::ErrorKind::WouldBlock));
}
return Ok(output);
}
}
trait WatchTermsSource {
fn terms(&self) -> WatchTerms;
}
struct TermsOfRequest<'a> {
request: &'a GitRequest,
}
impl WatchTermsSource for TermsOfRequest<'_> {
fn terms(&self) -> WatchTerms {
return terms_for_request(self.request);
}
}
fn run_git_once(context: &GitContext, request: &GitRequest) -> std::io::Result<GitOutput> {
return run_git_once_with(context, request, &TermsOfRequest { request });
}
pub(crate) fn terms_for_request(request: &GitRequest) -> WatchTerms {
return WatchTerms::for_class(class_of(request.arguments.as_slice()));
}
fn run_git_once_with(
context: &GitContext,
request: &GitRequest,
terms: &dyn WatchTermsSource,
) -> std::io::Result<GitOutput> {
let first: std::io::Result<GitOutput> = run_git_watched(context, request, terms.terms());
return match first {
Err(error) if error.kind() == std::io::ErrorKind::TimedOut && may_be_rerun(request) && !crate::child_watch::is_ceiling(&error) => {
let _written = writeln!(
std::io::stderr(),
"monochromatic-git-policy-cli: {} showed no sign of work and was ended; running it again",
step_subject(request)
);
run_git_watched(context, request, terms.terms())
}
other => other,
};
}
fn step_subject(request: &GitRequest) -> String {
return format!(
"{}, {}",
command_text(&request.arguments),
class_of(request.arguments.as_slice()).label()
);
}
fn run_git_watched(context: &GitContext, request: &GitRequest, terms: WatchTerms) -> std::io::Result<GitOutput> {
let mut command: Command = build_command(context, request);
match &request.input {
Some(bytes) => {
command.stdin(Stdio::from(input_file(bytes.as_slice(), context.common_dir.as_deref())?));
}
None => {
command.stdin(Stdio::null());
}
}
let subject: String = step_subject(request);
let ledger_step: String = crate::add_timing::git_step_label(request.arguments.as_slice());
let bound: StepBound = step_bound(request);
let _recorded = if matches!(bound, StepBound::Unbounded) {
crate::service_busy_step_record_on_this_thread::enter_step(format!("{} in {}", command_text(&request.arguments), request.cwd.display()))
} else {
None
};
let output: std::process::Output =
output_watched_within(&mut command, subject.as_str(), ledger_step.as_str(), terms, may_be_rerun(request), bound)?;
return Ok(GitOutput {
code: output.status.code(),
stdout: output.stdout,
stderr: output.stderr,
});
}
pub fn run_git_checked(
context: &GitContext,
request: &GitRequest,
) -> Result<GitOutput, GitFailure> {
return checked_from(run_git(context, request), request.arguments.as_slice());
}
pub(crate) fn checked_from(outcome: std::io::Result<GitOutput>, arguments: &[OsString]) -> Result<GitOutput, GitFailure> {
let output: GitOutput = match outcome {
Ok(finished) => finished,
Err(error) if error.kind() == std::io::ErrorKind::TimedOut => {
return Err(GitFailure(error.to_string()));
}
Err(error) => {
return Err(GitFailure(format!("{} could not start: {error}", command_text(arguments))));
}
};
if !output.succeeded() {
return Err(GitFailure(format!("{} failed: {}", command_text(arguments), output.error_text())));
}
return Ok(output);
}
#[cfg(test)]
#[path = "transaction_git_tests.rs"]
mod tests;