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