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 let known: std::collections::HashSet<&[u8]> = committed.iter().map(Vec::as_slice).collect();
396 let mut added: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
397 for path in revalidated {
398 if known.contains(path.as_slice()) || !added.insert(path.clone()) {
399 continue;
400 }
401 paths.push(path);
402 }
403 return paths;
404}
405
406pub fn replayed_events(
416 raced: Vec<PolicyEvent>,
417 prepared_oid: &str,
418 base: &Base,
419 onto: &str,
420 rebuilt: &ReplayedCommit,
421) -> Vec<PolicyEvent> {
422 let mut events: Vec<PolicyEvent> = raced;
423 events.push(PolicyEvent::CommitReplayed {
424 prepared_oid: String::from(prepared_oid),
425 from_base: match base {
426 Base::Commit(oid) => Some(oid.clone()),
427 Base::Unborn => None,
428 },
429 onto: String::from(onto),
430 oid: rebuilt.oid.clone(),
431 });
432 if !rebuilt.dropped_headers.is_empty() {
433 events.push(PolicyEvent::ReplayHeadersDropped {
434 prepared_oid: String::from(prepared_oid),
435 oid: rebuilt.oid.clone(),
436 headers: rebuilt.dropped_headers.clone(),
437 });
438 }
439 return events;
440}
441
442pub fn next_attempt_number(attempt: i64) -> i64 {
452 return attempt + 1;
453}
454
455pub fn lost_races_after(lost_races: u64) -> u64 {
465 return lost_races + 1;
466}
467
468fn replay_error_text(error: &ReplayError) -> String {
478 return match error {
479 ReplayError::Shared(inner) => format!("{inner:?}"),
480 ReplayError::Capture(inner) => format!("{inner:?}"),
481 ReplayError::Subsumption(inner) => format!("{inner:?}"),
482 ReplayError::Merge(inner) => format!("{inner:?}"),
483 ReplayError::Git(inner) => inner.0.clone(),
484 ReplayError::Object(text) => text.clone(),
485 };
486}
487
488fn reservation_stop(error: ReservationError) -> TransactionStop {
498 return failed(format!("the landing reservation failed: {error}"));
499}
500
501pub fn land_with_replay<F: RepositoryFacts>(mut input: LoopInput<'_, F>) -> Result<LoopOutcome, TransactionStop> {
512 let layout = &input.capture.layout;
513 let nested: bool = landing_is_nested(layout, input.invocation.environment);
514 let mut reservation: LandingReservation = LandingReservation::open(
515 layout.registry_root.as_path(),
516 input.workspace.landing.directory.as_path(),
517 input.workspace.landing.transaction_id.as_str(),
518 nested,
519 );
520 let result: Result<LoopOutcome, TransactionStop> = run_loop(&mut input, &mut reservation);
521 if let Err(error) = reservation.release() {
523 debug(TAG, format!("releasing the landing reservation failed: {error:?}").as_str());
524 }
525 return result;
526}
527
528fn run_loop<F: RepositoryFacts>(
538 input: &mut LoopInput<'_, F>,
539 reservation: &mut LandingReservation,
540) -> Result<LoopOutcome, TransactionStop> {
541 let mut candidate: Candidate = Candidate {
542 new_oid: input.prepared.oid.clone(),
543 tree: input.prepared.tree_oid.clone(),
544 expected_old: input.capture.base.clone(),
545 index: input.workspace.landing.commit_index.clone(),
546 exact_private_index: true,
547 approved_tree: input.prepared.tree_oid.clone(),
548 committed_paths: input.committed_paths.clone(),
549 added_paths: input.added_paths.clone(),
550 worktree_records: input.worktree_records.clone(),
551 hook_changes: input.hook_changes.clone(),
552 events: Vec::new(),
553 attempt: 1,
554 lost_races: 0,
555 };
556 loop {
557 let payload: LandingPayload = LandingPayload::Commit {
558 new_oid: candidate.new_oid.clone(),
559 landed_tree_oid: candidate.tree.clone(),
560 expected_old: candidate.expected_old.clone(),
561 landed_index: candidate.index.clone(),
562 exact_private_index: candidate.exact_private_index,
563 guarded_paths: candidate.hook_changes.paths.clone(),
564 };
565 let request: LandingRequest<'_> = LandingRequest {
566 capture: input.capture,
567 mode: input.mode,
568 payload: &payload,
569 committed_paths: candidate.committed_paths.as_slice(),
570 added_paths: candidate.added_paths.as_slice(),
571 selected_worktree_paths: candidate.worktree_records.as_slice(),
572 index_lock_timeout_ms: input.index_lock_timeout_ms,
573 attempt: candidate.attempt,
574 };
575 let outcome: LandingOutcome = {
576 let lock: OwnerLock = acquire_reserved_lock(
578 input.invocation.context,
579 input.invocation.cwd,
580 &input.capture.layout,
581 input.invocation.environment,
582 reservation,
583 )
584 .map_err(reservation_stop)?;
585 land_transaction(
586 input.invocation.context,
587 input.invocation.cwd,
588 &lock,
589 &mut input.workspace.landing,
590 &request,
591 )?
592 };
593 let onto: String = match &outcome {
594 LandingOutcome::Landed { oid } => {
595 return Ok(LoopOutcome::Landed {
596 oid: oid.clone(),
597 events: candidate.events,
598 worktree_records: candidate.worktree_records,
599 added_paths: candidate.added_paths,
600 });
601 }
602 LandingOutcome::HeadMoved {
603 current: Base::Commit(oid),
604 replayable: true,
605 } => oid.clone(),
606 _ => {
607 let mut last: Vec<PolicyEvent> = Vec::new();
608 if let Some(event) = landing_finding(&outcome, input.capture, Some(input.prepared.oid.as_str()), false) {
609 last.push(event);
610 }
611 return Ok(LoopOutcome::Failed {
612 events: failed_events(input.settled_events.as_slice(), candidate.events.as_slice(), last),
613 exit_code: LANDING_FINDING_EXIT_CODE,
614 });
615 }
616 };
617 let lost_races: u64 = lost_races_after(candidate.lost_races);
618 debug(TAG, format!("lost landing race {lost_races} to {onto}").as_str());
619 let reserved: bool = lost_races >= input.reserve_after_lost_races
620 && reservation.reserve(lost_races).map_err(reservation_stop)?;
621 reach_transaction_phase(Phase::RaceLost, Some(lost_races))?;
622 let mut raced: Vec<PolicyEvent> = candidate.events.clone();
623 raced.push(PolicyEvent::LandingRaceLost {
624 attempt: lost_races,
625 winning_oid: onto.clone(),
626 });
627 if reserved {
628 raced.push(PolicyEvent::LandingReserved { lost_races });
629 }
630 match replay_candidate(input, &candidate, raced, onto.as_str())? {
631 Ok(next) => candidate = next,
632 Err(ended) => return Ok(ended),
633 }
634 }
635}
636
637#[cfg(test)]
639#[path = "commit_landing_loop_tests.rs"]
640mod tests;