git_policy_cli/post_landing.rs
1//! What:
2//! The post-landing completion of one landed commit, in the incumbent's order:
3//! added-path worktree copies, shadow and transaction removal with capture pruning,
4//! automatic maintenance.
5//! Why:
6//! The incumbent's `commit-transaction-conclusion.ts` runs these steps after a landing.
7//! Worktree copies come first, so an interruption before cleanup leaves the journal for recovery to finish them.
8//! Maintenance bounds the pack count each landing grows.
9//! A maintenance failure is logged and never fails the landed commit; a copy or disposal failure fails the command
10//! with its message, as the incumbent's thrown error does.
11//! No hook and no program named by a configuration runs after the landing.
12//!
13//! In TS you'd write (pseudocode):
14//! ```ts
15//! await installAddedWorktreeFiles({ gitPath, cwd, repositoryRoot, records: [...addedPaths, ...worktreeRecords], objectDirectory });
16//! await workspace[Symbol.asyncDispose]();
17//! await runAutoMaintenance({ gitPath, cwd: repositoryRoot, globalArgs });
18//! ```
19
20use super::diagnostic_log::{debug, warn};
21use super::landed_disposal::{LandedTransactionPaths, dispose_landed_transaction};
22use super::config_snapshot::ConfigSnapshot;
23use super::maintenance_run::{AutoMaintenanceOutcome, run_auto_maintenance_with};
24use super::recovery_error::RecoveryError;
25use super::transaction_git::GitContext;
26use super::transaction_journal::AddedPath;
27use super::worktree_completion::{InstallResult, install_added_worktree_files};
28use std::fmt::{Display, Formatter};
29use std::path::PathBuf;
30
31/// What:
32/// What a landing reports about the commit it made, and the transaction paths that landing left behind.
33/// Why:
34/// The landing lane fills this from its outcome; this lane only reads it.
35/// `worktree_records` holds the selected corrections followed by the hook changes, as the incumbent orders them.
36///
37/// In TS you'd write (pseudocode):
38/// ```ts
39/// type LandedTransaction = { oid; branch; cwd; repositoryRoot; directory; shadowPath; gitDir; registryRoot; realIndexPath; addedPaths; worktreeRecords };
40/// ```
41#[derive(Clone, Debug, Eq, PartialEq)]
42pub struct LandedTransaction {
43 /// Landed commit object name.
44 pub landed_oid: String,
45 /// Branch the commit landed on, named in diagnostics.
46 pub branch: String,
47 /// Effective repository directory the Git commands of the transaction ran in.
48 pub cwd: PathBuf,
49 /// Canonical worktree root, where maintenance and `post-commit` run.
50 pub repository_root: PathBuf,
51 /// Durable transaction directory under the registry.
52 pub directory: PathBuf,
53 /// Shadow repository whose object store holds originals that never landed.
54 pub shadow: PathBuf,
55 /// Real Git directory holding the capture store.
56 pub git_dir: PathBuf,
57 /// Transaction registry root.
58 pub registry_root: PathBuf,
59 /// Paths a policy added, with their worktree completion records.
60 pub added_paths: Vec<AddedPath>,
61 /// Selected corrections whose worktree copies the landing completes.
62 pub worktree_records: Vec<AddedPath>,
63 /// The configuration read when the commit began, which supplies the maintenance keys when it models them.
64 pub config_snapshot: Option<ConfigSnapshot>,
65}
66
67/// What:
68/// The inputs of one post-landing completion.
69/// Why:
70/// The context carries the caller's global options for maintenance.
71///
72/// In TS you'd write (pseudocode):
73/// ```ts
74/// { gitPath, globalArgs, landed }
75/// ```
76#[derive(Clone, Copy, Debug)]
77pub struct PostLandingRequest<'a> {
78 /// Real Git and the caller's global options.
79 pub context: &'a GitContext,
80 /// The landed commit and its transaction paths.
81 pub landed: &'a LandedTransaction,
82}
83
84/// What:
85/// What post-landing completion did, reported for the caller's diagnostics.
86/// Why:
87/// An action's ending is reported on standard error by the runner and never changes the exit status.
88///
89/// In TS you'd write (pseudocode):
90/// ```ts
91/// { landedOid, policyResult, committed: true }
92/// ```
93#[derive(Clone, Debug, Eq, PartialEq)]
94pub struct PostLandingOutcome {
95 /// Landed commit object name.
96 pub landed_oid: String,
97 /// Worktree copies rewritten and kept because they changed concurrently.
98 pub worktree: InstallResult,
99 /// Landed-capture records pruning removed.
100 pub pruned_captures: usize,
101 /// Maintenance outcome, or `None` when maintenance could not start and the failure was logged.
102 pub maintenance: Option<AutoMaintenanceOutcome>,
103}
104
105/// What:
106/// A failure of one post-landing step, naming the step that failed.
107/// Why:
108/// The caller prints the message and exits 1; each variant keeps what the recovery may still need.
109///
110/// In TS you'd write (pseudocode):
111/// ```ts
112/// throw error; // from installAddedWorktreeFiles or workspace disposal
113/// ```
114#[derive(Clone, Debug, Eq, PartialEq)]
115pub enum PostLandingError {
116 /// Worktree copies could not be completed; the shadow and the directory remain for recovery.
117 Install(RecoveryError),
118 /// The shadow or the transaction directory could not be removed.
119 Dispose(RecoveryError),
120}
121
122impl Display for PostLandingError {
123 /// What:
124 /// The failure's message, without a prefix naming the step.
125 /// Why:
126 /// The incumbent prints the thrown error's message alone.
127 ///
128 /// In TS you'd write (pseudocode):
129 /// ```ts
130 /// console.error(error.message);
131 /// ```
132 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
133 return match self {
134 PostLandingError::Install(error) | PostLandingError::Dispose(error) => {
135 write!(formatter, "{}", error.0)
136 }
137 };
138 }
139}
140
141/// What:
142/// Complete a landed commit's transaction in the incumbent's order and report what each step did.
143/// Why:
144/// Each step runs only after the one before it succeeded, except maintenance, whose failure never stops the completion.
145///
146/// In TS you'd write (pseudocode):
147/// ```ts
148/// await installAddedWorktreeFiles(...); await workspace[Symbol.asyncDispose](); await runAutoMaintenance(...);
149/// ```
150pub fn complete_landed_transaction(
151 request: PostLandingRequest,
152) -> Result<PostLandingOutcome, PostLandingError> {
153 let landed: &LandedTransaction = request.landed;
154 debug(
155 "completeLandedTransaction",
156 format!("completing {} on {}", landed.landed_oid, landed.branch).as_str(),
157 );
158 let mut records: Vec<AddedPath> = landed.added_paths.clone();
159 records.extend(landed.worktree_records.iter().cloned());
160 let objects: PathBuf = landed.shadow.join("objects");
161 let worktree: InstallResult = install_added_worktree_files(
162 request.context,
163 landed.cwd.as_path(),
164 landed.repository_root.as_path(),
165 records.as_slice(),
166 Some(objects.as_path()),
167 )
168 .map_err(PostLandingError::Install)?;
169 let pruned_captures: usize = dispose_landed_transaction(LandedTransactionPaths {
170 shadow: landed.shadow.as_path(),
171 directory: landed.directory.as_path(),
172 git_dir: landed.git_dir.as_path(),
173 registry_root: landed.registry_root.as_path(),
174 })
175 .map_err(PostLandingError::Dispose)?;
176 let maintenance: Option<AutoMaintenanceOutcome> = match run_auto_maintenance_with(
177 request.context,
178 landed.repository_root.as_path(),
179 landed.config_snapshot.as_ref(),
180 ) {
181 Ok(outcome) => Some(outcome),
182 Err(error) => {
183 warn(
184 "completeLandedTransaction",
185 format!(
186 "automatic maintenance could not run, and the landed commit stands: {error:?}"
187 )
188 .as_str(),
189 );
190 None
191 }
192 };
193 return Ok(PostLandingOutcome {
194 landed_oid: landed.landed_oid.clone(),
195 worktree,
196 pruned_captures,
197 maintenance,
198 });
199}
200
201/// Post-landing controls stay out of the release executable.
202#[cfg(test)]
203#[path = "post_landing_tests.rs"]
204mod tests;