1use super::capture_records::record_landed_capture_or_warn;
27use super::capture_invocation::{InvocationCapture, base_revision, resolve_ref_commit, resolve_symbolic_head};
29use super::conclusion_cleanup::reproduce_conclusion_cleanup;
31use super::commit_index::copy_index_with_timestamps;
33use super::diagnostic_log::{debug, warn};
35use super::index_writer_coordination::CoordinationFailure;
37use super::landing_index::{PostIndexInput, compute_landing_post_index};
39use super::landing_objects::migrate_shadow_objects;
41use super::landing_support::{CompareAndSwap, compare_and_swap_target, same_target};
43use super::pack_keep::{remove_pack_keep, transaction_keep_message};
45use super::owner_lock::OwnerLock;
47use super::private_storage::{sync_directory, write_private_file};
49use super::recovery_error::RecoveryError;
51use super::recovery_files::file_identity_of;
53use super::real_index_lock::{RealIndexLock, acquire_landing_index_lock};
55use super::transaction_git::{GitContext, GitFailure};
57use super::transaction_journal::{
59 AddedPath, Base, Conclusion, FileIdentity, INDEX_INSTALLED_FILENAME, LandingOperation, LandingRecord,
60 REF_UPDATED_FILENAME, SymbolicHead, TransactionMode, landing_record_filename, post_index_filename,
61 pre_landing_index_filename,
62};
63use super::transaction_journal_encode::{encode_landing, encode_ref_updated};
65use super::transaction_phase::{Phase, PhaseMarkerError, reach_transaction_phase};
67use std::path::{Path, PathBuf};
69
70const TAG: &str = "landTransaction";
72
73#[derive(Clone, Debug, Eq, PartialEq)]
84pub enum LandingPayload {
85 Commit {
87 new_oid: String,
89 landed_tree_oid: String,
91 expected_old: Base,
95 landed_index: PathBuf,
97 exact_private_index: bool,
100 guarded_paths: Vec<Vec<u8>>,
103 },
104 NormalizeOnly {
106 landed_tree_oid: String,
108 },
109}
110
111#[derive(Clone, Debug, Eq, PartialEq)]
121pub enum LandingOutcome {
122 Landed {
125 oid: String,
129 },
130 HeadMoved {
133 current: Base,
135 replayable: bool,
140 },
141 BranchSwitched {
146 current: SymbolicHead,
148 },
149}
150
151#[derive(Debug)]
162pub enum LandingError {
163 Lock(CoordinationFailure),
165 Git(GitFailure),
167 Recovery(RecoveryError),
171 Journal(String),
173 Phase(PhaseMarkerError),
175 Release(String),
177 LandedThenFailed {
180 oid: String,
182 target: String,
184 cause: Box<LandingError>,
186 },
187}
188
189impl std::fmt::Display for LandingError {
191 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 return match self {
194 Self::Lock(failure) => write!(formatter, "{failure:?}"),
195 Self::Git(failure) => write!(formatter, "{failure}"),
196 Self::Recovery(failure) => write!(formatter, "{}", failure.0),
197 Self::Journal(text) | Self::Release(text) => formatter.write_str(text.as_str()),
198 Self::Phase(failure) => write!(formatter, "{failure}"),
199 Self::LandedThenFailed { oid, target, cause } => write!(
200 formatter,
201 "the commit {oid} landed on {target}, but completing its records failed: {cause}. \
202The commit is done, so do not retry it; check it with git log and git status."
203 ),
204 };
205 }
206}
207
208#[derive(Clone, Debug, Eq, PartialEq)]
220pub struct LandingWorkspace {
221 pub transaction_id: String,
224 pub directory: PathBuf,
226 pub shadow: PathBuf,
228 pub captured_index: PathBuf,
230 pub commit_index: PathBuf,
232 pub preserved: bool,
236}
237
238#[derive(Clone, Copy, Debug)]
248pub struct LandingRequest<'request> {
249 pub capture: &'request InvocationCapture,
251 pub mode: TransactionMode,
253 pub payload: &'request LandingPayload,
255 pub committed_paths: &'request [Vec<u8>],
257 pub added_paths: &'request [AddedPath],
259 pub selected_worktree_paths: &'request [AddedPath],
261 pub index_lock_timeout_ms: u64,
263 pub attempt: i64,
266}
267
268fn write_journal(directory: &Path, filename: &str, bytes: &[u8]) -> Result<(), LandingError> {
282 let path: PathBuf = directory.join(filename);
283 if let Err(error) = write_private_file(path.as_path(), bytes) {
284 return Err(LandingError::Journal(format!("writing {} failed: {error}", path.display())));
285 }
286 if let Err(error) = sync_directory(directory) {
287 return Err(LandingError::Journal(format!("syncing {} failed: {error}", directory.display())));
288 }
289 return Ok(());
290}
291
292fn reach(phase: Phase) -> Result<(), LandingError> {
302 return reach_transaction_phase(phase, None).map_err(LandingError::Phase);
303}
304
305fn artifact_identity(path: &Path) -> Result<FileIdentity, LandingError> {
316 return match file_identity_of(path) {
317 Ok((device, inode, _)) => Ok(FileIdentity { device, inode }),
318 Err(error) => Err(LandingError::Journal(format!("reading the identity of {} failed: {error}", path.display()))),
319 };
320}
321
322fn names_commit(target: &Base) -> bool {
332 return matches!(target, Base::Commit(_));
333}
334
335struct Held {
346 lock: Option<RealIndexLock>,
349 for_recovery: bool,
352}
353
354pub fn land_transaction(
371 context: &GitContext,
372 cwd: &Path,
373 _landing_lock: &OwnerLock,
374 workspace: &mut LandingWorkspace,
375 request: &LandingRequest<'_>,
376) -> Result<LandingOutcome, LandingError> {
377 debug(TAG, format!("landing attempt {} of transaction {}", request.attempt, workspace.transaction_id).as_str());
378 let acquired: RealIndexLock = acquire_landing_index_lock(
379 request.capture.layout.real_index_path.as_path(),
380 workspace.directory.as_path(),
381 request.attempt,
382 request.index_lock_timeout_ms,
383 "landed nothing",
384 )
385 .map_err(LandingError::Lock)?;
386 let mut held: Held = Held { lock: Some(acquired), for_recovery: false };
387 let result: Result<LandingOutcome, LandingError> = land_locked(context, cwd, workspace, request, &mut held);
388 let Some(lock) = held.lock.take() else {
389 return result;
390 };
391 if held.for_recovery {
392 debug(TAG, "the target advanced before the install failed; the index lock is left for recovery");
393 return result;
394 }
395 match (lock.release_after_landing(), result) {
396 (Ok(()), outcome) => return outcome,
397 (Err(release), Ok(_)) => return Err(LandingError::Release(release)),
398 (Err(release), Err(error)) => {
399 warn(TAG, format!("the index lock could not be released after a failed landing: {release}").as_str());
400 return Err(error);
401 }
402 }
403}
404
405fn land_locked(
416 context: &GitContext,
417 cwd: &Path,
418 workspace: &mut LandingWorkspace,
419 request: &LandingRequest<'_>,
420 held: &mut Held,
421) -> Result<LandingOutcome, LandingError> {
422 reach(Phase::LandingLocked)?;
423 let capture: &InvocationCapture = request.capture;
424 let symbolic_head: SymbolicHead = resolve_symbolic_head(context, cwd).map_err(LandingError::Git)?;
425 if symbolic_head != capture.layout.symbolic_head {
426 debug(TAG, "HEAD names another target than at invocation");
427 return Ok(LandingOutcome::BranchSwitched { current: symbolic_head });
428 }
429 let expected_old: Base = match request.payload {
430 LandingPayload::Commit { expected_old, .. } => expected_old.clone(),
431 LandingPayload::NormalizeOnly { .. } => capture.base.clone(),
432 };
433 let current: Base = resolve_ref_commit(context, cwd, capture.layout.target_ref.as_str()).map_err(LandingError::Git)?;
434 if !same_target(¤t, &expected_old) {
435 debug(TAG, format!("{} moved since preparation", capture.layout.target_ref).as_str());
436 let replayable: bool = matches!(request.payload, LandingPayload::Commit { .. })
437 && capture.layout.conclusion == Conclusion::None
438 && names_commit(¤t);
439 return Ok(LandingOutcome::HeadMoved { current, replayable });
440 }
441 let pack_name: Option<String> = match request.payload {
442 LandingPayload::Commit { new_oid, .. } => Some(
443 migrate_shadow_objects(
444 context,
445 cwd,
446 workspace.shadow.as_path(),
447 new_oid.as_str(),
448 &expected_old,
449 Some(transaction_keep_message(workspace.transaction_id.as_str()).as_str()),
450 )
451 .map_err(LandingError::Git)?,
452 ),
453 LandingPayload::NormalizeOnly { .. } => None,
454 };
455 reach(Phase::ObjectsMigrated)?;
456 let landed_tree: &str = match request.payload {
457 LandingPayload::Commit { landed_tree_oid, .. } | LandingPayload::NormalizeOnly { landed_tree_oid } => landed_tree_oid.as_str(),
458 };
459 let pre_landing: PathBuf = workspace.directory.join(pre_landing_index_filename(request.attempt));
460 let post: PathBuf = workspace.directory.join(post_index_filename(request.attempt));
461 if let Err(error) = copy_index_with_timestamps(capture.layout.real_index_path.as_path(), pre_landing.as_path()) {
462 return Err(LandingError::Journal(format!("copying the real index to {} failed: {error}", pre_landing.display())));
463 }
464 compute_post_index(context, cwd, workspace, request, landed_tree, pre_landing.as_path(), post.as_path())?;
465 workspace.preserved = true;
467 let lock_identity = match held.lock.as_ref() {
468 Some(lock) => lock.identity().clone(),
469 None => return Err(LandingError::Journal(String::from("the real index lock is not held"))),
470 };
471 let record: LandingRecord = LandingRecord {
472 attempt: request.attempt,
473 operation: match request.payload {
474 LandingPayload::Commit { .. } => LandingOperation::Commit,
475 LandingPayload::NormalizeOnly { .. } => LandingOperation::NormalizeOnly,
476 },
477 expected_old: expected_old.clone(),
478 new_oid: match request.payload {
479 LandingPayload::Commit { new_oid, .. } => Some(new_oid.clone()),
480 LandingPayload::NormalizeOnly { .. } => None,
481 },
482 landed_tree_oid: landed_tree.to_string(),
483 pre_landing_index: artifact_identity(pre_landing.as_path())?,
484 post_index: artifact_identity(post.as_path())?,
485 lock: lock_identity,
486 pack_name: pack_name.clone(),
487 added_paths: request.added_paths.to_vec(),
488 selected_worktree_paths: request.selected_worktree_paths.to_vec(),
489 };
490 write_journal(
491 workspace.directory.as_path(),
492 landing_record_filename(request.attempt).as_str(),
493 encode_landing(&record).as_bytes(),
494 )?;
495 if let LandingPayload::Commit { new_oid, .. } = request.payload {
496 let pack: &str = pack_name.as_deref().unwrap_or("");
497 if let Some(lost) = advance_target(context, cwd, workspace, request, held, new_oid.as_str(), &expected_old, pack)? {
498 return Ok(lost);
499 }
500 }
501 if let Err(error) = complete_landing(context, cwd, workspace, request, held, post.as_path()) {
502 return Err(match request.payload {
504 LandingPayload::Commit { new_oid, .. } => LandingError::LandedThenFailed {
505 oid: new_oid.clone(),
506 target: capture.layout.target_ref.clone(),
507 cause: Box::new(error),
508 },
509 LandingPayload::NormalizeOnly { .. } => error,
510 });
511 }
512 debug(TAG, format!("landed attempt {}", request.attempt).as_str());
513 let oid: String = match request.payload {
514 LandingPayload::Commit { new_oid, .. } => new_oid.clone(),
515 LandingPayload::NormalizeOnly { .. } => match &capture.base {
516 Base::Commit(oid) => oid.clone(),
517 Base::Unborn => String::new(),
518 },
519 };
520 return Ok(LandingOutcome::Landed { oid });
521}
522
523fn complete_landing(
534 context: &GitContext,
535 cwd: &Path,
536 workspace: &LandingWorkspace,
537 request: &LandingRequest<'_>,
538 held: &mut Held,
539 post: &Path,
540) -> Result<(), LandingError> {
541 held.for_recovery = true;
543 let Some(installing) = held.lock.take() else {
544 return Err(LandingError::Journal(String::from("the real index lock is not held")));
545 };
546 installing.install(post).map_err(LandingError::Recovery)?;
547 write_journal(workspace.directory.as_path(), INDEX_INSTALLED_FILENAME, b"")?;
548 reach(Phase::IndexInstalled)?;
549 if matches!(request.payload, LandingPayload::Commit { .. }) {
550 reproduce_conclusion_cleanup(
551 context,
552 cwd,
553 request.capture.layout.git_dir.as_path(),
554 workspace.shadow.as_path(),
555 workspace.directory.as_path(),
556 request.capture.layout.ref_format,
557 )
558 .map_err(LandingError::Recovery)?;
559 }
560 return Ok(());
561}
562
563fn compute_post_index(
574 context: &GitContext,
575 cwd: &Path,
576 workspace: &LandingWorkspace,
577 request: &LandingRequest<'_>,
578 landed_tree: &str,
579 pre_landing: &Path,
580 post: &Path,
581) -> Result<(), LandingError> {
582 let base: String = base_revision(request.capture);
583 let (landed_index, exact_private_index, guarded_paths): (&Path, bool, &[Vec<u8>]) = match request.payload {
584 LandingPayload::Commit { landed_index, exact_private_index, guarded_paths, .. } => {
585 (landed_index.as_path(), *exact_private_index, guarded_paths.as_slice())
586 }
587 LandingPayload::NormalizeOnly { .. } => (workspace.commit_index.as_path(), true, &[]),
588 };
589 let input: PostIndexInput<'_> = PostIndexInput {
590 mode: request.mode,
591 pre_landing_index: pre_landing,
592 captured_index: workspace.captured_index.as_path(),
593 landed_index,
594 post_index: post,
595 landed_tree,
596 base_revision: base.as_str(),
597 committed_paths: request.committed_paths,
598 guarded_paths,
599 exact_private_index,
600 };
601 return compute_landing_post_index(context, cwd, &input).map_err(LandingError::Git);
602}
603
604fn advance_target(
619 context: &GitContext,
620 cwd: &Path,
621 workspace: &mut LandingWorkspace,
622 request: &LandingRequest<'_>,
623 held: &mut Held,
624 new_oid: &str,
625 expected_old: &Base,
626 pack_name: &str,
627) -> Result<Option<LandingOutcome>, LandingError> {
628 let capture: &InvocationCapture = request.capture;
629 let swap: CompareAndSwap<'_> = CompareAndSwap {
630 target_ref: capture.layout.target_ref.as_str(),
631 detached: capture.layout.symbolic_head == SymbolicHead::Detached,
632 nonce: workspace.transaction_id.as_str(),
633 new_oid,
634 expected_old,
635 hash_length: capture.layout.empty_tree_oid.len(),
636 };
637 let output = compare_and_swap_target(context, cwd, &swap).map_err(LandingError::Git)?;
638 let objects: &Path = capture.layout.object_directory.as_path();
639 if !output.succeeded() {
640 debug(TAG, format!("compare-and-swap lost: {}", String::from_utf8_lossy(&output.stderr).trim()).as_str());
641 remove_pack_keep(objects, pack_name).map_err(LandingError::Recovery)?;
642 workspace.preserved = false;
643 let winner: Base = resolve_ref_commit(context, cwd, capture.layout.target_ref.as_str()).map_err(LandingError::Git)?;
644 let replayable: bool = capture.layout.conclusion == Conclusion::None && names_commit(&winner);
645 return Ok(Some(LandingOutcome::HeadMoved { current: winner, replayable }));
646 }
647 held.for_recovery = true;
649 write_journal(workspace.directory.as_path(), REF_UPDATED_FILENAME, encode_ref_updated(new_oid).as_bytes())?;
650 reach(Phase::RefUpdated)?;
651 remove_pack_keep(objects, pack_name).map_err(LandingError::Recovery)?;
652 record_landed_capture_or_warn(
653 capture.layout.git_dir.as_path(),
654 workspace.directory.as_path(),
655 workspace.transaction_id.as_str(),
656 new_oid,
657 );
658 return Ok(None);
659}
660
661#[cfg(test)]
663#[path = "landing_attempt_tests.rs"]
664mod tests;