use crate::auto_push_single_flight::{PushGit, run_query};
use crate::lfs_upload_plan::{GitQuery, resolve_push_remote};
use crate::lfs_output_mask::mask_userinfo_text;
use crate::manual_push_cause::ManualPushCause;
pub use crate::manual_push_probe_error::{GitRefusal, ManualPushProbeError};
use crate::manual_push_probe_error::{cause_of_query, probe_error};
use crate::manual_push_porcelain::{PorcelainBlock, PorcelainUpdate, RemoteBefore, parse_porcelain};
use crate::parallel_budget::{BudgetRequest, HostResources, worker_budget};
use crate::line_sink::LineSink;
use crate::parallel_runner::{Event, OrderedEvents, OrderedRun, OrderedWork, run_ordered};
use crate::command_options::{Boundary, DEFAULT_MODE, parse_options};
use crate::command_push::{PUSH_TABLE, push_shape};
use std::ffi::OsString;
pub use crate::manual_push_update_and_query_limits::{LOCAL_QUERY_GAIN_STOPS_AT, MEASURED_WHOLE_HISTORY_QUERY, MOST_CONCURRENT_LOCAL_QUERIES, MOST_CONCURRENT_REMOTE_QUERIES, PushUpdate};
pub const QUERY_MEMORY: u64 = MEASURED_WHOLE_HISTORY_QUERY.next_power_of_two();
pub fn probe_arguments(global_prefix: &[OsString], post_subcommand: &[OsString]) -> Vec<OsString> {
let separator = match parse_options(post_subcommand, PUSH_TABLE, DEFAULT_MODE, &[]) {
Ok(parsed) => match parsed.boundary {
Boundary::DashDash(at) | Boundary::EndOfOptions(at) => Some(at),
Boundary::End | Boundary::NonOption(_) => None,
},
Err(_) => {
let mut first: Option<usize> = None;
for (position, argument) in post_subcommand.iter().enumerate() {
if argument.as_os_str() == "--" {
first = Some(position);
break;
}
}
first
}
};
let insertion = separator.unwrap_or(post_subcommand.len());
let mut arguments: Vec<OsString> = global_prefix.to_vec();
for setting in [
format!("core.hooksPath={}", crate::hooks_off::NO_HOOKS_DIRECTORY),
String::from("hook.pre-push.enabled=false"),
String::from("core.abbrev=no"),
] {
arguments.push(OsString::from("-c"));
arguments.push(OsString::from(setting));
}
arguments.push(OsString::from("push"));
arguments.extend(post_subcommand[..insertion].iter().cloned());
arguments.push(OsString::from("--no-quiet"));
arguments.push(OsString::from("--dry-run"));
arguments.push(OsString::from("--porcelain"));
arguments.extend(post_subcommand[insertion..].iter().cloned());
return arguments;
}
fn looks_like_location(typed: &str) -> bool {
if typed.contains("://") || typed.starts_with('/') || typed.starts_with("./") || typed.starts_with("../") {
return true;
}
return match typed.split_once(':') {
Some((front, _)) => !front.contains('/'),
None => false,
};
}
fn answer(git: &PushGit, arguments: &[OsString]) -> Option<String> {
let output = run_query(git, arguments).ok()?;
if output.code != Some(0) {
return None;
}
return Some(String::from_utf8_lossy(output.stdout.as_slice()).trim().to_owned());
}
fn locations_of(git: &PushGit, global_prefix: &[OsString], remote: &str) -> Vec<String> {
let mut arguments: Vec<OsString> = global_prefix.to_vec();
arguments.extend(["remote", "get-url", "--push", "--all", remote].map(OsString::from));
return match answer(git, arguments.as_slice()) {
Some(listing) if !listing.is_empty() => listing.lines().map(str::to_owned).collect(),
Some(_) | None => vec![remote.to_owned()],
};
}
fn local_objects(git: &PushGit, global_prefix: &[OsString], names: &[&str]) -> Result<Vec<String>, ManualPushProbeError> {
if names.is_empty() {
return Ok(Vec::new());
}
let mut arguments: Vec<OsString> = global_prefix.to_vec();
arguments.push(OsString::from("rev-parse"));
arguments.extend(names.iter().map(OsString::from));
let Some(listing) = answer(git, arguments.as_slice()) else {
return Err(probe_error(ManualPushCause::ContentMissing, format!("the local objects of {names:?} could not be read")));
};
let objects: Vec<String> = listing.lines().map(str::to_owned).collect();
if objects.len() != names.len() {
return Err(probe_error(ManualPushCause::UnexpectedOutput, format!("rev-parse answered {} names for {}", objects.len(), names.len())));
}
return Ok(objects);
}
pub fn confirm_group(
git: &PushGit,
remote_location: &str,
remote_name: &str,
updates: &[(PorcelainUpdate, Option<String>)],
) -> Result<Vec<PushUpdate>, ManualPushProbeError> {
let mut arguments: Vec<OsString> = vec![
OsString::from("ls-remote"),
OsString::from("--refs"),
OsString::from(remote_location),
];
for (update, _local) in updates {
arguments.push(OsString::from(&update.to));
}
let output = match run_query(git, arguments.as_slice()) {
Ok(found) => found,
Err(error) => return Err(probe_error(cause_of_query(&error), mask_userinfo_text(format!("ls-remote of {remote_location} failed: {error}").as_str()))),
};
if output.code != Some(0) {
return Err(probe_error(
ManualPushCause::RemoteUnreachable,
mask_userinfo_text(
format!("ls-remote of {remote_location} failed: {}", String::from_utf8_lossy(&output.stderr).trim_end()).as_str(),
),
));
}
let listing: String = String::from_utf8_lossy(&output.stdout).into_owned();
let mut confirmed: Vec<PushUpdate> = Vec::new();
for (update, local) in updates {
let mut authoritative: Option<String> = None;
for line in listing.lines() {
if let Some((oid, name)) = line.split_once('\t')
&& name == update.to
{
authoritative = Some(oid.to_owned());
break;
}
}
let changed: bool = match &update.before {
RemoteBefore::Absent => authoritative.is_some(),
RemoteBefore::Known(reported) => authoritative.as_ref() != Some(reported),
RemoteBefore::Unknown => false,
};
if changed {
return Err(probe_error(ManualPushCause::RemoteMoved, format!("Remote ref changed during manual-push discovery: {}", update.to)));
}
confirmed.push(PushUpdate {
local_oid: local.clone(),
remote_oid: authoritative,
remote_name: remote_name.to_owned(),
remote_ref: update.to.clone(),
});
}
return Ok(confirmed);
}
pub fn probe_manual_push_updates(
git: &PushGit,
global_prefix: &[OsString],
post_subcommand: &[OsString],
) -> Result<Vec<PushUpdate>, ManualPushProbeError> {
return probe_manual_push_updates_on(&HostResources::read(), git, global_prefix, post_subcommand);
}
pub fn probe_manual_push_updates_on(
host: &HostResources,
git: &PushGit,
global_prefix: &[OsString],
post_subcommand: &[OsString],
) -> Result<Vec<PushUpdate>, ManualPushProbeError> {
let arguments: Vec<OsString> = probe_arguments(global_prefix, post_subcommand);
let output = match run_query(git, arguments.as_slice()) {
Ok(found) => found,
Err(error) => return Err(probe_error(cause_of_query(&error), format!("probe push could not run: {error}"))),
};
let report: String = String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
let blocks = match parse_porcelain(report.as_str()) {
Ok(found) => found,
Err(message) => return Err(probe_error(ManualPushCause::UnexpectedOutput, message)),
};
if blocks.is_empty() {
if output.code == Some(0) {
return Ok(Vec::new());
}
let said: String = String::from_utf8_lossy(&output.stderr).into_owned();
let message: String = mask_userinfo_text(format!("Git could not determine push updates: {}", said.trim_end()).as_str());
return Err(match output.code {
Some(code) if !said.trim().is_empty() => {
ManualPushProbeError { message, refusal: Some(GitRefusal { code, stderr: said }), cause: ManualPushCause::SelfExplained }
}
Some(_) | None => probe_error(ManualPushCause::GitSilent, message),
});
}
let mut named = None;
if let Some(shape) = push_shape(post_subcommand) {
named = shape.repository;
}
let mut typed_location: Option<String> = None;
if let Some(typed) = named.as_ref() {
typed_location = Some(typed.to_string_lossy().into_owned());
}
let mut asking: RemoteResolverQuery<'_> = RemoteResolverQuery { git, global_prefix };
let resolved_remote = match resolve_push_remote(named, &mut asking) {
Ok(found) => found,
Err(error) => return Err(probe_error(ManualPushCause::SelfExplained, format!("Git could not tell which remote this push goes to: {}", error.0))),
};
let remote_name: String = resolved_remote.to_string_lossy().into_owned();
let locations: Vec<String> = match typed_location {
Some(typed) if looks_like_location(typed.as_str()) => vec![typed],
_ => locations_of(git, global_prefix, remote_name.as_str()),
};
if locations.len() != blocks.len() {
return Err(probe_error(
ManualPushCause::SelfExplained,
mask_userinfo_text(
format!("Git reported {} destinations but {remote_name} has {} push locations", blocks.len(), locations.len()).as_str(),
),
));
}
let destinations: Vec<(&PorcelainBlock, &String)> = blocks.iter().zip(locations.iter()).collect();
let mut lanes: Vec<Vec<usize>> = Vec::new();
for index in 0..destinations.len() {
lanes.push(vec![index]);
}
let workers: usize = worker_budget(&BudgetRequest { items: destinations.len(), per_item_memory: QUERY_MEMORY, ceiling: Some(MOST_CONCURRENT_REMOTE_QUERIES), cap: None, host });
let mut confirmed: Vec<PushUpdate> = Vec::new();
let mut failure: Option<ManualPushProbeError> = None;
run_ordered(OrderedRun {
items: destinations.as_slice(),
lanes: lanes.as_slice(),
workers,
work: &ConfirmEachDestination { git, global_prefix, remote_name: remote_name.as_str() },
on_event: &mut KeepConfirmedUntilTheFirstFailure { confirmed: &mut confirmed, failure: &mut failure },
});
return match failure {
Some(error) => Err(error),
None => Ok(confirmed),
};
}
struct RemoteResolverQuery<'a> {
git: &'a PushGit,
global_prefix: &'a [OsString],
}
impl GitQuery for RemoteResolverQuery<'_> {
fn ask(&mut self, query: &[&str]) -> Result<String, String> {
let mut owned: Vec<OsString> = self.global_prefix.to_vec();
for part in query {
owned.push(OsString::from(part));
}
let answered = match run_query(self.git, owned.as_slice()) {
Ok(found) => found,
Err(error) => return Err(error.to_string()),
};
let text: String = String::from_utf8_lossy(answered.stdout.as_slice()).trim().to_owned();
return match answered.code {
Some(0) => Ok(text),
Some(1) if text.is_empty() => Ok(text),
code => Err(format!("exited with {code:?}")),
};
}
}
struct ConfirmEachDestination<'a> {
git: &'a PushGit,
global_prefix: &'a [OsString],
remote_name: &'a str,
}
impl OrderedWork<(&PorcelainBlock, &String), Result<Vec<PushUpdate>, ManualPushProbeError>> for ConfirmEachDestination<'_> {
fn run(&self, _index: usize, destination: &(&PorcelainBlock, &String), _say: &mut dyn LineSink) -> Result<Vec<PushUpdate>, ManualPushProbeError> {
return confirm_block(self.git, self.global_prefix, self.remote_name, destination.0, destination.1.as_str());
}
}
struct KeepConfirmedUntilTheFirstFailure<'a> {
confirmed: &'a mut Vec<PushUpdate>,
failure: &'a mut Option<ManualPushProbeError>,
}
impl OrderedEvents<Result<Vec<PushUpdate>, ManualPushProbeError>> for KeepConfirmedUntilTheFirstFailure<'_> {
fn on_event(&mut self, event: Event<'_, Result<Vec<PushUpdate>, ManualPushProbeError>>) {
if let Event::End(_index, outcome) = event {
match outcome {
Ok(updates) if self.failure.is_none() => self.confirmed.extend(updates),
Err(error) if self.failure.is_none() => *self.failure = Some(error),
Ok(_) | Err(_) => {}
}
}
}
}
fn confirm_block(git: &PushGit, global_prefix: &[OsString], remote_name: &str, block: &PorcelainBlock, location: &str) -> Result<Vec<PushUpdate>, ManualPushProbeError> {
let mut unnamed: Vec<&str> = Vec::new();
for update in &block.updates {
if !update.from.is_empty() && update.after.is_none() {
unnamed.push(update.from.as_str());
}
}
let mut resolved = local_objects(git, global_prefix, unnamed.as_slice())?.into_iter();
let mut group: Vec<(PorcelainUpdate, Option<String>)> = Vec::new();
for update in &block.updates {
let local: Option<String> = if update.from.is_empty() {
None
} else if update.after.is_some() {
update.after.clone()
} else {
resolved.next()
};
group.push((update.clone(), local));
}
return confirm_group(git, location, remote_name, group.as_slice());
}
#[cfg(test)]
#[path = "manual_push_probe_tests.rs"]
mod tests;