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<String>,
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}
178
179impl std::fmt::Display for LandingError {
181 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 return match self {
184 Self::Lock(failure) => write!(formatter, "{failure:?}"),
185 Self::Git(failure) => write!(formatter, "{failure}"),
186 Self::Recovery(failure) => write!(formatter, "{}", failure.0),
187 Self::Journal(text) | Self::Release(text) => formatter.write_str(text.as_str()),
188 Self::Phase(failure) => write!(formatter, "{failure}"),
189 };
190 }
191}
192
193#[derive(Clone, Debug, Eq, PartialEq)]
205pub struct LandingWorkspace {
206 pub transaction_id: String,
209 pub directory: PathBuf,
211 pub shadow: PathBuf,
213 pub captured_index: PathBuf,
215 pub commit_index: PathBuf,
217 pub preserved: bool,
221}
222
223#[derive(Clone, Copy, Debug)]
233pub struct LandingRequest<'request> {
234 pub capture: &'request InvocationCapture,
236 pub mode: TransactionMode,
238 pub payload: &'request LandingPayload,
240 pub committed_paths: &'request [String],
242 pub added_paths: &'request [AddedPath],
244 pub selected_worktree_paths: &'request [AddedPath],
246 pub index_lock_timeout_ms: u64,
248 pub attempt: i64,
251}
252
253fn write_journal(directory: &Path, filename: &str, bytes: &[u8]) -> Result<(), LandingError> {
267 let path: PathBuf = directory.join(filename);
268 if let Err(error) = write_private_file(path.as_path(), bytes) {
269 return Err(LandingError::Journal(format!("writing {} failed: {error}", path.display())));
270 }
271 if let Err(error) = sync_directory(directory) {
272 return Err(LandingError::Journal(format!("syncing {} failed: {error}", directory.display())));
273 }
274 return Ok(());
275}
276
277fn reach(phase: Phase) -> Result<(), LandingError> {
287 return reach_transaction_phase(phase, None).map_err(LandingError::Phase);
288}
289
290fn artifact_identity(path: &Path) -> Result<FileIdentity, LandingError> {
301 return match file_identity_of(path) {
302 Ok((device, inode, _)) => Ok(FileIdentity { device, inode }),
303 Err(error) => Err(LandingError::Journal(format!("reading the identity of {} failed: {error}", path.display()))),
304 };
305}
306
307fn names_commit(target: &Base) -> bool {
317 return matches!(target, Base::Commit(_));
318}
319
320struct Held {
331 lock: Option<RealIndexLock>,
334 for_recovery: bool,
337}
338
339pub fn land_transaction(
356 context: &GitContext,
357 cwd: &Path,
358 _landing_lock: &OwnerLock,
359 workspace: &mut LandingWorkspace,
360 request: &LandingRequest<'_>,
361) -> Result<LandingOutcome, LandingError> {
362 debug(TAG, format!("landing attempt {} of transaction {}", request.attempt, workspace.transaction_id).as_str());
363 let acquired: RealIndexLock = acquire_landing_index_lock(
364 request.capture.layout.real_index_path.as_path(),
365 workspace.directory.as_path(),
366 request.attempt,
367 request.index_lock_timeout_ms,
368 "landed nothing",
369 )
370 .map_err(LandingError::Lock)?;
371 let mut held: Held = Held { lock: Some(acquired), for_recovery: false };
372 let result: Result<LandingOutcome, LandingError> = land_locked(context, cwd, workspace, request, &mut held);
373 let Some(lock) = held.lock.take() else {
374 return result;
375 };
376 if held.for_recovery {
377 debug(TAG, "the target advanced before the install failed; the index lock is left for recovery");
378 return result;
379 }
380 match (lock.release_after_landing(), result) {
381 (Ok(()), outcome) => return outcome,
382 (Err(release), Ok(_)) => return Err(LandingError::Release(release)),
383 (Err(release), Err(error)) => {
384 warn(TAG, format!("the index lock could not be released after a failed landing: {release}").as_str());
385 return Err(error);
386 }
387 }
388}
389
390fn land_locked(
401 context: &GitContext,
402 cwd: &Path,
403 workspace: &mut LandingWorkspace,
404 request: &LandingRequest<'_>,
405 held: &mut Held,
406) -> Result<LandingOutcome, LandingError> {
407 reach(Phase::LandingLocked)?;
408 let capture: &InvocationCapture = request.capture;
409 let symbolic_head: SymbolicHead = resolve_symbolic_head(context, cwd).map_err(LandingError::Git)?;
410 if symbolic_head != capture.layout.symbolic_head {
411 debug(TAG, "HEAD names another target than at invocation");
412 return Ok(LandingOutcome::BranchSwitched { current: symbolic_head });
413 }
414 let expected_old: Base = match request.payload {
415 LandingPayload::Commit { expected_old, .. } => expected_old.clone(),
416 LandingPayload::NormalizeOnly { .. } => capture.base.clone(),
417 };
418 let current: Base = resolve_ref_commit(context, cwd, capture.layout.target_ref.as_str()).map_err(LandingError::Git)?;
419 if !same_target(¤t, &expected_old) {
420 debug(TAG, format!("{} moved since preparation", capture.layout.target_ref).as_str());
421 let replayable: bool = matches!(request.payload, LandingPayload::Commit { .. })
422 && capture.layout.conclusion == Conclusion::None
423 && names_commit(¤t);
424 return Ok(LandingOutcome::HeadMoved { current, replayable });
425 }
426 let pack_name: Option<String> = match request.payload {
427 LandingPayload::Commit { new_oid, .. } => Some(
428 migrate_shadow_objects(
429 context,
430 cwd,
431 workspace.shadow.as_path(),
432 new_oid.as_str(),
433 &expected_old,
434 Some(transaction_keep_message(workspace.transaction_id.as_str()).as_str()),
435 )
436 .map_err(LandingError::Git)?,
437 ),
438 LandingPayload::NormalizeOnly { .. } => None,
439 };
440 reach(Phase::ObjectsMigrated)?;
441 let landed_tree: &str = match request.payload {
442 LandingPayload::Commit { landed_tree_oid, .. } | LandingPayload::NormalizeOnly { landed_tree_oid } => landed_tree_oid.as_str(),
443 };
444 let pre_landing: PathBuf = workspace.directory.join(pre_landing_index_filename(request.attempt));
445 let post: PathBuf = workspace.directory.join(post_index_filename(request.attempt));
446 if let Err(error) = copy_index_with_timestamps(capture.layout.real_index_path.as_path(), pre_landing.as_path()) {
447 return Err(LandingError::Journal(format!("copying the real index to {} failed: {error}", pre_landing.display())));
448 }
449 compute_post_index(context, cwd, workspace, request, landed_tree, pre_landing.as_path(), post.as_path())?;
450 workspace.preserved = true;
452 let lock_identity = match held.lock.as_ref() {
453 Some(lock) => lock.identity().clone(),
454 None => return Err(LandingError::Journal(String::from("the real index lock is not held"))),
455 };
456 let record: LandingRecord = LandingRecord {
457 attempt: request.attempt,
458 operation: match request.payload {
459 LandingPayload::Commit { .. } => LandingOperation::Commit,
460 LandingPayload::NormalizeOnly { .. } => LandingOperation::NormalizeOnly,
461 },
462 expected_old: expected_old.clone(),
463 new_oid: match request.payload {
464 LandingPayload::Commit { new_oid, .. } => Some(new_oid.clone()),
465 LandingPayload::NormalizeOnly { .. } => None,
466 },
467 landed_tree_oid: landed_tree.to_string(),
468 pre_landing_index: artifact_identity(pre_landing.as_path())?,
469 post_index: artifact_identity(post.as_path())?,
470 lock: lock_identity,
471 pack_name: pack_name.clone(),
472 added_paths: request.added_paths.to_vec(),
473 selected_worktree_paths: request.selected_worktree_paths.to_vec(),
474 };
475 write_journal(
476 workspace.directory.as_path(),
477 landing_record_filename(request.attempt).as_str(),
478 encode_landing(&record).as_bytes(),
479 )?;
480 if let LandingPayload::Commit { new_oid, .. } = request.payload {
481 let pack: &str = pack_name.as_deref().unwrap_or("");
482 if let Some(lost) = advance_target(context, cwd, workspace, request, held, new_oid.as_str(), &expected_old, pack)? {
483 return Ok(lost);
484 }
485 }
486 held.for_recovery = true;
488 let Some(installing) = held.lock.take() else {
489 return Err(LandingError::Journal(String::from("the real index lock is not held")));
490 };
491 installing.install(post.as_path()).map_err(LandingError::Recovery)?;
492 write_journal(workspace.directory.as_path(), INDEX_INSTALLED_FILENAME, b"")?;
493 reach(Phase::IndexInstalled)?;
494 if matches!(request.payload, LandingPayload::Commit { .. }) {
495 reproduce_conclusion_cleanup(
496 context,
497 cwd,
498 capture.layout.git_dir.as_path(),
499 workspace.shadow.as_path(),
500 workspace.directory.as_path(),
501 capture.layout.ref_format,
502 )
503 .map_err(LandingError::Recovery)?;
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 compute_post_index(
527 context: &GitContext,
528 cwd: &Path,
529 workspace: &LandingWorkspace,
530 request: &LandingRequest<'_>,
531 landed_tree: &str,
532 pre_landing: &Path,
533 post: &Path,
534) -> Result<(), LandingError> {
535 let base: String = base_revision(request.capture);
536 let (landed_index, exact_private_index, guarded_paths): (&Path, bool, &[String]) = match request.payload {
537 LandingPayload::Commit { landed_index, exact_private_index, guarded_paths, .. } => {
538 (landed_index.as_path(), *exact_private_index, guarded_paths.as_slice())
539 }
540 LandingPayload::NormalizeOnly { .. } => (workspace.commit_index.as_path(), true, &[]),
541 };
542 let input: PostIndexInput<'_> = PostIndexInput {
543 mode: request.mode,
544 pre_landing_index: pre_landing,
545 captured_index: workspace.captured_index.as_path(),
546 landed_index,
547 post_index: post,
548 landed_tree,
549 base_revision: base.as_str(),
550 committed_paths: request.committed_paths,
551 guarded_paths,
552 exact_private_index,
553 };
554 return compute_landing_post_index(context, cwd, &input).map_err(LandingError::Git);
555}
556
557fn advance_target(
572 context: &GitContext,
573 cwd: &Path,
574 workspace: &mut LandingWorkspace,
575 request: &LandingRequest<'_>,
576 held: &mut Held,
577 new_oid: &str,
578 expected_old: &Base,
579 pack_name: &str,
580) -> Result<Option<LandingOutcome>, LandingError> {
581 let capture: &InvocationCapture = request.capture;
582 let swap: CompareAndSwap<'_> = CompareAndSwap {
583 target_ref: capture.layout.target_ref.as_str(),
584 detached: capture.layout.symbolic_head == SymbolicHead::Detached,
585 nonce: workspace.transaction_id.as_str(),
586 new_oid,
587 expected_old,
588 hash_length: capture.layout.empty_tree_oid.len(),
589 };
590 let output = compare_and_swap_target(context, cwd, &swap).map_err(LandingError::Git)?;
591 let objects: &Path = capture.layout.object_directory.as_path();
592 if !output.succeeded() {
593 debug(TAG, format!("compare-and-swap lost: {}", String::from_utf8_lossy(&output.stderr).trim()).as_str());
594 remove_pack_keep(objects, pack_name).map_err(LandingError::Recovery)?;
595 workspace.preserved = false;
596 let winner: Base = resolve_ref_commit(context, cwd, capture.layout.target_ref.as_str()).map_err(LandingError::Git)?;
597 let replayable: bool = capture.layout.conclusion == Conclusion::None && names_commit(&winner);
598 return Ok(Some(LandingOutcome::HeadMoved { current: winner, replayable }));
599 }
600 held.for_recovery = true;
602 write_journal(workspace.directory.as_path(), REF_UPDATED_FILENAME, encode_ref_updated(new_oid).as_bytes())?;
603 reach(Phase::RefUpdated)?;
604 remove_pack_keep(objects, pack_name).map_err(LandingError::Recovery)?;
605 record_landed_capture_or_warn(
606 capture.layout.git_dir.as_path(),
607 workspace.directory.as_path(),
608 workspace.transaction_id.as_str(),
609 new_oid,
610 );
611 return Ok(None);
612}
613
614#[cfg(test)]
616#[path = "landing_attempt_tests.rs"]
617mod tests;