1use super::capture_records::record_landed_capture_or_warn;
27use super::capture_invocation::{InvocationCapture, base_revision, resolve_ref_commit, resolve_symbolic_head};
29use super::ref_files::RefFilesLayout;
31use super::conclusion_cleanup::reproduce_conclusion_cleanup;
33use super::commit_index::copy_index_with_timestamps;
35use super::content_digest::{ContentDigest, digest_file};
37use super::diagnostic_log::{debug, warn};
39use super::index_writer_coordination::CoordinationFailure;
41use super::landing_index::{PostIndexInput, compute_landing_post_index};
43use super::landing_objects::migrate_shadow_objects;
45use super::landing_support::{CompareAndSwap, compare_and_swap_target, same_target};
47use super::pack_keep::{remove_pack_keep, transaction_keep_message};
49use super::owner_lock::OwnerLock;
51use super::journal_record::{RecordSync, write_record};
53use super::recovery_error::RecoveryError;
55use super::recovery_files::file_identity_of;
57use super::real_index_lock::{RealIndexLock, acquire_landing_index_lock};
59use super::transaction_git::{GitContext, GitFailure};
61use super::transaction_journal::{
63 AddedPath, Base, Conclusion, FileIdentity, INDEX_INSTALLED_FILENAME, LandingOperation, LandingRecord,
64 REF_UPDATED_FILENAME, SymbolicHead, TransactionMode, landing_record_filename, post_index_filename,
65 pre_landing_index_filename,
66};
67use super::transaction_journal_encode::{encode_landing, encode_ref_updated};
69use super::transaction_phase::{Phase, PhaseMarkerError, reach_transaction_phase};
71use std::path::{Path, PathBuf};
73
74const TAG: &str = "landTransaction";
76
77#[derive(Clone, Debug, Eq, PartialEq)]
88pub enum LandingPayload {
89 Commit {
91 new_oid: String,
93 landed_tree_oid: String,
95 expected_old: Base,
99 landed_index: PathBuf,
101 exact_private_index: bool,
104 guarded_paths: Vec<Vec<u8>>,
107 },
108 NormalizeOnly {
110 landed_tree_oid: String,
112 },
113}
114
115#[derive(Clone, Debug, Eq, PartialEq)]
125pub enum LandingOutcome {
126 Landed {
129 oid: String,
133 },
134 HeadMoved {
137 current: Base,
139 replayable: bool,
144 },
145 BranchSwitched {
150 current: SymbolicHead,
152 },
153}
154
155#[derive(Debug)]
166pub enum LandingError {
167 Lock(CoordinationFailure),
169 Git(GitFailure),
171 Recovery(RecoveryError),
175 Journal(String),
177 Phase(PhaseMarkerError),
179 Release(String),
181 LandedThenFailed {
184 oid: String,
186 target: String,
188 cause: Box<LandingError>,
190 },
191}
192
193impl std::fmt::Display for LandingError {
195 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 return match self {
198 Self::Lock(failure) => write!(formatter, "{failure:?}"),
199 Self::Git(failure) => write!(formatter, "{failure}"),
200 Self::Recovery(failure) => write!(formatter, "{}", failure.0),
201 Self::Journal(text) | Self::Release(text) => formatter.write_str(text.as_str()),
202 Self::Phase(failure) => write!(formatter, "{failure}"),
203 Self::LandedThenFailed { oid, target, cause } => write!(
204 formatter,
205 "the commit {oid} landed on {target}, but completing its records failed: {cause}. \
206The commit is done, so do not retry it; check it with git log and git status."
207 ),
208 };
209 }
210}
211
212#[derive(Clone, Debug, Eq, PartialEq)]
224pub struct LandingWorkspace {
225 pub transaction_id: String,
228 pub directory: PathBuf,
230 pub shadow: PathBuf,
232 pub captured_index: PathBuf,
234 pub commit_index: PathBuf,
236 pub preserved: bool,
240}
241
242#[derive(Clone, Copy, Debug)]
252pub struct LandingRequest<'request> {
253 pub capture: &'request InvocationCapture,
255 pub mode: TransactionMode,
257 pub payload: &'request LandingPayload,
259 pub committed_paths: &'request [Vec<u8>],
261 pub added_paths: &'request [AddedPath],
263 pub selected_worktree_paths: &'request [AddedPath],
265 pub index_lock_timeout_ms: u64,
267 pub attempt: i64,
270}
271
272fn write_journal(directory: &Path, filename: &str, bytes: &[u8], sync: RecordSync) -> Result<(), LandingError> {
286 return write_record(directory, filename, bytes, sync).map_err(LandingError::Journal);
287}
288
289fn reach(phase: Phase) -> Result<(), LandingError> {
299 return reach_transaction_phase(phase, None).map_err(LandingError::Phase);
300}
301
302fn artifact_identity(path: &Path) -> Result<FileIdentity, LandingError> {
313 return match file_identity_of(path) {
314 Ok((device, inode, _)) => Ok(FileIdentity { device, inode }),
315 Err(error) => Err(LandingError::Journal(format!("reading the identity of {} failed: {error}", path.display()))),
316 };
317}
318
319fn artifact_digest(path: &Path) -> Result<ContentDigest, LandingError> {
330 return digest_file(path)
331 .map_err(|error| { return LandingError::Journal(format!("reading {} to digest it failed: {error}", path.display())); });
332}
333
334fn names_commit(target: &Base) -> bool {
344 return matches!(target, Base::Commit(_));
345}
346
347struct Held {
358 lock: Option<RealIndexLock>,
361 for_recovery: bool,
364}
365
366pub fn land_transaction(
383 context: &GitContext,
384 cwd: &Path,
385 _landing_lock: &OwnerLock,
386 workspace: &mut LandingWorkspace,
387 request: &LandingRequest<'_>,
388) -> Result<LandingOutcome, LandingError> {
389 debug(TAG, format!("landing attempt {} of transaction {}", request.attempt, workspace.transaction_id).as_str());
390 let acquired: RealIndexLock = acquire_landing_index_lock(
391 request.capture.layout.real_index_path.as_path(),
392 workspace.directory.as_path(),
393 request.attempt,
394 request.index_lock_timeout_ms,
395 "landed nothing",
396 )
397 .map_err(LandingError::Lock)?;
398 let mut held: Held = Held { lock: Some(acquired), for_recovery: false };
399 let result: Result<LandingOutcome, LandingError> = land_locked(context, cwd, workspace, request, &mut held);
400 let Some(lock) = held.lock.take() else {
401 return result;
402 };
403 if held.for_recovery {
404 debug(TAG, "the target advanced before the install failed; the index lock is left for recovery");
405 return result;
406 }
407 match (lock.release_after_landing(), result) {
408 (Ok(()), outcome) => return outcome,
409 (Err(release), Ok(_)) => return Err(LandingError::Release(release)),
410 (Err(release), Err(error)) => {
411 warn(TAG, format!("the index lock could not be released after a failed landing: {release}").as_str());
412 return Err(error);
413 }
414 }
415}
416
417fn land_locked(
428 context: &GitContext,
429 cwd: &Path,
430 workspace: &mut LandingWorkspace,
431 request: &LandingRequest<'_>,
432 held: &mut Held,
433) -> Result<LandingOutcome, LandingError> {
434 reach(Phase::LandingLocked)?;
435 let capture: &InvocationCapture = request.capture;
436 let ref_files: RefFilesLayout<'_> = RefFilesLayout {
438 git_dir: capture.layout.git_dir.as_path(),
439 common_dir: capture.layout.common_dir.as_path(),
440 ref_format: capture.layout.ref_format,
441 object_format: capture.layout.object_format.as_str(),
442 };
443 let symbolic_head: SymbolicHead = resolve_symbolic_head(context, cwd, &ref_files).map_err(LandingError::Git)?;
444 if symbolic_head != capture.layout.symbolic_head {
445 debug(TAG, "HEAD names another target than at invocation");
446 return Ok(LandingOutcome::BranchSwitched { current: symbolic_head });
447 }
448 let expected_old: Base = match request.payload {
449 LandingPayload::Commit { expected_old, .. } => expected_old.clone(),
450 LandingPayload::NormalizeOnly { .. } => capture.base.clone(),
451 };
452 let current: Base = resolve_ref_commit(context, cwd, capture.layout.target_ref.as_str()).map_err(LandingError::Git)?;
453 if !same_target(¤t, &expected_old) {
454 debug(TAG, format!("{} moved since preparation", capture.layout.target_ref).as_str());
455 let replayable: bool = matches!(request.payload, LandingPayload::Commit { .. })
456 && capture.layout.conclusion == Conclusion::None
457 && names_commit(¤t);
458 return Ok(LandingOutcome::HeadMoved { current, replayable });
459 }
460 let pack_name: Option<String> = match request.payload {
461 LandingPayload::Commit { new_oid, .. } => Some(
462 migrate_shadow_objects(
463 context,
464 cwd,
465 workspace.shadow.as_path(),
466 new_oid.as_str(),
467 &expected_old,
468 Some(transaction_keep_message(workspace.transaction_id.as_str()).as_str()),
469 )
470 .map_err(LandingError::Git)?,
471 ),
472 LandingPayload::NormalizeOnly { .. } => None,
473 };
474 let post: PathBuf = match record_landing(context, cwd, workspace, request, held, &expected_old, pack_name.as_ref()) {
475 Ok(post) => post,
476 Err(error) => {
477 discard_unrecorded_keep(capture.layout.object_directory.as_path(), workspace, pack_name.as_deref());
478 return Err(error);
479 }
480 };
481 if let LandingPayload::Commit { new_oid, .. } = request.payload {
482 let pack: &str = pack_name.as_deref().unwrap_or("");
483 if let Some(lost) = advance_target(context, cwd, workspace, request, held, new_oid.as_str(), &expected_old, pack)? {
484 return Ok(lost);
485 }
486 }
487 if let Err(error) = complete_landing(context, cwd, workspace, request, held, post.as_path()) {
488 return Err(match request.payload {
490 LandingPayload::Commit { new_oid, .. } => LandingError::LandedThenFailed {
491 oid: new_oid.clone(),
492 target: capture.layout.target_ref.clone(),
493 cause: Box::new(error),
494 },
495 LandingPayload::NormalizeOnly { .. } => error,
496 });
497 }
498 debug(TAG, format!("landed attempt {}", request.attempt).as_str());
499 let oid: String = match request.payload {
500 LandingPayload::Commit { new_oid, .. } => new_oid.clone(),
501 LandingPayload::NormalizeOnly { .. } => match &capture.base {
502 Base::Commit(oid) => oid.clone(),
503 Base::Unborn => String::new(),
504 },
505 };
506 return Ok(LandingOutcome::Landed { oid });
507}
508
509fn complete_landing(
520 context: &GitContext,
521 cwd: &Path,
522 workspace: &LandingWorkspace,
523 request: &LandingRequest<'_>,
524 held: &mut Held,
525 post: &Path,
526) -> Result<(), LandingError> {
527 held.for_recovery = true;
529 let Some(installing) = held.lock.take() else {
530 return Err(LandingError::Journal(String::from("the real index lock is not held")));
531 };
532 installing.install(post).map_err(LandingError::Recovery)?;
533 write_journal(workspace.directory.as_path(), INDEX_INSTALLED_FILENAME, b"", RecordSync::Nothing)?;
535 reach(Phase::IndexInstalled)?;
536 if matches!(request.payload, LandingPayload::Commit { .. }) {
537 reproduce_conclusion_cleanup(
538 context,
539 cwd,
540 request.capture.layout.git_dir.as_path(),
541 workspace.shadow.as_path(),
542 workspace.directory.as_path(),
543 request.capture.layout.ref_format,
544 )
545 .map_err(LandingError::Recovery)?;
546 }
547 return Ok(());
548}
549
550fn compute_post_index(
561 context: &GitContext,
562 cwd: &Path,
563 workspace: &LandingWorkspace,
564 request: &LandingRequest<'_>,
565 landed_tree: &str,
566 pre_landing: &Path,
567 post: &Path,
568) -> Result<(), LandingError> {
569 let base: String = base_revision(request.capture);
570 let (landed_index, exact_private_index, guarded_paths): (&Path, bool, &[Vec<u8>]) = match request.payload {
571 LandingPayload::Commit { landed_index, exact_private_index, guarded_paths, .. } => {
572 (landed_index.as_path(), *exact_private_index, guarded_paths.as_slice())
573 }
574 LandingPayload::NormalizeOnly { .. } => (workspace.commit_index.as_path(), true, &[]),
575 };
576 let input: PostIndexInput<'_> = PostIndexInput {
577 mode: request.mode,
578 pre_landing_index: pre_landing,
579 captured_index: workspace.captured_index.as_path(),
580 landed_index,
581 post_index: post,
582 landed_tree,
583 base_revision: base.as_str(),
584 committed_paths: request.committed_paths,
585 guarded_paths,
586 exact_private_index,
587 };
588 return compute_landing_post_index(context, cwd, &input).map_err(LandingError::Git);
589}
590
591fn record_landing(
605 context: &GitContext,
606 cwd: &Path,
607 workspace: &mut LandingWorkspace,
608 request: &LandingRequest<'_>,
609 held: &Held,
610 expected_old: &Base,
611 pack_name: Option<&String>,
612) -> Result<PathBuf, LandingError> {
613 reach(Phase::ObjectsMigrated)?;
614 let capture: &InvocationCapture = request.capture;
615 let landed_tree: &str = match request.payload {
616 LandingPayload::Commit { landed_tree_oid, .. } | LandingPayload::NormalizeOnly { landed_tree_oid } => landed_tree_oid.as_str(),
617 };
618 let pre_landing: PathBuf = workspace.directory.join(pre_landing_index_filename(request.attempt));
619 let post: PathBuf = workspace.directory.join(post_index_filename(request.attempt));
620 if let Err(error) = copy_index_with_timestamps(capture.layout.real_index_path.as_path(), pre_landing.as_path()) {
621 return Err(LandingError::Journal(format!("copying the real index to {} failed: {error}", pre_landing.display())));
622 }
623 compute_post_index(context, cwd, workspace, request, landed_tree, pre_landing.as_path(), post.as_path())?;
624 workspace.preserved = true;
626 let lock_identity = match held.lock.as_ref() {
627 Some(lock) => lock.identity().clone(),
628 None => return Err(LandingError::Journal(String::from("the real index lock is not held"))),
629 };
630 let record: LandingRecord = LandingRecord {
631 attempt: request.attempt,
632 operation: match request.payload {
633 LandingPayload::Commit { .. } => LandingOperation::Commit,
634 LandingPayload::NormalizeOnly { .. } => LandingOperation::NormalizeOnly,
635 },
636 expected_old: expected_old.clone(),
637 new_oid: match request.payload {
638 LandingPayload::Commit { new_oid, .. } => Some(new_oid.clone()),
639 LandingPayload::NormalizeOnly { .. } => None,
640 },
641 landed_tree_oid: landed_tree.to_string(),
642 pre_landing_index: artifact_identity(pre_landing.as_path())?,
643 post_index: artifact_identity(post.as_path())?,
644 lock: lock_identity,
645 pack_name: pack_name.cloned(),
646 added_paths: request.added_paths.to_vec(),
647 selected_worktree_paths: request.selected_worktree_paths.to_vec(),
648 pre_landing_digest: Some(artifact_digest(pre_landing.as_path())?),
649 post_digest: Some(artifact_digest(post.as_path())?),
650 };
651 write_journal(
652 workspace.directory.as_path(),
653 landing_record_filename(request.attempt).as_str(),
654 encode_landing(&record).as_bytes(),
655 RecordSync::FileAndDirectory,
657 )?;
658 return Ok(post);
659}
660
661fn discard_unrecorded_keep(objects: &Path, workspace: &LandingWorkspace, pack_name: Option<&str>) {
672 if workspace.preserved {
673 return;
674 }
675 let Some(pack) = pack_name else {
676 return;
677 };
678 if let Err(error) = remove_pack_keep(objects, pack) {
679 warn(TAG, format!("the pack keep of a landing that failed before its record could not be removed: {error}").as_str());
680 }
681}
682
683fn advance_target(
698 context: &GitContext,
699 cwd: &Path,
700 workspace: &mut LandingWorkspace,
701 request: &LandingRequest<'_>,
702 held: &mut Held,
703 new_oid: &str,
704 expected_old: &Base,
705 pack_name: &str,
706) -> Result<Option<LandingOutcome>, LandingError> {
707 let capture: &InvocationCapture = request.capture;
708 let swap: CompareAndSwap<'_> = CompareAndSwap {
709 target_ref: capture.layout.target_ref.as_str(),
710 detached: capture.layout.symbolic_head == SymbolicHead::Detached,
711 nonce: workspace.transaction_id.as_str(),
712 new_oid,
713 expected_old,
714 hash_length: capture.layout.empty_tree_oid.len(),
715 };
716 let output = compare_and_swap_target(context, cwd, &swap).map_err(LandingError::Git)?;
717 let objects: &Path = capture.layout.object_directory.as_path();
718 if !output.succeeded() {
719 debug(TAG, format!("compare-and-swap lost: {}", String::from_utf8_lossy(&output.stderr).trim()).as_str());
720 remove_pack_keep(objects, pack_name).map_err(LandingError::Recovery)?;
721 workspace.preserved = false;
722 let winner: Base = resolve_ref_commit(context, cwd, capture.layout.target_ref.as_str()).map_err(LandingError::Git)?;
723 let replayable: bool = capture.layout.conclusion == Conclusion::None && names_commit(&winner);
724 return Ok(Some(LandingOutcome::HeadMoved { current: winner, replayable }));
725 }
726 held.for_recovery = true;
728 write_journal(
729 workspace.directory.as_path(),
730 REF_UPDATED_FILENAME,
731 encode_ref_updated(new_oid).as_bytes(),
732 RecordSync::FileOnly,
734 )?;
735 reach(Phase::RefUpdated)?;
736 remove_pack_keep(objects, pack_name).map_err(LandingError::Recovery)?;
737 record_landed_capture_or_warn(
738 capture.layout.git_dir.as_path(),
739 workspace.directory.as_path(),
740 workspace.transaction_id.as_str(),
741 new_oid,
742 );
743 return Ok(None);
744}
745
746#[cfg(test)]
748#[path = "landing_attempt_tests.rs"]
749mod tests;