git_policy_cli/push/
manual_push_lifecycle.rs1use crate::auto_push_single_flight::PushGit;
15use crate::candidate_object::parse_object_id;
16use crate::candidate_prediction::PushedScope;
17use crate::manual_push_cause::ManualPushCause;
18use crate::manual_push_probe::{GitRefusal, MOST_CONCURRENT_LOCAL_QUERIES, PushUpdate, QUERY_MEMORY, probe_manual_push_updates};
19use crate::parallel_budget::{BudgetRequest, HostResources, worker_budget};
20use crate::parallel_runner::{Event, OrderedRun, run_ordered};
21use crate::manual_push_range::{
22 ManualPushRangeError, UpdateScope, peel_commits, published_tips, tracking_commits, update_scope,
23};
24use std::collections::BTreeMap;
26use std::ffi::OsString;
28
29#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct ManualPushFailure {
40 pub message: String,
42 pub refusal: Option<GitRefusal>,
44 pub cause: ManualPushCause,
46}
47
48impl ManualPushFailure {
49 pub fn step(cause: ManualPushCause, message: String) -> Self {
51 return Self { message, refusal: None, cause };
52 }
53}
54
55impl std::fmt::Display for ManualPushFailure {
56 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 return formatter.write_str(&self.message);
59 }
60}
61
62impl std::error::Error for ManualPushFailure {}
63
64fn from_probe(error: &crate::manual_push_probe::ManualPushProbeError) -> ManualPushFailure {
71 return ManualPushFailure {
72 message: error.message.clone(),
73 refusal: error.refusal.clone(),
74 cause: error.cause,
75 };
76}
77
78fn from_range(error: &ManualPushRangeError) -> ManualPushFailure {
85 return ManualPushFailure::step(error.cause, error.message.clone());
86}
87
88pub fn scopes_of_updates(
98 git: &PushGit,
99 updates: &[PushUpdate],
100) -> Result<Vec<UpdateScope>, ManualPushFailure> {
101 return scopes_of_updates_on(&HostResources::read(), git, updates);
102}
103
104pub fn scopes_of_updates_on(
106 host: &HostResources,
107 git: &PushGit,
108 updates: &[PushUpdate],
109) -> Result<Vec<UpdateScope>, ManualPushFailure> {
110 let remote_names: Vec<String> = updates.iter().fold(Vec::new(), |mut names, update| {
111 if !names.contains(&update.remote_name) {
112 names.push(update.remote_name.clone());
113 }
114 return names;
115 });
116 let tracking: BTreeMap<String, Vec<String>> =
117 tracking_commits(git, &remote_names).map_err(|error| {
118 return from_range(&error);
119 })?;
120 let prior_oids: Vec<String> = updates
121 .iter()
122 .filter_map(|update| {
123 return update.remote_oid.clone();
124 })
125 .collect();
126 let prior: BTreeMap<String, String> = peel_commits(git, &prior_oids).map_err(|error| {
127 return from_range(&error);
128 })?;
129 let tips: BTreeMap<String, Vec<String>> = published_tips(updates, &prior, &tracking);
130 let lanes: Vec<Vec<usize>> = (0..updates.len()).map(|index: usize| { return vec![index]; }).collect();
134 let workers: usize = worker_budget(&BudgetRequest { items: updates.len(), per_item_memory: QUERY_MEMORY, ceiling: Some(MOST_CONCURRENT_LOCAL_QUERIES), cap: None, host });
135 let mut scopes: Vec<UpdateScope> = Vec::new();
136 let mut failure: Option<ManualPushRangeError> = None;
137 run_ordered(OrderedRun {
138 items: updates,
139 lanes: lanes.as_slice(),
140 workers,
141 work: &|_index: usize, update: &PushUpdate, _say: &mut dyn FnMut(&str)| {
142 let published: Vec<String> = tips.get(&update.remote_name).cloned().unwrap_or_default();
143 return update_scope(git, update, &published);
144 },
145 on_event: &mut |event: Event<'_, Result<UpdateScope, ManualPushRangeError>>| {
146 if let Event::End(_index, outcome) = event {
147 match outcome {
148 Ok(scope) if failure.is_none() => scopes.push(scope),
149 Err(error) if failure.is_none() => failure = Some(error),
150 Ok(_) | Err(_) => {}
151 }
152 }
153 },
154 });
155 return match failure {
156 Some(error) => Err(from_range(&error)),
157 None => Ok(scopes),
158 };
159}
160
161pub fn pushed_scope(scopes: &[UpdateScope]) -> Result<PushedScope, ManualPushFailure> {
171 let mut commits: Vec<crate::candidate_object::ObjectId> = Vec::new();
172 let mut trees: Vec<crate::candidate_object::ObjectId> = Vec::new();
173 for scope in scopes {
174 match scope {
175 UpdateScope::None => {}
176 UpdateScope::Commits(names) => {
177 for name in names {
178 let oid = parse_object_id(name.as_bytes()).ok_or_else(|| {
179 return ManualPushFailure::step(ManualPushCause::UnexpectedOutput, format!(
180 "Git named a commit that is not an object name: {name}"
181 ));
182 })?;
183 if !commits.contains(&oid) {
184 commits.push(oid);
185 }
186 }
187 }
188 UpdateScope::FinalTree(tip) => {
189 let oid = parse_object_id(tip.as_bytes()).ok_or_else(|| {
190 return ManualPushFailure::step(ManualPushCause::UnexpectedOutput, format!("Git named a commit that is not an object name: {tip}"));
191 })?;
192 if !trees.contains(&oid) {
193 trees.push(oid);
194 }
195 }
196 }
197 }
198 return Ok(PushedScope { commits, trees });
199}
200
201pub fn manual_push_scope(
213 git: &PushGit,
214 global_prefix: &[OsString],
215 post_subcommand: &[OsString],
216) -> Result<PushedScope, ManualPushFailure> {
217 let updates =
218 probe_manual_push_updates(git, global_prefix, post_subcommand).map_err(|error| {
219 return from_probe(&error);
220 })?;
221 let scopes = scopes_of_updates(git, &updates)?;
222 return pushed_scope(&scopes);
223}
224
225#[cfg(test)]
227#[path = "manual_push_lifecycle_tests.rs"]
228mod tests;
229
230#[cfg(test)]
232#[path = "manual_push_parallel_tests.rs"]
233mod parallel_tests;