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