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    /// Hook dispatcher directory.
76    pub hooks_directory: PathBuf,
77    /// Git directory of the owning worktree, holding the capture store.
78    git_dir: PathBuf,
79    /// Transaction registry root.
80    registry_root: PathBuf,
81    /// Whether disposal already ran.
82    disposed: bool,
83}
84
85/// What:
86///  Publish a fresh transaction directory owned by this process.
87/// Why:
88///   The owner record is published with the directory, so recovery never sees an unowned directory.
89///
90/// In TS you'd write (pseudocode):
91/// ```ts
92/// const workspace = await createCommitTransactionWorkspace({ capture });
93/// ```
94pub fn create_commit_workspace(layout: &InvocationLayout) -> Result<CommitWorkspace, WorkspaceError> {
95    ensure_transaction_root(layout.registry_root.as_path()).map_err(WorkspaceError::Registry)?;
96    let transaction_id: String = random_uuid().map_err(|error| {
97        return WorkspaceError::Random(format!("a transaction ID could not be generated: {error:?}"));
98    })?;
99    let owner: TransactionOwner =
100        current_transaction_owner(transaction_id.as_str(), layout.invoked_at.as_str()).map_err(WorkspaceError::Identity)?;
101    let directory: PathBuf = publish_transaction_directory(
102        layout.registry_root.as_path(),
103        transaction_id.as_str(),
104        encode_transaction_owner(&owner).as_bytes(),
105    )
106    .map_err(WorkspaceError::Registry)?;
107    let shadow: PathBuf = shadow_repository_path(layout.common_dir.as_path(), transaction_id.as_str());
108    debug(TAG, format!("published transaction {transaction_id}").as_str());
109    return Ok(CommitWorkspace {
110        landing: LandingWorkspace {
111            transaction_id,
112            commit_index: directory.join("commit.index"),
113            captured_index: directory.join("captured.index"),
114            shadow: shadow.clone(),
115            directory: directory.clone(),
116            preserved: false,
117        },
118        object_directory: shadow.join("objects"),
119        hooks_directory: directory.join("hooks"),
120        git_dir: layout.git_dir.clone(),
121        registry_root: layout.registry_root.clone(),
122        disposed: false,
123    });
124}
125
126impl CommitWorkspace {
127    /// What:
128    ///  Mark durable completion, so disposal removes the recovery artifacts.
129    /// Why:
130    ///   After the landed commit's cleanup the landing's `preserved` mark no longer protects anything.
131    ///
132    /// In TS you'd write (pseudocode):
133    /// ```ts
134    /// workspace.finishTransaction();
135    /// ```
136    pub fn finish_transaction(&mut self) {
137        self.landing.preserved = false;
138    }
139
140    /// What:
141    ///  Remove the shadow repository, then the directory, then prune landed captures, unless recovery owns them.
142    /// Why:
143    ///   Later calls do nothing, and a failed removal is reported while the directory stays for recovery.
144    ///
145    /// In TS you'd write (pseudocode):
146    /// ```ts
147    /// await workspace[Symbol.asyncDispose]();
148    /// ```
149    pub fn dispose(&mut self) -> Result<(), RecoveryError> {
150        if self.disposed {
151            return Ok(());
152        }
153        self.disposed = true;
154        if self.landing.preserved {
155            debug(
156                TAG,
157                format!("leaving transaction {} for recovery", self.landing.transaction_id).as_str(),
158            );
159            return Ok(());
160        }
161        dispose_landed_transaction(LandedTransactionPaths {
162            shadow: self.landing.shadow.as_path(),
163            directory: self.landing.directory.as_path(),
164            git_dir: self.git_dir.as_path(),
165            registry_root: self.registry_root.as_path(),
166        })?;
167        return Ok(());
168    }
169
170    /// What:
171    ///  The Git directory of the owning worktree.
172    /// Why:
173    ///   Post-landing completion prunes the capture store there.
174    ///
175    /// In TS you'd write (pseudocode):
176    /// ```ts
177    /// capture.gitDir
178    /// ```
179    pub fn git_dir(&self) -> &std::path::Path {
180        return self.git_dir.as_path();
181    }
182
183    /// What:
184    ///  The transaction registry root.
185    /// Why:
186    ///   Post-landing completion and the reservation read it.
187    ///
188    /// In TS you'd write (pseudocode):
189    /// ```ts
190    /// capture.registryRoot
191    /// ```
192    pub fn registry_root(&self) -> &std::path::Path {
193        return self.registry_root.as_path();
194    }
195
196    /// What:
197    ///  Hand the disposal to a later step, which has already removed what the workspace owns.
198    /// Why:
199    ///   The post-landing completion removes the shadow and the directory itself, so the drop must not repeat it.
200    ///
201    /// In TS you'd write (pseudocode):
202    /// ```ts
203    /// workspace.finishTransaction(); await workspace[Symbol.asyncDispose]();
204    /// ```
205    pub fn mark_disposed(&mut self) {
206        self.disposed = true;
207    }
208}
209
210impl Drop for CommitWorkspace {
211    /// What:
212    ///  Dispose of the workspace when the transaction ends by any route.
213    /// Why:
214    ///   A failure before the landing must not leave a shadow repository or a directory behind.
215    ///
216    /// In TS you'd write (pseudocode):
217    /// ```ts
218    /// await using workspace = ...;
219    /// ```
220    fn drop(&mut self) {
221        if let Err(error) = self.dispose() {
222            warn(TAG, format!("disposing of the workspace failed: {}", error.0).as_str());
223        }
224    }
225}
226
227/// Controls for this module stay out of the release executable.
228#[cfg(test)]
229#[path = "commit_workspace_tests.rs"]
230mod tests;