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::{HookChanges, settled_entry};
24use super::diagnostic_log::debug;
25use super::landing_attempt::{LandingOutcome, LandingPayload, LandingRequest, land_transaction};
26use super::landing_findings::{LANDING_FINDING_EXIT_CODE, landing_finding};
27use super::landing_objects::migrate_shadow_objects;
28use super::owner_lock::OwnerLock;
29use super::policy_checks::ShippedChecks;
30use super::policy_engine::StageRequest;
31use super::policy_events::PolicyEvent;
32use super::replay_commit::{MergeStep, ReplayError, ReplayRequest, merge_replay, rebuild_replay};
33use super::replay_merge::ReplayedCommit;
34use super::replay_reservation::{LandingReservation, ReservationError};
35use super::repository_facts::RepositoryFacts;
36use super::transaction_journal::{AddedPath, Base, TransactionMode};
37use super::transaction_phase::{Phase, reach_transaction_phase};
38use std::path::{Path, PathBuf};
39
40const TAG: &str = "landWithReplay";
42
43struct Candidate {
53 new_oid: String,
55 tree: String,
57 expected_old: Base,
59 index: PathBuf,
61 exact_private_index: bool,
63 approved_tree: String,
65 committed_paths: Vec<String>,
67 added_paths: Vec<AddedPath>,
69 worktree_records: Vec<AddedPath>,
71 hook_changes: HookChanges,
73 events: Vec<PolicyEvent>,
75 attempt: i64,
77 lost_races: u64,
79}
80
81pub struct LoopInput<'a, F: RepositoryFacts> {
91 pub invocation: &'a CommitInvocation<'a>,
93 pub workspace: &'a mut CommitWorkspace,
95 pub capture: &'a InvocationCapture,
97 pub mode: TransactionMode,
99 pub request: &'a StageRequest,
101 pub checks: &'a mut ShippedChecks<PrivateIndexFacts<F>>,
103 pub prepared: &'a PreparedCommit,
105 pub settled_events: Vec<PolicyEvent>,
107 pub options: &'a ReplayOptions,
109 pub global_args: &'a [String],
111 pub read_only_selection: bool,
113 pub committed_paths: Vec<String>,
115 pub added_paths: Vec<AddedPath>,
117 pub worktree_records: Vec<AddedPath>,
119 pub hook_changes: HookChanges,
121 pub index_lock_timeout_ms: u64,
123 pub reserve_after_lost_races: u64,
125}
126
127pub enum LoopOutcome {
137 Landed {
139 oid: String,
141 events: Vec<PolicyEvent>,
143 worktree_records: Vec<AddedPath>,
145 added_paths: Vec<AddedPath>,
147 },
148 Failed {
150 events: Vec<PolicyEvent>,
152 exit_code: i32,
154 },
155}
156
157fn retarget_records(
167 input: &CommitInvocation<'_>,
168 index: &Path,
169 records: &[AddedPath],
170) -> Result<Vec<AddedPath>, TransactionStop> {
171 let mut retargeted: Vec<AddedPath> = Vec::new();
172 for record in records {
173 let Some((mode, oid)) = settled_entry(input.context, input.cwd, index, record.path.as_bytes())? else {
174 continue;
175 };
176 if mode != record.git_mode {
177 continue;
178 }
179 retargeted.push(AddedPath {
180 intended_oid: oid,
181 ..record.clone()
182 });
183 }
184 return Ok(retargeted);
185}
186
187fn replay_request<'a, F: RepositoryFacts>(
197 input: &'a LoopInput<'a, F>,
198 replay: u64,
199 onto: &'a str,
200) -> ReplayRequest<'a> {
201 return ReplayRequest {
202 context: input.invocation.context,
203 shadow: input.workspace.landing.shadow.as_path(),
204 object_directory: input.workspace.object_directory.as_path(),
205 worktree_root: input.capture.layout.repository_root.as_path(),
206 git_dir: input.capture.layout.git_dir.as_path(),
207 transaction_directory: input.workspace.landing.directory.as_path(),
208 replay,
209 base: match &input.capture.base {
210 Base::Commit(oid) => Some(oid.as_str()),
211 Base::Unborn => None,
212 },
213 empty_tree: input.capture.layout.empty_tree_oid.as_str(),
214 current: onto,
215 prepared: input.prepared.oid.as_str(),
216 signing_key: input.options.signing_key.as_deref(),
217 global_arguments: input.global_args,
218 };
219}
220
221fn replay_failure(capture: &InvocationCapture, onto: &str, cause: String) -> LoopOutcome {
232 return LoopOutcome::Failed {
233 events: vec![PolicyEvent::EngineFailure {
234 code: super::diagnostics::EngineFailureCode::TransactionFailed,
235 message: format!(
236 "{} moved while this commit was prepared, and replaying it onto {onto} failed; nothing landed: {cause}",
237 capture.layout.target_ref
238 ),
239 trigger: Some(super::policy_trigger::Trigger::PreForward),
240 policy: None,
241 path: None,
242 }],
243 exit_code: 2,
244 };
245}
246
247fn failed_events(settled: &[PolicyEvent], raced: &[PolicyEvent], last: Vec<PolicyEvent>) -> Vec<PolicyEvent> {
257 let mut events: Vec<PolicyEvent> = settled.to_vec();
258 events.extend(raced.iter().cloned());
259 events.extend(last);
260 return events;
261}
262
263fn replay_candidate<F: RepositoryFacts>(
273 input: &mut LoopInput<'_, F>,
274 candidate: &Candidate,
275 raced: Vec<PolicyEvent>,
276 onto: &str,
277) -> Result<Result<Candidate, LoopOutcome>, TransactionStop> {
278 let replay: u64 = lost_races_after(candidate.lost_races);
279 let merge: Result<MergeStep, ReplayError> = merge_replay(&replay_request(input, replay, onto));
280 let merged_tree: String = match merge {
281 Err(error) => return Ok(Err(replay_failure(input.capture, onto, replay_error_text(&error)))),
282 Ok(MergeStep::Conflict(conflict)) => {
283 migrate_shadow_objects(
285 input.invocation.context,
286 input.invocation.cwd,
287 input.workspace.landing.shadow.as_path(),
288 input.prepared.oid.as_str(),
289 &input.capture.base,
290 None,
291 )?;
292 let last: Vec<PolicyEvent> = vec![PolicyEvent::ReplayConflict {
293 target_ref: input.capture.layout.target_ref.clone(),
294 paths: conflict.paths,
295 winning_oid: conflict.winning_oid,
296 prepared_oid: conflict.prepared_oid,
297 }];
298 return Ok(Err(LoopOutcome::Failed {
299 events: failed_events(input.settled_events.as_slice(), raced.as_slice(), last),
300 exit_code: 1,
301 }));
302 }
303 Ok(MergeStep::Merged { tree, subsumed_paths, kept_landed_paths }) => {
304 debug(
305 TAG,
306 format!("replay onto {onto} keeps the prepared entries of {subsumed_paths:?} and the landed entries of {kept_landed_paths:?}").as_str(),
307 );
308 tree
309 }
310 };
311 let revalidation: Revalidation = {
312 let context: RevalidationContext<'_> = RevalidationContext {
313 invocation: input.invocation,
314 capture: input.capture,
315 read_only_selection: input.read_only_selection,
316 global_args: input.global_args,
317 no_verify: input.options.no_verify,
318 };
319 revalidate_replay(
320 &context,
321 input.workspace,
322 input.request,
323 input.checks,
324 replay,
325 merged_tree.as_str(),
326 onto,
327 candidate.approved_tree.as_str(),
328 )?
329 };
330 let (tree, index, revalidation_events, revalidated_paths, revalidated_hooks): (String, PathBuf, Vec<PolicyEvent>, Vec<String>, HookChanges) =
331 match revalidation {
332 Revalidation::Blocked { events, exit_code } => {
333 return Ok(Err(LoopOutcome::Failed {
334 events: failed_events(input.settled_events.as_slice(), raced.as_slice(), events),
335 exit_code,
336 }));
337 }
338 Revalidation::Revalidated {
339 tree,
340 index,
341 events,
342 committed_paths,
343 hook_changes,
344 } => (tree, index, events, committed_paths, hook_changes),
345 };
346 let rebuilt = match rebuild_replay(&replay_request(input, replay, onto), tree.as_str()) {
347 Ok(found) => found,
348 Err(error) => return Ok(Err(replay_failure(input.capture, onto, replay_error_text(&error)))),
349 };
350 let committed_paths: Vec<String> = union_paths(input.committed_paths.as_slice(), revalidated_paths);
351 let mut worktree_records: Vec<AddedPath> = retarget_records(input.invocation, index.as_path(), input.worktree_records.as_slice())?;
352 worktree_records.extend(revalidated_hooks.worktree_records.iter().cloned());
353 let mut events: Vec<PolicyEvent> = replayed_events(
354 raced,
355 input.prepared.oid.as_str(),
356 &input.capture.base,
357 onto,
358 &rebuilt,
359 );
360 events.extend(revalidation_events);
361 return Ok(Ok(Candidate {
362 new_oid: rebuilt.oid,
363 tree: tree.clone(),
364 expected_old: Base::Commit(String::from(onto)),
365 index: index.clone(),
366 exact_private_index: false,
367 approved_tree: tree,
368 committed_paths,
369 added_paths: retarget_records(input.invocation, index.as_path(), input.added_paths.as_slice())?,
370 worktree_records,
371 hook_changes: HookChanges::combine(&input.hook_changes, &revalidated_hooks),
372 events,
373 attempt: next_attempt_number(candidate.attempt),
374 lost_races: lost_races_after(candidate.lost_races),
375 }));
376}
377
378pub fn union_paths(committed: &[String], revalidated: Vec<String>) -> Vec<String> {
388 let mut paths: Vec<String> = committed.to_vec();
389 for path in revalidated {
390 if !paths.contains(&path) {
391 paths.push(path);
392 }
393 }
394 return paths;
395}
396
397pub fn replayed_events(
407 raced: Vec<PolicyEvent>,
408 prepared_oid: &str,
409 base: &Base,
410 onto: &str,
411 rebuilt: &ReplayedCommit,
412) -> Vec<PolicyEvent> {
413 let mut events: Vec<PolicyEvent> = raced;
414 events.push(PolicyEvent::CommitReplayed {
415 prepared_oid: String::from(prepared_oid),
416 from_base: match base {
417 Base::Commit(oid) => Some(oid.clone()),
418 Base::Unborn => None,
419 },
420 onto: String::from(onto),
421 oid: rebuilt.oid.clone(),
422 });
423 if !rebuilt.dropped_headers.is_empty() {
424 events.push(PolicyEvent::ReplayHeadersDropped {
425 prepared_oid: String::from(prepared_oid),
426 oid: rebuilt.oid.clone(),
427 headers: rebuilt.dropped_headers.clone(),
428 });
429 }
430 return events;
431}
432
433pub fn next_attempt_number(attempt: i64) -> i64 {
443 return attempt + 1;
444}
445
446pub fn lost_races_after(lost_races: u64) -> u64 {
456 return lost_races + 1;
457}
458
459fn replay_error_text(error: &ReplayError) -> String {
469 return match error {
470 ReplayError::Shared(inner) => format!("{inner:?}"),
471 ReplayError::Capture(inner) => format!("{inner:?}"),
472 ReplayError::Subsumption(inner) => format!("{inner:?}"),
473 ReplayError::Merge(inner) => format!("{inner:?}"),
474 ReplayError::Git(inner) => inner.0.clone(),
475 ReplayError::Object(text) => text.clone(),
476 };
477}
478
479fn reservation_stop(error: ReservationError) -> TransactionStop {
489 return failed(format!("the landing reservation failed: {error:?}"));
490}
491
492pub fn land_with_replay<F: RepositoryFacts>(mut input: LoopInput<'_, F>) -> Result<LoopOutcome, TransactionStop> {
503 let layout = &input.capture.layout;
504 let nested: bool = landing_is_nested(layout, input.invocation.environment);
505 let mut reservation: LandingReservation = LandingReservation::open(
506 layout.registry_root.as_path(),
507 input.workspace.landing.directory.as_path(),
508 input.workspace.landing.transaction_id.as_str(),
509 nested,
510 );
511 let result: Result<LoopOutcome, TransactionStop> = run_loop(&mut input, &mut reservation);
512 if let Err(error) = reservation.release() {
514 debug(TAG, format!("releasing the landing reservation failed: {error:?}").as_str());
515 }
516 return result;
517}
518
519fn run_loop<F: RepositoryFacts>(
529 input: &mut LoopInput<'_, F>,
530 reservation: &mut LandingReservation,
531) -> Result<LoopOutcome, TransactionStop> {
532 let mut candidate: Candidate = Candidate {
533 new_oid: input.prepared.oid.clone(),
534 tree: input.prepared.tree_oid.clone(),
535 expected_old: input.capture.base.clone(),
536 index: input.workspace.landing.commit_index.clone(),
537 exact_private_index: true,
538 approved_tree: input.prepared.tree_oid.clone(),
539 committed_paths: input.committed_paths.clone(),
540 added_paths: input.added_paths.clone(),
541 worktree_records: input.worktree_records.clone(),
542 hook_changes: input.hook_changes.clone(),
543 events: Vec::new(),
544 attempt: 1,
545 lost_races: 0,
546 };
547 loop {
548 let payload: LandingPayload = LandingPayload::Commit {
549 new_oid: candidate.new_oid.clone(),
550 landed_tree_oid: candidate.tree.clone(),
551 expected_old: candidate.expected_old.clone(),
552 landed_index: candidate.index.clone(),
553 exact_private_index: candidate.exact_private_index,
554 guarded_paths: candidate.hook_changes.paths.clone(),
555 };
556 let request: LandingRequest<'_> = LandingRequest {
557 capture: input.capture,
558 mode: input.mode,
559 payload: &payload,
560 committed_paths: candidate.committed_paths.as_slice(),
561 added_paths: candidate.added_paths.as_slice(),
562 selected_worktree_paths: candidate.worktree_records.as_slice(),
563 index_lock_timeout_ms: input.index_lock_timeout_ms,
564 attempt: candidate.attempt,
565 };
566 let outcome: LandingOutcome = {
567 let lock: OwnerLock = acquire_reserved_lock(
569 input.invocation.context,
570 input.invocation.cwd,
571 &input.capture.layout,
572 input.invocation.environment,
573 reservation,
574 )
575 .map_err(reservation_stop)?;
576 land_transaction(
577 input.invocation.context,
578 input.invocation.cwd,
579 &lock,
580 &mut input.workspace.landing,
581 &request,
582 )?
583 };
584 let onto: String = match &outcome {
585 LandingOutcome::Landed { oid } => {
586 return Ok(LoopOutcome::Landed {
587 oid: oid.clone(),
588 events: candidate.events,
589 worktree_records: candidate.worktree_records,
590 added_paths: candidate.added_paths,
591 });
592 }
593 LandingOutcome::HeadMoved {
594 current: Base::Commit(oid),
595 replayable: true,
596 } => oid.clone(),
597 _ => {
598 let mut last: Vec<PolicyEvent> = Vec::new();
599 if let Some(event) = landing_finding(&outcome, input.capture, Some(input.prepared.oid.as_str()), false) {
600 last.push(event);
601 }
602 return Ok(LoopOutcome::Failed {
603 events: failed_events(input.settled_events.as_slice(), candidate.events.as_slice(), last),
604 exit_code: LANDING_FINDING_EXIT_CODE,
605 });
606 }
607 };
608 let lost_races: u64 = lost_races_after(candidate.lost_races);
609 debug(TAG, format!("lost landing race {lost_races} to {onto}").as_str());
610 let reserved: bool = lost_races >= input.reserve_after_lost_races
611 && reservation.reserve(lost_races).map_err(reservation_stop)?;
612 reach_transaction_phase(Phase::RaceLost, Some(lost_races))?;
613 let mut raced: Vec<PolicyEvent> = candidate.events.clone();
614 raced.push(PolicyEvent::LandingRaceLost {
615 attempt: lost_races,
616 winning_oid: onto.clone(),
617 });
618 if reserved {
619 raced.push(PolicyEvent::LandingReserved { lost_races });
620 }
621 match replay_candidate(input, &candidate, raced, onto.as_str())? {
622 Ok(next) => candidate = next,
623 Err(ended) => return Ok(ended),
624 }
625 }
626}
627
628#[cfg(test)]
630#[path = "commit_landing_loop_tests.rs"]
631mod tests;