git_policy_cli/replay_commit.rs
1//! What:
2//! One replay of a prepared commit onto a moved target, as the landing loop runs it outside both locks:
3//! shared paths, capture order, subsumption, the merge, and either the replayed commit or the conflict.
4//! Why:
5//! This is the replay result the integrator consumes.
6//! Revalidation after a clean merge (policies, `pre-commit`) and the events are the integrator's;
7//! this function returns the data for `commit-replayed`, `replay-headers-dropped`, and
8//! `concurrent-commit/replay-conflict`, and the replayed tree the revalidation reads.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! // commit-replay.ts + commit-capture-order-replay.ts + commit-replay-subsumption.ts, composed as the landing step does
13//! ```
14
15/// Shared paths, capture order, subsumption, the merge, and the rebuild.
16use super::replay_capture_history::{CaptureHistoryError, decide_shared_paths};
17use super::replay_merge::{ReplayMerge, ReplayMergeError, ReplayedCommit, first_commit_touching, merge_replay_tree, replay_merge_base, write_replayed_commit};
18use super::replay_object::{RawCommit, parse_raw_commit};
19use super::replay_shadow::run_shadow_checked;
20use super::replay_shared_paths::{SharedPathError, list_shared_paths};
21use super::replay_subsumption::{SubsumptionBase, SubsumptionError, subsume_landed_changes};
22use super::transaction_git::{GitContext, GitFailure};
23/// Capture-order decisions keyed by path.
24use std::collections::BTreeMap;
25/// Filesystem paths.
26use std::path::Path;
27use super::replay_capture_order::CaptureOrderDecision;
28
29/// What:
30/// One replay's inputs: the Git context and directories, the transaction, the commit IDs, and the signing options.
31/// Why:
32/// A struct keeps the call site short; every field is named once.
33///
34/// In TS you'd write (pseudocode):
35/// ```ts
36/// type ReplayRequest = { gitPath; shadowPath; objectDirectory; cwd; gitDir; directory; replay; base; emptyTree; current; prepared; signingKey; globalArgs };
37/// ```
38pub struct ReplayRequest<'a> {
39 /// Real Git executable and the variables every child receives.
40 pub context: &'a GitContext,
41 /// Shadow repository, as `--git-dir`.
42 pub shadow: &'a Path,
43 /// Shadow object store, whose alternates reach the real objects.
44 pub object_directory: &'a Path,
45 /// Worktree root, where Git reports conflicted paths from.
46 pub worktree_root: &'a Path,
47 /// Git directory of the owning worktree, holding the capture store.
48 pub git_dir: &'a Path,
49 /// This transaction's directory, holding `captured.json`.
50 pub transaction_directory: &'a Path,
51 /// Replay number, naming the private files.
52 pub replay: u64,
53 /// Preparation base commit; `None` for an unborn branch.
54 pub base: Option<&'a str>,
55 /// Empty tree of the repository's object format, for an unborn base.
56 pub empty_tree: &'a str,
57 /// Target that won.
58 pub current: &'a str,
59 /// Prepared commit.
60 pub prepared: &'a str,
61 /// Key the invocation passed to `-S`; `None` signs with the configured key.
62 pub signing_key: Option<&'a str>,
63 /// The caller's kept global options, which may configure signing.
64 pub global_arguments: &'a [String],
65}
66
67/// What:
68/// The replay result of a clean merge:
69/// the replayed commit and the data for `commit-replayed` and `replay-headers-dropped`.
70/// Why:
71/// The integrator runs revalidation on `tree`, then emits the events and lands `oid`.
72///
73/// In TS you'd write (pseudocode):
74/// ```ts
75/// type Replayed = { oid; tree; onto; preparedOid; fromBase; droppedHeaders; subsumedPaths; keptLandedPaths };
76/// ```
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub struct Replayed {
79 /// Replayed commit in the shadow store.
80 pub oid: String,
81 /// Merged tree the replayed commit holds.
82 pub tree: String,
83 /// The target the replay parent is.
84 pub onto: String,
85 /// The prepared commit that was replayed.
86 pub prepared_oid: String,
87 /// The preparation base, or `None` for an unborn branch.
88 pub from_base: Option<String>,
89 /// Custom headers a signed rebuild dropped.
90 pub dropped_headers: Vec<String>,
91 /// Paths whose prepared entries replay as they are.
92 pub subsumed_paths: Vec<String>,
93 /// Paths whose landed entries stay.
94 pub kept_landed_paths: Vec<String>,
95}
96
97/// What:
98/// The replay result of an overlapping merge:
99/// the data for `concurrent-commit/replay-conflict`.
100/// Why:
101/// The named prepared commit stays in the object store so the user can cherry-pick it.
102///
103/// In TS you'd write (pseudocode):
104/// ```ts
105/// type ReplayConflict = { winningOid; paths; preparedOid };
106/// ```
107#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct ReplayConflict {
109 /// First landed commit after the base that touches a conflicting path.
110 pub winning_oid: String,
111 /// Conflicting paths in Git path byte order.
112 pub paths: Vec<String>,
113 /// The prepared commit the replay could not land.
114 pub prepared_oid: String,
115}
116
117/// What:
118/// The outcome of one replay.
119/// Why:
120/// The landing loop branches on it: a clean replay is revalidated and landed, a conflict fails without landing.
121///
122/// In TS you'd write (pseudocode):
123/// ```ts
124/// type ReplayOutcome = { kind: 'replayed'; ... } | { kind: 'conflict'; ... };
125/// ```
126#[derive(Clone, Debug, Eq, PartialEq)]
127pub enum ReplayOutcome {
128 /// The change applies cleanly; the replayed commit exists in the shadow store.
129 Replayed(Replayed),
130 /// The change conflicts with the target.
131 Conflict(ReplayConflict),
132}
133
134/// What:
135/// A replay failure:
136/// one of its steps failed, and the replay is `transaction-failed` rather than a conflict.
137/// Why:
138/// Each step reports its own failure type, so the integrator sees which step broke.
139///
140/// In TS you'd write (pseudocode):
141/// ```ts
142/// throw new CommitTransactionGitError(message);
143/// ```
144#[derive(Debug)]
145pub enum ReplayError {
146 /// Listing the shared paths failed.
147 Shared(SharedPathError),
148 /// Capture order could not read its records or history.
149 Capture(CaptureHistoryError),
150 /// Subsumption failed.
151 Subsumption(SubsumptionError),
152 /// The merge or the rebuild failed.
153 Merge(ReplayMergeError),
154 /// Reading the prepared commit failed.
155 Git(GitFailure),
156 /// The prepared commit's raw object could not be parsed.
157 Object(String),
158}
159
160/// What:
161/// Reads the prepared commit's raw object through the shadow.
162/// Why:
163/// The rebuild needs its tree header, identities, encoding, message, and signature state.
164///
165/// In TS you'd write (pseudocode):
166/// ```ts
167/// parseRawCommit(await runShadowGit({ args: ['cat-file', 'commit', prepared] }).stdout);
168/// ```
169fn prepared_commit(request: &ReplayRequest<'_>) -> Result<RawCommit, ReplayError> {
170 let output = run_shadow_checked(
171 request.context,
172 request.shadow,
173 request.worktree_root,
174 &["cat-file", "commit", request.prepared],
175 None,
176 None,
177 &[],
178 )
179 .map_err(ReplayError::Git)?;
180 return parse_raw_commit(&output.stdout).map_err(|error| { return ReplayError::Object(error.0); });
181}
182
183/// What:
184/// The result of the merge half of a replay: the merged tree and what subsumption decided, or the conflict.
185/// Why:
186/// The incumbent revalidates the merged tree before it rebuilds the commit, so the two halves are separate steps.
187///
188/// In TS you'd write (pseudocode):
189/// ```ts
190/// type MergeStep = { kind: 'merged'; treeOid; subsumedPaths; keptLandedPaths } | { kind: 'conflict'; ... };
191/// ```
192#[derive(Clone, Debug, Eq, PartialEq)]
193pub enum MergeStep {
194 /// The change applies cleanly.
195 Merged {
196 /// The merged tree.
197 tree: String,
198 /// Paths whose prepared entries replay as they are.
199 subsumed_paths: Vec<String>,
200 /// Paths whose landed entries stay.
201 kept_landed_paths: Vec<String>,
202 },
203 /// The change conflicts with the target.
204 Conflict(ReplayConflict),
205}
206
207/// What:
208/// Lists the shared paths, decides capture order, settles subsumed paths and merges, naming the first landed commit
209/// that touched a conflicting path when the merge conflicts.
210/// Why:
211/// Every step runs outside both landing locks in the shadow store; the merged tree is revalidated before the
212/// commit is rebuilt on it.
213///
214/// In TS you'd write (pseudocode):
215/// ```ts
216/// const subsumption = await subsumeLandedChanges(...); const merge = await mergeReplayTree(...);
217/// ```
218pub fn merge_replay(request: &ReplayRequest<'_>) -> Result<MergeStep, ReplayError> {
219 let merge_base: String = replay_merge_base(request.context, request.shadow, request.worktree_root, request.base, request.empty_tree)
220 .map_err(ReplayError::Merge)?;
221 let shared = list_shared_paths(request.context, request.shadow, request.worktree_root, &merge_base, request.current, request.prepared)
222 .map_err(ReplayError::Shared)?;
223 let names: Vec<String> = shared.iter().map(|entry| { return entry.path.clone(); }).collect();
224 let order: BTreeMap<String, CaptureOrderDecision> = decide_shared_paths(
225 request.context,
226 request.shadow,
227 request.worktree_root,
228 request.git_dir,
229 request.transaction_directory,
230 request.base,
231 request.current,
232 &names,
233 )
234 .map_err(ReplayError::Capture)?;
235 let subsumption: SubsumptionBase = subsume_landed_changes(
236 request.context,
237 request.shadow,
238 request.object_directory,
239 request.worktree_root,
240 request.transaction_directory,
241 request.replay,
242 &merge_base,
243 &shared,
244 &order,
245 )
246 .map_err(ReplayError::Subsumption)?;
247 let merged: ReplayMerge = merge_replay_tree(
248 request.context,
249 request.shadow,
250 request.worktree_root,
251 &subsumption.merge_base,
252 request.current,
253 request.prepared,
254 )
255 .map_err(ReplayError::Merge)?;
256 match merged {
257 ReplayMerge::Conflict { paths } => {
258 let winning_oid: String = first_commit_touching(request.context, request.worktree_root, request.base, request.current, &paths)
259 .map_err(ReplayError::Merge)?;
260 return Ok(MergeStep::Conflict(ReplayConflict {
261 winning_oid,
262 paths,
263 prepared_oid: String::from(request.prepared),
264 }));
265 }
266 ReplayMerge::Merged { tree_oid } => {
267 return Ok(MergeStep::Merged {
268 tree: tree_oid,
269 subsumed_paths: subsumption.subsumed_paths,
270 kept_landed_paths: subsumption.kept_landed_paths,
271 });
272 }
273 }
274}
275
276/// What:
277/// Rebuilds the prepared commit on a tree, with the target that won as its parent.
278/// Why:
279/// The tree is the merged tree, or the tree revalidation settled; a signed commit is re-signed.
280///
281/// In TS you'd write (pseudocode):
282/// ```ts
283/// await writeReplayedCommit({ gitPath, shadowPath, directory, attempt, commit: prepared.raw, treeOid, parentOid: onto, globalArgs, signingKey });
284/// ```
285pub fn rebuild_replay(request: &ReplayRequest<'_>, tree: &str) -> Result<ReplayedCommit, ReplayError> {
286 let commit: RawCommit = prepared_commit(request)?;
287 return write_replayed_commit(
288 request.context,
289 request.shadow,
290 request.worktree_root,
291 request.transaction_directory,
292 request.replay,
293 &commit,
294 tree,
295 request.current,
296 request.signing_key,
297 request.global_arguments,
298 )
299 .map_err(ReplayError::Merge);
300}
301
302/// What:
303/// Replays the prepared commit onto `current`:
304/// lists the shared paths, decides capture order, settles subsumed paths, merges, and rebuilds on a clean merge;
305/// on a conflict, names the first landed commit that touched a conflicting path.
306/// Why:
307/// Every step runs outside both landing locks in the shadow store, so the landing loop can take the locks only for the
308/// revalidated commit it then lands.
309///
310/// In TS you'd write (pseudocode):
311/// ```ts
312/// // replay in commit-replay.ts; order decisions and subsumption composed as the landing step does
313/// ```
314pub fn replay_commit(request: &ReplayRequest<'_>) -> Result<ReplayOutcome, ReplayError> {
315 let (tree, subsumed_paths, kept_landed_paths): (String, Vec<String>, Vec<String>) = match merge_replay(request)? {
316 MergeStep::Conflict(conflict) => return Ok(ReplayOutcome::Conflict(conflict)),
317 MergeStep::Merged {
318 tree,
319 subsumed_paths,
320 kept_landed_paths,
321 } => (tree, subsumed_paths, kept_landed_paths),
322 };
323 let replayed: ReplayedCommit = rebuild_replay(request, tree.as_str())?;
324 return Ok(ReplayOutcome::Replayed(Replayed {
325 oid: replayed.oid,
326 tree,
327 onto: String::from(request.current),
328 prepared_oid: String::from(request.prepared),
329 from_base: request.base.map(String::from),
330 dropped_headers: replayed.dropped_headers,
331 subsumed_paths,
332 kept_landed_paths,
333 }));
334}
335
336#[cfg(test)]
337#[path = "replay_commit_tests.rs"]
338mod tests;