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::private_storage::{sync_directory, write_private_file};
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]) -> Result<(), LandingError> {
286 let path: PathBuf = directory.join(filename);
287 if let Err(error) = write_private_file(path.as_path(), bytes) {
288 return Err(LandingError::Journal(format!("writing {} failed: {error}", path.display())));
289 }
290 if let Err(error) = sync_directory(directory) {
291 return Err(LandingError::Journal(format!("syncing {} failed: {error}", directory.display())));
292 }
293 return Ok(());
294}
295
296fn reach(phase: Phase) -> Result<(), LandingError> {
306 return reach_transaction_phase(phase, None).map_err(LandingError::Phase);
307}
308
309fn artifact_identity(path: &Path) -> Result<FileIdentity, LandingError> {
320 return match file_identity_of(path) {
321 Ok((device, inode, _)) => Ok(FileIdentity { device, inode }),
322 Err(error) => Err(LandingError::Journal(format!("reading the identity of {} failed: {error}", path.display()))),
323 };
324}
325
326fn artifact_digest(path: &Path) -> Result<ContentDigest, LandingError> {
337 return digest_file(path)
338 .map_err(|error| { return LandingError::Journal(format!("reading {} to digest it failed: {error}", path.display())); });
339}
340
341fn names_commit(target: &Base) -> bool {
351 return matches!(target, Base::Commit(_));
352}
353
354struct Held {
365 lock: Option<RealIndexLock>,
368 for_recovery: bool,
371}
372
373pub fn land_transaction(
390 context: &GitContext,
391 cwd: &Path,
392 _landing_lock: &OwnerLock,
393 workspace: &mut LandingWorkspace,
394 request: &LandingRequest<'_>,
395) -> Result<LandingOutcome, LandingError> {
396 debug(TAG, format!("landing attempt {} of transaction {}", request.attempt, workspace.transaction_id).as_str());
397 let acquired: RealIndexLock = acquire_landing_index_lock(
398 request.capture.layout.real_index_path.as_path(),
399 workspace.directory.as_path(),
400 request.attempt,
401 request.index_lock_timeout_ms,
402 "landed nothing",
403 )
404 .map_err(LandingError::Lock)?;
405 let mut held: Held = Held { lock: Some(acquired), for_recovery: false };
406 let result: Result<LandingOutcome, LandingError> = land_locked(context, cwd, workspace, request, &mut held);
407 let Some(lock) = held.lock.take() else {
408 return result;
409 };
410 if held.for_recovery {
411 debug(TAG, "the target advanced before the install failed; the index lock is left for recovery");
412 return result;
413 }
414 match (lock.release_after_landing(), result) {
415 (Ok(()), outcome) => return outcome,
416 (Err(release), Ok(_)) => return Err(LandingError::Release(release)),
417 (Err(release), Err(error)) => {
418 warn(TAG, format!("the index lock could not be released after a failed landing: {release}").as_str());
419 return Err(error);
420 }
421 }
422}
423
424fn land_locked(
435 context: &GitContext,
436 cwd: &Path,
437 workspace: &mut LandingWorkspace,
438 request: &LandingRequest<'_>,
439 held: &mut Held,
440) -> Result<LandingOutcome, LandingError> {
441 reach(Phase::LandingLocked)?;
442 let capture: &InvocationCapture = request.capture;
443 let ref_files: RefFilesLayout<'_> = RefFilesLayout {
445 git_dir: capture.layout.git_dir.as_path(),
446 common_dir: capture.layout.common_dir.as_path(),
447 ref_format: capture.layout.ref_format,
448 object_format: capture.layout.object_format.as_str(),
449 };
450 let symbolic_head: SymbolicHead = resolve_symbolic_head(context, cwd, &ref_files).map_err(LandingError::Git)?;
451 if symbolic_head != capture.layout.symbolic_head {
452 debug(TAG, "HEAD names another target than at invocation");
453 return Ok(LandingOutcome::BranchSwitched { current: symbolic_head });
454 }
455 let expected_old: Base = match request.payload {
456 LandingPayload::Commit { expected_old, .. } => expected_old.clone(),
457 LandingPayload::NormalizeOnly { .. } => capture.base.clone(),
458 };
459 let current: Base = resolve_ref_commit(context, cwd, capture.layout.target_ref.as_str()).map_err(LandingError::Git)?;
460 if !same_target(¤t, &expected_old) {
461 debug(TAG, format!("{} moved since preparation", capture.layout.target_ref).as_str());
462 let replayable: bool = matches!(request.payload, LandingPayload::Commit { .. })
463 && capture.layout.conclusion == Conclusion::None
464 && names_commit(¤t);
465 return Ok(LandingOutcome::HeadMoved { current, replayable });
466 }
467 let pack_name: Option<String> = match request.payload {
468 LandingPayload::Commit { new_oid, .. } => Some(
469 migrate_shadow_objects(
470 context,
471 cwd,
472 workspace.shadow.as_path(),
473 new_oid.as_str(),
474 &expected_old,
475 Some(transaction_keep_message(workspace.transaction_id.as_str()).as_str()),
476 )
477 .map_err(LandingError::Git)?,
478 ),
479 LandingPayload::NormalizeOnly { .. } => None,
480 };
481 let post: PathBuf = match record_landing(context, cwd, workspace, request, held, &expected_old, pack_name.as_ref()) {
482 Ok(post) => post,
483 Err(error) => {
484 discard_unrecorded_keep(capture.layout.object_directory.as_path(), workspace, pack_name.as_deref());
485 return Err(error);
486 }
487 };
488 if let LandingPayload::Commit { new_oid, .. } = request.payload {
489 let pack: &str = pack_name.as_deref().unwrap_or("");
490 if let Some(lost) = advance_target(context, cwd, workspace, request, held, new_oid.as_str(), &expected_old, pack)? {
491 return Ok(lost);
492 }
493 }
494 if let Err(error) = complete_landing(context, cwd, workspace, request, held, post.as_path()) {
495 return Err(match request.payload {
497 LandingPayload::Commit { new_oid, .. } => LandingError::LandedThenFailed {
498 oid: new_oid.clone(),
499 target: capture.layout.target_ref.clone(),
500 cause: Box::new(error),
501 },
502 LandingPayload::NormalizeOnly { .. } => error,
503 });
504 }
505 debug(TAG, format!("landed attempt {}", request.attempt).as_str());
506 let oid: String = match request.payload {
507 LandingPayload::Commit { new_oid, .. } => new_oid.clone(),
508 LandingPayload::NormalizeOnly { .. } => match &capture.base {
509 Base::Commit(oid) => oid.clone(),
510 Base::Unborn => String::new(),
511 },
512 };
513 return Ok(LandingOutcome::Landed { oid });
514}
515
516fn complete_landing(
527 context: &GitContext,
528 cwd: &Path,
529 workspace: &LandingWorkspace,
530 request: &LandingRequest<'_>,
531 held: &mut Held,
532 post: &Path,
533) -> Result<(), LandingError> {
534 held.for_recovery = true;
536 let Some(installing) = held.lock.take() else {
537 return Err(LandingError::Journal(String::from("the real index lock is not held")));
538 };
539 installing.install(post).map_err(LandingError::Recovery)?;
540 write_journal(workspace.directory.as_path(), INDEX_INSTALLED_FILENAME, b"")?;
541 reach(Phase::IndexInstalled)?;
542 if matches!(request.payload, LandingPayload::Commit { .. }) {
543 reproduce_conclusion_cleanup(
544 context,
545 cwd,
546 request.capture.layout.git_dir.as_path(),
547 workspace.shadow.as_path(),
548 workspace.directory.as_path(),
549 request.capture.layout.ref_format,
550 )
551 .map_err(LandingError::Recovery)?;
552 }
553 return Ok(());
554}
555
556fn compute_post_index(
567 context: &GitContext,
568 cwd: &Path,
569 workspace: &LandingWorkspace,
570 request: &LandingRequest<'_>,
571 landed_tree: &str,
572 pre_landing: &Path,
573 post: &Path,
574) -> Result<(), LandingError> {
575 let base: String = base_revision(request.capture);
576 let (landed_index, exact_private_index, guarded_paths): (&Path, bool, &[Vec<u8>]) = match request.payload {
577 LandingPayload::Commit { landed_index, exact_private_index, guarded_paths, .. } => {
578 (landed_index.as_path(), *exact_private_index, guarded_paths.as_slice())
579 }
580 LandingPayload::NormalizeOnly { .. } => (workspace.commit_index.as_path(), true, &[]),
581 };
582 let input: PostIndexInput<'_> = PostIndexInput {
583 mode: request.mode,
584 pre_landing_index: pre_landing,
585 captured_index: workspace.captured_index.as_path(),
586 landed_index,
587 post_index: post,
588 landed_tree,
589 base_revision: base.as_str(),
590 committed_paths: request.committed_paths,
591 guarded_paths,
592 exact_private_index,
593 };
594 return compute_landing_post_index(context, cwd, &input).map_err(LandingError::Git);
595}
596
597fn record_landing(
611 context: &GitContext,
612 cwd: &Path,
613 workspace: &mut LandingWorkspace,
614 request: &LandingRequest<'_>,
615 held: &Held,
616 expected_old: &Base,
617 pack_name: Option<&String>,
618) -> Result<PathBuf, LandingError> {
619 reach(Phase::ObjectsMigrated)?;
620 let capture: &InvocationCapture = request.capture;
621 let landed_tree: &str = match request.payload {
622 LandingPayload::Commit { landed_tree_oid, .. } | LandingPayload::NormalizeOnly { landed_tree_oid } => landed_tree_oid.as_str(),
623 };
624 let pre_landing: PathBuf = workspace.directory.join(pre_landing_index_filename(request.attempt));
625 let post: PathBuf = workspace.directory.join(post_index_filename(request.attempt));
626 if let Err(error) = copy_index_with_timestamps(capture.layout.real_index_path.as_path(), pre_landing.as_path()) {
627 return Err(LandingError::Journal(format!("copying the real index to {} failed: {error}", pre_landing.display())));
628 }
629 compute_post_index(context, cwd, workspace, request, landed_tree, pre_landing.as_path(), post.as_path())?;
630 workspace.preserved = true;
632 let lock_identity = match held.lock.as_ref() {
633 Some(lock) => lock.identity().clone(),
634 None => return Err(LandingError::Journal(String::from("the real index lock is not held"))),
635 };
636 let record: LandingRecord = LandingRecord {
637 attempt: request.attempt,
638 operation: match request.payload {
639 LandingPayload::Commit { .. } => LandingOperation::Commit,
640 LandingPayload::NormalizeOnly { .. } => LandingOperation::NormalizeOnly,
641 },
642 expected_old: expected_old.clone(),
643 new_oid: match request.payload {
644 LandingPayload::Commit { new_oid, .. } => Some(new_oid.clone()),
645 LandingPayload::NormalizeOnly { .. } => None,
646 },
647 landed_tree_oid: landed_tree.to_string(),
648 pre_landing_index: artifact_identity(pre_landing.as_path())?,
649 post_index: artifact_identity(post.as_path())?,
650 lock: lock_identity,
651 pack_name: pack_name.cloned(),
652 added_paths: request.added_paths.to_vec(),
653 selected_worktree_paths: request.selected_worktree_paths.to_vec(),
654 pre_landing_digest: Some(artifact_digest(pre_landing.as_path())?),
655 post_digest: Some(artifact_digest(post.as_path())?),
656 };
657 write_journal(
658 workspace.directory.as_path(),
659 landing_record_filename(request.attempt).as_str(),
660 encode_landing(&record).as_bytes(),
661 )?;
662 return Ok(post);
663}
664
665fn discard_unrecorded_keep(objects: &Path, workspace: &LandingWorkspace, pack_name: Option<&str>) {
676 if workspace.preserved {
677 return;
678 }
679 let Some(pack) = pack_name else {
680 return;
681 };
682 if let Err(error) = remove_pack_keep(objects, pack) {
683 warn(TAG, format!("the pack keep of a landing that failed before its record could not be removed: {error}").as_str());
684 }
685}
686
687fn advance_target(
702 context: &GitContext,
703 cwd: &Path,
704 workspace: &mut LandingWorkspace,
705 request: &LandingRequest<'_>,
706 held: &mut Held,
707 new_oid: &str,
708 expected_old: &Base,
709 pack_name: &str,
710) -> Result<Option<LandingOutcome>, LandingError> {
711 let capture: &InvocationCapture = request.capture;
712 let swap: CompareAndSwap<'_> = CompareAndSwap {
713 target_ref: capture.layout.target_ref.as_str(),
714 detached: capture.layout.symbolic_head == SymbolicHead::Detached,
715 nonce: workspace.transaction_id.as_str(),
716 new_oid,
717 expected_old,
718 hash_length: capture.layout.empty_tree_oid.len(),
719 };
720 let output = compare_and_swap_target(context, cwd, &swap).map_err(LandingError::Git)?;
721 let objects: &Path = capture.layout.object_directory.as_path();
722 if !output.succeeded() {
723 debug(TAG, format!("compare-and-swap lost: {}", String::from_utf8_lossy(&output.stderr).trim()).as_str());
724 remove_pack_keep(objects, pack_name).map_err(LandingError::Recovery)?;
725 workspace.preserved = false;
726 let winner: Base = resolve_ref_commit(context, cwd, capture.layout.target_ref.as_str()).map_err(LandingError::Git)?;
727 let replayable: bool = capture.layout.conclusion == Conclusion::None && names_commit(&winner);
728 return Ok(Some(LandingOutcome::HeadMoved { current: winner, replayable }));
729 }
730 held.for_recovery = true;
732 write_journal(workspace.directory.as_path(), REF_UPDATED_FILENAME, encode_ref_updated(new_oid).as_bytes())?;
733 reach(Phase::RefUpdated)?;
734 remove_pack_keep(objects, pack_name).map_err(LandingError::Recovery)?;
735 record_landed_capture_or_warn(
736 capture.layout.git_dir.as_path(),
737 workspace.directory.as_path(),
738 workspace.transaction_id.as_str(),
739 new_oid,
740 );
741 return Ok(None);
742}
743
744#[cfg(test)]
746#[path = "landing_attempt_tests.rs"]
747mod tests;