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_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;