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