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::line_sink::LineSink;
use crate::parallel_runner::{Event, OrderedEvents, OrderedRun, OrderedWork, 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);
}
struct ScopeOfEachUpdate<'a> {
git: &'a PushGit,
tips: &'a BTreeMap<String, Vec<String>>,
}
impl OrderedWork<PushUpdate, Result<UpdateScope, ManualPushRangeError>> for ScopeOfEachUpdate<'_> {
fn run(&self, _index: usize, update: &PushUpdate, _say: &mut dyn LineSink) -> Result<UpdateScope, ManualPushRangeError> {
let published: Vec<String> = self.tips.get(&update.remote_name).cloned().unwrap_or_default();
return update_scope(self.git, update, &published);
}
}
struct KeepScopesUntilTheFirstFailure<'a> {
scopes: &'a mut Vec<UpdateScope>,
failure: &'a mut Option<ManualPushRangeError>,
}
impl OrderedEvents<Result<UpdateScope, ManualPushRangeError>> for KeepScopesUntilTheFirstFailure<'_> {
fn on_event(&mut self, event: Event<'_, Result<UpdateScope, ManualPushRangeError>>) {
if let Event::End(_index, outcome) = event {
match outcome {
Ok(scope) if self.failure.is_none() => self.scopes.push(scope),
Err(error) if self.failure.is_none() => *self.failure = Some(error),
Ok(_) | Err(_) => {}
}
}
}
}
pub fn scopes_of_updates_on(
host: &HostResources,
git: &PushGit,
updates: &[PushUpdate],
) -> Result<Vec<UpdateScope>, ManualPushFailure> {
let mut remote_names: Vec<String> = Vec::new();
for update in updates {
if !remote_names.contains(&update.remote_name) {
remote_names.push(update.remote_name.clone());
}
}
let tracking: BTreeMap<String, Vec<String>> = match tracking_commits(git, &remote_names) {
Ok(found) => found,
Err(error) => return Err(from_range(&error)),
};
let mut prior_oids: Vec<String> = Vec::new();
for update in updates {
if let Some(oid) = update.remote_oid.clone() {
prior_oids.push(oid);
}
}
let prior: BTreeMap<String, String> = match peel_commits(git, &prior_oids) {
Ok(found) => found,
Err(error) => return Err(from_range(&error)),
};
let tips: BTreeMap<String, Vec<String>> = published_tips(updates, &prior, &tracking);
let mut lanes: Vec<Vec<usize>> = Vec::new();
for index in 0..updates.len() {
lanes.push(vec![index]);
}
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: &ScopeOfEachUpdate { git, tips: &tips },
on_event: &mut KeepScopesUntilTheFirstFailure { scopes: &mut scopes, failure: &mut failure },
});
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 Some(oid) = parse_object_id(name.as_bytes()) else {
return Err(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 Some(oid) = parse_object_id(tip.as_bytes()) else {
return Err(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 = match probe_manual_push_updates(git, global_prefix, post_subcommand) {
Ok(found) => found,
Err(error) => return Err(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;