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;