use crate::auto_push_single_flight::PushGit;
use crate::candidate_object::parse_object_id;
use crate::candidate_prediction::PushedScope;
use crate::manual_push_cause::ManualPushCause;
use crate::manual_push_probe::{GitRefusal, MOST_CONCURRENT_LOCAL_QUERIES, PushUpdate, QUERY_MEMORY, probe_manual_push_updates};
use crate::parallel_budget::{BudgetRequest, HostResources, worker_budget};
use crate::parallel_runner::{Event, OrderedRun, run_ordered};
use crate::manual_push_range::{
ManualPushRangeError, UpdateScope, peel_commits, published_tips, tracking_commits, update_scope,
};
use std::collections::BTreeMap;
use std::ffi::OsString;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ManualPushFailure {
pub message: String,
pub refusal: Option<GitRefusal>,
pub cause: ManualPushCause,
}
impl ManualPushFailure {
pub fn step(cause: ManualPushCause, message: String) -> Self {
return Self { message, refusal: None, cause };
}
}
impl std::fmt::Display for ManualPushFailure {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str(&self.message);
}
}
impl std::error::Error for ManualPushFailure {}
fn from_probe(error: &crate::manual_push_probe::ManualPushProbeError) -> ManualPushFailure {
return ManualPushFailure {
message: error.message.clone(),
refusal: error.refusal.clone(),
cause: error.cause,
};
}
fn from_range(error: &ManualPushRangeError) -> ManualPushFailure {
return ManualPushFailure::step(error.cause, error.message.clone());
}
pub fn scopes_of_updates(
git: &PushGit,
updates: &[PushUpdate],
) -> Result<Vec<UpdateScope>, ManualPushFailure> {
return scopes_of_updates_on(&HostResources::read(), git, updates);
}
pub fn scopes_of_updates_on(
host: &HostResources,
git: &PushGit,
updates: &[PushUpdate],
) -> Result<Vec<UpdateScope>, ManualPushFailure> {
let remote_names: Vec<String> = updates.iter().fold(Vec::new(), |mut names, update| {
if !names.contains(&update.remote_name) {
names.push(update.remote_name.clone());
}
return names;
});
let tracking: BTreeMap<String, Vec<String>> =
tracking_commits(git, &remote_names).map_err(|error| {
return from_range(&error);
})?;
let prior_oids: Vec<String> = updates
.iter()
.filter_map(|update| {
return update.remote_oid.clone();
})
.collect();
let prior: BTreeMap<String, String> = peel_commits(git, &prior_oids).map_err(|error| {
return from_range(&error);
})?;
let tips: BTreeMap<String, Vec<String>> = published_tips(updates, &prior, &tracking);
let lanes: Vec<Vec<usize>> = (0..updates.len()).map(|index: usize| { return vec![index]; }).collect();
let workers: usize = worker_budget(&BudgetRequest { items: updates.len(), per_item_memory: QUERY_MEMORY, ceiling: Some(MOST_CONCURRENT_LOCAL_QUERIES), cap: None, host });
let mut scopes: Vec<UpdateScope> = Vec::new();
let mut failure: Option<ManualPushRangeError> = None;
run_ordered(OrderedRun {
items: updates,
lanes: lanes.as_slice(),
workers,
work: &|_index: usize, update: &PushUpdate, _say: &mut dyn FnMut(&str)| {
let published: Vec<String> = tips.get(&update.remote_name).cloned().unwrap_or_default();
return update_scope(git, update, &published);
},
on_event: &mut |event: Event<'_, Result<UpdateScope, ManualPushRangeError>>| {
if let Event::End(_index, outcome) = event {
match outcome {
Ok(scope) if failure.is_none() => scopes.push(scope),
Err(error) if failure.is_none() => failure = Some(error),
Ok(_) | Err(_) => {}
}
}
},
});
return match failure {
Some(error) => Err(from_range(&error)),
None => Ok(scopes),
};
}
pub fn pushed_scope(scopes: &[UpdateScope]) -> Result<PushedScope, ManualPushFailure> {
let mut commits: Vec<crate::candidate_object::ObjectId> = Vec::new();
let mut trees: Vec<crate::candidate_object::ObjectId> = Vec::new();
for scope in scopes {
match scope {
UpdateScope::None => {}
UpdateScope::Commits(names) => {
for name in names {
let oid = parse_object_id(name.as_bytes()).ok_or_else(|| {
return ManualPushFailure::step(ManualPushCause::UnexpectedOutput, format!(
"Git named a commit that is not an object name: {name}"
));
})?;
if !commits.contains(&oid) {
commits.push(oid);
}
}
}
UpdateScope::FinalTree(tip) => {
let oid = parse_object_id(tip.as_bytes()).ok_or_else(|| {
return ManualPushFailure::step(ManualPushCause::UnexpectedOutput, format!("Git named a commit that is not an object name: {tip}"));
})?;
if !trees.contains(&oid) {
trees.push(oid);
}
}
}
}
return Ok(PushedScope { commits, trees });
}
pub fn manual_push_scope(
git: &PushGit,
global_prefix: &[OsString],
post_subcommand: &[OsString],
) -> Result<PushedScope, ManualPushFailure> {
let updates =
probe_manual_push_updates(git, global_prefix, post_subcommand).map_err(|error| {
return from_probe(&error);
})?;
let scopes = scopes_of_updates(git, &updates)?;
return pushed_scope(&scopes);
}
#[cfg(test)]
#[path = "manual_push_lifecycle_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "manual_push_parallel_tests.rs"]
mod parallel_tests;