Skip to main content

git_policy_cli/
transaction_gate.rs

1//! What:
2//!  Startup recovery as a step of a wrapped or direct command:
3//!  recover,
4//!  then report how the
5//!       command may go on.
6//! Why:
7//!  Every command that reads the repository location recovers dead transactions first.
8//!  A
9//!      guarded command beside a live transaction is still refused until index writers coordinate
10//!      with live transactions;
11//!  a read-only command goes on.
12//!  A legacy directory or a recovery
13//!      failure stops the command with exit 2,
14//!  as the incumbent's `content-unavailable` engine
15//!      failure does.
16//!
17//! In TS you'd write (pseudocode):
18//! ```ts
19//! await recoverCommitTransaction({ args, gitPath, identity }); // throws CommitTransactionRecoveryError
20//! ```
21
22/// Startup recovery.
23use super::commit_recovery::{
24    RecoveryStop, leaves_live_transactions, legacy_notice, recover_commit_transactions,
25    recovery_directories,
26};
27/// The engine failure event.
28use super::diagnostics::{EngineFailureCode, render_engine_failure};
29/// Running real Git.
30use super::transaction_git::GitContext;
31/// Registry names.
32use super::transaction_registry::TRANSACTION_ROOT_NAME;
33/// The repository shape the invocation selects.
34use super::worktree_identity::WorktreeIdentity;
35/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
36use std::path::{Path, PathBuf};
37
38/// What:
39///  How a command may go on after recovery.
40/// Why:
41///   Callers refuse,
42///  stop or continue from this.
43///
44/// In TS you'd write (pseudocode):
45/// ```ts
46/// type Gate = { kind: 'clear' } | { kind: 'live'; registry: string } | { kind: 'stop'; stderr: string };
47/// ```
48#[derive(Clone, Debug, Eq, PartialEq)]
49pub enum Gate {
50    /// No transaction remains that another process may be running.
51    Clear,
52    /// Live transactions remain in this registry.
53    Live(PathBuf),
54    /// Recovery stopped the command;
55    ///  this is its standard error.
56    Stop(String),
57}
58
59/// What:
60///  Recover the invocation's worktree and say how the command may go on.
61/// Why:
62///   Shared by wrapped commands,
63///  read-only commands and direct commands.
64///
65/// In TS you'd write (pseudocode):
66/// ```ts
67/// async function recoverForCommand({ gitPath, identity }): Promise<Gate>;
68/// ```
69pub fn recover_for_command(context: &GitContext, identity: &WorktreeIdentity) -> Gate {
70    // Git runs where this process runs; the context's global options select the repository.
71    match recover_commit_transactions(context, Path::new("."), identity) {
72        Ok(outcomes) => {
73            if !leaves_live_transactions(outcomes.as_slice()) {
74                return Gate::Clear;
75            }
76            let Some((git_dir, _)) = recovery_directories(identity) else {
77                return Gate::Clear;
78            };
79            return Gate::Live(git_dir.join(TRANSACTION_ROOT_NAME));
80        }
81        Err(RecoveryStop::Legacy(directory)) => {
82            return Gate::Stop(legacy_notice(directory.as_path()));
83        }
84        Err(RecoveryStop::Failure(error)) => {
85            return Gate::Stop(render_engine_failure(
86                0,
87                EngineFailureCode::ContentUnavailable,
88                error.0.as_str(),
89            ));
90        }
91    }
92}
93
94/// Gate controls stay out of the release executable.
95#[cfg(test)]
96#[path = "transaction_gate_tests.rs"]
97mod tests;