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::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 approved_tree: String,
66 committed_paths: Vec<Vec<u8>>,
68 added_paths: Vec<AddedPath>,
70 worktree_records: Vec<AddedPath>,
72 hook_changes: HookChanges,
74 events: Vec<PolicyEvent>,
76 attempt: i64,
78 lost_races: u64,
80}
81
82pub struct LoopInput<'a, F: RepositoryFacts> {
92 pub invocation: &'a CommitInvocation<'a>,
94 pub workspace: &'a mut CommitWorkspace,
96 pub capture: &'a InvocationCapture,
98 pub mode: TransactionMode,
100 pub request: &'a StageRequest,
102 pub checks: &'a mut ShippedChecks<PrivateIndexFacts<F>>,
104 pub prepared: &'a PreparedCommit,
106 pub settled_events: Vec<PolicyEvent>,
108 pub options: &'a ReplayOptions,
110 pub global_args: &'a [String],
112 pub read_only_selection: bool,
114 pub committed_paths: Vec<Vec<u8>>,
116 pub added_paths: Vec<AddedPath>,
118 pub worktree_records: Vec<AddedPath>,
120 pub hook_changes: HookChanges,
122 pub index_lock_timeout_ms: u64,
124 pub reserve_after_lost_races: u64,
126}
127
128pub enum LoopOutcome {
138 Landed {
140 oid: String,
142 events: Vec<PolicyEvent>,
144 worktree_records: Vec<AddedPath>,
146 added_paths: Vec<AddedPath>,
148 },
149 Failed {
151 events: Vec<PolicyEvent>,
153 exit_code: i32,
155 },
156}
157
158fn retarget_records(
168 input: &CommitInvocation<'_>,
169 index: &Path,
170 records: &[AddedPath],
171) -> Result<Vec<AddedPath>, TransactionStop> {
172 let mut retargeted: Vec<AddedPath> = Vec::new();
173 for record in records {
174 let Some((mode, oid)) = settled_entry(input.context, input.cwd, index, record.path.as_slice())? else {
175 continue;
176 };
177 if mode != record.git_mode {
178 continue;
179 }
180 retargeted.push(AddedPath {
181 intended_oid: oid,
182 ..record.clone()
183 });
184 }
185 return Ok(retargeted);
186}
187
188fn replay_request<'a, F: RepositoryFacts>(
198 input: &'a LoopInput<'a, F>,
199 replay: u64,
200 onto: &'a str,
201) -> ReplayRequest<'a> {
202 return ReplayRequest {
203 context: input.invocation.context,
204 shadow: input.workspace.landing.shadow.as_path(),
205 object_directory: input.workspace.object_directory.as_path(),
206 worktree_root: input.capture.layout.repository_root.as_path(),
207 git_dir: input.capture.layout.git_dir.as_path(),
208 transaction_directory: input.workspace.landing.directory.as_path(),
209 replay,
210 base: match &input.capture.base {
211 Base::Commit(oid) => Some(oid.as_str()),
212 Base::Unborn => None,
213 },
214 empty_tree: input.capture.layout.empty_tree_oid.as_str(),
215 current: onto,
216 prepared: input.prepared.oid.as_str(),
217 signing_key: input.options.signing_key.as_deref(),
218 global_arguments: input.global_args,
219 };
220}
221
222fn replay_failure(capture: &InvocationCapture, onto: &str, cause: String) -> LoopOutcome {
233 return LoopOutcome::Failed {
234 events: vec![PolicyEvent::EngineFailure {
235 code: super::diagnostics::EngineFailureCode::TransactionFailed,
236 message: format!(
237 "{} moved while this commit was prepared, and replaying it onto {onto} failed; nothing landed: {cause}",
238 capture.layout.target_ref
239 ),
240 trigger: Some(super::policy_trigger::Trigger::PreForward),
241 policy: None,
242 path: None,
243 }],
244 exit_code: 2,
245 };
246}
247
248fn failed_events(settled: &[PolicyEvent], raced: &[PolicyEvent], last: Vec<PolicyEvent>) -> Vec<PolicyEvent> {
258 let mut events: Vec<PolicyEvent> = settled.to_vec();
259 events.extend(raced.iter().cloned());
260 events.extend(last);
261 return events;
262}
263
264fn replay_candidate<F: RepositoryFacts>(
274 input: &mut LoopInput<'_, F>,
275 candidate: &Candidate,
276 raced: Vec<PolicyEvent>,
277 onto: &str,
278) -> Result<Result<Candidate, LoopOutcome>, TransactionStop> {
279 let replay: u64 = lost_races_after(candidate.lost_races);
280 let merge: Result<MergeStep, ReplayError> = merge_replay(&replay_request(input, replay, onto));
281 let merged_tree: String = match merge {
282 Err(error) => return Ok(Err(replay_failure(input.capture, onto, replay_error_text(&error)))),
283 Ok(MergeStep::Conflict(conflict)) => {
284 migrate_shadow_objects(
286 input.invocation.context,
287 input.invocation.cwd,
288 input.workspace.landing.shadow.as_path(),
289 input.prepared.oid.as_str(),
290 &input.capture.base,
291 None,
292 )?;
293 let last: Vec<PolicyEvent> = vec![PolicyEvent::ReplayConflict {
294 target_ref: input.capture.layout.target_ref.clone(),
295 paths: conflict
296 .paths
297 .iter()
298 .map(|path| { return EventPath::from_git_bytes(path.as_slice()); })
299 .collect(),
300 winning_oid: conflict.winning_oid,
301 prepared_oid: conflict.prepared_oid,
302 }];
303 return Ok(Err(LoopOutcome::Failed {
304 events: failed_events(input.settled_events.as_slice(), raced.as_slice(), last),
305 exit_code: 1,
306 }));
307 }
308 Ok(MergeStep::Merged { tree, subsumed_paths, kept_landed_paths }) => {
309 debug(
310 TAG,
311 format!("replay onto {onto} keeps the prepared entries of {subsumed_paths:?} and the landed entries of {kept_landed_paths:?}").as_str(),
312 );
313 tree
314 }
315 };
316 let revalidation: Revalidation = {
317 let context: RevalidationContext<'_> = RevalidationContext {
318 invocation: input.invocation,
319 capture: input.capture,
320 read_only_selection: input.read_only_selection,
321 global_args: input.global_args,
322 no_verify: input.options.no_verify,
323 };
324 revalidate_replay(
325 &context,
326 input.workspace,
327 input.request,
328 input.checks,
329 replay,
330 merged_tree.as_str(),
331 onto,
332 candidate.approved_tree.as_str(),
333 )?
334 };
335 let (tree, index, revalidation_events, revalidated_paths, revalidated_hooks): (String, PathBuf, Vec<PolicyEvent>, Vec<Vec<u8>>, HookChanges) =
336 match revalidation {
337 Revalidation::Blocked { events, exit_code } => {
338 return Ok(Err(LoopOutcome::Failed {
339 events: failed_events(input.settled_events.as_slice(), raced.as_slice(), events),
340 exit_code,
341 }));
342 }
343 Revalidation::Revalidated {
344 tree,
345 index,
346 events,
347 committed_paths,
348 hook_changes,
349 } => (tree, index, events, committed_paths, hook_changes),
350 };
351 let rebuilt = match rebuild_replay(&replay_request(input, replay, onto), tree.as_str()) {
352 Ok(found) => found,
353 Err(error) => return Ok(Err(replay_failure(input.capture, onto, replay_error_text(&error)))),
354 };
355 let committed_paths: Vec<Vec<u8>> = union_paths(input.committed_paths.as_slice(), revalidated_paths);
356 let mut worktree_records: Vec<AddedPath> = retarget_records(input.invocation, index.as_path(), input.worktree_records.as_slice())?;
357 worktree_records.extend(revalidated_hooks.worktree_records.iter().cloned());
358 let mut events: Vec<PolicyEvent> = replayed_events(
359 raced,
360 input.prepared.oid.as_str(),
361 &input.capture.base,
362 onto,
363 &rebuilt,
364 );
365 events.extend(revalidation_events);
366 return Ok(Ok(Candidate {
367 new_oid: rebuilt.oid,
368 tree: tree.clone(),
369 expected_old: Base::Commit(String::from(onto)),
370 index: index.clone(),
371 exact_private_index: false,
372 approved_tree: tree,
373 committed_paths,
374 added_paths: retarget_records(input.invocation, index.as_path(), input.added_paths.as_slice())?,
375 worktree_records,
376 hook_changes: HookChanges::combine(&input.hook_changes, &revalidated_hooks),
377 events,
378 attempt: next_attempt_number(candidate.attempt),
379 lost_races: lost_races_after(candidate.lost_races),
380 }));
381}
382
383pub fn union_paths(committed: &[Vec<u8>], revalidated: Vec<Vec<u8>>) -> Vec<Vec<u8>> {
393 let mut paths: Vec<Vec<u8>> = committed.to_vec();
394 for path in revalidated {
395 if !paths.contains(&path) {
396 paths.push(path);
397 }
398 }
399 return paths;
400}
401
402pub fn replayed_events(
412 raced: Vec<PolicyEvent>,
413 prepared_oid: &str,
414 base: &Base,
415 onto: &str,
416 rebuilt: &ReplayedCommit,
417) -> Vec<PolicyEvent> {
418 let mut events: Vec<PolicyEvent> = raced;
419 events.push(PolicyEvent::CommitReplayed {
420 prepared_oid: String::from(prepared_oid),
421 from_base: match base {
422 Base::Commit(oid) => Some(oid.clone()),
423 Base::Unborn => None,
424 },
425 onto: String::from(onto),
426 oid: rebuilt.oid.clone(),
427 });
428 if !rebuilt.dropped_headers.is_empty() {
429 events.push(PolicyEvent::ReplayHeadersDropped {
430 prepared_oid: String::from(prepared_oid),
431 oid: rebuilt.oid.clone(),
432 headers: rebuilt.dropped_headers.clone(),
433 });
434 }
435 return events;
436}
437
438pub fn next_attempt_number(attempt: i64) -> i64 {
448 return attempt + 1;
449}
450
451pub fn lost_races_after(lost_races: u64) -> u64 {
461 return lost_races + 1;
462}
463
464fn replay_error_text(error: &ReplayError) -> String {
474 return match error {
475 ReplayError::Shared(inner) => format!("{inner:?}"),
476 ReplayError::Capture(inner) => format!("{inner:?}"),
477 ReplayError::Subsumption(inner) => format!("{inner:?}"),
478 ReplayError::Merge(inner) => format!("{inner:?}"),
479 ReplayError::Git(inner) => inner.0.clone(),
480 ReplayError::Object(text) => text.clone(),
481 };
482}
483
484fn reservation_stop(error: ReservationError) -> TransactionStop {
494 return failed(format!("the landing reservation failed: {error}"));
495}
496
497pub fn land_with_replay<F: RepositoryFacts>(mut input: LoopInput<'_, F>) -> Result<LoopOutcome, TransactionStop> {
508 let layout = &input.capture.layout;
509 let nested: bool = landing_is_nested(layout, input.invocation.environment);
510 let mut reservation: LandingReservation = LandingReservation::open(
511 layout.registry_root.as_path(),
512 input.workspace.landing.directory.as_path(),
513 input.workspace.landing.transaction_id.as_str(),
514 nested,
515 );
516 let result: Result<LoopOutcome, TransactionStop> = run_loop(&mut input, &mut reservation);
517 if let Err(error) = reservation.release() {
519 debug(TAG, format!("releasing the landing reservation failed: {error:?}").as_str());
520 }
521 return result;
522}
523
524fn run_loop<F: RepositoryFacts>(
534 input: &mut LoopInput<'_, F>,
535 reservation: &mut LandingReservation,
536) -> Result<LoopOutcome, TransactionStop> {
537 let mut candidate: Candidate = Candidate {
538 new_oid: input.prepared.oid.clone(),
539 tree: input.prepared.tree_oid.clone(),
540 expected_old: input.capture.base.clone(),
541 index: input.workspace.landing.commit_index.clone(),
542 exact_private_index: true,
543 approved_tree: input.prepared.tree_oid.clone(),
544 committed_paths: input.committed_paths.clone(),
545 added_paths: input.added_paths.clone(),
546 worktree_records: input.worktree_records.clone(),
547 hook_changes: input.hook_changes.clone(),
548 events: Vec::new(),
549 attempt: 1,
550 lost_races: 0,
551 };
552 loop {
553 let payload: LandingPayload = LandingPayload::Commit {
554 new_oid: candidate.new_oid.clone(),
555 landed_tree_oid: candidate.tree.clone(),
556 expected_old: candidate.expected_old.clone(),
557 landed_index: candidate.index.clone(),
558 exact_private_index: candidate.exact_private_index,
559 guarded_paths: candidate.hook_changes.paths.clone(),
560 };
561 let request: LandingRequest<'_> = LandingRequest {
562 capture: input.capture,
563 mode: input.mode,
564 payload: &payload,
565 committed_paths: candidate.committed_paths.as_slice(),
566 added_paths: candidate.added_paths.as_slice(),
567 selected_worktree_paths: candidate.worktree_records.as_slice(),
568 index_lock_timeout_ms: input.index_lock_timeout_ms,
569 attempt: candidate.attempt,
570 };
571 let outcome: LandingOutcome = {
572 let lock: OwnerLock = acquire_reserved_lock(
574 input.invocation.context,
575 input.invocation.cwd,
576 &input.capture.layout,
577 input.invocation.environment,
578 reservation,
579 )
580 .map_err(reservation_stop)?;
581 land_transaction(
582 input.invocation.context,
583 input.invocation.cwd,
584 &lock,
585 &mut input.workspace.landing,
586 &request,
587 )?
588 };
589 let onto: String = match &outcome {
590 LandingOutcome::Landed { oid } => {
591 return Ok(LoopOutcome::Landed {
592 oid: oid.clone(),
593 events: candidate.events,
594 worktree_records: candidate.worktree_records,
595 added_paths: candidate.added_paths,
596 });
597 }
598 LandingOutcome::HeadMoved {
599 current: Base::Commit(oid),
600 replayable: true,
601 } => oid.clone(),
602 _ => {
603 let mut last: Vec<PolicyEvent> = Vec::new();
604 if let Some(event) = landing_finding(&outcome, input.capture, Some(input.prepared.oid.as_str()), false) {
605 last.push(event);
606 }
607 return Ok(LoopOutcome::Failed {
608 events: failed_events(input.settled_events.as_slice(), candidate.events.as_slice(), last),
609 exit_code: LANDING_FINDING_EXIT_CODE,
610 });
611 }
612 };
613 let lost_races: u64 = lost_races_after(candidate.lost_races);
614 debug(TAG, format!("lost landing race {lost_races} to {onto}").as_str());
615 let reserved: bool = lost_races >= input.reserve_after_lost_races
616 && reservation.reserve(lost_races).map_err(reservation_stop)?;
617 reach_transaction_phase(Phase::RaceLost, Some(lost_races))?;
618 let mut raced: Vec<PolicyEvent> = candidate.events.clone();
619 raced.push(PolicyEvent::LandingRaceLost {
620 attempt: lost_races,
621 winning_oid: onto.clone(),
622 });
623 if reserved {
624 raced.push(PolicyEvent::LandingReserved { lost_races });
625 }
626 match replay_candidate(input, &candidate, raced, onto.as_str())? {
627 Ok(next) => candidate = next,
628 Err(ended) => return Ok(ended),
629 }
630 }
631}
632
633#[cfg(test)]
635#[path = "commit_landing_loop_tests.rs"]
636mod tests;