Skip to main content

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;