Skip to main content

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;