1use super::capture_invocation::InvocationCapture;
16use super::commit_facts::PrivateIndexFacts;
17use super::commit_landing_lock::{acquire_reserved_lock, landing_is_nested};
18use super::commit_preparation::PreparedCommit;
19use super::commit_replay_options::ReplayOptions;
20use super::commit_revalidation::{Revalidation, RevalidationContext, revalidate_replay};
21use super::commit_transaction::{CommitInvocation, TransactionStop, failed};
22use super::commit_workspace::CommitWorkspace;
23use super::commit_worktree_records::settled_entry;
24use super::diagnostic_log::debug;
25use super::event_path::EventPath;
26use super::landing_attempt::{LandingOutcome, LandingPayload, LandingRequest, land_transaction};
27use super::landing_findings::{LANDING_FINDING_EXIT_CODE, landing_finding};
28use super::landing_objects::migrate_shadow_objects;
29use super::owner_lock::OwnerLock;
30use super::policy_checks::ShippedChecks;
31use super::policy_engine::StageRequest;
32use super::policy_events::PolicyEvent;
33use super::replay_commit::{MergeStep, ReplayError, ReplayRequest, merge_replay, rebuild_replay};
34use super::replay_merge::ReplayedCommit;
35use super::replay_reservation::{LandingReservation, ReservationError};
36use super::repository_facts::RepositoryFacts;
37use super::transaction_journal::{AddedPath, Base, TransactionMode};
38use super::transaction_phase::{Phase, reach_transaction_phase};
39use std::path::{Path, PathBuf};
40
41const TAG: &str = "landWithReplay";
43
44struct Candidate {
54 new_oid: String,
56 tree: String,
58 expected_old: Base,
60 index: PathBuf,
62 exact_private_index: bool,
64 committed_paths: Vec<Vec<u8>>,
66 added_paths: Vec<AddedPath>,
68 worktree_records: Vec<AddedPath>,
70 events: Vec<PolicyEvent>,
72 attempt: i64,
74 lost_races: u64,
76}
77
78pub struct LoopInput<'a, F: RepositoryFacts> {
88 pub invocation: &'a CommitInvocation<'a>,
90 pub workspace: &'a mut CommitWorkspace,
92 pub capture: &'a InvocationCapture,
94 pub mode: TransactionMode,
96 pub request: &'a StageRequest,
98 pub checks: &'a mut ShippedChecks<PrivateIndexFacts<F>>,
100 pub prepared: &'a PreparedCommit,
102 pub settled_events: Vec<PolicyEvent>,
104 pub options: &'a ReplayOptions,
106 pub global_args: &'a [String],
108 pub read_only_selection: bool,
110 pub committed_paths: Vec<Vec<u8>>,
112 pub added_paths: Vec<AddedPath>,
114 pub worktree_records: Vec<AddedPath>,
116 pub index_lock_timeout_ms: u64,
118 pub reserve_after_lost_races: u64,
120}
121
122pub enum LoopOutcome {
132 Landed {
134 oid: String,
136 events: Vec<PolicyEvent>,
138 worktree_records: Vec<AddedPath>,
140 added_paths: Vec<AddedPath>,
142 },
143 Failed {
145 events: Vec<PolicyEvent>,
147 exit_code: i32,
149 },
150}
151
152fn retarget_records(
162 input: &CommitInvocation<'_>,
163 index: &Path,
164 records: &[AddedPath],
165) -> Result<Vec<AddedPath>, TransactionStop> {
166 let mut retargeted: Vec<AddedPath> = Vec::new();
167 for record in records {
168 let Some((mode, oid)) = settled_entry(input.context, input.cwd, index, record.path.as_slice())? else {
169 continue;
170 };
171 if mode != record.git_mode {
172 continue;
173 }
174 retargeted.push(AddedPath {
175 intended_oid: oid,
176 ..record.clone()
177 });
178 }
179 return Ok(retargeted);
180}
181
182fn replay_request<'a, F: RepositoryFacts>(
192 input: &'a LoopInput<'a, F>,
193 replay: u64,
194 onto: &'a str,
195) -> ReplayRequest<'a> {
196 return ReplayRequest {
197 context: input.invocation.context,
198 shadow: input.workspace.landing.shadow.as_path(),
199 object_directory: input.workspace.object_directory.as_path(),
200 worktree_root: input.capture.layout.repository_root.as_path(),
201 git_dir: input.capture.layout.git_dir.as_path(),
202 transaction_directory: input.workspace.landing.directory.as_path(),
203 replay,
204 base: match &input.capture.base {
205 Base::Commit(oid) => Some(oid.as_str()),
206 Base::Unborn => None,
207 },
208 empty_tree: input.capture.layout.empty_tree_oid.as_str(),
209 current: onto,
210 prepared: input.prepared.oid.as_str(),
211 signing_key: input.options.signing_key.as_deref(),
212 global_arguments: input.global_args,
213 };
214}
215
216fn replay_failure(capture: &InvocationCapture, onto: &str, cause: String) -> LoopOutcome {
227 return LoopOutcome::Failed {
228 events: vec![PolicyEvent::EngineFailure {
229 code: super::diagnostics::EngineFailureCode::TransactionFailed,
230 message: format!(
231 "{} moved while this commit was prepared, and replaying it onto {onto} failed; nothing landed: {cause}",
232 capture.layout.target_ref
233 ),
234 trigger: Some(super::policy_trigger::Trigger::PreForward),
235 policy: None,
236 path: None,
237 }],
238 exit_code: 2,
239 };
240}
241
242fn failed_events(settled: &[PolicyEvent], raced: &[PolicyEvent], last: Vec<PolicyEvent>) -> Vec<PolicyEvent> {
252 let mut events: Vec<PolicyEvent> = settled.to_vec();
253 events.extend(raced.iter().cloned());
254 events.extend(last);
255 return events;
256}
257
258fn replay_candidate<F: RepositoryFacts>(
268 input: &mut LoopInput<'_, F>,
269 candidate: &Candidate,
270 raced: Vec<PolicyEvent>,
271 onto: &str,
272) -> Result<Result<Candidate, LoopOutcome>, TransactionStop> {
273 let replay: u64 = lost_races_after(candidate.lost_races);
274 let merge: Result<MergeStep, ReplayError> = merge_replay(&replay_request(input, replay, onto));
275 let merged_tree: String = match merge {
276 Err(error) => return Ok(Err(replay_failure(input.capture, onto, replay_error_text(&error)))),
277 Ok(MergeStep::Conflict(conflict)) => {
278 migrate_shadow_objects(
280 input.invocation.context,
281 input.invocation.cwd,
282 input.workspace.landing.shadow.as_path(),
283 input.prepared.oid.as_str(),
284 &input.capture.base,
285 None,
286 )?;
287 let last: Vec<PolicyEvent> = vec![PolicyEvent::ReplayConflict {
288 target_ref: input.capture.layout.target_ref.clone(),
289 paths: conflict
290 .paths
291 .iter()
292 .map(|path| { return EventPath::from_git_bytes(path.as_slice()); })
293 .collect(),
294 winning_oid: conflict.winning_oid,
295 prepared_oid: conflict.prepared_oid,
296 }];
297 return Ok(Err(LoopOutcome::Failed {
298 events: failed_events(input.settled_events.as_slice(), raced.as_slice(), last),
299 exit_code: 1,
300 }));
301 }
302 Ok(MergeStep::Merged { tree, subsumed_paths, kept_landed_paths }) => {
303 debug(
304 TAG,
305 format!("replay onto {onto} keeps the prepared entries of {subsumed_paths:?} and the landed entries of {kept_landed_paths:?}").as_str(),
306 );
307 tree
308 }
309 };
310 let revalidation: Revalidation = {
311 let context: RevalidationContext<'_> = RevalidationContext {
312 invocation: input.invocation,
313 capture: input.capture,
314 read_only_selection: input.read_only_selection,
315 };
316 revalidate_replay(
317 &context,
318 input.workspace,
319 input.request,
320 input.checks,
321 replay,
322 merged_tree.as_str(),
323 onto,
324 )?
325 };
326 let (tree, index, revalidation_events, revalidated_paths): (String, PathBuf, Vec<PolicyEvent>, Vec<Vec<u8>>) =
327 match revalidation {
328 Revalidation::Blocked { events, exit_code } => {
329 return Ok(Err(LoopOutcome::Failed {
330 events: failed_events(input.settled_events.as_slice(), raced.as_slice(), events),
331 exit_code,
332 }));
333 }
334 Revalidation::Revalidated {
335 tree,
336 index,
337 events,
338 committed_paths,
339 } => (tree, index, events, committed_paths),
340 };
341 let rebuilt = match rebuild_replay(&replay_request(input, replay, onto), tree.as_str()) {
342 Ok(found) => found,
343 Err(error) => return Ok(Err(replay_failure(input.capture, onto, replay_error_text(&error)))),
344 };
345 let committed_paths: Vec<Vec<u8>> = union_paths(input.committed_paths.as_slice(), revalidated_paths);
346 let worktree_records: Vec<AddedPath> = retarget_records(input.invocation, index.as_path(), input.worktree_records.as_slice())?;
347 let mut events: Vec<PolicyEvent> = replayed_events(
348 raced,
349 input.prepared.oid.as_str(),
350 &input.capture.base,
351 onto,
352 &rebuilt,
353 );
354 events.extend(revalidation_events);
355 return Ok(Ok(Candidate {
356 new_oid: rebuilt.oid,
357 tree: tree.clone(),
358 expected_old: Base::Commit(String::from(onto)),
359 index: index.clone(),
360 exact_private_index: false,
361 committed_paths,
362 added_paths: retarget_records(input.invocation, index.as_path(), input.added_paths.as_slice())?,
363 worktree_records,
364 events,
365 attempt: next_attempt_number(candidate.attempt),
366 lost_races: lost_races_after(candidate.lost_races),
367 }));
368}
369
370pub fn union_paths(committed: &[Vec<u8>], revalidated: Vec<Vec<u8>>) -> Vec<Vec<u8>> {
380 let mut paths: Vec<Vec<u8>> = committed.to_vec();
381 let known: std::collections::HashSet<&[u8]> = committed.iter().map(Vec::as_slice).collect();
383 let mut added: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
384 for path in revalidated {
385 if known.contains(path.as_slice()) || !added.insert(path.clone()) {
386 continue;
387 }
388 paths.push(path);
389 }
390 return paths;
391}
392
393pub fn replayed_events(
403 raced: Vec<PolicyEvent>,
404 prepared_oid: &str,
405 base: &Base,
406 onto: &str,
407 rebuilt: &ReplayedCommit,
408) -> Vec<PolicyEvent> {
409 let mut events: Vec<PolicyEvent> = raced;
410 events.push(PolicyEvent::CommitReplayed {
411 prepared_oid: String::from(prepared_oid),
412 from_base: match base {
413 Base::Commit(oid) => Some(oid.clone()),
414 Base::Unborn => None,
415 },
416 onto: String::from(onto),
417 oid: rebuilt.oid.clone(),
418 });
419 if !rebuilt.dropped_headers.is_empty() {
420 events.push(PolicyEvent::ReplayHeadersDropped {
421 prepared_oid: String::from(prepared_oid),
422 oid: rebuilt.oid.clone(),
423 headers: rebuilt.dropped_headers.clone(),
424 });
425 }
426 return events;
427}
428
429pub fn next_attempt_number(attempt: i64) -> i64 {
439 return attempt + 1;
440}
441
442pub fn lost_races_after(lost_races: u64) -> u64 {
452 return lost_races + 1;
453}
454
455fn replay_error_text(error: &ReplayError) -> String {
465 return match error {
466 ReplayError::Shared(inner) => format!("{inner:?}"),
467 ReplayError::Capture(inner) => format!("{inner:?}"),
468 ReplayError::Subsumption(inner) => format!("{inner:?}"),
469 ReplayError::Merge(inner) => format!("{inner:?}"),
470 ReplayError::Git(inner) => inner.0.clone(),
471 ReplayError::Object(text) => text.clone(),
472 };
473}
474
475fn reservation_stop(error: ReservationError) -> TransactionStop {
485 return failed(format!("the landing reservation failed: {error}"));
486}
487
488pub fn land_with_replay<F: RepositoryFacts>(mut input: LoopInput<'_, F>) -> Result<LoopOutcome, TransactionStop> {
499 let layout = &input.capture.layout;
500 let nested: bool = landing_is_nested(layout, input.invocation.environment);
501 let mut reservation: LandingReservation = LandingReservation::open(
502 layout.registry_root.as_path(),
503 input.workspace.landing.directory.as_path(),
504 input.workspace.landing.transaction_id.as_str(),
505 nested,
506 );
507 let result: Result<LoopOutcome, TransactionStop> = run_loop(&mut input, &mut reservation);
508 if let Err(error) = reservation.release() {
510 debug(TAG, format!("releasing the landing reservation failed: {error:?}").as_str());
511 }
512 return result;
513}
514
515fn run_loop<F: RepositoryFacts>(
525 input: &mut LoopInput<'_, F>,
526 reservation: &mut LandingReservation,
527) -> Result<LoopOutcome, TransactionStop> {
528 let mut candidate: Candidate = Candidate {
529 new_oid: input.prepared.oid.clone(),
530 tree: input.prepared.tree_oid.clone(),
531 expected_old: input.capture.base.clone(),
532 index: input.workspace.landing.commit_index.clone(),
533 exact_private_index: true,
534 committed_paths: input.committed_paths.clone(),
535 added_paths: input.added_paths.clone(),
536 worktree_records: input.worktree_records.clone(),
537 events: Vec::new(),
538 attempt: 1,
539 lost_races: 0,
540 };
541 loop {
542 let payload: LandingPayload = LandingPayload::Commit {
543 new_oid: candidate.new_oid.clone(),
544 landed_tree_oid: candidate.tree.clone(),
545 expected_old: candidate.expected_old.clone(),
546 landed_index: candidate.index.clone(),
547 exact_private_index: candidate.exact_private_index,
548 guarded_paths: Vec::new(),
549 };
550 let request: LandingRequest<'_> = LandingRequest {
551 capture: input.capture,
552 mode: input.mode,
553 payload: &payload,
554 committed_paths: candidate.committed_paths.as_slice(),
555 added_paths: candidate.added_paths.as_slice(),
556 selected_worktree_paths: candidate.worktree_records.as_slice(),
557 index_lock_timeout_ms: input.index_lock_timeout_ms,
558 attempt: candidate.attempt,
559 };
560 let outcome: LandingOutcome = {
561 let lock: OwnerLock = acquire_reserved_lock(
563 input.invocation.context,
564 input.invocation.cwd,
565 &input.capture.layout,
566 input.invocation.environment,
567 reservation,
568 )
569 .map_err(reservation_stop)?;
570 land_transaction(
571 input.invocation.context,
572 input.invocation.cwd,
573 &lock,
574 &mut input.workspace.landing,
575 &request,
576 )?
577 };
578 let onto: String = match &outcome {
579 LandingOutcome::Landed { oid } => {
580 return Ok(LoopOutcome::Landed {
581 oid: oid.clone(),
582 events: candidate.events,
583 worktree_records: candidate.worktree_records,
584 added_paths: candidate.added_paths,
585 });
586 }
587 LandingOutcome::HeadMoved {
588 current: Base::Commit(oid),
589 replayable: true,
590 } => oid.clone(),
591 _ => {
592 let mut last: Vec<PolicyEvent> = Vec::new();
593 if let Some(event) = landing_finding(&outcome, input.capture, Some(input.prepared.oid.as_str()), false) {
594 last.push(event);
595 }
596 return Ok(LoopOutcome::Failed {
597 events: failed_events(input.settled_events.as_slice(), candidate.events.as_slice(), last),
598 exit_code: LANDING_FINDING_EXIT_CODE,
599 });
600 }
601 };
602 let lost_races: u64 = lost_races_after(candidate.lost_races);
603 debug(TAG, format!("lost landing race {lost_races} to {onto}").as_str());
604 let reserved: bool = lost_races >= input.reserve_after_lost_races
605 && reservation.reserve(lost_races).map_err(reservation_stop)?;
606 reach_transaction_phase(Phase::RaceLost, Some(lost_races))?;
607 let mut raced: Vec<PolicyEvent> = candidate.events.clone();
608 raced.push(PolicyEvent::LandingRaceLost {
609 attempt: lost_races,
610 winning_oid: onto.clone(),
611 });
612 if reserved {
613 raced.push(PolicyEvent::LandingReserved { lost_races });
614 }
615 match replay_candidate(input, &candidate, raced, onto.as_str())? {
616 Ok(next) => candidate = next,
617 Err(ended) => return Ok(ended),
618 }
619 }
620}
621
622#[cfg(test)]
624#[path = "commit_landing_loop_tests.rs"]
625mod tests;