Skip to main content

git_policy_cli/
landing_attempt.rs

1//! What:
2//!  One landing attempt inside the landing critical section:
3//!  the real `index.lock`,
4//!  the branch-switch and head-moved checks,
5//!  object migration,
6//!  the post-index,
7//!  `landing-<n>.json`,
8//!  the compare-and-swap with the nonce reflog message,
9//!  `ref-updated.json`,
10//!  the index install,
11//!  the `index-installed` marker and conclusion cleanup.
12//! Why:
13//!  A landing is serial under the landing lock and the real `index.lock`, and it journals every state
14//!  so recovery can finish or discard a landing that died anywhere inside
15//!  (`src/policy-engine/commit-landing.ts`).
16//!  The caller takes the landing lock, checks the starvation reservation and replays a moved target;
17//!  the caller's post-landing step disposes of the transaction directory.
18//!  This module never runs hooks, the editor, signing, network operations or the hook lock.
19//!
20//! In TS you'd write (pseudocode):
21//! ```ts
22//! const outcome = await landTransaction({ gitPath, cwd, capture, workspace, mode, payload, committedPaths, addedPaths, selectedWorktreePaths, indexLockTimeoutMs, attempt });
23//! ```
24
25/// Recording the capture of a landed commit.
26use super::capture_records::record_landed_capture_or_warn;
27/// The invocation facts, and the baseline revision the post-index reads.
28use super::capture_invocation::{InvocationCapture, base_revision, resolve_ref_commit, resolve_symbolic_head};
29/// Native conclusion cleanup after the index is installed.
30use super::conclusion_cleanup::reproduce_conclusion_cleanup;
31/// The index copy that keeps the timestamps Git reads.
32use super::commit_index::copy_index_with_timestamps;
33/// Debug and warning text to the diagnostic log.
34use super::diagnostic_log::{debug, warn};
35/// Failure of waiting for or creating the real index lock.
36use super::index_writer_coordination::CoordinationFailure;
37/// The post-index computation and its inputs.
38use super::landing_index::{PostIndexInput, compute_landing_post_index};
39/// Object migration into a kept pack.
40use super::landing_objects::migrate_shadow_objects;
41/// Target comparison and the compare-and-swap.
42use super::landing_support::{CompareAndSwap, compare_and_swap_target, same_target};
43/// The keep message and its removal.
44use super::pack_keep::{remove_pack_keep, transaction_keep_message};
45/// The held landing lock, which the caller must prove it holds.
46use super::owner_lock::OwnerLock;
47/// Journal files are written durably.
48use super::private_storage::{sync_directory, write_private_file};
49/// Recovery failures that the shared steps report.
50use super::recovery_error::RecoveryError;
51/// File identities of the landing artifacts.
52use super::recovery_files::file_identity_of;
53/// The real index lock, taken for the attempt.
54use super::real_index_lock::{RealIndexLock, acquire_landing_index_lock};
55/// Running real Git.
56use super::transaction_git::{GitContext, GitFailure};
57/// Journal records, names and filenames.
58use 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};
63/// Record encoders.
64use super::transaction_journal_encode::{encode_landing, encode_ref_updated};
65/// The test-only phase markers.
66use super::transaction_phase::{Phase, PhaseMarkerError, reach_transaction_phase};
67/// `Path` and `PathBuf` are borrowed and owned filesystem paths.
68use std::path::{Path, PathBuf};
69
70/// Module tag for diagnostics.
71const TAG: &str = "landTransaction";
72
73/// What:
74///  What one landing installs.
75/// Why:
76///   A prepared commit advances the target;
77///  a normalization installs only the settled index.
78///
79/// In TS you'd write (pseudocode):
80/// ```ts
81/// type LandingPayload = { operation: 'commit'; newOid; landedTreeOid; expectedOld; landedIndexPath; exactPrivateIndex; guardedPaths } | { operation: 'normalize-only'; landedTreeOid };
82/// ```
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub enum LandingPayload {
85    /// A prepared commit.
86    Commit {
87        /// Prepared commit.
88        new_oid: String,
89        /// Prepared or replayed tree.
90        landed_tree_oid: String,
91        /// Target value the commit's parent names:
92        ///  the preparation base,
93        ///  or the replay parent.
94        expected_old: Base,
95        /// Private index the commit's tree came from.
96        landed_index: PathBuf,
97        /// Whether `landed_index` is the exact index native preparation committed,
98        ///  false after a replay.
99        exact_private_index: bool,
100        /// Paths a commit hook changed,
101        ///  reconciled only while their real index entry is unchanged since invocation.
102        guarded_paths: Vec<String>,
103    },
104    /// A normalization whose settled tree equals the base.
105    NormalizeOnly {
106        /// Base tree the index settles to.
107        landed_tree_oid: String,
108    },
109}
110
111/// What:
112///  The result of one landing attempt that ran to a decision.
113/// Why:
114///   The caller replays a moved target, reports a switched branch, and completes a landing.
115///
116/// In TS you'd write (pseudocode):
117/// ```ts
118/// type LandingOutcome = { kind: 'landed'; oid } | { kind: 'head-moved'; current; replayable } | { kind: 'branch-switched'; current };
119/// ```
120#[derive(Clone, Debug, Eq, PartialEq)]
121pub enum LandingOutcome {
122    /// The target advanced,
123    ///  or a normalization installed its index.
124    Landed {
125        /// Landed commit;
126        ///  the base for a normalization,
127        ///  empty when that base is unborn.
128        oid: String,
129    },
130    /// The target moved since preparation;
131    ///  nothing landed.
132    HeadMoved {
133        /// Target value that won.
134        current: Base,
135        /// Whether replay can land this commit on the new target;
136        ///  amends,
137        ///  conclusions,
138        ///  normalizations and a deleted target never replay.
139        replayable: bool,
140    },
141    /// `HEAD` names another branch,
142    ///  or is detached,
143    ///  unlike at invocation;
144    ///  nothing landed.
145    BranchSwitched {
146        /// Current symbolic `HEAD` target.
147        current: SymbolicHead,
148    },
149}
150
151/// What:
152///  Why a landing attempt stopped without a decision.
153/// Why:
154///   Each variant names the step that failed,
155///  and the workspace's `preserved` flag says whether recovery now owns the transaction directory.
156///
157/// In TS you'd write (pseudocode):
158/// ```ts
159/// type LandingFailure = IndexLockUnprovenOwnerError | SubprocessError | Error;
160/// ```
161#[derive(Debug)]
162pub enum LandingError {
163    /// The real index lock could not be taken.
164    Lock(CoordinationFailure),
165    /// A Git step failed.
166    Git(GitFailure),
167    /// A shared recovery step failed:
168    ///  pack keep removal,
169    ///  index install or conclusion cleanup.
170    Recovery(RecoveryError),
171    /// A journal record or marker could not be written.
172    Journal(String),
173    /// The test-only phase marker variable is malformed or its marker could not be written.
174    Phase(PhaseMarkerError),
175    /// The landing decided but could not release the real index lock.
176    Release(String),
177}
178
179/// The wording of the failure.
180impl std::fmt::Display for LandingError {
181    /// Writes the wording of the failing step.
182    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/// What:
194///  The transaction state a landing reads and marks.
195/// Why:
196///   `CommitTransactionWorkspace`:
197///  the landing needs the directory and its private indexes,
198///  and tells the caller whether recovery now owns the directory.
199///
200/// In TS you'd write (pseudocode):
201/// ```ts
202/// type CommitTransactionWorkspace = { transactionId; directory; commitIndexPath; capturedIndexPath; shadowPath; preserveForRecovery(); finishTransaction() };
203/// ```
204#[derive(Clone, Debug, Eq, PartialEq)]
205pub struct LandingWorkspace {
206    /// Transaction ID,
207    ///  also the reflog nonce.
208    pub transaction_id: String,
209    /// Durable transaction directory.
210    pub directory: PathBuf,
211    /// Shadow repository holding the prepared commit.
212    pub shadow: PathBuf,
213    /// Exact real index snapshot taken at invocation.
214    pub captured_index: PathBuf,
215    /// Private commit index.
216    pub commit_index: PathBuf,
217    /// Whether a landing record hands the directory to recovery,
218    ///  so the caller's disposal must leave it in place.
219    ///  Set before `landing-<n>.json` is written and cleared again when the compare-and-swap is lost.
220    pub preserved: bool,
221}
222
223/// What:
224///  The facts of one attempt beside the workspace.
225/// Why:
226///   They stay fixed across attempts of one commit, except the payload and the attempt number.
227///
228/// In TS you'd write (pseudocode):
229/// ```ts
230/// { capture, mode, payload, committedPaths, addedPaths, selectedWorktreePaths, indexLockTimeoutMs, attempt }
231/// ```
232#[derive(Clone, Copy, Debug)]
233pub struct LandingRequest<'request> {
234    /// Invocation capture naming the target and the real index.
235    pub capture: &'request InvocationCapture,
236    /// Commit selection mode.
237    pub mode: TransactionMode,
238    /// The commit or normalization to land.
239    pub payload: &'request LandingPayload,
240    /// Paths an explicit-path commit carries.
241    pub committed_paths: &'request [String],
242    /// Policy-added path records.
243    pub added_paths: &'request [AddedPath],
244    /// Selected newline correction records.
245    pub selected_worktree_paths: &'request [AddedPath],
246    /// Backoff budget for a foreign `index.lock`.
247    pub index_lock_timeout_ms: u64,
248    /// Landing attempt number,
249    ///  from 1.
250    pub attempt: i64,
251}
252
253/// What:
254///  Write one journal record durably:
255///  exclusively,
256///  then the directory entry.
257/// Why:
258///   `writeJournalRecord`:
259///  a state file is never rewritten,
260///  so a crash leaves the newest complete state readable.
261///
262/// In TS you'd write (pseudocode):
263/// ```ts
264/// await writeJournalRecord({ directory, filename, record });
265/// ```
266fn 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
277/// What:
278///  Reach one test-only phase marker.
279/// Why:
280///   A malformed marker variable fails the landing at every phase it reaches.
281///
282/// In TS you'd write (pseudocode):
283/// ```ts
284/// await reachTransactionPhase({ phase });
285/// ```
286fn reach(phase: Phase) -> Result<(), LandingError> {
287    return reach_transaction_phase(phase, None).map_err(LandingError::Phase);
288}
289
290/// What:
291///  The device and inode of a landing artifact.
292/// Why:
293///   `fileIdentity`:
294///  recovery proves an artifact is the exact file written.
295///
296/// In TS you'd write (pseudocode):
297/// ```ts
298/// await fileIdentity(path)
299/// ```
300fn 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
307/// What:
308///  Whether a target value names a commit.
309/// Why:
310///   Only a target that still names a commit can be replayed onto.
311///
312/// In TS you'd write (pseudocode):
313/// ```ts
314/// current.kind === 'commit'
315/// ```
316fn names_commit(target: &Base) -> bool {
317    return matches!(target, Base::Commit(_));
318}
319
320/// What:
321///  The real index lock while a landing holds it.
322/// Why:
323///   The lock moves out when the install consumes it,
324///  and a lock that recovery needs for the install stays in place when the attempt fails.
325///
326/// In TS you'd write (pseudocode):
327/// ```ts
328/// await using indexLock = await acquireRealIndexLock({ realIndexPath, transactionDirectory, attempt, timeoutMs });
329/// ```
330struct Held {
331    /// The lock,
332    ///  until the install consumes it.
333    lock: Option<RealIndexLock>,
334    /// Whether the landing reached the point after which only the install is left,
335    ///  so recovery installs through this lock if the attempt dies or fails.
336    for_recovery: bool,
337}
338
339/// What:
340///  Runs one landing attempt.
341/// Why:
342///   `landTransaction`:
343///  the caller already holds the landing lock (`_landing_lock` is the witness) and has dealt with the starvation reservation.
344///  The real `index.lock` is released on every exit except an install,
345///  and except a failure after the target advanced (or, for a normalization, just before the install),
346///  because recovery completes the install through the same lock and fails closed without it.
347///  A target that moved ends the attempt with `HeadMoved`;
348///  when `replayable`,
349///  the caller replays and revalidates outside both locks and lands again.
350///
351/// In TS you'd write (pseudocode):
352/// ```ts
353/// await landTransaction({ gitPath, cwd, capture, workspace, mode, payload, committedPaths, addedPaths, selectedWorktreePaths, indexLockTimeoutMs, attempt });
354/// ```
355pub 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
390/// What:
391///  The landing steps that run while this process holds the real index lock.
392/// Why:
393///   The lock moves out of `held` only when the install consumes it,
394///  so the caller releases whatever is still held.
395///
396/// In TS you'd write (pseudocode):
397/// ```ts
398/// // the body of landTransaction after `await using indexLock`
399/// ```
400fn 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(&current, &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(&current);
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    // The record below hands the directory to recovery, so the mark comes first, as the incumbent's does.
451    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    // A normalization has only the install left once its record is durable.
487    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
516/// What:
517///  Computes the post-index against the real index as it is now.
518/// Why:
519///   A normalization lands the commit index as an exact private index with no guarded paths,
520///  as the incumbent's defaults do.
521///
522/// In TS you'd write (pseudocode):
523/// ```ts
524/// await computeLandingPostIndex({ gitPath, cwd, mode, preLandingIndexPath, capturedIndexPath, postIndexPath, landedTreeOid, baseRevision, committedPaths, landedIndexPath });
525/// ```
526fn 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
557/// What:
558///  Advances the target by compare-and-swap and journals the result.
559///  Returns the outcome when the swap was lost.
560/// Why:
561///   A lost swap removes the pack keep and releases the directory from recovery,
562///  then reports who won;
563///  a won swap writes `ref-updated.json`,
564///  removes the keep,
565///  and records the landed capture.
566///
567/// In TS you'd write (pseudocode):
568/// ```ts
569/// const swap = await compareAndSwapTarget({ gitPath, cwd, capture, transactionId, newOid, expectedOld });
570/// ```
571fn 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    // From here recovery finds the landed commit by its nonce and installs the index through this lock.
601    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/// Landing attempt controls stay out of the release executable.
615#[cfg(test)]
616#[path = "landing_attempt_tests.rs"]
617mod tests;