Skip to main content

git_policy_cli/
commit_workspace.rs

1//! What:
2//!  The disposable, recoverable workspace of one commit transaction:
3//!  a published transaction directory with its owner record, the shadow repository path, and disposal that leaves
4//!  everything in place once a landing record handed it to recovery.
5//! Why:
6//!  The incumbent's `createCommitTransactionWorkspace` (`src/policy-engine/commit-transaction-workspace.ts`)
7//!  publishes the directory before any Git mutation, so recovery can skip a live owner and retire a dead one,
8//!  and disposal removes the shadow before the directory so no shadow outlives the journal that names it.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! await using workspace = await createCommitTransactionWorkspace({ capture });
13//! ```
14
15use super::capture_invocation::InvocationLayout;
16use super::diagnostic_log::{debug, warn};
17use super::landed_disposal::{LandedTransactionPaths, dispose_landed_transaction};
18use super::landing_attempt::LandingWorkspace;
19use super::process_identity::ProcessIdentityError;
20use super::random_id::random_uuid;
21use super::recovery_error::RecoveryError;
22use super::shadow_git::shadow_repository_path;
23use super::transaction_owner::{TransactionOwner, current_transaction_owner, encode_transaction_owner};
24use super::transaction_registry::{ensure_transaction_root, publish_transaction_directory};
25use std::path::PathBuf;
26
27/// Module tag for diagnostics.
28const TAG: &str = "commitWorkspace";
29
30/// What:
31///  Why the workspace could not be published.
32/// Why:
33///   The transaction fails closed before any Git mutation, naming the step.
34///
35/// In TS you'd write (pseudocode):
36/// ```ts
37/// throw new Error(message);
38/// ```
39#[derive(Debug)]
40pub enum WorkspaceError {
41    /// The registry root or the directory could not be created or published.
42    Registry(RecoveryError),
43    /// The process identity could not be read.
44    Identity(ProcessIdentityError),
45    /// The random transaction ID could not be generated.
46    Random(String),
47}
48
49impl std::fmt::Display for WorkspaceError {
50    /// Writes the wording of the failing step.
51    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        return match self {
53            Self::Registry(error) => formatter.write_str(error.0.as_str()),
54            Self::Identity(error) => write!(formatter, "{error:?}"),
55            Self::Random(text) => formatter.write_str(text.as_str()),
56        };
57    }
58}
59
60/// What:
61///  The owned state of one transaction.
62/// Why:
63///   The landing reads the `LandingWorkspace` part; the rest names the places disposal and preparation use.
64///
65/// In TS you'd write (pseudocode):
66/// ```ts
67/// type CommitTransactionWorkspace = { transactionId; directory; commitIndexPath; capturedIndexPath; shadowPath; objectDirectory; hooksDirectory };
68/// ```
69#[derive(Debug)]
70pub struct CommitWorkspace {
71    /// The part the landing attempt reads and marks.
72    pub landing: LandingWorkspace,
73    /// Shadow object store receiving every object the transaction writes before landing migrates them.
74    pub object_directory: PathBuf,
75    /// Git directory of the owning worktree, holding the capture store.
76    git_dir: PathBuf,
77    /// Transaction registry root.
78    registry_root: PathBuf,
79    /// Whether disposal already ran.
80    disposed: bool,
81}
82
83/// What:
84///  Publish a fresh transaction directory owned by this process.
85/// Why:
86///   The owner record is published with the directory, so recovery never sees an unowned directory.
87///
88/// In TS you'd write (pseudocode):
89/// ```ts
90/// const workspace = await createCommitTransactionWorkspace({ capture });
91/// ```
92pub fn create_commit_workspace(layout: &InvocationLayout) -> Result<CommitWorkspace, WorkspaceError> {
93    ensure_transaction_root(layout.registry_root.as_path()).map_err(WorkspaceError::Registry)?;
94    let transaction_id: String = random_uuid().map_err(|error| {
95        return WorkspaceError::Random(format!("a transaction ID could not be generated: {error:?}"));
96    })?;
97    let owner: TransactionOwner =
98        current_transaction_owner(transaction_id.as_str(), layout.invoked_at.as_str()).map_err(WorkspaceError::Identity)?;
99    let directory: PathBuf = publish_transaction_directory(
100        layout.registry_root.as_path(),
101        transaction_id.as_str(),
102        encode_transaction_owner(&owner).as_bytes(),
103    )
104    .map_err(WorkspaceError::Registry)?;
105    let shadow: PathBuf = shadow_repository_path(layout.common_dir.as_path(), transaction_id.as_str());
106    debug(TAG, format!("published transaction {transaction_id}").as_str());
107    return Ok(CommitWorkspace {
108        landing: LandingWorkspace {
109            transaction_id,
110            commit_index: directory.join("commit.index"),
111            captured_index: directory.join("captured.index"),
112            shadow: shadow.clone(),
113            directory: directory.clone(),
114            preserved: false,
115        },
116        object_directory: shadow.join("objects"),
117        git_dir: layout.git_dir.clone(),
118        registry_root: layout.registry_root.clone(),
119        disposed: false,
120    });
121}
122
123impl CommitWorkspace {
124    /// What:
125    ///  Mark durable completion, so disposal removes the recovery artifacts.
126    /// Why:
127    ///   After the landed commit's cleanup the landing's `preserved` mark no longer protects anything.
128    ///
129    /// In TS you'd write (pseudocode):
130    /// ```ts
131    /// workspace.finishTransaction();
132    /// ```
133    pub fn finish_transaction(&mut self) {
134        self.landing.preserved = false;
135    }
136
137    /// What:
138    ///  Remove the shadow repository, then the directory, then prune landed captures, unless recovery owns them.
139    /// Why:
140    ///   Later calls do nothing, and a failed removal is reported while the directory stays for recovery.
141    ///
142    /// In TS you'd write (pseudocode):
143    /// ```ts
144    /// await workspace[Symbol.asyncDispose]();
145    /// ```
146    pub fn dispose(&mut self) -> Result<(), RecoveryError> {
147        if self.disposed {
148            return Ok(());
149        }
150        self.disposed = true;
151        if self.landing.preserved {
152            debug(
153                TAG,
154                format!("leaving transaction {} for recovery", self.landing.transaction_id).as_str(),
155            );
156            return Ok(());
157        }
158        dispose_landed_transaction(LandedTransactionPaths {
159            shadow: self.landing.shadow.as_path(),
160            directory: self.landing.directory.as_path(),
161            git_dir: self.git_dir.as_path(),
162            registry_root: self.registry_root.as_path(),
163        })?;
164        return Ok(());
165    }
166
167    /// What:
168    ///  The Git directory of the owning worktree.
169    /// Why:
170    ///   Post-landing completion prunes the capture store there.
171    ///
172    /// In TS you'd write (pseudocode):
173    /// ```ts
174    /// capture.gitDir
175    /// ```
176    pub fn git_dir(&self) -> &std::path::Path {
177        return self.git_dir.as_path();
178    }
179
180    /// What:
181    ///  The transaction registry root.
182    /// Why:
183    ///   Post-landing completion and the reservation read it.
184    ///
185    /// In TS you'd write (pseudocode):
186    /// ```ts
187    /// capture.registryRoot
188    /// ```
189    pub fn registry_root(&self) -> &std::path::Path {
190        return self.registry_root.as_path();
191    }
192
193    /// What:
194    ///  Hand the disposal to a later step, which has already removed what the workspace owns.
195    /// Why:
196    ///   The post-landing completion removes the shadow and the directory itself, so the drop must not repeat it.
197    ///
198    /// In TS you'd write (pseudocode):
199    /// ```ts
200    /// workspace.finishTransaction(); await workspace[Symbol.asyncDispose]();
201    /// ```
202    pub fn mark_disposed(&mut self) {
203        self.disposed = true;
204    }
205}
206
207impl Drop for CommitWorkspace {
208    /// What:
209    ///  Dispose of the workspace when the transaction ends by any route.
210    /// Why:
211    ///   A failure before the landing must not leave a shadow repository or a directory behind.
212    ///
213    /// In TS you'd write (pseudocode):
214    /// ```ts
215    /// await using workspace = ...;
216    /// ```
217    fn drop(&mut self) {
218        if let Err(error) = self.dispose() {
219            warn(TAG, format!("disposing of the workspace failed: {}", error.0).as_str());
220        }
221    }
222}
223
224/// Controls for this module stay out of the release executable.
225#[cfg(test)]
226#[path = "commit_workspace_tests.rs"]
227mod tests;