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